pax_global_header00006660000000000000000000000064142077471410014521gustar00rootroot0000000000000052 comment=8ea06e8a78ecd61726352d8665c2c97104854dab blueprint-compiler-main/000077500000000000000000000000001420774714100156235ustar00rootroot00000000000000blueprint-compiler-main/.coveragerc000066400000000000000000000002001420774714100177340ustar00rootroot00000000000000[report] exclude_lines = pragma: no cover raise AssertionError raise NotImplementedError raise CompilerBugError blueprint-compiler-main/.gitignore000066400000000000000000000002441420774714100176130ustar00rootroot00000000000000__pycache__ /build /dist *.egg-info blueprint-compiler.pc /.coverage /htmlcov coverage.xml .mypy_cache /subprojects/gtk-blueprint-tool /blueprint-regression-tests blueprint-compiler-main/.gitlab-ci.yml000066400000000000000000000013631420774714100202620ustar00rootroot00000000000000stages: - build - pages build: image: registry.gitlab.gnome.org/jwestman/blueprint-compiler stage: build script: - mypy blueprintcompiler - coverage run -m unittest - coverage html - coverage xml - meson _build -Ddocs=true - ninja -C _build - ninja -C _build test - ninja -C _build install - git clone https://gitlab.gnome.org/jwestman/blueprint-regression-tests.git - cd blueprint-regression-tests - ./test.sh - cd .. artifacts: paths: - _build - htmlcov reports: cobertura: coverage.xml pages: stage: pages dependencies: - build script: - mv _build/docs/en public - mv htmlcov public/coverage artifacts: paths: - public only: - main blueprint-compiler-main/COPYING000066400000000000000000000167441420774714100166720ustar00rootroot00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. blueprint-compiler-main/README.md000066400000000000000000000050471420774714100171100ustar00rootroot00000000000000# Blueprint A markup language for GTK user interface files. ## Motivation GtkBuilder XML format is quite verbose, and many app developers don't like using WYSIWYG editors for creating UIs. Blueprint files are intended to be a concise, easy-to-read format that makes it easier to create and edit GTK UIs. Internally, it compiles to GtkBuilder XML as part of an app's build system. It adds no new features, just makes the features that exist more accessible. Another goal is to have excellent developer tooling--including a language server--so that less knowledge of the format is required. Hopefully this will increase adoption of cool advanced features like GtkExpression. ## Example Here is what [the libshumate demo's UI definition](https://gitlab.gnome.org/GNOME/libshumate/-/blob/main/demos/shumate-demo-window.ui) looks like ported to this new format: ``` using Gtk 4.0; using Shumate 1.0; template ShumateDemoWindow : Gtk.ApplicationWindow { can-focus: yes; title: _("Shumate Demo"); default-width: 800; default-height: 600; [titlebar] Gtk.HeaderBar { Gtk.DropDown layers_dropdown { notify::selected => on_layers_dropdown_notify_selected() swapped; } } Gtk.Overlay overlay { vexpand: true; Shumate.Map map {} [overlay] Shumate.Scale scale { halign: start; valign: end; } [overlay] Gtk.Box { orientation: vertical; halign: end; valign: end; Shumate.Compass compass { halign: end; map: map; } Shumate.License license { halign: end; } } } } ``` ## Editor plugins ### Vim - [Syntax highlighting by thetek42](https://github.com/thetek42/vim-blueprint-syntax) - [Syntax highlighting by gabmus](https://gitlab.com/gabmus/vim-blueprint) ## License Copyright (C) 2021 James Westman This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . ## Donate You can support my work on GitHub Sponsors! blueprint-compiler-main/blueprint-compiler.pc.in000066400000000000000000000001541420774714100223700ustar00rootroot00000000000000Name: blueprint-compiler Description: Markup compiler for GTK user interface definitions Version: @VERSION@ blueprint-compiler-main/blueprint-compiler.py000077500000000000000000000015601420774714100220160ustar00rootroot00000000000000#!/usr/bin/env python3 # blueprint-compiler.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later from blueprintcompiler import main if __name__ == "__main__": main.main() blueprint-compiler-main/blueprintcompiler/000077500000000000000000000000001420774714100213625ustar00rootroot00000000000000blueprint-compiler-main/blueprintcompiler/__init__.py000066400000000000000000000000001420774714100234610ustar00rootroot00000000000000blueprint-compiler-main/blueprintcompiler/ast_utils.py000066400000000000000000000136351420774714100237530ustar00rootroot00000000000000# ast_utils.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later import typing as T from collections import ChainMap, defaultdict from .errors import * from .lsp_utils import SemanticToken from .utils import lazy_prop from .xml_emitter import XmlEmitter class Children: """ Allows accessing children by type using array syntax. """ def __init__(self, children): self._children = children def __iter__(self): return iter(self._children) def __getitem__(self, key): return [child for child in self._children if isinstance(child, key)] class AstNode: """ Base class for nodes in the abstract syntax tree. """ completers: T.List = [] def __init__(self, group, children, tokens, incomplete=False): self.group = group self.children = Children(children) self.tokens = ChainMap(tokens, defaultdict(lambda: None)) self.incomplete = incomplete self.parent = None for child in self.children: child.parent = self def __init_subclass__(cls): cls.completers = [] cls.validators = [getattr(cls, f) for f in dir(cls) if hasattr(getattr(cls, f), "_validator")] @property def root(self): if self.parent is None: return self else: return self.parent.root def parent_by_type(self, type): if self.parent is None: return None elif isinstance(self.parent, type): return self.parent else: return self.parent.parent_by_type(type) @lazy_prop def errors(self): return list(self._get_errors()) def _get_errors(self): for validator in self.validators: try: validator(self) except CompileError as e: yield e for child in self.children: yield from child._get_errors() def _attrs_by_type(self, attr_type): for name in dir(type(self)): item = getattr(type(self), name) if isinstance(item, attr_type): yield name, item def generate(self) -> str: """ Generates an XML string from the node. """ xml = XmlEmitter() self.emit_xml(xml) return xml.result def emit_xml(self, xml: XmlEmitter): """ Emits the XML representation of this AST node to the XmlEmitter. """ raise NotImplementedError() def get_docs(self, idx: int) -> T.Optional[str]: for name, attr in self._attrs_by_type(Docs): if attr.token_name: token = self.group.tokens.get(attr.token_name) if token and token.start <= idx < token.end: return getattr(self, name) else: return getattr(self, name) for child in self.children: if child.group.start <= idx < child.group.end: docs = child.get_docs(idx) if docs is not None: return docs return None def get_semantic_tokens(self) -> T.Iterator[SemanticToken]: for child in self.children: yield from child.get_semantic_tokens() def iterate_children_recursive(self) -> T.Iterator["AstNode"]: yield self for child in self.children: yield from child.iterate_children_recursive() def validate(token_name=None, end_token_name=None, skip_incomplete=False): """ Decorator for functions that validate an AST node. Exceptions raised during validation are marked with range information from the tokens. """ def decorator(func): def inner(self): if skip_incomplete and self.incomplete: return try: func(self) except CompileError as e: # If the node is only partially complete, then an error must # have already been reported at the parsing stage if self.incomplete: return # This mess of code sets the error's start and end positions # from the tokens passed to the decorator, if they have not # already been set if e.start is None: if token := self.group.tokens.get(token_name): e.start = token.start else: e.start = self.group.start if e.end is None: if token := self.group.tokens.get(end_token_name): e.end = token.end elif token := self.group.tokens.get(token_name): e.end = token.end else: e.end = self.group.end # Re-raise the exception raise e inner._validator = True return inner return decorator class Docs: def __init__(self, func, token_name=None): self.func = func self.token_name = token_name def __get__(self, instance, owner): if instance is None: return self return self.func(instance) def docs(*args, **kwargs): """ Decorator for functions that return documentation for tokens. """ def decorator(func): return Docs(func, *args, **kwargs) return decorator blueprint-compiler-main/blueprintcompiler/completions.py000066400000000000000000000120501420774714100242660ustar00rootroot00000000000000# completions.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later import typing as T from . import gir, language from .ast_utils import AstNode from .completions_utils import * from .lsp_utils import Completion, CompletionItemKind from .parser import SKIP_TOKENS from .tokenizer import TokenType, Token Pattern = T.List[T.Tuple[TokenType, T.Optional[str]]] def _complete(ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int) -> T.Iterator[Completion]: for child in ast_node.children: if child.group.start <= idx and (idx < child.group.end or (idx == child.group.end and child.incomplete)): yield from _complete(child, tokens, idx, token_idx) return prev_tokens: T.List[Token] = [] # collect the 5 previous non-skipped tokens while len(prev_tokens) < 5 and token_idx >= 0: token = tokens[token_idx] if token.type not in SKIP_TOKENS: prev_tokens.insert(0, token) token_idx -= 1 for completer in ast_node.completers: yield from completer(prev_tokens, ast_node) def complete(ast_node: AstNode, tokens: T.List[Token], idx: int) -> T.Iterator[Completion]: token_idx = 0 # find the current token for i, token in enumerate(tokens): if token.start < idx <= token.end: token_idx = i # if the current token is an identifier or whitespace, move to the token before it while tokens[token_idx].type in [TokenType.IDENT, TokenType.WHITESPACE]: idx = tokens[token_idx].start token_idx -= 1 yield from _complete(ast_node, tokens, idx, token_idx) @completer([language.GtkDirective]) def using_gtk(ast_node, match_variables): yield Completion("using Gtk 4.0;", CompletionItemKind.Keyword) @completer( applies_in=[language.UI, language.ObjectContent, language.Template], matches=new_statement_patterns ) def namespace(ast_node, match_variables): yield Completion("Gtk", CompletionItemKind.Module, text="Gtk.") for ns in ast_node.root.children[language.Import]: if ns.gir_namespace is not None: yield Completion(ns.gir_namespace.name, CompletionItemKind.Module, text=ns.gir_namespace.name + ".") @completer( applies_in=[language.UI, language.ObjectContent, language.Template], matches=[ [(TokenType.IDENT, None), (TokenType.OP, "."), (TokenType.IDENT, None)], [(TokenType.IDENT, None), (TokenType.OP, ".")], ] ) def object_completer(ast_node, match_variables): ns = ast_node.root.gir.namespaces.get(match_variables[0]) if ns is not None: for c in ns.classes.values(): yield Completion(c.name, CompletionItemKind.Class, docs=c.doc) @completer( applies_in=[language.ObjectContent], matches=new_statement_patterns, ) def property_completer(ast_node, match_variables): if ast_node.gir_class: for prop in ast_node.gir_class.properties: yield Completion(prop, CompletionItemKind.Property, snippet=f"{prop}: $0;") @completer( applies_in=[language.Property, language.BaseTypedAttribute], matches=[ [(TokenType.IDENT, None), (TokenType.OP, ":")] ], ) def prop_value_completer(ast_node, match_variables): if isinstance(ast_node.value_type, gir.Enumeration): for name, member in ast_node.value_type.members.items(): yield Completion(name, CompletionItemKind.EnumMember, docs=member.doc) elif isinstance(ast_node.value_type, gir.BoolType): yield Completion("true", CompletionItemKind.Constant) yield Completion("false", CompletionItemKind.Constant) @completer( applies_in=[language.ObjectContent], matches=new_statement_patterns, ) def signal_completer(ast_node, match_variables): if ast_node.gir_class: for signal in ast_node.gir_class.signals: if not isinstance(ast_node.parent, language.Object): name = "on" else: name = "on_" + (ast_node.parent.tokens["id"] or ast_node.parent.tokens["class_name"].lower()) yield Completion(signal, CompletionItemKind.Property, snippet=f"{signal} => ${{1:{name}_{signal.replace('-', '_')}}}()$0;") @completer( applies_in=[language.UI], matches=new_statement_patterns ) def template_completer(ast_node, match_variables): yield Completion( "template", CompletionItemKind.Snippet, snippet="template ${1:ClassName} : ${2:ParentClass} {\n $0\n}" ) blueprint-compiler-main/blueprintcompiler/completions_utils.py000066400000000000000000000052661420774714100255210ustar00rootroot00000000000000# completions_utils.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later import typing as T from .tokenizer import Token, TokenType from .lsp_utils import Completion new_statement_patterns = [ [(TokenType.PUNCTUATION, "{")], [(TokenType.PUNCTUATION, "}")], [(TokenType.PUNCTUATION, ";")], ] def applies_to(*ast_types): """ Decorator describing which AST nodes the completer should apply in. """ def decorator(func): for c in ast_types: c.completers.append(func) return func return decorator def completer(applies_in: T.List, matches: T.List=[], applies_in_subclass=None): def decorator(func): def inner(prev_tokens: T.List[Token], ast_node): # For completers that apply in ObjectContent nodes, we can further # check that the object is the right class if applies_in_subclass is not None: type = ast_node.root.gir.get_type(applies_in_subclass[1], applies_in_subclass[0]) if ast_node.gir_class and not ast_node.gir_class.assignable_to(type): return any_match = len(matches) == 0 match_variables: T.List[str] = [] for pattern in matches: match_variables = [] if len(pattern) <= len(prev_tokens): for i in range(0, len(pattern)): type, value = pattern[i] token = prev_tokens[i - len(pattern)] if token.type != type or (value is not None and str(token) != value): break if value is None: match_variables.append(str(token)) else: any_match = True break if not any_match: return yield from func(ast_node, match_variables) for c in applies_in: c.completers.append(inner) return inner return decorator blueprint-compiler-main/blueprintcompiler/decompiler.py000066400000000000000000000220331420774714100240570ustar00rootroot00000000000000# decompiler.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later import re from enum import Enum import typing as T from dataclasses import dataclass from .xml_reader import Element, parse from .gir import * from .utils import Colors __all__ = ["decompile"] _DECOMPILERS: T.Dict = {} _CLOSING = { "{": "}", "[": "]", } _NAMESPACES = [ ("GLib", "2.0"), ("GObject", "2.0"), ("Gio", "2.0"), ("Adw", "1.0"), ] class LineType(Enum): NONE = 1 STMT = 2 BLOCK_START = 3 BLOCK_END = 4 class DecompileCtx: def __init__(self): self._result = "" self.gir = GirContext() self._indent = 0 self._blocks_need_end = [] self._last_line_type = LineType.NONE self.gir.add_namespace(get_namespace("Gtk", "4.0")) @property def result(self): imports = "\n".join([ f"using {ns} {namespace.version};" for ns, namespace in self.gir.namespaces.items() ]) return imports + "\n" + self._result def type_by_cname(self, cname): if type := self.gir.get_type_by_cname(cname): return type for ns, version in _NAMESPACES: try: namespace = get_namespace(ns, version) if type := namespace.get_type_by_cname(cname): self.gir.add_namespace(namespace) return type except: pass def start_block(self): self._blocks_need_end.append(None) def end_block(self): if close := self._blocks_need_end.pop(): self.print(close) def end_block_with(self, text): self._blocks_need_end[-1] = text def print(self, line, newline=True): if line == "}" or line == "]": self._indent -= 1 # Add blank lines between different types of lines, for neatness if newline: if line == "}" or line == "]": line_type = LineType.BLOCK_END elif line.endswith("{") or line.endswith("]"): line_type = LineType.BLOCK_START elif line.endswith(";"): line_type = LineType.STMT else: line_type = LineType.NONE if line_type != self._last_line_type and self._last_line_type != LineType.BLOCK_START and line_type != LineType.BLOCK_END: self._result += "\n" self._last_line_type = line_type self._result += (" " * self._indent) + line if newline: self._result += "\n" if line.endswith("{") or line.endswith("["): if len(self._blocks_need_end): self._blocks_need_end[-1] = _CLOSING[line[-1]] self._indent += 1 def print_attribute(self, name, value, type): if type is None: self.print(f"{name}: \"{escape_quote(value)}\";") elif type.assignable_to(FloatType()): self.print(f"{name}: {value};") elif type.assignable_to(BoolType()): val = truthy(value) self.print(f"{name}: {'true' if val else 'false'};") elif ( type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Pixbuf")) or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Texture")) or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Paintable")) or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutAction")) or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutTrigger")) ): self.print(f"{name}: \"{escape_quote(value)}\";") elif type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("GObject.Object")): self.print(f"{name}: {value};") elif isinstance(type, Enumeration): for member in type.members.values(): if member.nick == value or member.c_ident == value: self.print(f"{name}: {member.name};") break else: self.print(f"{name}: {value.replace('-', '_')};") elif isinstance(type, Bitfield): flags = re.sub(r"\s*\|\s*", " | ", value).replace("-", "_") self.print(f"{name}: {flags};") else: self.print(f"{name}: \"{escape_quote(value)}\";") def _decompile_element(ctx: DecompileCtx, gir, xml): try: decompiler = _DECOMPILERS.get(xml.tag) if decompiler is None: raise UnsupportedError(f"unsupported XML tag: <{xml.tag}>") args = {canon(name): value for name, value in xml.attrs.items()} if decompiler._cdata: if len(xml.children): args["cdata"] = None else: args["cdata"] = xml.cdata ctx.start_block() gir = decompiler(ctx, gir, **args) for child_type in xml.children.values(): for child in child_type: _decompile_element(ctx, gir, child) ctx.end_block() except UnsupportedError as e: raise e except TypeError as e: raise UnsupportedError(tag=xml.tag) def decompile(data): ctx = DecompileCtx() xml = parse(data) _decompile_element(ctx, None, xml) return ctx.result def canon(string: str) -> str: if string == "class": return "klass" else: return string.replace("-", "_").lower() def truthy(string: str) -> bool: return string.lower() in ["yes", "true", "t", "y", "1"] def full_name(gir): return gir.name if gir.full_name.startswith("Gtk.") else gir.full_name def lookup_by_cname(gir, cname: str): if isinstance(gir, GirContext): return gir.get_type_by_cname(cname) else: return gir.get_containing(Repository).get_type_by_cname(cname) def decompiler(tag, cdata=False): def decorator(func): func._cdata = cdata _DECOMPILERS[tag] = func return func return decorator def escape_quote(string: str) -> str: return (string .replace("\\", "\\\\") .replace("\'", "\\'") .replace("\"", "\\\"") .replace("\n", "\\n")) @decompiler("interface") def decompile_interface(ctx, gir): return gir @decompiler("requires") def decompile_requires(ctx, gir, lib=None, version=None): return gir @decompiler("property", cdata=True) def decompile_property(ctx, gir, name, cdata, bind_source=None, bind_property=None, bind_flags=None, translatable="false", comments=None, context=None): name = name.replace("_", "-") if comments is not None: ctx.print(f"/* Translators: {comments} */") if cdata is None: ctx.print(f"{name}: ", False) ctx.end_block_with(";") elif bind_source: flags = "" bind_flags = bind_flags or [] if "sync-create" not in bind_flags: flags += " no-sync-create" if "invert-boolean" in bind_flags: flags += " inverted" if "bidirectional" in bind_flags: flags += " bidirectional" ctx.print(f"{name}: bind {bind_source}.{bind_property}{flags};") elif truthy(translatable): if context is not None: ctx.print(f"{name}: C_(\"{escape_quote(context)}\", \"{escape_quote(cdata)}\");") else: ctx.print(f"{name}: _(\"{escape_quote(cdata)}\");") elif gir is None or gir.properties.get(name) is None: ctx.print(f"{name}: \"{escape_quote(cdata)}\";") else: ctx.print_attribute(name, cdata, gir.properties.get(name).type) return gir @decompiler("attribute", cdata=True) def decompile_attribute(ctx, gir, name, cdata, translatable="false", comments=None, context=None): decompile_property(ctx, gir, name, cdata, translatable=translatable, comments=comments, context=context) @decompiler("attributes") def decompile_attributes(ctx, gir): ctx.print("attributes {") @dataclass class UnsupportedError(Exception): message: str = "unsupported feature" tag: T.Optional[str] = None def print(self, filename: str): print(f"\n{Colors.RED}{Colors.BOLD}error: {self.message}{Colors.CLEAR}") print(f"in {Colors.UNDERLINE}{filename}{Colors.NO_UNDERLINE}") if self.tag: print(f"in tag {Colors.BLUE}{self.tag}{Colors.CLEAR}") print(f"""{Colors.FAINT}The gtk-blueprint-tool compiler might support this feature, but the porting tool does not. You probably need to port this file manually.{Colors.CLEAR}\n""") blueprint-compiler-main/blueprintcompiler/errors.py000066400000000000000000000110711420774714100232500ustar00rootroot00000000000000# errors.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later from dataclasses import dataclass import typing as T import sys, traceback from . import utils from .utils import Colors class PrintableError(Exception): """ Parent class for errors that can be pretty-printed for the user, e.g. compilation warnings and errors. """ def pretty_print(self, filename, code): raise NotImplementedError() class CompileError(PrintableError): """ A PrintableError with a start/end position and optional hints """ category = "error" color = Colors.RED def __init__(self, message, start=None, end=None, did_you_mean=None, hints=None, actions=None): super().__init__(message) self.message = message self.start = start self.end = end self.hints = hints or [] self.actions = actions or [] if did_you_mean is not None: self._did_you_mean(*did_you_mean) def hint(self, hint: str): self.hints.append(hint) return self def _did_you_mean(self, word: str, options: T.List[str]): if word.replace("_", "-") in options: self.hint(f"use '-', not '_': `{word.replace('_', '-')}`") return recommend = utils.did_you_mean(word, options) if recommend is not None: if word.casefold() == recommend.casefold(): self.hint(f"Did you mean `{recommend}` (note the capitalization)?") else: self.hint(f"Did you mean `{recommend}`?") self.actions.append(CodeAction(f"Change to `{recommend}`", recommend)) else: self.hint("Did you check your spelling?") self.hint("Are your dependencies up to date?") def pretty_print(self, filename, code, stream=sys.stdout): line_num, col_num = utils.idx_to_pos(self.start + 1, code) line = code.splitlines(True)[line_num] # Display 1-based line numbers line_num += 1 stream.write(f"""{self.color}{Colors.BOLD}{self.category}: {self.message}{Colors.CLEAR} at {filename} line {line_num} column {col_num}: {Colors.FAINT}{line_num :>4} |{Colors.CLEAR}{line.rstrip()}\n {Colors.FAINT}|{" "*(col_num-1)}^{Colors.CLEAR}\n""") for hint in self.hints: stream.write(f"{Colors.FAINT}hint: {hint}{Colors.CLEAR}\n") stream.write("\n") class CompileWarning(CompileError): category = "warning" color = Colors.YELLOW class UnexpectedTokenError(CompileError): def __init__(self, start, end): super().__init__("Unexpected tokens", start, end) @dataclass class CodeAction: title: str replace_with: str class MultipleErrors(PrintableError): """ If multiple errors occur during compilation, they can be collected into a list and re-thrown using the MultipleErrors exception. It will pretty-print all of the errors and a count of how many errors there are. """ def __init__(self, errors: T.List[CompileError]): super().__init__() self.errors = errors def pretty_print(self, filename, code) -> None: for error in self.errors: error.pretty_print(filename, code) if len(self.errors) != 1: print(f"{len(self.errors)} errors") class CompilerBugError(Exception): """ Emitted on assertion errors """ def assert_true(truth: bool, message:str=None): if not truth: raise CompilerBugError(message) def report_bug(): # pragma: no cover """ Report an error and ask people to report it. """ print(traceback.format_exc()) print(f"Arguments: {sys.argv}\n") print(f"""{Colors.BOLD}{Colors.RED}***** COMPILER BUG ***** The blueprint-compiler program has crashed. Please report the above stacktrace, along with the input file(s) if possible, on GitLab: {Colors.BOLD}{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue {Colors.CLEAR}""") blueprint-compiler-main/blueprintcompiler/gir.py000066400000000000000000000357531420774714100225320ustar00rootroot00000000000000# gir.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later import typing as T import os, sys from .errors import CompileError, CompilerBugError from .utils import lazy_prop from . import xml_reader extra_search_paths: T.List[str] = [] _namespace_cache = {} _search_paths = [] xdg_data_home = os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share")) _search_paths.append(os.path.join(xdg_data_home, "gir-1.0")) xdg_data_dirs = os.environ.get("XDG_DATA_DIRS", "/usr/share:/usr/local/share").split(":") _search_paths += [os.path.join(dir, "gir-1.0") for dir in xdg_data_dirs] def get_namespace(namespace, version): filename = f"{namespace}-{version}.gir" if filename not in _namespace_cache: for search_path in _search_paths: path = os.path.join(search_path, filename) if os.path.exists(path) and os.path.isfile(path): xml = xml_reader.parse(path, xml_reader.PARSE_GIR) repository = Repository(xml) _namespace_cache[filename] = repository.namespaces.get(namespace) break if filename not in _namespace_cache: raise CompileError(f"Namespace {namespace}-{version} could not be found") return _namespace_cache[filename] class GirType: @property def doc(self): return None def assignable_to(self, other) -> bool: raise NotImplementedError() @property def full_name(self) -> str: raise NotImplementedError() class BasicType(GirType): name: str = "unknown type" @property def full_name(self) -> str: return self.name class BoolType(BasicType): name = "bool" def assignable_to(self, other) -> bool: return isinstance(other, BoolType) class IntType(BasicType): name = "int" def assignable_to(self, other) -> bool: return isinstance(other, IntType) or isinstance(other, UIntType) or isinstance(other, FloatType) class UIntType(BasicType): name = "uint" def assignable_to(self, other) -> bool: return isinstance(other, IntType) or isinstance(other, UIntType) or isinstance(other, FloatType) class FloatType(BasicType): name = "float" def assignable_to(self, other) -> bool: return isinstance(other, FloatType) class StringType(BasicType): name = "string" def assignable_to(self, other) -> bool: return isinstance(other, StringType) _BASIC_TYPES = { "gboolean": BoolType, "int": IntType, "gint": IntType, "gint64": IntType, "guint": UIntType, "guint64": UIntType, "gfloat": FloatType, "gdouble": FloatType, "float": FloatType, "double": FloatType, "utf8": StringType, } class GirNode: def __init__(self, container, xml): self.container = container self.xml = xml def get_containing(self, container_type): if self.container is None: return None elif isinstance(self.container, container_type): return self.container else: return self.container.get_containing(container_type) @lazy_prop def glib_type_name(self): return self.xml["glib:type-name"] @lazy_prop def full_name(self): if self.container is None: return self.name else: return f"{self.container.name}.{self.name}" @lazy_prop def name(self) -> str: return self.xml["name"] @lazy_prop def cname(self) -> str: return self.xml["c:type"] @lazy_prop def available_in(self) -> str: return self.xml.get("version") @lazy_prop def doc(self) -> T.Optional[str]: sections = [] if self.signature: sections.append("```\n" + self.signature + "\n```") el = self.xml.get_elements("doc") if len(el) == 1: sections.append(el[0].cdata.strip()) return "\n\n---\n\n".join(sections) @property def signature(self) -> T.Optional[str]: return None @property def type_name(self): return self.xml.get_elements('type')[0]['name'] @property def type(self): return self.get_containing(Namespace).lookup_type(self.type_name) class Property(GirNode): def __init__(self, klass, xml: xml_reader.Element): super().__init__(klass, xml) @property def signature(self): return f"{self.type_name} {self.container.name}.{self.name}" class Parameter(GirNode): def __init__(self, container: GirNode, xml: xml_reader.Element): super().__init__(container, xml) class Signal(GirNode): def __init__(self, klass, xml: xml_reader.Element): super().__init__(klass, xml) if parameters := xml.get_elements('parameters'): self.params = [Parameter(self, child) for child in parameters[0].get_elements('parameter')] else: self.params = [] @property def signature(self): args = ", ".join([f"{p.type_name} {p.name}" for p in self.params]) return f"signal {self.container.name}.{self.name} ({args})" class Interface(GirNode, GirType): def __init__(self, ns, xml: xml_reader.Element): super().__init__(ns, xml) self.properties = {child["name"]: Property(self, child) for child in xml.get_elements("property")} self.signals = {child["name"]: Signal(self, child) for child in xml.get_elements("glib:signal")} self.prerequisites = [child["name"] for child in xml.get_elements("prerequisite")] def assignable_to(self, other) -> bool: if self == other: return True for pre in self.prerequisites: if self.get_containing(Namespace).lookup_type(pre).assignable_to(other): return True return False class Class(GirNode, GirType): def __init__(self, ns, xml: xml_reader.Element): super().__init__(ns, xml) self._parent = xml["parent"] self.implements = [impl["name"] for impl in xml.get_elements("implements")] self.own_properties = {child["name"]: Property(self, child) for child in xml.get_elements("property")} self.own_signals = {child["name"]: Signal(self, child) for child in xml.get_elements("glib:signal")} @property def signature(self): result = f"class {self.container.name}.{self.name}" if self.parent is not None: result += f" : {self.parent.container.name}.{self.parent.name}" if len(self.implements): result += " implements " + ", ".join(self.implements) return result @lazy_prop def properties(self): return { p.name: p for p in self._enum_properties() } @lazy_prop def signals(self): return { s.name: s for s in self._enum_signals() } @lazy_prop def parent(self): if self._parent is None: return None return self.get_containing(Namespace).lookup_type(self._parent) def assignable_to(self, other) -> bool: if self == other: return True elif self.parent and self.parent.assignable_to(other): return True else: for iface in self.implements: if self.get_containing(Namespace).lookup_type(iface).assignable_to(other): return True return False def _enum_properties(self): yield from self.own_properties.values() if self.parent is not None: yield from self.parent.properties.values() for impl in self.implements: yield from self.get_containing(Namespace).lookup_type(impl).properties.values() def _enum_signals(self): yield from self.own_signals.values() if self.parent is not None: yield from self.parent.signals.values() for impl in self.implements: yield from self.get_containing(Namespace).lookup_type(impl).signals.values() class EnumMember(GirNode): def __init__(self, ns, xml: xml_reader.Element): super().__init__(ns, xml) self._value = xml["value"] @property def value(self): return self._value @property def nick(self): return self.xml["glib:nick"] @property def c_ident(self): return self.xml["c:identifier"] @property def signature(self): return f"enum member {self.full_name} = {self.value}" class Enumeration(GirNode, GirType): def __init__(self, ns, xml: xml_reader.Element): super().__init__(ns, xml) self.members = { child["name"]: EnumMember(self, child) for child in xml.get_elements("member") } @property def signature(self): return f"enum {self.full_name}" def assignable_to(self, type): return type == self class BitfieldMember(GirNode): def __init__(self, ns, xml: xml_reader.Element): super().__init__(ns, xml) self._value = xml["value"] @property def value(self): return self._value @property def signature(self): return f"bitfield member {self.full_name} = {bin(self.value)}" class Bitfield(GirNode, GirType): def __init__(self, ns, xml: xml_reader.Element): super().__init__(ns, xml) self.members = { child["name"]: EnumMember(self, child) for child in xml.get_elements("member") } @property def signature(self): return f"bitfield {self.full_name}" def assignable_to(self, type): return type == self class Namespace(GirNode): def __init__(self, repo, xml: xml_reader.Element): super().__init__(repo, xml) self.classes = { child["name"]: Class(self, child) for child in xml.get_elements("class") } self.interfaces = { child["name"]: Interface(self, child) for child in xml.get_elements("interface") } self.enumerations = { child["name"]: Enumeration(self, child) for child in xml.get_elements("enumeration") } self.bitfields = { child["name"]: Bitfield(self, child) for child in xml.get_elements("bitfield") } self.version = xml["version"] @property def signature(self): return f"namespace {self.name} {self.version}" def get_type(self, name): """ Gets a type (class, interface, enum, etc.) from this namespace. """ return ( self.classes.get(name) or self.interfaces.get(name) or self.enumerations.get(name) or self.bitfields.get(name) ) def get_type_by_cname(self, cname: str): """ Gets a type from this namespace by its C name. """ for item in [*self.classes.values(), *self.interfaces.values(), *self.enumerations.values()]: if item.cname == cname: return item def lookup_type(self, type_name: str): """ Looks up a type in the scope of this namespace (including in the namespace's dependencies). """ if type_name in _BASIC_TYPES: return _BASIC_TYPES[type_name]() elif "." in type_name: ns, name = type_name.split(".", 1) return self.get_containing(Repository).get_type(name, ns) else: return self.get_type(type_name) class Repository(GirNode): def __init__(self, xml: xml_reader.Element): super().__init__(None, xml) self.namespaces = { child["name"]: Namespace(self, child) for child in xml.get_elements("namespace") } try: self.includes = { include["name"]: get_namespace(include["name"], include["version"]) for include in xml.get_elements("include") } except: raise CompilerBugError(f"Failed to load dependencies.") def get_type(self, name: str, ns: str) -> T.Optional[GirNode]: if namespace := self.namespaces.get(ns): return namespace.get_type(name) else: return self.lookup_namespace(ns).get_type(name) def get_type_by_cname(self, name: str) -> T.Optional[GirNode]: for ns in self.namespaces.values(): if type := ns.get_type_by_cname(name): return type return None def lookup_namespace(self, ns: str): """ Finds a namespace among this namespace's dependencies. """ if namespace := self.namespaces.get(ns): return namespace else: for include in self.includes.values(): if namespace := include.get_containing(Repository).lookup_namespace(ns): return namespace class GirContext: def __init__(self): self.namespaces = {} def add_namespace(self, namespace: Namespace): other = self.namespaces.get(namespace.name) if other is not None and other.version != namespace.version: raise CompileError(f"Namespace {namespace.name}-{namespace.version} can't be imported because version {other.version} was imported earlier") self.namespaces[namespace.name] = namespace def get_type_by_cname(self, name: str) -> T.Optional[GirNode]: for ns in self.namespaces.values(): if type := ns.get_type_by_cname(name): return type return None def get_type(self, name: str, ns: str) -> T.Optional[GirNode]: ns = ns or "Gtk" if ns not in self.namespaces: return None return self.namespaces[ns].get_type(name) def get_class(self, name: str, ns: str) -> T.Optional[Class]: type = self.get_type(name, ns) if isinstance(type, Class): return type else: return None def validate_ns(self, ns: str): """ Raises an exception if there is a problem looking up the given namespace. """ ns = ns or "Gtk" if ns not in self.namespaces: raise CompileError( f"Namespace {ns} was not imported", did_you_mean=(ns, self.namespaces.keys()), ) def validate_class(self, name: str, ns: str): """ Raises an exception if there is a problem looking up the given class (it doesn't exist, it isn't a class, etc.) """ ns = ns or "Gtk" self.validate_ns(ns) type = self.get_type(name, ns) if type is None: raise CompileError( f"Namespace {ns} does not contain a class called {name}", did_you_mean=(name, self.namespaces[ns].classes.keys()), ) elif not isinstance(type, Class): raise CompileError( f"{ns}.{name} is not a class", did_you_mean=(name, self.namespaces[ns].classes.keys()), ) blueprint-compiler-main/blueprintcompiler/interactive_port.py000066400000000000000000000226261420774714100253250ustar00rootroot00000000000000# interactive_port.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later import typing as T import difflib import os from . import decompiler, tokenizer, parser from .errors import MultipleErrors, PrintableError from .utils import Colors # A tool to interactively port projects to blueprints. class CouldNotPort: def __init__(self, message): self.message = message def change_suffix(f): return f.removesuffix(".ui") + ".blp" def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]: if os.path.exists(out_file): return CouldNotPort("already exists") try: decompiled = decompiler.decompile(in_file) try: # make sure the output compiles tokens = tokenizer.tokenize(decompiled) ast, errors, warnings = parser.parse(tokens) for warning in warnings: warning.pretty_print(out_file, decompiled) if errors: raise errors if len(ast.errors): raise MultipleErrors(ast.errors) ast.generate() except PrintableError as e: e.pretty_print(out_file, decompiled) print(f"{Colors.RED}{Colors.BOLD}error: the generated file does not compile{Colors.CLEAR}") print(f"in {Colors.UNDERLINE}{out_file}{Colors.NO_UNDERLINE}") print( f"""{Colors.FAINT}Either the original XML file had an error, or there is a bug in the porting tool. If you think it's a bug (which is likely), please file an issue on GitLab: {Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue{Colors.CLEAR}\n""") return CouldNotPort("does not compile") return decompiled except decompiler.UnsupportedError as e: e.print(in_file) return CouldNotPort("could not convert") def listdir_recursive(subdir): files = os.listdir(subdir) for file in files: if file in ["_build", "build"]: continue full = os.path.join(subdir, file) if full == "./subprojects": # skip the subprojects directory return if os.path.isfile(full): yield full elif os.path.isdir(full): yield from listdir_recursive(full) def yesno(prompt): while True: response = input(f"{Colors.BOLD}{prompt} [y/n] {Colors.CLEAR}") if response.lower() in ["yes", "y"]: return True elif response.lower() in ["no", "n"]: return False def enter(): input(f"{Colors.BOLD}Press Enter when you have done that: {Colors.CLEAR}") def step1(): print(f"{Colors.BOLD}STEP 1: Create subprojects/blueprint-compiler.wrap{Colors.CLEAR}") if os.path.exists("subprojects/blueprint-compiler.wrap"): print("subprojects/blueprint-compiler.wrap already exists, skipping\n") return if yesno("Create subprojects/blueprint-compiler.wrap?"): try: os.mkdir("subprojects") except: pass with open("subprojects/blueprint-compiler.wrap", "w") as wrap: wrap.write("""[wrap-git] directory = blueprint-compiler url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git revision = main depth = 1 [provide] program_names = blueprint-compiler""") print() def step2(): print(f"{Colors.BOLD}STEP 2: Set up .gitignore{Colors.CLEAR}") if os.path.exists(".gitignore"): with open(".gitignore", "r+") as gitignore: ignored = [line.strip() for line in gitignore.readlines()] if "/subprojects/blueprint-compiler" not in ignored: if yesno("Add '/subprojects/blueprint-compiler' to .gitignore?"): gitignore.write("\n/subprojects/blueprint-compiler\n") else: print("'/subprojects/blueprint-compiler' already in .gitignore, skipping") else: if yesno("Create .gitignore with '/subprojects/blueprint-compiler'?"): with open(".gitignore", "w") as gitignore: gitignore.write("/subprojects/blueprint-compiler\n") print() def step3(): print(f"{Colors.BOLD}STEP 3: Convert UI files{Colors.CLEAR}") files = [ (file, change_suffix(file), decompile_file(file, change_suffix(file))) for file in listdir_recursive(".") if file.endswith(".ui") ] success = 0 for in_file, out_file, result in files: if isinstance(result, CouldNotPort): if result.message == "already exists": print(Colors.FAINT, end="") print(f"{Colors.RED}will not port {Colors.UNDERLINE}{in_file}{Colors.NO_UNDERLINE} -> {Colors.UNDERLINE}{out_file}{Colors.NO_UNDERLINE} [{result.message}]{Colors.CLEAR}") else: print(f"will port {Colors.UNDERLINE}{in_file}{Colors.CLEAR} -> {Colors.UNDERLINE}{out_file}{Colors.CLEAR}") success += 1 print() if len(files) == 0: print(f"{Colors.RED}No UI files found.{Colors.CLEAR}") elif success == len(files): print(f"{Colors.GREEN}All files were converted.{Colors.CLEAR}") elif success > 0: print(f"{Colors.RED}{success} file(s) were converted, {len(files) - success} were not.{Colors.CLEAR}") else: print(f"{Colors.RED}None of the files could be converted.{Colors.CLEAR}") if success > 0 and yesno("Save these changes?"): for in_file, out_file, result in files: if not isinstance(result, CouldNotPort): with open(out_file, "x") as file: file.write(result) print() results = [ (in_file, out_file) for in_file, out_file, result in files if not isinstance(result, CouldNotPort) or result.message == "already exists" ] if len(results): return zip(*results) else: return ([], []) def step4(ported): print(f"{Colors.BOLD}STEP 4: Set up meson.build{Colors.CLEAR}") print(f"{Colors.BOLD}{Colors.YELLOW}NOTE: Depending on your build system setup, you may need to make some adjustments to this step.{Colors.CLEAR}") meson_files = [file for file in listdir_recursive(".") if os.path.basename(file) == "meson.build"] for meson_file in meson_files: with open(meson_file, "r") as f: if "gnome.compile_resources" in f.read(): parent = os.path.dirname(meson_file) file_list = "\n ".join([ f"'{os.path.relpath(file, parent)}'," for file in ported if file.startswith(parent) ]) if len(file_list): print(f"{Colors.BOLD}Paste the following into {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR}") print(f""" blueprints = custom_target('blueprints', input: files( {file_list} ), output: '.', command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'], ) """) enter() print(f"""{Colors.BOLD}Paste the following into the 'gnome.compile_resources()' arguments in {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR} dependencies: blueprints, """) enter() print() def step5(in_files): print(f"{Colors.BOLD}STEP 5: Update POTFILES.in{Colors.CLEAR}") if not os.path.exists("po/POTFILES.in"): print(f"{Colors.UNDERLINE}po/POTFILES.in{Colors.NO_UNDERLINE} does not exist, skipping\n") return with open("po/POTFILES.in", "r") as potfiles: old_lines = potfiles.readlines() lines = old_lines.copy() for in_file in in_files: for i, line in enumerate(lines): if line.strip() == in_file.removeprefix("./"): lines[i] = change_suffix(line.strip()) + "\n" new_data = "".join(lines) print(f"{Colors.BOLD}Will make the following changes to {Colors.UNDERLINE}po/POTFILES.in{Colors.CLEAR}") print( "".join([ (Colors.GREEN if line.startswith('+') else Colors.RED + Colors.FAINT if line.startswith('-') else '') + line + Colors.CLEAR for line in difflib.unified_diff(old_lines, lines) ]) ) if yesno("Is this ok?"): with open("po/POTFILES.in", "w") as potfiles: potfiles.writelines(lines) print() def step6(in_files): print(f"{Colors.BOLD}STEP 6: Clean up{Colors.CLEAR}") if yesno("Delete old XML files?"): for file in in_files: try: os.remove(file) except: pass def run(opts): step1() step2() in_files, out_files = step3() step4(out_files) step5(in_files) step6(in_files) print(f"{Colors.BOLD}STEP 6: Done! Make sure your app still builds and runs correctly.{Colors.CLEAR}") blueprint-compiler-main/blueprintcompiler/language/000077500000000000000000000000001420774714100231455ustar00rootroot00000000000000blueprint-compiler-main/blueprintcompiler/language/__init__.py000066400000000000000000000021721420774714100252600ustar00rootroot00000000000000""" Contains all the syntax beyond basic objects, properties, signal, and templates. """ from .attributes import BaseAttribute, BaseTypedAttribute from .gobject_object import Object, ObjectContent from .gobject_property import Property from .gobject_signal import Signal from .gtk_a11y import A11y from .gtk_combo_box_text import Items from .gtk_file_filter import mime_types, patterns, suffixes from .gtk_layout import Layout from .gtk_menu import menu from .gtk_size_group import Widgets from .gtk_string_list import Strings from .gtk_styles import Styles from .gtkbuilder_child import Child from .gtkbuilder_template import Template from .imports import GtkDirective, Import from .ui import UI from .values import IdentValue, TranslatedStringValue, FlagsValue, LiteralValue from .common import * OBJECT_HOOKS.children = [ menu, Object, ] OBJECT_CONTENT_HOOKS.children = [ Signal, Property, A11y, Styles, Layout, mime_types, patterns, suffixes, Widgets, Items, Strings, Child, ] VALUE_HOOKS.children = [ TranslatedStringValue, FlagsValue, IdentValue, LiteralValue, ] blueprint-compiler-main/blueprintcompiler/language/attributes.py000066400000000000000000000027201420774714100257060ustar00rootroot00000000000000# attributes.py # # Copyright 2022 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later from .values import Value, TranslatedStringValue from .common import * class BaseAttribute(AstNode): """ A helper class for attribute syntax of the form `name: literal_value;`""" tag_name: str = "" attr_name: str = "name" def emit_xml(self, xml: XmlEmitter): value = self.children[Value][0] attrs = { self.attr_name: self.tokens["name"] } if isinstance(value, TranslatedStringValue): attrs = { **attrs, **value.attrs } xml.start_tag(self.tag_name, **attrs) value.emit_xml(xml) xml.end_tag() class BaseTypedAttribute(BaseAttribute): """ A BaseAttribute whose parent has a value_type property that can assist in validation. """ blueprint-compiler-main/blueprintcompiler/language/common.py000066400000000000000000000024661420774714100250170ustar00rootroot00000000000000# common.py # # Copyright 2022 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later from .. import gir from ..ast_utils import AstNode, validate, docs from ..errors import CompileError, MultipleErrors from ..completions_utils import * from .. import decompiler as decompile from ..decompiler import DecompileCtx, decompiler from ..gir import StringType, BoolType, IntType, FloatType, GirType from ..lsp_utils import Completion, CompletionItemKind, SemanticToken, SemanticTokenType from ..parse_tree import * from ..parser_utils import * from ..xml_emitter import XmlEmitter OBJECT_HOOKS = AnyOf() OBJECT_CONTENT_HOOKS = AnyOf() VALUE_HOOKS = AnyOf() blueprint-compiler-main/blueprintcompiler/language/gobject_object.py000066400000000000000000000104621420774714100264650ustar00rootroot00000000000000# gobject_object.py # # Copyright 2022 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later import typing as T from functools import cached_property from .common import * from .response_id import ResponseId class ObjectContent(AstNode): grammar = ["{", Until(OBJECT_CONTENT_HOOKS, "}")] @property def gir_class(self): return self.parent.gir_class # @validate() # def only_one_style_class(self): # if len(self.children[Style]) > 1: # raise CompileError( # f"Only one style directive allowed per object, but this object contains {len(self.children[Style])}", # start=self.children[Style][1].group.start, # ) def emit_xml(self, xml: XmlEmitter): for x in self.children: x.emit_xml(xml) class Object(AstNode): grammar: T.Any = [ class_name, Optional(UseIdent("id")), ObjectContent, ] @validate("namespace") def gir_ns_exists(self): if not self.tokens["ignore_gir"]: self.root.gir.validate_ns(self.tokens["namespace"]) @validate("class_name") def gir_class_exists(self): if self.tokens["class_name"] and not self.tokens["ignore_gir"] and self.gir_ns is not None: self.root.gir.validate_class(self.tokens["class_name"], self.tokens["namespace"]) @property def gir_ns(self): if not self.tokens["ignore_gir"]: return self.root.gir.namespaces.get(self.tokens["namespace"] or "Gtk") @property def gir_class(self): if self.tokens["class_name"] and not self.tokens["ignore_gir"]: return self.root.gir.get_class(self.tokens["class_name"], self.tokens["namespace"]) @docs("namespace") def namespace_docs(self): if ns := self.root.gir.namespaces.get(self.tokens["namespace"]): return ns.doc @docs("class_name") def class_docs(self): if self.gir_class: return self.gir_class.doc @cached_property def action_widgets(self) -> T.List[ResponseId]: """Get list of widget's action widgets. Empty if object doesn't have action widgets. """ from .gtkbuilder_child import Child return [ child.response_id for child in self.children[ObjectContent][0].children[Child] if child.response_id ] def emit_xml(self, xml: XmlEmitter): from .gtkbuilder_child import Child xml.start_tag("object", **{ "class": self.gir_class.glib_type_name if self.gir_class else self.tokens["class_name"], "id": self.tokens["id"], }) for child in self.children: child.emit_xml(xml) # List action widgets action_widgets = self.action_widgets if action_widgets: xml.start_tag("action-widgets") for action_widget in action_widgets: action_widget.emit_action_widget(xml) xml.end_tag() xml.end_tag() def validate_parent_type(node, ns: str, name: str, err_msg: str): parent = node.root.gir.get_type(name, ns) container_type = node.parent_by_type(Object).gir_class if container_type and not container_type.assignable_to(parent): raise CompileError(f"{container_type.full_name} is not a {parent.full_name}, so it doesn't have {err_msg}") @decompiler("object") def decompile_object(ctx, gir, klass, id=None): gir_class = ctx.type_by_cname(klass) klass_name = decompile.full_name(gir_class) if gir_class is not None else "." + klass if id is None: ctx.print(f"{klass_name} {{") else: ctx.print(f"{klass_name} {id} {{") return gir_class blueprint-compiler-main/blueprintcompiler/language/gobject_property.py000066400000000000000000000110341420774714100270770ustar00rootroot00000000000000# gobject_property.py # # Copyright 2022 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later from .gobject_object import Object from .gtkbuilder_template import Template from .values import Value, TranslatedStringValue from .common import * class Property(AstNode): grammar = AnyOf( Statement( UseIdent("name"), ":", "bind", UseIdent("bind_source").expected("the ID of a source object to bind from"), ".", UseIdent("bind_property").expected("a property name to bind from"), ZeroOrMore(AnyOf( ["no-sync-create", UseLiteral("no_sync_create", True)], ["inverted", UseLiteral("inverted", True)], ["bidirectional", UseLiteral("bidirectional", True)], Match("sync-create").warn("sync-create is deprecated in favor of no-sync-create"), )), ), Statement( UseIdent("name"), ":", AnyOf( OBJECT_HOOKS, VALUE_HOOKS, ).expected("a value"), ), ) @property def gir_class(self): return self.parent.parent.gir_class @property def gir_property(self): if self.gir_class is not None: return self.gir_class.properties.get(self.tokens["name"]) @property def value_type(self): if self.gir_property is not None: return self.gir_property.type @validate("name") def property_exists(self): if self.gir_class is None: # Objects that we have no gir data on should not be validated # This happens for classes defined by the app itself return if isinstance(self.parent.parent, Template): # If the property is part of a template, it might be defined by # the application and thus not in gir return if self.gir_property is None: raise CompileError( f"Class {self.gir_class.full_name} does not contain a property called {self.tokens['name']}", did_you_mean=(self.tokens["name"], self.gir_class.properties.keys()) ) @validate() def obj_property_type(self): if len(self.children[Object]) == 0: return object = self.children[Object][0] type = self.value_type if object and type and object.gir_class and not object.gir_class.assignable_to(type): raise CompileError( f"Cannot assign {object.gir_class.full_name} to {type.full_name}" ) @docs("name") def property_docs(self): if self.gir_property is not None: return self.gir_property.doc def emit_xml(self, xml: XmlEmitter): values = self.children[Value] value = values[0] if len(values) == 1 else None bind_flags = [] if self.tokens["bind_source"] and not self.tokens["no_sync_create"]: bind_flags.append("sync-create") if self.tokens["inverted"]: bind_flags.append("invert-boolean") if self.tokens["bidirectional"]: bind_flags.append("bidirectional") bind_flags_str = "|".join(bind_flags) or None props = { "name": self.tokens["name"], "bind-source": self.tokens["bind_source"], "bind-property": self.tokens["bind_property"], "bind-flags": bind_flags_str, } if isinstance(value, TranslatedStringValue): props = { **props, **value.attrs } if len(self.children[Object]) == 1: xml.start_tag("property", **props) self.children[Object][0].emit_xml(xml) xml.end_tag() elif value is None: xml.put_self_closing("property", **props) else: xml.start_tag("property", **props) value.emit_xml(xml) xml.end_tag() blueprint-compiler-main/blueprintcompiler/language/gobject_signal.py000066400000000000000000000070031420774714100264710ustar00rootroot00000000000000# gobject_signal.py # # Copyright 2022 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later from .gtkbuilder_template import Template from .common import * class Signal(AstNode): grammar = Statement( UseIdent("name"), Optional([ "::", UseIdent("detail_name").expected("a signal detail name"), ]), "=>", UseIdent("handler").expected("the name of a function to handle the signal"), Match("(").expected("argument list"), Optional(UseIdent("object")).expected("object identifier"), Match(")").expected(), ZeroOrMore(AnyOf( [Keyword("swapped"), UseLiteral("swapped", True)], [Keyword("after"), UseLiteral("after", True)], )), ) @property def gir_signal(self): if self.gir_class is not None: return self.gir_class.signals.get(self.tokens["name"]) @property def gir_class(self): return self.parent.parent.gir_class @validate("name") def signal_exists(self): if self.gir_class is None: # Objects that we have no gir data on should not be validated # This happens for classes defined by the app itself return if isinstance(self.parent.parent, Template): # If the signal is part of a template, it might be defined by # the application and thus not in gir return if self.gir_signal is None: raise CompileError( f"Class {self.gir_class.full_name} does not contain a signal called {self.tokens['name']}", did_you_mean=(self.tokens["name"], self.gir_class.signals.keys()) ) @validate("object") def object_exists(self): object_id = self.tokens["object"] if object_id is None: return if self.root.objects_by_id.get(object_id) is None: raise CompileError( f"Could not find object with ID '{object_id}'" ) @docs("name") def signal_docs(self): if self.gir_signal is not None: return self.gir_signal.doc def emit_xml(self, xml: XmlEmitter): name = self.tokens["name"] if self.tokens["detail_name"]: name += "::" + self.tokens["detail_name"] xml.put_self_closing( "signal", name=name, handler=self.tokens["handler"], swapped="true" if self.tokens["swapped"] else None, object=self.tokens["object"] ) @decompiler("signal") def decompile_signal(ctx, gir, name, handler, swapped="false", object=None): object_name = object or "" name = name.replace("_", "-") if decompile.truthy(swapped): ctx.print(f"{name} => {handler}({object_name}) swapped;") else: ctx.print(f"{name} => {handler}({object_name});") return gir blueprint-compiler-main/blueprintcompiler/language/gtk_a11y.py000066400000000000000000000136451420774714100251500ustar00rootroot00000000000000# gtk_a11y.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later from .gobject_object import ObjectContent, validate_parent_type from .attributes import BaseTypedAttribute from .values import Value from .common import * def get_property_types(gir): # from return { "autocomplete": gir.get_type("AccessibleAutocomplete", "Gtk"), "description": StringType(), "has_popup": BoolType(), "key_shortcuts": StringType(), "label": StringType(), "level": IntType(), "modal": BoolType(), "multi_line": BoolType(), "multi_selectable": BoolType(), "orientation": gir.get_type("Orientation", "Gtk"), "placeholder": StringType(), "read_only": BoolType(), "required": BoolType(), "role_description": StringType(), "sort": gir.get_type("AccessibleSort", "Gtk"), "value_max": FloatType(), "value_min": FloatType(), "value_now": FloatType(), "value_text": StringType(), } def get_relation_types(gir): # from widget = gir.get_type("Widget", "Gtk") return { "active_descendant": widget, "col_count": IntType(), "col_index": IntType(), "col_index_text": StringType(), "col_span": IntType(), "controls": widget, "described_by": widget, "details": widget, "error_message": widget, "flow_to": widget, "labelled_by": widget, "owns": widget, "pos_in_set": IntType(), "row_count": IntType(), "row_index": IntType(), "row_index_text": StringType(), "row_span": IntType(), "set_size": IntType(), } def get_state_types(gir): # from return { "busy": BoolType(), "checked": gir.get_type("AccessibleTristate", "Gtk"), "disabled": BoolType(), "expanded": BoolType(), "hidden": BoolType(), "invalid": gir.get_type("AccessibleInvalidState", "Gtk"), "pressed": gir.get_type("AccessibleTristate", "Gtk"), "selected": BoolType(), } def get_types(gir): return { **get_property_types(gir), **get_relation_types(gir), **get_state_types(gir), } def _get_docs(gir, name): return ( gir.get_type("AccessibleProperty", "Gtk").members.get(name) or gir.get_type("AccessibleRelation", "Gtk").members.get(name) or gir.get_type("AccessibleState", "Gtk").members.get(name) ).doc class A11yProperty(BaseTypedAttribute): grammar = Statement( UseIdent("name"), ":", VALUE_HOOKS.expected("a value"), ) @property def tag_name(self): name = self.tokens["name"] gir = self.root.gir if name in get_property_types(gir): return "property" elif name in get_relation_types(gir): return "relation" elif name in get_state_types(gir): return "state" else: raise CompilerBugError() @property def value_type(self) -> GirType: return get_types(self.root.gir).get(self.tokens["name"]) @validate("name") def is_valid_property(self): types = get_types(self.root.gir) if self.tokens["name"] not in types: raise CompileError( f"'{self.tokens['name']}' is not an accessibility property, relation, or state", did_you_mean=(self.tokens["name"], types.keys()), ) @docs("name") def prop_docs(self): if self.tokens["name"] in get_types(self.root.gir): return _get_docs(self.root.gir, self.tokens["name"]) class A11y(AstNode): grammar = [ Keyword("accessibility"), "{", Until(A11yProperty, "}"), ] @validate("accessibility") def container_is_widget(self): validate_parent_type(self, "Gtk", "Widget", "accessibility properties") def emit_xml(self, xml: XmlEmitter): xml.start_tag("accessibility") for child in self.children: child.emit_xml(xml) xml.end_tag() @completer( applies_in=[ObjectContent], matches=new_statement_patterns, ) def a11y_completer(ast_node, match_variables): yield Completion( "accessibility", CompletionItemKind.Snippet, snippet="accessibility {\n $0\n}" ) @completer( applies_in=[A11y], matches=new_statement_patterns, ) def a11y_name_completer(ast_node, match_variables): for name, type in get_types(ast_node.root.gir).items(): yield Completion(name, CompletionItemKind.Property, docs=_get_docs(ast_node.root.gir, type)) @decompiler("relation", cdata=True) def decompile_relation(ctx, gir, name, cdata): ctx.print_attribute(name, cdata, get_types(ctx.gir).get(name)) @decompiler("state", cdata=True) def decompile_state(ctx, gir, name, cdata, translatable="false"): if decompile.truthy(translatable): ctx.print(f"{name}: _(\"{_escape_quote(cdata)}\");") else: ctx.print_attribute(name, cdata, get_types(ctx.gir).get(name)) @decompiler("accessibility") def decompile_accessibility(ctx, gir): ctx.print("accessibility {") blueprint-compiler-main/blueprintcompiler/language/gtk_combo_box_text.py000066400000000000000000000035671420774714100274120ustar00rootroot00000000000000# gtk_combo_box_text.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later from .attributes import BaseTypedAttribute from .gobject_object import ObjectContent, validate_parent_type from .common import * class Item(BaseTypedAttribute): tag_name = "item" attr_name = "id" @property def value_type(self): return StringType() item = Group( Item, [ Optional([ UseIdent("name"), ":", ]), VALUE_HOOKS, ] ) class Items(AstNode): grammar = [ Keyword("items"), "[", Delimited(item, ","), "]", ] @validate("items") def container_is_combo_box_text(self): validate_parent_type(self, "Gtk", "ComboBoxText", "combo box items") def emit_xml(self, xml: XmlEmitter): xml.start_tag("items") for child in self.children: child.emit_xml(xml) xml.end_tag() @completer( applies_in=[ObjectContent], applies_in_subclass=("Gtk", "ComboBoxText"), matches=new_statement_patterns, ) def items_completer(ast_node, match_variables): yield Completion( "items", CompletionItemKind.Snippet, snippet="items [$0]" ) blueprint-compiler-main/blueprintcompiler/language/gtk_file_filter.py000066400000000000000000000057711420774714100266620ustar00rootroot00000000000000# gtk_file_filter.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later from .gobject_object import ObjectContent, validate_parent_type from .common import * class Filters(AstNode): @validate() def container_is_file_filter(self): validate_parent_type(self, "Gtk", "FileFilter", "file filter properties") def emit_xml(self, xml: XmlEmitter): xml.start_tag(self.tokens["tag_name"]) for child in self.children: child.emit_xml(xml) xml.end_tag() class FilterString(AstNode): def emit_xml(self, xml): xml.start_tag(self.tokens["tag_name"]) xml.put_text(self.tokens["name"]) xml.end_tag() def create_node(tag_name: str, singular: str): return Group( Filters, [ Keyword(tag_name), UseLiteral("tag_name", tag_name), "[", Delimited( Group( FilterString, [ UseQuoted("name"), UseLiteral("tag_name", singular), ] ), ",", ), "]", ] ) mime_types = create_node("mime-types", "mime-type") patterns = create_node("patterns", "pattern") suffixes = create_node("suffixes", "suffix") @completer( applies_in=[ObjectContent], applies_in_subclass=("Gtk", "FileFilter"), matches=new_statement_patterns, ) def file_filter_completer(ast_node, match_variables): yield Completion("mime-types", CompletionItemKind.Snippet, snippet="mime-types [\"$0\"]") yield Completion("patterns", CompletionItemKind.Snippet, snippet="patterns [\"$0\"]") yield Completion("suffixes", CompletionItemKind.Snippet, snippet="suffixes [\"$0\"]") @decompiler("mime-types") def decompile_mime_types(ctx, gir): ctx.print("mime-types [") @decompiler("mime-type", cdata=True) def decompile_mime_type(ctx, gir, cdata): ctx.print(f'"{cdata}",') @decompiler("patterns") def decompile_patterns(ctx, gir): ctx.print("patterns [") @decompiler("pattern", cdata=True) def decompile_pattern(ctx, gir, cdata): ctx.print(f'"{cdata}",') @decompiler("suffixes") def decompile_suffixes(ctx, gir): ctx.print("suffixes [") @decompiler("suffix", cdata=True) def decompile_suffix(ctx, gir, cdata): ctx.print(f'"{cdata}",') blueprint-compiler-main/blueprintcompiler/language/gtk_layout.py000066400000000000000000000037351420774714100257110ustar00rootroot00000000000000# gtk_layout.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later from .attributes import BaseAttribute from .gobject_object import ObjectContent, validate_parent_type from .common import * class LayoutProperty(BaseAttribute): tag_name = "property" @property def value_type(self): # there isn't really a way to validate these return None layout_prop = Group( LayoutProperty, Statement( UseIdent("name"), ":", VALUE_HOOKS.expected("a value"), ) ) class Layout(AstNode): grammar = Sequence( Keyword("layout"), "{", Until(layout_prop, "}"), ) @validate("layout") def container_is_widget(self): validate_parent_type(self, "Gtk", "Widget", "layout properties") def emit_xml(self, xml: XmlEmitter): xml.start_tag("layout") for child in self.children: child.emit_xml(xml) xml.end_tag() @completer( applies_in=[ObjectContent], applies_in_subclass=("Gtk", "Widget"), matches=new_statement_patterns, ) def layout_completer(ast_node, match_variables): yield Completion( "layout", CompletionItemKind.Snippet, snippet="layout {\n $0\n}" ) @decompiler("layout") def decompile_layout(ctx, gir): ctx.print("layout {") blueprint-compiler-main/blueprintcompiler/language/gtk_menu.py000066400000000000000000000114241420774714100253320ustar00rootroot00000000000000# gtk_menus.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later from .attributes import BaseAttribute from .gobject_object import Object, ObjectContent from .ui import UI from .common import * class Menu(Object): def emit_xml(self, xml: XmlEmitter): xml.start_tag(self.tokens["tag"], id=self.tokens["id"]) for child in self.children: child.emit_xml(xml) xml.end_tag() @property def gir_class(self): return self.root.gir.namespaces["Gtk"].lookup_type("Gio.MenuModel") class MenuAttribute(BaseAttribute): tag_name = "attribute" @property def value_type(self): return None menu_contents = Sequence() menu_section = Group( Menu, [ "section", UseLiteral("tag", "section"), Optional(UseIdent("id")), menu_contents ] ) menu_submenu = Group( Menu, [ "submenu", UseLiteral("tag", "submenu"), Optional(UseIdent("id")), menu_contents ] ) menu_attribute = Group( MenuAttribute, [ UseIdent("name"), ":", VALUE_HOOKS.expected("a value"), Match(";").expected(), ] ) menu_item = Group( Menu, [ "item", UseLiteral("tag", "item"), Optional(UseIdent("id")), Match("{").expected(), Until(menu_attribute, "}"), ] ) menu_item_shorthand = Group( Menu, [ "item", UseLiteral("tag", "item"), "(", Group( MenuAttribute, [UseLiteral("name", "label"), VALUE_HOOKS], ), Optional([ ",", Optional([ Group( MenuAttribute, [UseLiteral("name", "action"), VALUE_HOOKS], ), Optional([ ",", Group( MenuAttribute, [UseLiteral("name", "icon"), VALUE_HOOKS], ), ]) ]) ]), Match(")").expected(), ] ) menu_contents.children = [ Match("{"), Until(AnyOf( menu_section, menu_submenu, menu_item_shorthand, menu_item, menu_attribute, ), "}"), ] menu = Group( Menu, [ "menu", UseLiteral("tag", "menu"), Optional(UseIdent("id")), menu_contents ], ) @completer( applies_in=[UI], matches=new_statement_patterns, ) def menu_completer(ast_node, match_variables): yield Completion( "menu", CompletionItemKind.Snippet, snippet="menu {\n $0\n}" ) @completer( applies_in=[Menu], matches=new_statement_patterns, ) def menu_content_completer(ast_node, match_variables): yield Completion( "submenu", CompletionItemKind.Snippet, snippet="submenu {\n $0\n}" ) yield Completion( "section", CompletionItemKind.Snippet, snippet="section {\n $0\n}" ) yield Completion( "item", CompletionItemKind.Snippet, snippet="item {\n $0\n}" ) yield Completion( "item (shorthand)", CompletionItemKind.Snippet, snippet='item (_("${1:Label}"), "${2:action-name}", "${3:icon-name}")' ) yield Completion( "label", CompletionItemKind.Snippet, snippet='label: $0;' ) yield Completion( "action", CompletionItemKind.Snippet, snippet='action: "$0";' ) yield Completion( "icon", CompletionItemKind.Snippet, snippet='icon: "$0";' ) @decompiler("menu") def decompile_menu(ctx, gir, id=None): if id: ctx.print(f"menu {id} {{") else: ctx.print("menu {") @decompiler("submenu") def decompile_submenu(ctx, gir, id=None): if id: ctx.print(f"submenu {id} {{") else: ctx.print("submenu {") @decompiler("item") def decompile_item(ctx, gir, id=None): if id: ctx.print(f"item {id} {{") else: ctx.print("item {") @decompiler("section") def decompile_section(ctx, gir, id=None): if id: ctx.print(f"section {id} {{") else: ctx.print("section {") blueprint-compiler-main/blueprintcompiler/language/gtk_size_group.py000066400000000000000000000044411420774714100265550ustar00rootroot00000000000000# gtk_size_group.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later from .gobject_object import ObjectContent, validate_parent_type from .common import * class Widget(AstNode): grammar = UseIdent("name") @validate("name") def obj_widget(self): object = self.root.objects_by_id.get(self.tokens["name"]) type = self.root.gir.get_type("Widget", "Gtk") if object is None: raise CompileError( f"Could not find object with ID {self.tokens['name']}", did_you_mean=(self.tokens['name'], self.root.objects_by_id.keys()), ) elif object.gir_class and not object.gir_class.assignable_to(type): raise CompileError( f"Cannot assign {object.gir_class.full_name} to {type.full_name}" ) def emit_xml(self, xml: XmlEmitter): xml.put_self_closing("widget", name=self.tokens["name"]) class Widgets(AstNode): grammar = [ Keyword("widgets"), "[", Delimited(Widget, ","), "]", ] @validate("widgets") def container_is_size_group(self): validate_parent_type(self, "Gtk", "SizeGroup", "size group properties") def emit_xml(self, xml: XmlEmitter): xml.start_tag("widgets") for child in self.children: child.emit_xml(xml) xml.end_tag() @completer( applies_in=[ObjectContent], applies_in_subclass=("Gtk", "SizeGroup"), matches=new_statement_patterns, ) def size_group_completer(ast_node, match_variables): yield Completion("widgets", CompletionItemKind.Snippet, snippet="widgets [$0]") blueprint-compiler-main/blueprintcompiler/language/gtk_string_list.py000066400000000000000000000040001420774714100267170ustar00rootroot00000000000000# gtk_combo_box_text.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later from .attributes import BaseTypedAttribute from .gobject_object import ObjectContent, validate_parent_type from .values import Value, TranslatedStringValue from .common import * class Item(AstNode): grammar = VALUE_HOOKS @property def value_type(self): return StringType() def emit_xml(self, xml: XmlEmitter): value = self.children[Value][0] attrs = value.attrs if isinstance(value, TranslatedStringValue) else {} xml.start_tag("item", **attrs) value.emit_xml(xml) xml.end_tag() class Strings(AstNode): grammar = [ Keyword("strings"), "[", Delimited(Item, ","), "]", ] @validate("items") def container_is_string_list(self): validate_parent_type(self, "Gtk", "StringList", "StringList items") def emit_xml(self, xml: XmlEmitter): xml.start_tag("items") for child in self.children: child.emit_xml(xml) xml.end_tag() @completer( applies_in=[ObjectContent], applies_in_subclass=("Gtk", "StringList"), matches=new_statement_patterns, ) def strings_completer(ast_node, match_variables): yield Completion( "strings", CompletionItemKind.Snippet, snippet="strings [$0]" ) blueprint-compiler-main/blueprintcompiler/language/gtk_styles.py000066400000000000000000000035221420774714100257110ustar00rootroot00000000000000# gtk_styles.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later from .gobject_object import ObjectContent, validate_parent_type from .common import * class StyleClass(AstNode): grammar = UseQuoted("name") def emit_xml(self, xml): xml.put_self_closing("class", name=self.tokens["name"]) class Styles(AstNode): grammar = [ Keyword("styles"), "[", Delimited(StyleClass, ","), "]", ] @validate("styles") def container_is_widget(self): validate_parent_type(self, "Gtk", "Widget", "style classes") def emit_xml(self, xml: XmlEmitter): xml.start_tag("style") for child in self.children: child.emit_xml(xml) xml.end_tag() @completer( applies_in=[ObjectContent], applies_in_subclass=("Gtk", "Widget"), matches=new_statement_patterns, ) def style_completer(ast_node, match_variables): yield Completion("styles", CompletionItemKind.Keyword, snippet="styles [\"$0\"]") @decompiler("style") def decompile_style(ctx, gir): ctx.print(f"styles [") @decompiler("class") def decompile_style_class(ctx, gir, name): ctx.print(f'"{name}",') blueprint-compiler-main/blueprintcompiler/language/gtkbuilder_child.py000066400000000000000000000042411420774714100270170ustar00rootroot00000000000000# gtkbuilder_child.py # # Copyright 2022 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later from functools import cached_property from .gobject_object import Object from .response_id import ResponseId from .common import * class Child(AstNode): grammar = [ Optional([ "[", Optional(["internal-child", UseLiteral("internal_child", True)]), UseIdent("child_type").expected("a child type"), Optional(ResponseId), "]", ]), Object, ] @cached_property def response_id(self) -> T.Optional[ResponseId]: """Get action widget's response ID. If child is not action widget, returns `None`. """ response_ids = self.children[ResponseId] if response_ids: return response_ids[0] else: return None def emit_xml(self, xml: XmlEmitter): child_type = internal_child = None if self.tokens["internal_child"]: internal_child = self.tokens["child_type"] else: child_type = self.tokens["child_type"] xml.start_tag("child", type=child_type, internal_child=internal_child) for child in self.children: child.emit_xml(xml) xml.end_tag() @decompiler("child") def decompile_child(ctx, gir, type=None, internal_child=None): if type is not None: ctx.print(f"[{type}]") elif internal_child is not None: ctx.print(f"[internal-child {internal_child}]") return gir blueprint-compiler-main/blueprintcompiler/language/gtkbuilder_template.py000066400000000000000000000034741420774714100275560ustar00rootroot00000000000000# gtkbuilder_template.py # # Copyright 2022 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later from .gobject_object import Object, ObjectContent from .common import * class Template(Object): grammar = [ "template", UseIdent("name").expected("template class name"), Optional([ Match(":"), class_name.expected("parent class"), ]), ObjectContent, ] def emit_xml(self, xml: XmlEmitter): if self.gir_class: parent = self.gir_class.glib_type_name elif self.tokens["class_name"]: parent = self.tokens["class_name"] else: parent = None xml.start_tag("template", **{"class": self.tokens["name"]}, parent=parent) for child in self.children: child.emit_xml(xml) xml.end_tag() @decompiler("template") def decompile_template(ctx: DecompileCtx, gir, klass, parent="Widget"): gir_class = ctx.type_by_cname(parent) if gir_class is None: ctx.print(f"template {klass} : .{parent} {{") else: ctx.print(f"template {klass} : {decompile.full_name(gir_class)} {{") return gir_class blueprint-compiler-main/blueprintcompiler/language/imports.py000066400000000000000000000045101420774714100252140ustar00rootroot00000000000000# imports.py # # Copyright 2022 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later from .. import gir from .common import * class GtkDirective(AstNode): grammar = Statement( Match("using").err("File must start with a \"using Gtk\" directive (e.g. `using Gtk 4.0;`)"), Match("Gtk").err("File must start with a \"using Gtk\" directive (e.g. `using Gtk 4.0;`)"), UseNumberText("version").expected("a version number for GTK"), ) @validate("version") def gtk_version(self): if self.tokens["version"] not in ["4.0"]: err = CompileError("Only GTK 4 is supported") if self.tokens["version"].startswith("4"): err.hint("Expected the GIR version, not an exact version number. Use `using Gtk 4.0;`.") else: err.hint("Expected `using Gtk 4.0;`") raise err @property def gir_namespace(self): return gir.get_namespace("Gtk", self.tokens["version"]) def emit_xml(self, xml: XmlEmitter): xml.put_self_closing("requires", lib="gtk", version=self.tokens["version"]) class Import(AstNode): grammar = Statement( "using", UseIdent("namespace").expected("a GIR namespace"), UseNumberText("version").expected("a version number"), ) @validate("namespace", "version") def namespace_exists(self): gir.get_namespace(self.tokens["namespace"], self.tokens["version"]) @property def gir_namespace(self): try: return gir.get_namespace(self.tokens["namespace"], self.tokens["version"]) except CompileError: return None def emit_xml(self, xml): pass blueprint-compiler-main/blueprintcompiler/language/response_id.py000066400000000000000000000112411420774714100260300ustar00rootroot00000000000000# response_id.py # # Copyright 2022 Gleb Smirnov # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later import typing as T from .common import * class ResponseId(AstNode): """Response ID of action widget.""" ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [ ("Gtk", "Dialog"), ("Gtk", "InfoBar") ] grammar = [ UseIdent("response"), "=", AnyOf( UseIdent("response_id"), UseNumber("response_id") ), Optional([ Keyword("default"), UseLiteral("is_default", True) ]) ] @validate() def child_type_is_action(self) -> None: """Check that child type is "action".""" child_type = self.parent.tokens["child_type"] if child_type != "action": raise CompileError(f"Only action widget can have response ID") @validate() def parent_has_action_widgets(self) -> None: """Chech that parent widget has allowed type.""" from .gobject_object import Object container_type = self.parent_by_type(Object).gir_class gir = self.root.gir for namespace, name in ResponseId.ALLOWED_PARENTS: parent_type = gir.get_type(name, namespace) if container_type.assignable_to(parent_type): break else: raise CompileError( f"{container_type.full_name} doesn't have action widgets" ) @validate() def widget_have_id(self) -> None: """Check that action widget have ID.""" from .gobject_object import Object _object = self.parent.children[Object][0] if _object.tokens["id"] is None: raise CompileError(f"Action widget must have ID") @validate("response_id") def correct_response_type(self) -> None: """Validate response type. Response type might be GtkResponseType member or positive number. """ gir = self.root.gir response = self.tokens["response_id"] if isinstance(response, int): if response < 0: raise CompileError( "Numeric response type can't be negative") elif isinstance(response, float): raise CompileError( "Response type must be GtkResponseType member or integer," " not float" ) else: responses = gir.get_type("ResponseType", "Gtk").members.keys() if response not in responses: raise CompileError( f"Response type \"{response}\" doesn't exist") @validate("default") def no_multiple_default(self) -> None: """Only one action widget in dialog can be default.""" from .gtkbuilder_child import Child from .gobject_object import Object if not self.tokens["is_default"]: return action_widgets = self.parent_by_type(Object).action_widgets for widget in action_widgets: if widget == self: break if widget.tokens["is_default"]: raise CompileError("Default response is already set") @property def widget_id(self) -> str: """Get action widget ID.""" from .gobject_object import Object _object: Object = self.parent.children[Object][0] return _object.tokens["id"] def emit_xml(self, xml: XmlEmitter) -> None: """Emit nothing. Response ID don't have to emit any XML in place, but have to emit action-widget tag in separate place (see `ResponseId.emit_action_widget`) """ def emit_action_widget(self, xml: XmlEmitter) -> None: """Emit action-widget XML. Must be called while tag is open. For more details see `GtkDialog` and `GtkInfoBar` docs. """ xml.start_tag( "action-widget", response=self.tokens["response_id"], default=self.tokens["is_default"] ) xml.put_text(self.widget_id) xml.end_tag() blueprint-compiler-main/blueprintcompiler/language/ui.py000066400000000000000000000062461420774714100241440ustar00rootroot00000000000000# ui.py # # Copyright 2022 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later from .. import gir from .imports import GtkDirective, Import from .gtkbuilder_template import Template from .common import * class UI(AstNode): """ The AST node for the entire file """ grammar = [ GtkDirective, ZeroOrMore(Import), Until(AnyOf( Template, OBJECT_HOOKS, ), Eof()), ] @property def gir(self): gir_ctx = gir.GirContext() self._gir_errors = [] try: gir_ctx.add_namespace(self.children[GtkDirective][0].gir_namespace) except CompileError as e: e.start = self.children[GtkDirective][0].group.start e.end = self.children[GtkDirective][0].group.end self._gir_errors.append(e) for i in self.children[Import]: try: if i.gir_namespace is not None: gir_ctx.add_namespace(i.gir_namespace) except CompileError as e: e.start = i.group.tokens["namespace"].start e.end = i.group.tokens["version"].end self._gir_errors.append(e) return gir_ctx @property def objects_by_id(self): return { obj.tokens["id"]: obj for obj in self.iterate_children_recursive() if obj.tokens["id"] is not None } @validate() def gir_errors(self): # make sure gir is loaded self.gir if len(self._gir_errors): raise MultipleErrors(self._gir_errors) @validate() def at_most_one_template(self): if len(self.children[Template]) > 1: for template in self.children[Template][1:]: raise CompileError( f"Only one template may be defined per file, but this file contains {len(self.children[Template])}", template.group.tokens["name"].start, template.group.tokens["name"].end, ) @validate() def unique_ids(self): passed = {} for obj in self.iterate_children_recursive(): if obj.tokens["id"] is None: continue if obj.tokens["id"] in passed: token = obj.group.tokens["id"] raise CompileError(f"Duplicate object ID '{obj.tokens['id']}'", token.start, token.end) passed[obj.tokens["id"]] = obj def emit_xml(self, xml: XmlEmitter): xml.start_tag("interface") for x in self.children: x.emit_xml(xml) xml.end_tag() blueprint-compiler-main/blueprintcompiler/language/values.py000066400000000000000000000143411420774714100250210ustar00rootroot00000000000000# values.py # # Copyright 2022 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later from .common import * class Value(AstNode): pass class TranslatedStringValue(Value): grammar = AnyOf( [ "_", "(", UseQuoted("value").expected("a quoted string"), Match(")").expected(), ], [ "C_", "(", UseQuoted("context").expected("a quoted string"), ",", UseQuoted("value").expected("a quoted string"), Optional(","), Match(")").expected(), ], ) @property def attrs(self): attrs = { "translatable": "true" } if "context" in self.tokens: attrs["context"] = self.tokens["context"] return attrs def emit_xml(self, xml: XmlEmitter): xml.put_text(self.tokens["value"]) class LiteralValue(Value): grammar = AnyOf( UseNumber("value"), UseQuoted("value"), ) def emit_xml(self, xml: XmlEmitter): xml.put_text(self.tokens["value"]) @validate() def validate_for_type(self): type = self.parent.value_type if isinstance(type, gir.IntType): try: int(self.tokens["value"]) except: raise CompileError(f"Cannot convert {self.group.tokens['value']} to integer") elif isinstance(type, gir.UIntType): try: int(self.tokens["value"]) if int(self.tokens["value"]) < 0: raise Exception() except: raise CompileError(f"Cannot convert {self.group.tokens['value']} to unsigned integer") elif isinstance(type, gir.FloatType): try: float(self.tokens["value"]) except: raise CompileError(f"Cannot convert {self.group.tokens['value']} to float") elif isinstance(type, gir.StringType): pass elif isinstance(type, gir.Class) or isinstance(type, gir.Interface): parseable_types = [ "Gdk.Paintable", "Gdk.Texture", "Gdk.Pixbuf", "GLib.File", "Gtk.ShortcutTrigger", "Gtk.ShortcutAction", ] if type.full_name not in parseable_types: raise CompileError(f"Cannot convert {self.group.tokens['value']} to {type.full_name}") elif type is not None: raise CompileError(f"Cannot convert {self.group.tokens['value']} to {type.full_name}") class Flag(AstNode): grammar = UseIdent("value") @validate() def validate_for_type(self): type = self.parent.parent.value_type if isinstance(type, gir.Bitfield) and self.tokens["value"] not in type.members: raise CompileError( f"{self.tokens['value']} is not a member of {type.full_name}", did_you_mean=(self.tokens['value'], type.members.keys()), ) class FlagsValue(Value): grammar = [Flag, "|", Delimited(Flag, "|")] @validate() def parent_is_bitfield(self): type = self.parent.value_type if not isinstance(type, gir.Bitfield): raise CompileError(f"{type.full_name} is not a bitfield type") def emit_xml(self, xml: XmlEmitter): xml.put_text("|".join([flag.tokens["value"] for flag in self.children[Flag]])) class IdentValue(Value): grammar = UseIdent("value") def emit_xml(self, xml: XmlEmitter): if isinstance(self.parent.value_type, gir.Enumeration): xml.put_text(self.parent.value_type.members[self.tokens["value"]].nick) else: xml.put_text(self.tokens["value"]) @validate() def validate_for_type(self): type = self.parent.value_type if isinstance(type, gir.Enumeration) or isinstance(type, gir.Bitfield): if self.tokens["value"] not in type.members: raise CompileError( f"{self.tokens['value']} is not a member of {type.full_name}", did_you_mean=(self.tokens['value'], type.members.keys()), ) elif isinstance(type, gir.BoolType): if self.tokens["value"] not in ["true", "false"]: raise CompileError( f"Expected 'true' or 'false' for boolean value", did_you_mean=(self.tokens['value'], ["true", "false"]), ) elif type is not None: object = self.root.objects_by_id.get(self.tokens["value"]) if object is None: raise CompileError( f"Could not find object with ID {self.tokens['value']}", did_you_mean=(self.tokens['value'], self.root.objects_by_id.keys()), ) elif object.gir_class and not object.gir_class.assignable_to(type): raise CompileError( f"Cannot assign {object.gir_class.full_name} to {type.full_name}" ) @docs() def docs(self): type = self.parent.value_type if isinstance(type, gir.Enumeration): if member := type.members.get(self.tokens["value"]): return member.doc else: return type.doc elif isinstance(type, gir.GirNode): return type.doc def get_semantic_tokens(self) -> T.Iterator[SemanticToken]: if isinstance(self.parent.value_type, gir.Enumeration): token = self.group.tokens["value"] yield SemanticToken(token.start, token.end, SemanticTokenType.EnumMember) blueprint-compiler-main/blueprintcompiler/lsp.py000066400000000000000000000227151420774714100225410ustar00rootroot00000000000000# lsp.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later import typing as T import json, sys, traceback from .completions import complete from .errors import PrintableError, CompileError, MultipleErrors from .lsp_utils import * from . import tokenizer, parser, utils, xml_reader def command(json_method): def decorator(func): func._json_method = json_method return func return decorator class OpenFile: def __init__(self, uri, text, version): self.uri = uri self.text = text self.version = version self.ast = None self.tokens = None self._update() def apply_changes(self, changes): for change in changes: start = utils.pos_to_idx(change["range"]["start"]["line"], change["range"]["start"]["character"], self.text) end = utils.pos_to_idx(change["range"]["end"]["line"], change["range"]["end"]["character"], self.text) self.text = self.text[:start] + change["text"] + self.text[end:] self._update() def _update(self): self.diagnostics = [] try: self.tokens = tokenizer.tokenize(self.text) self.ast, errors, warnings = parser.parse(self.tokens) self.diagnostics += warnings if errors is not None: self.diagnostics += errors.errors self.diagnostics += self.ast.errors except MultipleErrors as e: self.diagnostics += e.errors except CompileError as e: self.diagnostics.append(e) def calc_semantic_tokens(self) -> T.List[int]: tokens = list(self.ast.get_semantic_tokens()) token_lists = [ [ *utils.idx_to_pos(token.start, self.text), # line and column token.end - token.start, # length token.type, 0, # token modifiers ] for token in tokens] # convert line, column numbers to deltas for i, token_list in enumerate(token_lists[1:]): token_list[0] -= token_lists[i][0] if token_list[0] == 0: token_list[1] -= token_lists[i][1] # flatten the list return [x for y in token_lists for x in y] class LanguageServer: commands: T.Dict[str, T.Callable] = {} def __init__(self, logfile=None): self.client_capabilities = {} self._open_files: {str: OpenFile} = {} self.logfile = logfile def run(self): # Read tags from gir files. During normal compilation these are # ignored. xml_reader.PARSE_GIR.add("doc") try: while True: line = "" content_len = -1 while content_len == -1 or (line != "\n" and line != "\r\n"): line = sys.stdin.buffer.readline().decode() if line == "": return if line.startswith("Content-Length:"): content_len = int(line.split("Content-Length:")[1].strip()) line = sys.stdin.buffer.read(content_len).decode() self._log("input: " + line) data = json.loads(line) method = data.get("method") id = data.get("id") params = data.get("params") if method in self.commands: self.commands[method](self, id, params) except Exception as e: self._log(traceback.format_exc()) def _send(self, data): data["jsonrpc"] = "2.0" line = json.dumps(data, separators=(",", ":")) + "\r\n" self._log("output: " + line) sys.stdout.write(f"Content-Length: {len(line.encode())}\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n{line}") sys.stdout.flush() def _log(self, msg): if self.logfile is not None: self.logfile.write(str(msg)) self.logfile.write("\n") self.logfile.flush() def _send_response(self, id, result): self._send({ "id": id, "result": result, }) def _send_notification(self, method, params): self._send({ "method": method, "params": params, }) @command("initialize") def initialize(self, id, params): self.client_capabilities = params.get("capabilities") self._send_response(id, { "capabilities": { "textDocumentSync": { "openClose": True, "change": TextDocumentSyncKind.Incremental, }, "semanticTokensProvider": { "legend": { "tokenTypes": ["enumMember"], }, "full": True, }, "completionProvider": {}, "codeActionProvider": {}, "hoverProvider": True, } }) @command("textDocument/didOpen") def didOpen(self, id, params): doc = params.get("textDocument") uri = doc.get("uri") version = doc.get("version") text = doc.get("text") open_file = OpenFile(uri, text, version) self._open_files[uri] = open_file self._send_file_updates(open_file) @command("textDocument/didChange") def didChange(self, id, params): if params is not None: open_file = self._open_files[params["textDocument"]["uri"]] open_file.apply_changes(params["contentChanges"]) self._send_file_updates(open_file) @command("textDocument/didClose") def didClose(self, id, params): del self._open_files[params["textDocument"]["uri"]] @command("textDocument/hover") def hover(self, id, params): open_file = self._open_files[params["textDocument"]["uri"]] docs = open_file.ast and open_file.ast.get_docs(utils.pos_to_idx(params["position"]["line"], params["position"]["character"], open_file.text)) if docs: self._send_response(id, { "contents": { "kind": "markdown", "value": docs, } }) else: self._send_response(id, None) @command("textDocument/completion") def completion(self, id, params): open_file = self._open_files[params["textDocument"]["uri"]] if open_file.ast is None: self._send_response(id, []) return idx = utils.pos_to_idx(params["position"]["line"], params["position"]["character"], open_file.text) completions = complete(open_file.ast, open_file.tokens, idx) self._send_response(id, [completion.to_json(True) for completion in completions]) @command("textDocument/semanticTokens/full") def semantic_tokens(self, id, params): open_file = self._open_files[params["textDocument"]["uri"]] self._send_response(id, { "data": open_file.calc_semantic_tokens(), }) @command("textDocument/codeAction") def code_actions(self, id, params): open_file = self._open_files[params["textDocument"]["uri"]] range_start = utils.pos_to_idx(params["range"]["start"]["line"], params["range"]["start"]["character"], open_file.text) range_end = utils.pos_to_idx(params["range"]["end"]["line"], params["range"]["end"]["character"], open_file.text) actions = [ { "title": action.title, "kind": "quickfix", "diagnostics": [self._create_diagnostic(open_file.text, diagnostic)], "edit": { "changes": { open_file.uri: [{ "range": utils.idxs_to_range(diagnostic.start, diagnostic.end, open_file.text), "newText": action.replace_with }] } } } for diagnostic in open_file.diagnostics if not (diagnostic.end < range_start or diagnostic.start > range_end) for action in diagnostic.actions ] self._send_response(id, actions) def _send_file_updates(self, open_file: OpenFile): self._send_notification("textDocument/publishDiagnostics", { "uri": open_file.uri, "diagnostics": [self._create_diagnostic(open_file.text, err) for err in open_file.diagnostics], }) def _create_diagnostic(self, text, err): return { "range": utils.idxs_to_range(err.start, err.end, text), "message": err.message, "severity": 1, } for name in dir(LanguageServer): item = getattr(LanguageServer, name) if callable(item) and hasattr(item, "_json_method"): LanguageServer.commands[item._json_method] = item blueprint-compiler-main/blueprintcompiler/lsp_utils.py000066400000000000000000000053351420774714100237600ustar00rootroot00000000000000# lsp_enums.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later from dataclasses import dataclass import enum import typing as T from .errors import * from .utils import * class TextDocumentSyncKind(enum.IntEnum): None_ = 0 Full = 1 Incremental = 2 class CompletionItemTag(enum.IntEnum): Deprecated = 1 class InsertTextFormat(enum.IntEnum): PlainText = 1 Snippet = 2 class CompletionItemKind(enum.IntEnum): Text = 1 Method = 2 Function = 3 Constructor = 4 Field = 5 Variable = 6 Class = 7 Interface = 8 Module = 9 Property = 10 Unit = 11 Value = 12 Enum = 13 Keyword = 14 Snippet = 15 Color = 16 File = 17 Reference = 18 Folder = 19 EnumMember = 20 Constant = 21 Struct = 22 Event = 23 Operator = 24 TypeParameter = 25 @dataclass class Completion: label: str kind: CompletionItemKind signature: T.Optional[str] = None deprecated: bool = False docs: T.Optional[str] = None text: T.Optional[str] = None snippet: T.Optional[str] = None def to_json(self, snippets: bool): insert_text = self.text or self.label insert_text_format = InsertTextFormat.PlainText if snippets and self.snippet: insert_text = self.snippet insert_text_format = InsertTextFormat.Snippet result = { "label": self.label, "kind": self.kind, "tags": [CompletionItemTag.Deprecated] if self.deprecated else None, "detail": self.signature, "documentation": { "kind": "markdown", "value": self.docs, } if self.docs else None, "deprecated": self.deprecated, "insertText": insert_text, "insertTextFormat": insert_text_format, } return { k: v for k, v in result.items() if v is not None } class SemanticTokenType(enum.IntEnum): EnumMember = 0 @dataclass class SemanticToken: start: int end: int type: SemanticTokenType blueprint-compiler-main/blueprintcompiler/main.py000066400000000000000000000117021420774714100226610ustar00rootroot00000000000000# main.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later import typing as T import argparse, json, os, sys from .errors import PrintableError, report_bug, MultipleErrors from .lsp import LanguageServer from . import parser, tokenizer, decompiler, interactive_port from .utils import Colors from .xml_emitter import XmlEmitter VERSION = "0.1.0" class BlueprintApp: def main(self): self.parser = argparse.ArgumentParser() self.subparsers = self.parser.add_subparsers(metavar="command") self.parser.set_defaults(func=self.cmd_help) compile = self.add_subcommand("compile", "Compile blueprint files", self.cmd_compile) compile.add_argument("--output", dest="output", default="-") compile.add_argument("input", metavar="filename", default=sys.stdin, type=argparse.FileType('r')) batch_compile = self.add_subcommand("batch-compile", "Compile many blueprint files at once", self.cmd_batch_compile) batch_compile.add_argument("output_dir", metavar="output-dir") batch_compile.add_argument("input_dir", metavar="input-dir") batch_compile.add_argument("inputs", nargs="+", metavar="filenames", default=sys.stdin, type=argparse.FileType('r')) port = self.add_subcommand("port", "Interactive porting tool", self.cmd_port) lsp = self.add_subcommand("lsp", "Run the language server (for internal use by IDEs)", self.cmd_lsp) lsp.add_argument("--logfile", dest="logfile", default=None, type=argparse.FileType('a')) self.add_subcommand("help", "Show this message", self.cmd_help) try: opts = self.parser.parse_args() opts.func(opts) except SystemExit as e: raise e except KeyboardInterrupt: print(f"\n\n{Colors.RED}{Colors.BOLD}Interrupted.{Colors.CLEAR}") except EOFError: print(f"\n\n{Colors.RED}{Colors.BOLD}Interrupted.{Colors.CLEAR}") except: report_bug() def add_subcommand(self, name, help, func): parser = self.subparsers.add_parser(name, help=help) parser.set_defaults(func=func) return parser def cmd_help(self, opts): self.parser.print_help() def cmd_compile(self, opts): data = opts.input.read() try: xml, warnings = self._compile(data) for warning in warnings: warning.pretty_print(opts.input.name, data, stream=sys.stderr) if opts.output == "-": print(xml) else: with open(opts.output, "w") as file: file.write(xml) except PrintableError as e: e.pretty_print(opts.input.name, data) sys.exit(1) def cmd_batch_compile(self, opts): for file in opts.inputs: data = file.read() try: if not os.path.commonpath([file.name, opts.input_dir]): print(f"{Colors.RED}{Colors.BOLD}error: input file '{file.name}' is not in input directory '{opts.input_dir}'{Colors.CLEAR}") sys.exit(1) xml, warnings = self._compile(data) for warning in warnings: warning.pretty_print(file.name, data, stream=sys.stderr) path = os.path.join( opts.output_dir, os.path.relpath( os.path.splitext(file.name)[0] + ".ui", opts.input_dir ) ) os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w") as file: file.write(xml) except PrintableError as e: e.pretty_print(file.name, data) sys.exit(1) def cmd_lsp(self, opts): langserv = LanguageServer(opts.logfile) langserv.run() def cmd_port(self, opts): interactive_port.run(opts) def _compile(self, data: str) -> T.Tuple[str, T.List[PrintableError]]: tokens = tokenizer.tokenize(data) ast, errors, warnings = parser.parse(tokens) if errors: raise errors if len(ast.errors): raise MultipleErrors(ast.errors) return ast.generate(), warnings def main(): BlueprintApp().main() blueprint-compiler-main/blueprintcompiler/parse_tree.py000066400000000000000000000434141420774714100240730ustar00rootroot00000000000000# parse_tree.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later """ Utilities for parsing an AST from a token stream. """ import typing as T from collections import defaultdict from enum import Enum from .errors import assert_true, CompilerBugError, CompileError, CompileWarning, UnexpectedTokenError from .tokenizer import Token, TokenType SKIP_TOKENS = [TokenType.COMMENT, TokenType.WHITESPACE] class ParseResult(Enum): """ Represents the result of parsing. The extra EMPTY result is necessary to avoid freezing the parser: imagine a ZeroOrMore node containing a node that can match empty. It will repeatedly match empty and never advance the parser. So, ZeroOrMore stops when a failed *or empty* match is made. """ SUCCESS = 0 FAILURE = 1 EMPTY = 2 def matched(self): return self == ParseResult.SUCCESS def succeeded(self): return self != ParseResult.FAILURE def failed(self): return self == ParseResult.FAILURE class ParseGroup: """ A matching group. Match groups have an AST type, children grouped by type, and key=value pairs. At the end of parsing, the match groups will be converted to AST nodes by passing the children and key=value pairs to the AST node constructor. """ def __init__(self, ast_type, start: int): self.ast_type = ast_type self.children: T.List[ParseGroup] = [] self.keys: T.Dict[str, T.Any] = {} self.tokens: T.Dict[str, Token] = {} self.start = start self.end = None self.incomplete = False def add_child(self, child): self.children.append(child) def set_val(self, key, val, token): assert_true(key not in self.keys) self.keys[key] = val self.tokens[key] = token def to_ast(self): """ Creates an AST node from the match group. """ children = [child.to_ast() for child in self.children] try: return self.ast_type(self, children, self.keys, incomplete=self.incomplete) except TypeError as e: raise CompilerBugError(f"Failed to construct ast.{self.ast_type.__name__} from ParseGroup. See the previous stacktrace.") def __str__(self): result = str(self.ast_type.__name__) result += "".join([f"\n{key}: {val}" for key, val in self.keys.items()]) + "\n" result += "\n".join([str(child) for children in self.children.values() for child in children]) return result.replace("\n", "\n ") class ParseContext: """ Contains the state of the parser. """ def __init__(self, tokens, index=0): self.tokens = list(tokens) self.index = index self.start = index self.group = None self.group_keys = {} self.group_children = [] self.last_group = None self.group_incomplete = False self.errors = [] self.warnings = [] def create_child(self): """ Creates a new ParseContext at this context's position. The new context will be used to parse one node. If parsing is successful, the new context will be applied to "self". If parsing fails, the new context will be discarded. """ ctx = ParseContext(self.tokens, self.index) ctx.errors = self.errors ctx.warnings = self.warnings return ctx def apply_child(self, other): """ Applies a child context to this context. """ if other.group is not None: # If the other context had a match group, collect all the matched # values into it and then add it to our own match group. for key, (val, token) in other.group_keys.items(): other.group.set_val(key, val, token) for child in other.group_children: other.group.add_child(child) other.group.end = other.tokens[other.index - 1].end other.group.incomplete = other.group_incomplete self.group_children.append(other.group) else: # If the other context had no match group of its own, collect all # its matched values self.group_keys = {**self.group_keys, **other.group_keys} self.group_children += other.group_children self.group_incomplete |= other.group_incomplete self.index = other.index # Propagate the last parsed group down the stack so it can be easily # retrieved at the end of the process if other.group: self.last_group = other.group elif other.last_group: self.last_group = other.last_group def start_group(self, ast_type): """ Sets this context to have its own match group. """ assert_true(self.group is None) self.group = ParseGroup(ast_type, self.tokens[self.index].start) def set_group_val(self, key, value, token): """ Sets a matched key=value pair on the current match group. """ assert_true(key not in self.group_keys) self.group_keys[key] = (value, token) def set_group_incomplete(self): """ Marks the current match group as incomplete (it could not be fully parsed, but the parser recovered). """ self.group_incomplete = True def skip(self): """ Skips whitespace and comments. """ while self.index < len(self.tokens) and self.tokens[self.index].type in SKIP_TOKENS: self.index += 1 def next_token(self) -> Token: """ Advances the token iterator and returns the next token. """ self.skip() token = self.tokens[self.index] self.index += 1 return token def peek_token(self) -> Token: """ Returns the next token without advancing the iterator. """ self.skip() token = self.tokens[self.index] return token def skip_unexpected_token(self): """ Skips a token and logs an "unexpected token" error. """ self.skip() start = self.tokens[self.index].start self.next_token() self.skip() end = self.tokens[self.index - 1].end if (len(self.errors) and isinstance((err := self.errors[-1]), UnexpectedTokenError) and err.end == start): err.end = end else: self.errors.append(UnexpectedTokenError(start, end)) def is_eof(self) -> Token: return self.index >= len(self.tokens) or self.peek_token().type == TokenType.EOF class ParseNode: """ Base class for the nodes in the parser tree. """ def parse(self, ctx: ParseContext) -> ParseResult: """ Attempts to match the ParseNode at the context's current location. """ start_idx = ctx.index inner_ctx = ctx.create_child() if self._parse(inner_ctx): ctx.apply_child(inner_ctx) if ctx.index == start_idx: return ParseResult.EMPTY else: return ParseResult.SUCCESS else: return ParseResult.FAILURE def _parse(self, ctx: ParseContext) -> bool: raise NotImplementedError() def err(self, message): """ Causes this ParseNode to raise an exception if it fails to parse. This prevents the parser from backtracking, so you should understand what it does and how the parser works before using it. """ return Err(self, message) def expected(self, expect): """ Convenience method for err(). """ return self.err("Expected " + expect) def warn(self, message): """ Causes this ParseNode to emit a warning if it parses successfully. """ return Warning(self, message) class Err(ParseNode): """ ParseNode that emits a compile error if it fails to parse. """ def __init__(self, child, message): self.child = to_parse_node(child) self.message = message def _parse(self, ctx): if self.child.parse(ctx).failed(): start_idx = ctx.start while ctx.tokens[start_idx].type in SKIP_TOKENS: start_idx += 1 start_token = ctx.tokens[start_idx] end_token = ctx.tokens[ctx.index] raise CompileError(self.message, start_token.start, end_token.end) return True class Warning(ParseNode): """ ParseNode that emits a compile warning if it parses successfully. """ def __init__(self, child, message): self.child = to_parse_node(child) self.message = message def _parse(self, ctx): ctx.skip() start_idx = ctx.index if self.child.parse(ctx).succeeded(): start_token = ctx.tokens[start_idx] end_token = ctx.tokens[ctx.index] ctx.warnings.append(CompileWarning(self.message, start_token.start, end_token.end)) return True class Fail(ParseNode): """ ParseNode that emits a compile error if it parses successfully. """ def __init__(self, child, message): self.child = to_parse_node(child) self.message = message def _parse(self, ctx): if self.child.parse(ctx).succeeded(): start_idx = ctx.start while ctx.tokens[start_idx].type in SKIP_TOKENS: start_idx += 1 start_token = ctx.tokens[start_idx] end_token = ctx.tokens[ctx.index] raise CompileError(self.message, start_token.start, end_token.end) return True class Group(ParseNode): """ ParseNode that creates a match group. """ def __init__(self, ast_type, child): self.ast_type = ast_type self.child = to_parse_node(child) def _parse(self, ctx: ParseContext) -> bool: ctx.skip() ctx.start_group(self.ast_type) return self.child.parse(ctx).succeeded() class Sequence(ParseNode): """ ParseNode that attempts to match all of its children in sequence. """ def __init__(self, *children): self.children = [to_parse_node(child) for child in children] def _parse(self, ctx) -> bool: for child in self.children: if child.parse(ctx).failed(): return False return True class Statement(ParseNode): """ ParseNode that attempts to match all of its children in sequence. If any child raises an error, the error will be logged but parsing will continue. """ def __init__(self, *children): self.children = [to_parse_node(child) for child in children] def _parse(self, ctx) -> bool: for child in self.children: try: if child.parse(ctx).failed(): return False except CompileError as e: ctx.errors.append(e) ctx.set_group_incomplete() return True token = ctx.peek_token() if str(token) != ";": ctx.errors.append(CompileError("Expected `;`", token.start, token.end)) else: ctx.next_token() return True class AnyOf(ParseNode): """ ParseNode that attempts to match exactly one of its children. Child nodes are attempted in order. """ def __init__(self, *children): self.children = children @property def children(self): return self._children @children.setter def children(self, children): self._children = [to_parse_node(child) for child in children] def _parse(self, ctx): for child in self.children: if child.parse(ctx).succeeded(): return True return False class Until(ParseNode): """ ParseNode that repeats its child until a delimiting token is found. If the child does not match, one token is skipped and the match is attempted again. """ def __init__(self, child, delimiter): self.child = to_parse_node(child) self.delimiter = to_parse_node(delimiter) def _parse(self, ctx): while not self.delimiter.parse(ctx).succeeded(): try: if not self.child.parse(ctx).matched(): ctx.skip_unexpected_token() except CompileError as e: ctx.errors.append(e) ctx.next_token() if ctx.is_eof(): return True return True class ZeroOrMore(ParseNode): """ ParseNode that matches its child any number of times (including zero times). It cannot fail to parse. If its child raises an exception, one token will be skipped and parsing will continue. """ def __init__(self, child): self.child = to_parse_node(child) def _parse(self, ctx): while True: try: if not self.child.parse(ctx).matched(): return True except CompileError as e: ctx.errors.append(e) ctx.next_token() class Delimited(ParseNode): """ ParseNode that matches its first child any number of times (including zero times) with its second child in between and optionally at the end. """ def __init__(self, child, delimiter): self.child = to_parse_node(child) self.delimiter = to_parse_node(delimiter) def _parse(self, ctx): while self.child.parse(ctx).matched() and self.delimiter.parse(ctx).matched(): pass return True class Optional(ParseNode): """ ParseNode that matches its child zero or one times. It cannot fail to parse. """ def __init__(self, child): self.child = to_parse_node(child) def _parse(self, ctx): self.child.parse(ctx) return True class Eof(ParseNode): """ ParseNode that matches an EOF token. """ def _parse(self, ctx: ParseContext) -> bool: token = ctx.next_token() return token.type == TokenType.EOF class Match(ParseNode): """ ParseNode that matches the given literal token. """ def __init__(self, op): self.op = op def _parse(self, ctx: ParseContext) -> bool: token = ctx.next_token() return str(token) == self.op def expected(self, expect: str = None): """ Convenience method for err(). """ if expect is None: return self.err(f"Expected '{self.op}'") else: return self.err("Expected " + expect) class UseIdent(ParseNode): """ ParseNode that matches any identifier and sets it in a key=value pair on the containing match group. """ def __init__(self, key): self.key = key def _parse(self, ctx: ParseContext): token = ctx.next_token() if token.type != TokenType.IDENT: return False ctx.set_group_val(self.key, str(token), token) return True class UseNumber(ParseNode): """ ParseNode that matches a number and sets it in a key=value pair on the containing match group. """ def __init__(self, key): self.key = key def _parse(self, ctx: ParseContext): token = ctx.next_token() if token.type != TokenType.NUMBER: return False number = token.get_number() if number % 1.0 == 0: number = int(number) ctx.set_group_val(self.key, number, token) return True class UseNumberText(ParseNode): """ ParseNode that matches a number, but sets its *original text* it in a key=value pair on the containing match group. """ def __init__(self, key): self.key = key def _parse(self, ctx: ParseContext): token = ctx.next_token() if token.type != TokenType.NUMBER: return False ctx.set_group_val(self.key, str(token), token) return True class UseQuoted(ParseNode): """ ParseNode that matches a quoted string and sets it in a key=value pair on the containing match group. """ def __init__(self, key): self.key = key def _parse(self, ctx: ParseContext): token = ctx.next_token() if token.type != TokenType.QUOTED: return False string = (str(token)[1:-1] .replace("\\n", "\n") .replace("\\\"", "\"") .replace("\\\\", "\\") .replace("\\'", "\'")) ctx.set_group_val(self.key, string, token) return True class UseLiteral(ParseNode): """ ParseNode that doesn't match anything, but rather sets a static key=value pair on the containing group. Useful for, e.g., property and signal flags: `Sequence(Keyword("swapped"), UseLiteral("swapped", True))` """ def __init__(self, key, literal): self.key = key self.literal = literal def _parse(self, ctx: ParseContext): ctx.set_group_val(self.key, self.literal, None) return True class Keyword(ParseNode): """ Matches the given identifier and sets it as a named token, with the name being the identifier itself. """ def __init__(self, kw): self.kw = kw self.set_token = True def _parse(self, ctx: ParseContext): token = ctx.next_token() ctx.set_group_val(self.kw, True, token) return str(token) == self.kw def to_parse_node(value) -> ParseNode: if isinstance(value, str): return Match(value) elif isinstance(value, list): return Sequence(*value) elif isinstance(value, type) and hasattr(value, "grammar"): return Group(value, getattr(value, "grammar")) elif isinstance(value, ParseNode): return value else: raise CompilerBugError() blueprint-compiler-main/blueprintcompiler/parser.py000066400000000000000000000025751420774714100232410ustar00rootroot00000000000000# parser.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later from .errors import MultipleErrors, PrintableError from .parse_tree import * from .parser_utils import * from .tokenizer import TokenType from .language import OBJECT_HOOKS, OBJECT_CONTENT_HOOKS, VALUE_HOOKS, Template, UI def parse(tokens) -> T.Tuple[UI, T.Optional[MultipleErrors], T.List[PrintableError]]: """ Parses a list of tokens into an abstract syntax tree. """ ctx = ParseContext(tokens) AnyOf(UI).parse(ctx) ast_node = ctx.last_group.to_ast() if ctx.last_group else None errors = MultipleErrors(ctx.errors) if len(ctx.errors) else None warnings = ctx.warnings return (ast_node, errors, warnings) blueprint-compiler-main/blueprintcompiler/parser_utils.py000066400000000000000000000020151420774714100244460ustar00rootroot00000000000000# parser_utils.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later from .parse_tree import * class_name = AnyOf( [ UseIdent("namespace"), ".", UseIdent("class_name"), ], [ ".", UseIdent("class_name"), UseLiteral("ignore_gir", True), ], UseIdent("class_name"), ) blueprint-compiler-main/blueprintcompiler/tokenizer.py000066400000000000000000000053201420774714100237460ustar00rootroot00000000000000# tokenizer.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later import typing as T import re from enum import Enum from .errors import CompileError class TokenType(Enum): EOF = 0 IDENT = 1 QUOTED = 2 NUMBER = 3 OP = 4 WHITESPACE = 5 COMMENT = 6 PUNCTUATION = 7 _tokens = [ (TokenType.IDENT, r"[A-Za-z_][\d\w\-_]*"), (TokenType.QUOTED, r'"(\\"|[^"\n])*"'), (TokenType.QUOTED, r"'(\\'|[^'\n])*'"), (TokenType.NUMBER, r"[-+]?[\d_]+(\.[\d_]+)?"), (TokenType.NUMBER, r"0x[A-Fa-f0-9]+"), (TokenType.WHITESPACE, r"\s+"), (TokenType.COMMENT, r"\/\*[\s\S]*?\*\/"), (TokenType.COMMENT, r"\/\/[^\n]*"), (TokenType.OP, r"[:=\.=\|<>\+\-/\*]+"), (TokenType.PUNCTUATION, r"\(|\)|\{|\}|;|\[|\]|\,"), ] _TOKENS = [(type, re.compile(regex)) for (type, regex) in _tokens] class Token: def __init__(self, type, start, end, string): self.type = type self.start = start self.end = end self.string = string def __str__(self): return self.string[self.start:self.end] def get_number(self): if self.type != TokenType.NUMBER: return None string = str(self) if string.startswith("0x"): return int(string, 16) else: return float(string) def _tokenize(ui_ml: str): i = 0 while i < len(ui_ml): matched = False for (type, regex) in _TOKENS: match = regex.match(ui_ml, i) if match is not None: yield Token(type, match.start(), match.end(), ui_ml) i = match.end() matched = True break if not matched: raise CompileError("Could not determine what kind of syntax is meant here", i, i) yield Token(TokenType.EOF, i, i, ui_ml) def tokenize(data: str) -> T.List[Token]: return list(_tokenize(data)) blueprint-compiler-main/blueprintcompiler/utils.py000066400000000000000000000057231420774714100231030ustar00rootroot00000000000000# utils.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later import typing as T class Colors: RED = '\033[91m' GREEN = '\033[92m' YELLOW = '\033[33m' FAINT = '\033[2m' BOLD = '\033[1m' BLUE = '\033[34m' UNDERLINE = '\033[4m' NO_UNDERLINE = '\033[24m' CLEAR = '\033[0m' def lazy_prop(func): key = "_lazy_prop_" + func.__name__ @property def real_func(self): if key not in self.__dict__: self.__dict__[key] = func(self) return self.__dict__[key] return real_func def did_you_mean(word: str, options: T.List[str]) -> T.Optional[str]: if len(options) == 0: return None def levenshtein(a, b): # see https://en.wikipedia.org/wiki/Levenshtein_distance m = len(a) n = len(b) distances = [[0 for j in range(n)] for i in range(m)] for i in range(m): distances[i][0] = i for j in range(n): distances[0][j] = j for j in range(1, n): for i in range(1, m): cost = 0 if a[i] != b[j]: if a[i].casefold() == b[j].casefold(): cost = 1 else: cost = 2 distances[i][j] = min(distances[i-1][j] + 2, distances[i][j-1] + 2, distances[i-1][j-1] + cost) return distances[m-1][n-1] distances = [(option, levenshtein(word, option)) for option in options] closest = min(distances, key=lambda item:item[1]) if closest[1] <= 5: return closest[0] return None def idx_to_pos(idx: int, text: str) -> T.Tuple[int, int]: if idx == 0: return (0, 0) sp = text[:idx].splitlines(keepends=True) line_num = len(sp) col_num = len(sp[-1]) return (line_num - 1, col_num) def pos_to_idx(line: int, col: int, text: str) -> int: lines = text.splitlines(keepends=True) return sum([len(line) for line in lines[:line]]) + col def idxs_to_range(start: int, end: int, text: str): start_l, start_c = idx_to_pos(start, text) end_l, end_c = idx_to_pos(end, text) return { "start": { "line": start_l, "character": start_c, }, "end": { "line": end_l, "character": end_c, }, } blueprint-compiler-main/blueprintcompiler/xml_emitter.py000066400000000000000000000041051420774714100242650ustar00rootroot00000000000000# xml_emitter.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later from xml.sax import saxutils class XmlEmitter: def __init__(self, indent=2): self.indent = indent self.result = '' self._tag_stack = [] self._needs_newline = False def start_tag(self, tag, **attrs): self._indent() self.result += f"<{tag}" for key, val in attrs.items(): if val is not None: self.result += f' {key.replace("_", "-")}="{saxutils.escape(str(val))}"' self.result += ">" self._tag_stack.append(tag) self._needs_newline = False def put_self_closing(self, tag, **attrs): self._indent() self.result += f"<{tag}" for key, val in attrs.items(): if val is not None: self.result += f' {key}="{saxutils.escape(str(val))}"' self.result += "/>" self._needs_newline = True def end_tag(self): tag = self._tag_stack.pop() if self._needs_newline: self._indent() self.result += f"" self._needs_newline = True def put_text(self, text): self.result += saxutils.escape(str(text)) self._needs_newline = False def _indent(self): if self.indent is not None: self.result += "\n" + " " * (self.indent * len(self._tag_stack)) blueprint-compiler-main/blueprintcompiler/xml_reader.py000066400000000000000000000054021420774714100240570ustar00rootroot00000000000000# xml_reader.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later from collections import defaultdict import typing as T from xml import sax from .utils import lazy_prop # To speed up parsing, we ignore all tags except these PARSE_GIR = set([ "repository", "namespace", "class", "interface", "property", "glib:signal", "include", "implements", "type", "parameter", "parameters", "enumeration", "member", "bitfield", ]) class Element: def __init__(self, tag, attrs: T.Dict[str, str]): self.tag = tag self.attrs = attrs self.children: T.Dict[str, T.List["Element"]] = defaultdict(list) self.cdata_chunks: T.List[str] = [] @lazy_prop def cdata(self): return ''.join(self.cdata_chunks) def get_elements(self, name) -> T.List["Element"]: return self.children.get(name, []) def __getitem__(self, key): return self.attrs.get(key) class Handler(sax.handler.ContentHandler): def __init__(self, parse_type): self.root = None self.stack = [] self.skipping = 0 self._interesting_elements = parse_type def startElement(self, name, attrs): if self._interesting_elements is not None and name not in self._interesting_elements: self.skipping += 1 if self.skipping > 0: return element = Element(name, attrs.copy()) if len(self.stack): last = self.stack[-1] last.children[name].append(element) else: self.root = element self.stack.append(element) def endElement(self, name): if self.skipping == 0: self.stack.pop() if self._interesting_elements is not None and name not in self._interesting_elements: self.skipping -= 1 def characters(self, content): if not self.skipping: self.stack[-1].cdata_chunks.append(content) def parse(filename, parse_type=None): parser = sax.make_parser() handler = Handler(parse_type) parser.setContentHandler(handler) parser.parse(filename) return handler.root blueprint-compiler-main/build-aux/000077500000000000000000000000001420774714100175155ustar00rootroot00000000000000blueprint-compiler-main/build-aux/Dockerfile000066400000000000000000000002631420774714100215100ustar00rootroot00000000000000FROM fedora:latest RUN dnf install -y meson python3-pip gtk4-devel gobject-introspection-devel libadwaita-devel RUN pip3 install furo mypy sphinx coverage RUN dnf install -y git blueprint-compiler-main/docs/000077500000000000000000000000001420774714100165535ustar00rootroot00000000000000blueprint-compiler-main/docs/conf.py000066400000000000000000000035001420774714100200500ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- Project information ----------------------------------------------------- project = 'Blueprint' copyright = '2021, James Westman' author = 'James Westman' # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'furo' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] blueprint-compiler-main/docs/examples.rst000066400000000000000000000152661420774714100211350ustar00rootroot00000000000000======== Examples ======== Namespaces and libraries ------------------------ GTK declaration ~~~~~~~~~~~~~~~ .. code-block:: // Required in every blueprint file. Defines the major version // of GTK the file is designed for. using Gtk 4.0; Importing libraries ~~~~~~~~~~~~~~~~~~~ .. code-block:: // Import Adwaita 1. The name given here is the GIR namespace name, which // might not match the library name or C prefix. using Adw 1; Objects ------- Defining objects with properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: Gtk.Box { orientation: vertical; Gtk.Label { label: "Hello, world!"; } } Referencing an object in code ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: // Your code can reference the object by `my_window` Gtk.Window my_window { title: "My window"; } Using classes defined by your app ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Use a leading ``.`` to tell the compiler that the class is defined in your app, not in the GIR, so it should skip validation. .. code-block:: .MyAppCustomWidget my_widget { my-custom-property: 3.14; } Templates --------- Defining a template ~~~~~~~~~~~~~~~~~~~ Many language bindings have a way to create subclasses that are defined both in code and in the blueprint file. Check your language's documentation on how to use this feature. In this example, we create a class called ``MyAppWindow`` that inherits from ``Gtk.ApplicationWindow``. .. code-block:: template MyAppWindow : Gtk.ApplicationWindow { my-custom-property: 3.14; } Properties ---------- Translations ~~~~~~~~~~~~ Use ``_("...")`` to mark strings as translatable. You can put a comment for translators on the line above if needed. .. code-block:: Gtk.Label label { /* Translators: This is the main text of the welcome screen */ label: _("Hello, world!"); } Use ``C_("context", "...")`` to add a *message context* to a string to disambiguate it, in case the same string appears in different places. Remember, two strings might be the same in one language but different in another depending on context. .. code-block:: Gtk.Label label { /* Translators: This is a section in the preferences window */ label: C_("preferences window", "Hello, world!"); } Referencing objects by ID ~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: Gtk.Range range1 { adjustment: my_adjustment; } Gtk.Range range2 { adjustment: my_adjustment; } Gtk.Adjustment my_adjustment { } Defining object properties inline ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: Gtk.Range { adjustment: Gtk.Adjustment my_adjustment { value: 10; }; } Gtk.Range range1 { // You can even still reference the object by ID adjustment: my_adjustment; } .. note:: Note the semicolon after the closing brace of the ``Gtk.Adjustment``. It is required. Bindings ~~~~~~~~ Use the ``bind`` keyword to bind a property to another object's property in the same file. .. code-block:: Gtk.ProgressBar bar1 { } Gtk.ProgressBar bar2 { value: bind bar1.value; } Binding Flags ~~~~~~~~~~~~~ Use the ``no-sync-create`` keyword to only update the target value when the source value changes, not when the binding is first created. .. code-block:: Gtk.ProgressBar bar1 { value: 10; } Gtk.ProgressBar bar2 { value: bind bar1.value no-sync-create; } Use the ``bidirectional`` keyword to bind properties in both directions. .. code-block:: // Text of entry1 is bound to text // of entry2 and vice versa Gtk.Entry entry1 { text: bind entry2.text bidirectional; } Gtk.Entry entry2 { } Use the ``inverted`` keyword to invert to bind a boolean property to inverted value of another one. .. code-block:: // When switch1 is on, switch2 will be off Gtk.Switch switch1 { active: bind switch2.active inverted bidirectional; } // When switch2 is on, switch1 will be off Gtk.Switch switch2 { } Signals ------- Basic Usage ~~~~~~~~~~~ .. code-block:: Gtk.Button { // on_button_clicked is defined in your application clicked => on_button_clicked(); } Flags ~~~~~ .. code-block:: Gtk.Button { clicked => on_button_clicked() swapped; } Object ~~~~~~ By default the widget is passed to callback as first argument. However, you can specify another object to use as first argument of callback. .. code-block:: Gtk.Entry { activate => grab_focus(another_entry); } Gtk.Entry another_entry { } CSS Styles ---------- Basic Usage ~~~~~~~~~~~ .. code-block:: Gtk.Label { styles ["dim-label", "title"] } Menus ----- Basic Usage ~~~~~~~~~~~ .. code-block:: menu my_menu { section { label: _("File"); item { label: _("Open"); action: "win.open"; } item { label: _("Save"); action: "win.save"; } submenu { label: _("Save As"); item { label: _("PDF"); action: "win.save_as_pdf"; } } } } Item Shorthand ~~~~~~~~~~~~~~ For menu items with only a label, action, and/or icon, you can define all three on one line. The action and icon are optional. .. code-block:: menu { item (_("Copy"), "app.copy", "copy-symbolic") } Layout Properties ----------------- Basic Usage ~~~~~~~~~~~ .. code-block:: Gtk.Grid { Gtk.Label { layout { row: 0; column: 1; } } } Accessibility Properties ------------------------ Basic Usage ~~~~~~~~~~~ .. code-block:: Gtk.Widget { accessibility { orientation: vertical; labelled_by: my_label; checked: true; } } Gtk.Label my_label {} Widget-Specific Items --------------------- Gtk.ComboBoxText ~~~~~~~~~~~~~~~~ .. code-block:: Gtk.ComboBoxText { items [ item1: "Item 1", item2: _("Items can be translated"), "The item ID is not required", ] } Gtk.FileFilter ~~~~~~~~~~~~~~ .. code-block:: Gtk.FileFilter { mime-types ["image/jpeg", "video/webm"] patterns ["*.txt"] suffixes ["png"] } Gtk.SizeGroup ~~~~~~~~~~~~~ .. code-block:: Gtk.SizeGroup { mode: both; widgets [label1, label2] } Gtk.Label label1 {} Gtk.Label label2 {} Gtk.StringList ~~~~~~~~~~~~~~ .. code-block:: Gtk.StringList { strings ["Hello, world!", _("Translated string")] } Gtk.Dialog and Gtk.InfoBar ~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: Gtk.Dialog { [action response=ok] Gtk.Button ok_response {} [action response=cancel] Gtk.Button cancel_response {} [action response=9] Gtk.Button app_defined_response {} } blueprint-compiler-main/docs/index.rst000066400000000000000000000031561420774714100204210ustar00rootroot00000000000000Overview ======== Blueprint is a markup language and compiler for GTK 4 user interfaces. .. toctree:: :maxdepth: 1 :caption: Contents: setup examples .. code-block:: using Gtk 4.0; template MyAppWindow : ApplicationWindow { default-width: 600; default-height: 300; title: _("Hello, Blueprint!"); [titlebar] HeaderBar {} Label { label: bind MyAppWindow.main_text; } } Blueprint helps you build user interfaces in GTK quickly and declaratively. It has modern IDE features like code completion and hover documentation, and the compiler points out mistakes early on so you can focus on making your app look amazing. Features -------- - **Easy setup.** A porting tool is available to help port your projects from XML. The compiler's only dependency is Python, and it can be included as a meson subproject. :doc:`See the Setup page for more information. ` - **Concise syntax.** No more clumsy XML! Blueprint is designed from the ground up to match GTK's widget model, including templates, child types, signal handlers, and menus. - **Easy to learn.** The syntax should be very familiar to most people. Scroll through the :doc:`examples page ` for a quick overview of the whole language. - **Modern tooling.** IDE integration for `GNOME Builder `_ is in progress, and a VS Code extension is also planned. Links ----- - `Source code `_ - `Vim syntax highlighting plugin `_ blueprint-compiler-main/docs/meson.build000066400000000000000000000004521420774714100207160ustar00rootroot00000000000000if get_option('docs') sphinx = find_program(['sphinx-build-3', 'sphinx-build'], required: true) custom_target('docs', command: [sphinx, '-b', 'html', '-c', meson.current_source_dir(), meson.current_source_dir(), '@OUTPUT@'], output: 'en', build_by_default: true ) endif blueprint-compiler-main/docs/setup.rst000066400000000000000000000033251420774714100204500ustar00rootroot00000000000000===== Setup ===== Setting up Blueprint on a new or existing project ------------------------------------------------- Using the porting tool ~~~~~~~~~~~~~~~~~~~~~~ Clone `blueprint-compiler `_ from source. You can install it using ``meson _build`` and ``ninja -C _build install``, or you can leave it uninstalled. In your project's directory, run ``blueprint-compiler port`` (or `` port``) to start the porting process. It will walk you through the steps outlined below. It should work for most projects, but if something goes wrong you may need to follow the manual steps instead. Manually ~~~~~~~~ blueprint-compiler works as a meson subproject. #. Save the following file as ``subprojects/blueprint-compiler.wrap``: .. code-block:: cfg [wrap-git] directory = blueprint-compiler url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git revision = main depth = 1 [provide] program_names = blueprint-compiler #. Add this to your ``.gitignore``: .. code-block:: /subprojects/blueprint-compiler #. Rewrite your .ui XML files in blueprint format. #. Add this to the ``meson.build`` file where you build your GResources: .. code-block:: meson.build blueprints = custom_target('blueprints', input: files( # LIST YOUR BLUEPRINT FILES HERE ), output: '.', command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'], ) #. In the same ``meson.build`` file, add this argument to your ``gnome.compile_resources`` command: .. code-block:: meson.build dependencies: blueprints, blueprint-compiler-main/meson.build000066400000000000000000000013741420774714100177720ustar00rootroot00000000000000project('blueprint-compiler', version: '0.1.0', ) subdir('docs') prefix = get_option('prefix') libdir = join_paths(prefix, get_option('libdir')) py = import('python').find_installation('python3') configure_file( input: 'blueprint-compiler.pc.in', output: 'blueprint-compiler.pc', configuration: { 'VERSION': meson.project_version() }, install: not meson.is_subproject(), install_dir: join_paths(libdir, 'pkgconfig'), ) install_data( 'blueprint-compiler.py', install_dir: get_option('bindir'), rename: 'blueprint-compiler', ) meson.override_find_program('blueprint-compiler', find_program('blueprint-compiler.py')) if not meson.is_subproject() install_subdir('blueprintcompiler', install_dir: py.get_install_dir()) endif subdir('tests') blueprint-compiler-main/meson_options.txt000066400000000000000000000000561420774714100212610ustar00rootroot00000000000000option('docs', type: 'boolean', value: false) blueprint-compiler-main/tests/000077500000000000000000000000001420774714100167655ustar00rootroot00000000000000blueprint-compiler-main/tests/__init__.py000066400000000000000000000000001420774714100210640ustar00rootroot00000000000000blueprint-compiler-main/tests/meson.build000066400000000000000000000001121420774714100211210ustar00rootroot00000000000000test('tests', py, args: ['-m', 'unittest'], workdir: meson.source_root()) blueprint-compiler-main/tests/sample_errors/000077500000000000000000000000001420774714100216425ustar00rootroot00000000000000blueprint-compiler-main/tests/sample_errors/a11y_in_non_widget.blp000066400000000000000000000001111420774714100260100ustar00rootroot00000000000000using Gtk 4.0; using GObject 2.0; GObject.Object { accessibility {} } blueprint-compiler-main/tests/sample_errors/a11y_in_non_widget.err000066400000000000000000000001271420774714100260320ustar00rootroot000000000000005,3,13,GObject.Object is not a Gtk.Widget, so it doesn't have accessibility properties blueprint-compiler-main/tests/sample_errors/a11y_prop_dne.blp000066400000000000000000000001221420774714100247750ustar00rootroot00000000000000using Gtk 4.0; Widget { accessibility { not_a_prop: "Hello, world!"; } } blueprint-compiler-main/tests/sample_errors/a11y_prop_dne.err000066400000000000000000000001111420774714100250060ustar00rootroot000000000000005,5,10,'not_a_prop' is not an accessibility property, relation, or state blueprint-compiler-main/tests/sample_errors/a11y_prop_obj_dne.blp000066400000000000000000000001211420774714100256260ustar00rootroot00000000000000using Gtk 4.0; Widget { accessibility { labelled_by: not_an_object; } } blueprint-compiler-main/tests/sample_errors/a11y_prop_obj_dne.err000066400000000000000000000000641420774714100256470ustar00rootroot000000000000005,18,13,Could not find object with ID not_an_object blueprint-compiler-main/tests/sample_errors/a11y_prop_type.blp000066400000000000000000000001051420774714100252110ustar00rootroot00000000000000using Gtk 4.0; Widget { accessibility { orientation: 1; } } blueprint-compiler-main/tests/sample_errors/a11y_prop_type.err000066400000000000000000000000531420774714100252260ustar00rootroot000000000000005,18,1,Cannot convert 1 to Gtk.Orientation blueprint-compiler-main/tests/sample_errors/action_widget_float_response.blp000066400000000000000000000001401420774714100302570ustar00rootroot00000000000000using Gtk 4.0; Dialog { [action response=17.9] Button float_response_button { } } blueprint-compiler-main/tests/sample_errors/action_widget_float_response.err000066400000000000000000000001121420774714100302710ustar00rootroot000000000000004,22,4,Response type must be GtkResponseType member or integer, not float blueprint-compiler-main/tests/sample_errors/action_widget_have_no_id.blp000066400000000000000000000001471420774714100273360ustar00rootroot00000000000000using Gtk 4.0; Dialog { [action response=cancel] Button { label: _("Cancel"); } } blueprint-compiler-main/tests/sample_errors/action_widget_have_no_id.err000066400000000000000000000000431420774714100273440ustar00rootroot000000000000004,13,15,Action widget must have ID blueprint-compiler-main/tests/sample_errors/action_widget_in_invalid_container.blp000066400000000000000000000001171420774714100314160ustar00rootroot00000000000000using Gtk 4.0; Box { [action response=ok] Button ok_button { } } blueprint-compiler-main/tests/sample_errors/action_widget_in_invalid_container.err000066400000000000000000000000541420774714100314310ustar00rootroot000000000000004,13,11,Gtk.Box doesn't have action widgets blueprint-compiler-main/tests/sample_errors/action_widget_multiple_default.blp000066400000000000000000000002341420774714100305770ustar00rootroot00000000000000using Gtk 4.0; Dialog { [action response=yes default] Button yes_button { } [action response=no default] Button no_button { } } blueprint-compiler-main/tests/sample_errors/action_widget_multiple_default.err000066400000000000000000000000471420774714100306140ustar00rootroot000000000000009,25,7,Default response is already set blueprint-compiler-main/tests/sample_errors/action_widget_negative_response.blp000066400000000000000000000001441420774714100307600ustar00rootroot00000000000000using Gtk 4.0; Dialog { [action response = -179] Button numeric_response_button { } } blueprint-compiler-main/tests/sample_errors/action_widget_negative_response.err000066400000000000000000000000571420774714100307760ustar00rootroot000000000000004,24,4,Numeric response type can't be negative blueprint-compiler-main/tests/sample_errors/action_widget_not_action.blp000066400000000000000000000001251420774714100273740ustar00rootroot00000000000000using Gtk 4.0; Dialog { [some_type response=ok] Button ok_button { } } blueprint-compiler-main/tests/sample_errors/action_widget_not_action.err000066400000000000000000000000601420774714100274050ustar00rootroot000000000000004,16,11,Only action widget can have response ID blueprint-compiler-main/tests/sample_errors/action_widget_response_dne.blp000066400000000000000000000001441420774714100277240ustar00rootroot00000000000000using Gtk 4.0; Dialog { [action response=hello-world] Button hello_world_button { } } blueprint-compiler-main/tests/sample_errors/action_widget_response_dne.err000066400000000000000000000000621420774714100277360ustar00rootroot000000000000004,22,11,Response type "hello-world" doesn't exist blueprint-compiler-main/tests/sample_errors/assign_inline_menu.blp000066400000000000000000000000551420774714100262070ustar00rootroot00000000000000using Gtk 4.0; Button { label: menu {}; } blueprint-compiler-main/tests/sample_errors/assign_inline_menu.err000066400000000000000000000000551420774714100262220ustar00rootroot000000000000004,3,15,Cannot assign Gio.MenuModel to string blueprint-compiler-main/tests/sample_errors/bitfield_member_dne.blp000066400000000000000000000001301420774714100262720ustar00rootroot00000000000000using Gtk 4.0; EventControllerScroll { flags: vertical | not_a_value; name: a|b; } blueprint-compiler-main/tests/sample_errors/bitfield_member_dne.err000066400000000000000000000001521420774714100263110ustar00rootroot000000000000004,21,11,not_a_value is not a member of Gtk.EventControllerScrollFlags 5,9,3,string is not a bitfield type blueprint-compiler-main/tests/sample_errors/class_assign.blp000066400000000000000000000000631420774714100250110ustar00rootroot00000000000000using Gtk 4.0; Box box {} Label { label: box; } blueprint-compiler-main/tests/sample_errors/class_assign.err000066400000000000000000000000471420774714100250260ustar00rootroot000000000000005,10,3,Cannot assign Gtk.Box to string blueprint-compiler-main/tests/sample_errors/class_dne.blp000066400000000000000000000000751420774714100242760ustar00rootroot00000000000000using Gtk 4.0; template TestTemplate : Gtk.NotARealClass {} blueprint-compiler-main/tests/sample_errors/class_dne.err000066400000000000000000000001041420774714100243020ustar00rootroot000000000000003,29,13,Namespace Gtk does not contain a class called NotARealClass blueprint-compiler-main/tests/sample_errors/consecutive_unexpected_tokens.blp000066400000000000000000000001271420774714100304770ustar00rootroot00000000000000using Gtk 4.0; Button { visible: false; not actually blueprint code; Label {} } blueprint-compiler-main/tests/sample_errors/consecutive_unexpected_tokens.err000066400000000000000000000000311420774714100305040ustar00rootroot000000000000005,3,31,Unexpected tokens blueprint-compiler-main/tests/sample_errors/does_not_implement.blp000066400000000000000000000000751420774714100262270ustar00rootroot00000000000000using Gtk 4.0; Label label {} DropDown { model: label; } blueprint-compiler-main/tests/sample_errors/does_not_implement.err000066400000000000000000000000601420774714100262340ustar00rootroot000000000000006,10,5,Cannot assign Gtk.Label to Gio.ListModel blueprint-compiler-main/tests/sample_errors/duplicate_obj_id.blp000066400000000000000000000000661420774714100256230ustar00rootroot00000000000000using Gtk 4.0; Gtk.Label label {} Gtk.Label label {} blueprint-compiler-main/tests/sample_errors/duplicate_obj_id.err000066400000000000000000000000431420774714100256310ustar00rootroot000000000000004,11,5,Duplicate object ID 'label' blueprint-compiler-main/tests/sample_errors/enum_member_dne.blp000066400000000000000000000000611420774714100254570ustar00rootroot00000000000000using Gtk 4.0; Box { orientation: diagonal; } blueprint-compiler-main/tests/sample_errors/enum_member_dne.err000066400000000000000000000000631420774714100254740ustar00rootroot000000000000004,16,8,diagonal is not a member of Gtk.Orientation blueprint-compiler-main/tests/sample_errors/filters_in_non_file_filter.blp000066400000000000000000000001071420774714100277130ustar00rootroot00000000000000using Gtk 4.0; Widget { mime-types [] patterns [] suffixes [] } blueprint-compiler-main/tests/sample_errors/filters_in_non_file_filter.err000066400000000000000000000003771420774714100277370ustar00rootroot000000000000004,3,13,Gtk.Widget is not a Gtk.FileFilter, so it doesn't have file filter properties 5,3,11,Gtk.Widget is not a Gtk.FileFilter, so it doesn't have file filter properties 6,3,11,Gtk.Widget is not a Gtk.FileFilter, so it doesn't have file filter properties blueprint-compiler-main/tests/sample_errors/invalid_bool.blp000066400000000000000000000000541420774714100250010ustar00rootroot00000000000000using Gtk 4.0; Label { visible: maybe; } blueprint-compiler-main/tests/sample_errors/invalid_bool.err000066400000000000000000000000641420774714100250150ustar00rootroot000000000000004,12,5,Expected 'true' or 'false' for boolean value blueprint-compiler-main/tests/sample_errors/layout_in_non_widget.blp000066400000000000000000000001021420774714100265520ustar00rootroot00000000000000using Gtk 4.0; using GObject 2.0; GObject.Object { layout {} } blueprint-compiler-main/tests/sample_errors/layout_in_non_widget.err000066400000000000000000000001171420774714100265730ustar00rootroot000000000000005,3,6,GObject.Object is not a Gtk.Widget, so it doesn't have layout properties blueprint-compiler-main/tests/sample_errors/not_a_class.blp000066400000000000000000000000721420774714100246250ustar00rootroot00000000000000using Gtk 4.0; template TestTemplate : Gtk.Orientable {} blueprint-compiler-main/tests/sample_errors/not_a_class.err000066400000000000000000000000461420774714100246410ustar00rootroot000000000000003,29,10,Gtk.Orientable is not a class blueprint-compiler-main/tests/sample_errors/ns_not_imported.blp000066400000000000000000000001011420774714100255340ustar00rootroot00000000000000using Gtk 4.0; template TestTemplate : Adw.ApplicationWindow {} blueprint-compiler-main/tests/sample_errors/ns_not_imported.err000066400000000000000000000000461420774714100255570ustar00rootroot000000000000003,25,3,Namespace Adw was not imported blueprint-compiler-main/tests/sample_errors/obj_class_dne.blp000066400000000000000000000000421420774714100251220ustar00rootroot00000000000000using Gtk 4.0; NotARealWidget {} blueprint-compiler-main/tests/sample_errors/obj_in_string_list.blp000066400000000000000000000001051420774714100262160ustar00rootroot00000000000000using Gtk 4.0; StringList { strings [ id, ] } Widget id {} blueprint-compiler-main/tests/sample_errors/obj_in_string_list.err000066400000000000000000000000511420774714100262310ustar00rootroot000000000000005,5,2,Cannot assign Gtk.Widget to string blueprint-compiler-main/tests/sample_errors/obj_prop_type.blp000066400000000000000000000000621420774714100252120ustar00rootroot00000000000000using Gtk 4.0; Scale { adjustment: Label {}; } blueprint-compiler-main/tests/sample_errors/obj_prop_type.err000066400000000000000000000000611420774714100252240ustar00rootroot000000000000004,3,21,Cannot assign Gtk.Label to Gtk.Adjustment blueprint-compiler-main/tests/sample_errors/object_dne.blp000066400000000000000000000000551420774714100244350ustar00rootroot00000000000000using Gtk 4.0; Label { label: my_label; } blueprint-compiler-main/tests/sample_errors/object_dne.err000066400000000000000000000000561420774714100244510ustar00rootroot000000000000004,10,8,Could not find object with ID my_label blueprint-compiler-main/tests/sample_errors/property_dne.blp000066400000000000000000000001021420774714100250440ustar00rootroot00000000000000using Gtk 4.0; Label { not-a-real-property: "Hello, world!"; } blueprint-compiler-main/tests/sample_errors/property_dne.err000066400000000000000000000001161420774714100250640ustar00rootroot000000000000004,3,19,Class Gtk.Label does not contain a property called not-a-real-property blueprint-compiler-main/tests/sample_errors/signal_dne.blp000066400000000000000000000001241420774714100244410ustar00rootroot00000000000000using Gtk 4.0; Button { eaten-by-velociraptors => on_eaten_by_velociraptors(); } blueprint-compiler-main/tests/sample_errors/signal_dne.err000066400000000000000000000001201420774714100244500ustar00rootroot000000000000004,3,22,Class Gtk.Button does not contain a signal called eaten-by-velociraptors blueprint-compiler-main/tests/sample_errors/signal_object_dne.blp000066400000000000000000000000751420774714100257740ustar00rootroot00000000000000using Gtk 4.0; Button { clicked => function(dinosaur); }blueprint-compiler-main/tests/sample_errors/signal_object_dne.err000066400000000000000000000000571420774714100260070ustar00rootroot000000000000004,25,8,Could not find object with ID 'dinosaur'blueprint-compiler-main/tests/sample_errors/size_group_non_widget.blp000066400000000000000000000001621420774714100267430ustar00rootroot00000000000000using Gtk 4.0; using GObject 2.0; SizeGroup { mode: horizontal; widgets [object] } GObject.Object object {} blueprint-compiler-main/tests/sample_errors/size_group_non_widget.err000066400000000000000000000000621420774714100267550ustar00rootroot000000000000006,12,6,Cannot assign GObject.Object to Gtk.Widget blueprint-compiler-main/tests/sample_errors/size_group_obj_dne.blp000066400000000000000000000001301420774714100262010ustar00rootroot00000000000000using Gtk 4.0; using GObject 2.0; SizeGroup { mode: horizontal; widgets [object] } blueprint-compiler-main/tests/sample_errors/size_group_obj_dne.err000066400000000000000000000000541420774714100262210ustar00rootroot000000000000006,12,6,Could not find object with ID object blueprint-compiler-main/tests/sample_errors/styles_in_non_widget.blp000066400000000000000000000001021420774714100265600ustar00rootroot00000000000000using Gtk 4.0; using GObject 2.0; GObject.Object { styles [] } blueprint-compiler-main/tests/sample_errors/styles_in_non_widget.err000066400000000000000000000001131420774714100265750ustar00rootroot000000000000005,3,6,GObject.Object is not a Gtk.Widget, so it doesn't have style classes blueprint-compiler-main/tests/sample_errors/two_templates.blp000066400000000000000000000001271420774714100252300ustar00rootroot00000000000000using Gtk 4.0; template ClassName : Gtk.Button {} template ClassName2 : Gtk.Button {} blueprint-compiler-main/tests/sample_errors/two_templates.err000066400000000000000000000001141420774714100252370ustar00rootroot000000000000004,10,10,Only one template may be defined per file, but this file contains 2 blueprint-compiler-main/tests/sample_errors/uint.blp000066400000000000000000000000621420774714100233160ustar00rootroot00000000000000using Gtk 4.0; FlowBox { column-spacing: -2; } blueprint-compiler-main/tests/sample_errors/uint.err000066400000000000000000000000551420774714100233330ustar00rootroot000000000000004,19,2,Cannot convert -2 to unsigned integer blueprint-compiler-main/tests/sample_errors/using_invalid_namespace.blp000066400000000000000000000000541420774714100272070ustar00rootroot00000000000000using Gtk 4.0; using NotARealNamespace 2.0; blueprint-compiler-main/tests/sample_errors/using_invalid_namespace.err000066400000000000000000000000721420774714100272220ustar00rootroot000000000000002,7,21,Namespace NotARealNamespace-2.0 could not be found blueprint-compiler-main/tests/sample_errors/widgets_in_non_size_group.blp000066400000000000000000000000501420774714100276100ustar00rootroot00000000000000using Gtk 4.0; Widget { widgets [] } blueprint-compiler-main/tests/sample_errors/widgets_in_non_size_group.err000066400000000000000000000001221420774714100276230ustar00rootroot000000000000004,3,7,Gtk.Widget is not a Gtk.SizeGroup, so it doesn't have size group properties blueprint-compiler-main/tests/samples/000077500000000000000000000000001420774714100204315ustar00rootroot00000000000000blueprint-compiler-main/tests/samples/accessibility.blp000066400000000000000000000002311420774714100237530ustar00rootroot00000000000000using Gtk 4.0; Gtk.Widget { accessibility { label: _("Hello, world!"); labelled_by: my_label; checked: true; } } Gtk.Label my_label {} blueprint-compiler-main/tests/samples/accessibility.ui000066400000000000000000000006251420774714100236220ustar00rootroot00000000000000 Hello, world! my_label true blueprint-compiler-main/tests/samples/accessibility_dec.blp000066400000000000000000000002221420774714100245660ustar00rootroot00000000000000using Gtk 4.0; Widget { accessibility { label: _("Hello, world!"); labelled_by: my_label; checked: true; } } Label my_label { } blueprint-compiler-main/tests/samples/action_widgets.blp000066400000000000000000000006251420774714100241360ustar00rootroot00000000000000using Gtk 4.0; Dialog { [action response=cancel] Button cancel_button { label: _("Cancel"); } [action reponse=9] Button custom_response_button { label: _("Reinstall Windows"); } [action response=ok default] Button ok_button { label: _("Ok"); } } InfoBar { [action response=ok] Button ok_info_button { label: _("Ok"); } } blueprint-compiler-main/tests/samples/action_widgets.ui000066400000000000000000000023641420774714100240000ustar00rootroot00000000000000 Cancel Reinstall Windows Ok cancel_button custom_response_button ok_button Ok ok_info_button blueprint-compiler-main/tests/samples/binding.blp000066400000000000000000000002561420774714100225450ustar00rootroot00000000000000using Gtk 4.0; Box { visible: bind box2.visible inverted; orientation: bind box2.orientation; spacing: bind box2.spacing no-sync-create; } Box box2 { spacing: 6; } blueprint-compiler-main/tests/samples/binding.ui000066400000000000000000000010151420774714100223770ustar00rootroot00000000000000 6 blueprint-compiler-main/tests/samples/child_type.blp000066400000000000000000000001621420774714100232530ustar00rootroot00000000000000using Gtk 4.0; Window { [titlebar] HeaderBar { } } Dialog { [internal-child context_area] Box { } } blueprint-compiler-main/tests/samples/child_type.ui000066400000000000000000000005541420774714100231200ustar00rootroot00000000000000 blueprint-compiler-main/tests/samples/combo_box_text.blp000066400000000000000000000001521420774714100241410ustar00rootroot00000000000000using Gtk 4.0; ComboBoxText { items [ "Hello, world!", _("Hello!"), item_id: "item", ] } blueprint-compiler-main/tests/samples/combo_box_text.ui000066400000000000000000000004421420774714100240030ustar00rootroot00000000000000 Hello, world! Hello! item blueprint-compiler-main/tests/samples/comments.blp000066400000000000000000000005621420774714100227600ustar00rootroot00000000000000using Gtk 4.0; using GObject 2.0; GObject.Object { /* multiline-style comment 1 */ } Gtk.Label { /* Translators: multiline-style comment 2 */ label: _("Test"); // single-line comment /**/ visible: false; /**/ } /* Note: The output XML does not need to contain translator comments. The translation tooling reads blueprint files, not the generated files. */ blueprint-compiler-main/tests/samples/comments.ui000066400000000000000000000004371420774714100226210ustar00rootroot00000000000000 Test false blueprint-compiler-main/tests/samples/enum.blp000066400000000000000000000001011420774714100220640ustar00rootroot00000000000000using Gtk 4.0; ScrolledWindow { window-placement: top_left; } blueprint-compiler-main/tests/samples/enum.ui000066400000000000000000000003211420774714100217300ustar00rootroot00000000000000 top-left blueprint-compiler-main/tests/samples/file_filter.blp000066400000000000000000000002551420774714100234160ustar00rootroot00000000000000using Gtk 4.0; FileFilter { name: "File Filter Name"; mime-types [ "text/plain", "image/ *", ] patterns [ "*.txt", ] suffixes [ "png", ] } blueprint-compiler-main/tests/samples/file_filter.ui000066400000000000000000000006621420774714100232600ustar00rootroot00000000000000 File Filter Name text/plain image/ * *.txt png blueprint-compiler-main/tests/samples/flags.blp000066400000000000000000000002051420774714100222210ustar00rootroot00000000000000using Gtk 4.0; using Gio 2.0; Gio.Application { flags: is_service | handles_open; } EventControllerScroll { flags: vertical; } blueprint-compiler-main/tests/samples/flags.ui000066400000000000000000000004671420774714100220730ustar00rootroot00000000000000 is_service|handles_open vertical blueprint-compiler-main/tests/samples/id_prop.blp000066400000000000000000000001011420774714100225540ustar00rootroot00000000000000using Gtk 4.0; Scale { adjustment: adj; } Adjustment adj { } blueprint-compiler-main/tests/samples/id_prop.ui000066400000000000000000000003601420774714100224230ustar00rootroot00000000000000 adj blueprint-compiler-main/tests/samples/inline_menu.blp000066400000000000000000000001031420774714100234240ustar00rootroot00000000000000using Gtk 4.0; MenuButton { menu-model: menu primary_menu {}; } blueprint-compiler-main/tests/samples/inline_menu.ui000066400000000000000000000003521420774714100232720ustar00rootroot00000000000000 blueprint-compiler-main/tests/samples/layout.blp000066400000000000000000000001311420774714100224400ustar00rootroot00000000000000using Gtk 4.0; Grid { Label { layout { column: 0; row: 1; } } } blueprint-compiler-main/tests/samples/layout.ui000066400000000000000000000005241420774714100223060ustar00rootroot00000000000000 0 1 blueprint-compiler-main/tests/samples/layout_dec.blp000066400000000000000000000001351420774714100232570ustar00rootroot00000000000000using Gtk 4.0; Grid { Label { layout { column: "0"; row: "1"; } } } blueprint-compiler-main/tests/samples/menu.blp000066400000000000000000000005431420774714100220760ustar00rootroot00000000000000using Gtk 4.0; menu { label: _("menu label"); test-custom-attribute: 3.1415; submenu { section { label: "test section"; } item { label: "test item"; } item ("test item shorthand 1") item ("test item shorthand 2", "app.test-action") item ("test item shorthand 3", "app.test-action", "test-symbolic") } } blueprint-compiler-main/tests/samples/menu.ui000066400000000000000000000016201420774714100217330ustar00rootroot00000000000000 menu label 3.1415
test section
test item test item shorthand 1 test item shorthand 2 app.test-action test item shorthand 3 app.test-action test-symbolic
blueprint-compiler-main/tests/samples/menu_dec.blp000066400000000000000000000007131420774714100227100ustar00rootroot00000000000000using Gtk 4.0; menu { label: _("menu label"); test-custom-attribute: "3.1415"; submenu { section { label: "test section"; } item { label: "test item"; } item { label: "test item shorthand 1"; } item { label: "test item shorthand 2"; action: "app.test-action"; } item { label: "test item shorthand 3"; action: "app.test-action"; icon: "test-symbolic"; } } } blueprint-compiler-main/tests/samples/object_prop.blp000066400000000000000000000001551420774714100234370ustar00rootroot00000000000000using Gtk 4.0; template TestTemplate : Label { test-property: Button { label: "Hello, world!"; }; } blueprint-compiler-main/tests/samples/object_prop.ui000066400000000000000000000005051420774714100232760ustar00rootroot00000000000000 blueprint-compiler-main/tests/samples/parseable.blp000066400000000000000000000000661420774714100230700ustar00rootroot00000000000000using Gtk 4.0; Gtk.Shortcut { trigger: "Escape"; } blueprint-compiler-main/tests/samples/parseable.ui000066400000000000000000000003001420774714100227170ustar00rootroot00000000000000 Escape blueprint-compiler-main/tests/samples/property.blp000066400000000000000000000000611420774714100230110ustar00rootroot00000000000000using Gtk 4.0; Box { orientation: vertical; } blueprint-compiler-main/tests/samples/property.ui000066400000000000000000000003011420774714100226460ustar00rootroot00000000000000 vertical blueprint-compiler-main/tests/samples/signal.blp000066400000000000000000000002451420774714100224060ustar00rootroot00000000000000using Gtk 4.0; Entry { activate => click(button); } Button button { clicked => on_button_clicked() swapped; notify::visible => on_button_notify_visible(); } blueprint-compiler-main/tests/samples/signal.ui000066400000000000000000000006211420774714100222440ustar00rootroot00000000000000 blueprint-compiler-main/tests/samples/size_group.blp000066400000000000000000000001551420774714100233170ustar00rootroot00000000000000using Gtk 4.0; SizeGroup { mode: horizontal; widgets [label, button] } Label label {} Button button {} blueprint-compiler-main/tests/samples/size_group.ui000066400000000000000000000005741420774714100231640ustar00rootroot00000000000000 horizontal blueprint-compiler-main/tests/samples/string_list.blp000066400000000000000000000002051420774714100234660ustar00rootroot00000000000000using Gtk 4.0; StringList greetings { strings [ "Hello, world!", _("Hello!"), ] } Gtk.DropDown { model: greetings; } blueprint-compiler-main/tests/samples/string_list.ui000066400000000000000000000005451420774714100233350ustar00rootroot00000000000000 Hello, world! Hello! greetings blueprint-compiler-main/tests/samples/strings.blp000066400000000000000000000000771420774714100226250ustar00rootroot00000000000000using Gtk 4.0; Label { label: "Test 1 2 3\n & 4 \"5\' 6"; } blueprint-compiler-main/tests/samples/strings.ui000066400000000000000000000003161420774714100224610ustar00rootroot00000000000000 Test 1 2 3 & 4 "5' 6 blueprint-compiler-main/tests/samples/style.blp000066400000000000000000000000721420774714100222670ustar00rootroot00000000000000using Gtk 4.0; Label { styles ["class-1", "class-2"] } blueprint-compiler-main/tests/samples/style.ui000066400000000000000000000003431420774714100221300ustar00rootroot00000000000000 blueprint-compiler-main/tests/samples/style_dec.blp000066400000000000000000000001071420774714100231010ustar00rootroot00000000000000using Gtk 4.0; Label { styles [ "class-1", "class-2", ] } blueprint-compiler-main/tests/samples/template.blp000066400000000000000000000002021420774714100227350ustar00rootroot00000000000000using Gtk 4.0; template TestTemplate : ApplicationWindow { test-property: "Hello, world"; test-signal => on_test_signal(); } blueprint-compiler-main/tests/samples/template.ui000066400000000000000000000004511420774714100226030ustar00rootroot00000000000000 blueprint-compiler-main/tests/samples/template_no_parent.blp000066400000000000000000000000501420774714100250030ustar00rootroot00000000000000using Gtk 4.0; template GtkListItem {} blueprint-compiler-main/tests/samples/template_no_parent.ui000066400000000000000000000002221420774714100246440ustar00rootroot00000000000000 blueprint-compiler-main/tests/samples/translated.blp000066400000000000000000000001571420774714100232740ustar00rootroot00000000000000using Gtk 4.0; Label { label: _("Hello, world!"); } Label { label: C_("translation context", "Hello"); } blueprint-compiler-main/tests/samples/translated.ui000066400000000000000000000005341420774714100231330ustar00rootroot00000000000000 Hello, world! Hello blueprint-compiler-main/tests/samples/uint.blp000066400000000000000000000000611420774714100221040ustar00rootroot00000000000000using Gtk 4.0; FlowBox { column-spacing: 2; } blueprint-compiler-main/tests/samples/uint.ui000066400000000000000000000003011420774714100217410ustar00rootroot00000000000000 2 blueprint-compiler-main/tests/samples/using.blp000066400000000000000000000000661420774714100222570ustar00rootroot00000000000000using Gtk 4.0; using GObject 2.0; GObject.Object { } blueprint-compiler-main/tests/samples/using.ui000066400000000000000000000002121420774714100221100ustar00rootroot00000000000000 blueprint-compiler-main/tests/test_samples.py000066400000000000000000000201251420774714100220420ustar00rootroot00000000000000# test_samples.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later import difflib # I love Python from pathlib import Path import traceback import unittest from blueprintcompiler import tokenizer, parser, decompiler from blueprintcompiler.errors import PrintableError, MultipleErrors, CompileError from blueprintcompiler.tokenizer import Token, TokenType, tokenize from blueprintcompiler import utils class TestSamples(unittest.TestCase): def assert_sample(self, name): try: with open((Path(__file__).parent / f"samples/{name}.blp").resolve()) as f: blueprint = f.read() with open((Path(__file__).parent / f"samples/{name}.ui").resolve()) as f: expected = f.read() tokens = tokenizer.tokenize(blueprint) ast, errors, warnings = parser.parse(tokens) if errors: raise errors if len(ast.errors): raise MultipleErrors(ast.errors) if len(warnings): raise MultipleErrors(warnings) actual = ast.generate() if actual.strip() != expected.strip(): # pragma: no cover diff = difflib.unified_diff(expected.splitlines(), actual.splitlines()) print("\n".join(diff)) raise AssertionError() except PrintableError as e: # pragma: no cover e.pretty_print(name + ".blp", blueprint) raise AssertionError() def assert_sample_error(self, name): try: with open((Path(__file__).parent / f"sample_errors/{name}.blp").resolve()) as f: blueprint = f.read() with open((Path(__file__).parent / f"sample_errors/{name}.err").resolve()) as f: expected = f.read() tokens = tokenizer.tokenize(blueprint) ast, errors, warnings = parser.parse(tokens) if errors: raise errors if len(ast.errors): raise MultipleErrors(ast.errors) if len(warnings): raise MultipleErrors(warnings) except PrintableError as e: def error_str(error): line, col = utils.idx_to_pos(error.start + 1, blueprint) len = error.end - error.start return ",".join([str(line + 1), str(col), str(len), error.message]) if isinstance(e, CompileError): actual = error_str(e) elif isinstance(e, MultipleErrors): actual = "\n".join([error_str(error) for error in e.errors]) else: # pragma: no cover raise AssertionError() if actual.strip() != expected.strip(): # pragma: no cover diff = difflib.unified_diff(expected.splitlines(), actual.splitlines()) print("\n".join(diff)) raise AssertionError() else: # pragma: no cover raise AssertionError("Expected a compiler error, but none was emitted") def assert_decompile(self, name): try: with open((Path(__file__).parent / f"samples/{name}.blp").resolve()) as f: expected = f.read() name = name.removesuffix("_dec") ui_path = (Path(__file__).parent / f"samples/{name}.ui").resolve() actual = decompiler.decompile(ui_path) if actual.strip() != expected.strip(): # pragma: no cover diff = difflib.unified_diff(expected.splitlines(), actual.splitlines()) print("\n".join(diff)) raise AssertionError() except PrintableError as e: # pragma: no cover e.pretty_print(name + ".blp", blueprint) raise AssertionError() def test_samples(self): self.assert_sample("accessibility") self.assert_sample("action_widgets") self.assert_sample("binding") self.assert_sample("child_type") self.assert_sample("combo_box_text") self.assert_sample("comments") self.assert_sample("enum") self.assert_sample("file_filter") self.assert_sample("flags") self.assert_sample("id_prop") self.assert_sample("inline_menu") self.assert_sample("layout") self.assert_sample("menu") self.assert_sample("object_prop") self.assert_sample("parseable") self.assert_sample("property") self.assert_sample("signal") self.assert_sample("size_group") self.assert_sample("string_list") self.assert_sample("strings") self.assert_sample("style") self.assert_sample("template") self.assert_sample("template_no_parent") self.assert_sample("translated") self.assert_sample("uint") self.assert_sample("using") def test_sample_errors(self): self.assert_sample_error("a11y_in_non_widget") self.assert_sample_error("a11y_prop_dne") self.assert_sample_error("a11y_prop_obj_dne") self.assert_sample_error("a11y_prop_type") self.assert_sample_error("assign_inline_menu") self.assert_sample_error("action_widget_float_response") self.assert_sample_error("action_widget_have_no_id") self.assert_sample_error("action_widget_multiple_default") self.assert_sample_error("action_widget_not_action") self.assert_sample_error("action_widget_in_invalid_container") self.assert_sample_error("action_widget_response_dne") self.assert_sample_error("action_widget_negative_response") self.assert_sample_error("bitfield_member_dne") self.assert_sample_error("class_assign") self.assert_sample_error("class_dne") self.assert_sample_error("consecutive_unexpected_tokens") self.assert_sample_error("does_not_implement") self.assert_sample_error("duplicate_obj_id") self.assert_sample_error("enum_member_dne") self.assert_sample_error("filters_in_non_file_filter") self.assert_sample_error("invalid_bool") self.assert_sample_error("layout_in_non_widget") self.assert_sample_error("ns_not_imported") self.assert_sample_error("not_a_class") self.assert_sample_error("object_dne") self.assert_sample_error("obj_in_string_list") self.assert_sample_error("obj_prop_type") self.assert_sample_error("property_dne") self.assert_sample_error("signal_dne") self.assert_sample_error("signal_object_dne") self.assert_sample_error("size_group_non_widget") self.assert_sample_error("size_group_obj_dne") self.assert_sample_error("styles_in_non_widget") self.assert_sample_error("two_templates") self.assert_sample_error("uint") self.assert_sample_error("using_invalid_namespace") self.assert_sample_error("widgets_in_non_size_group") def test_decompiler(self): self.assert_decompile("accessibility_dec") self.assert_decompile("binding") self.assert_decompile("child_type") self.assert_decompile("file_filter") self.assert_decompile("flags") self.assert_decompile("id_prop") self.assert_decompile("layout_dec") self.assert_decompile("menu_dec") self.assert_decompile("property") self.assert_decompile("signal") self.assert_decompile("strings") self.assert_decompile("style_dec") self.assert_decompile("template") self.assert_decompile("translated") self.assert_decompile("using") blueprint-compiler-main/tests/test_tokenizer.py000066400000000000000000000051441420774714100224140ustar00rootroot00000000000000# tokenizer.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later import unittest from blueprintcompiler.errors import PrintableError from blueprintcompiler.tokenizer import Token, TokenType, tokenize class TestTokenizer(unittest.TestCase): def assert_tokenize(self, string: str, expect: [Token]): try: tokens = tokenize(string) self.assertEqual(len(tokens), len(expect)) for token, (type, token_str) in zip(tokens, expect): self.assertEqual(token.type, type) self.assertEqual(str(token), token_str) except PrintableError as e: # pragma: no cover e.pretty_print("", string) raise e def test_basic(self): self.assert_tokenize("ident(){}; \n <<+>>*/=", [ (TokenType.IDENT, "ident"), (TokenType.PUNCTUATION, "("), (TokenType.PUNCTUATION, ")"), (TokenType.PUNCTUATION, "{"), (TokenType.PUNCTUATION, "}"), (TokenType.PUNCTUATION, ";"), (TokenType.WHITESPACE, " \n "), (TokenType.OP, "<<+>>*/="), (TokenType.EOF, ""), ]) def test_quotes(self): self.assert_tokenize(r'"this is a \n string""this is \\another \"string\""', [ (TokenType.QUOTED, r'"this is a \n string"'), (TokenType.QUOTED, r'"this is \\another \"string\""'), (TokenType.EOF, ""), ]) def test_comments(self): self.assert_tokenize('/* \n \\n COMMENT /* */', [ (TokenType.COMMENT, '/* \n \\n COMMENT /* */'), (TokenType.EOF, ""), ]) self.assert_tokenize('line // comment\nline', [ (TokenType.IDENT, 'line'), (TokenType.WHITESPACE, ' '), (TokenType.COMMENT, '// comment'), (TokenType.WHITESPACE, '\n'), (TokenType.IDENT, 'line'), (TokenType.EOF, ""), ])