diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f4bc92d..2790cd1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -18,7 +18,7 @@ build: - ninja -C _build docs/en - git clone https://gitlab.gnome.org/jwestman/blueprint-regression-tests.git - cd blueprint-regression-tests - - git checkout d14b95b6c1fc0cddd4b0ad21d224b05edee2d01f + - git checkout 94613f275efc810610768d5ee8b2aec28392c3e8 - ./test.sh - cd .. coverage: '/TOTAL.*\s([.\d]+)%/' diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index 1f3eb5b..cc5c44d 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -23,7 +23,6 @@ import typing as T from .errors import * from .lsp_utils import SemanticToken -from .xml_emitter import XmlEmitter class Children: @@ -96,16 +95,6 @@ class AstNode: 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: diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index b1ecf08..78335ee 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -28,11 +28,11 @@ from gi.repository import GIRepository # type: ignore from .errors import CompileError, CompilerBugError from . import typelib, xml_reader -_namespace_cache = {} +_namespace_cache: T.Dict[str, "Namespace"] = {} _xml_cache = {} -def get_namespace(namespace, version): +def get_namespace(namespace, version) -> "Namespace": search_paths = GIRepository.Repository.get_search_path() filename = f"{namespace}-{version}.typelib" @@ -518,11 +518,11 @@ class Namespace(GirNode): return get_xml(self.name, self.version).get_elements("namespace")[0] @cached_property - def name(self): + def name(self) -> str: return self.tl.HEADER_NAMESPACE @cached_property - def version(self): + def version(self) -> str: return self.tl.HEADER_NSVERSION @property diff --git a/blueprintcompiler/interactive_port.py b/blueprintcompiler/interactive_port.py index ff551df..10a9b56 100644 --- a/blueprintcompiler/interactive_port.py +++ b/blueprintcompiler/interactive_port.py @@ -23,6 +23,7 @@ import difflib import os from . import decompiler, tokenizer, parser +from .outputs.xml import XmlOutput from .errors import MultipleErrors, PrintableError from .utils import Colors @@ -57,7 +58,8 @@ def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]: if len(ast.errors): raise MultipleErrors(ast.errors) - ast.generate() + output = XmlOutput() + output.emit(ast) except PrintableError as e: e.pretty_print(out_file, decompiled) diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index ce247c3..da5a44c 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -1,16 +1,13 @@ -""" Contains all the syntax beyond basic objects, properties, signal, and -templates. """ - from .attributes import BaseAttribute, BaseTypedAttribute -from .expression import Expr +from .expression import IdentExpr, LookupOp, Expr 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_file_filter import mime_types, patterns, suffixes, Filters from .gtk_layout import Layout -from .gtk_menu import menu +from .gtk_menu import menu, Menu, MenuAttribute from .gtk_size_group import Widgets from .gtk_string_list import Strings from .gtk_styles import Styles @@ -18,7 +15,8 @@ from .gtkbuilder_child import Child from .gtkbuilder_template import Template from .imports import GtkDirective, Import from .ui import UI -from .values import TypeValue, IdentValue, TranslatedStringValue, FlagsValue, QuotedValue, NumberValue +from .types import ClassName +from .values import TypeValue, IdentValue, TranslatedStringValue, FlagsValue, Flag, QuotedValue, NumberValue, Value from .common import * diff --git a/blueprintcompiler/language/attributes.py b/blueprintcompiler/language/attributes.py index a6db9f1..66faa60 100644 --- a/blueprintcompiler/language/attributes.py +++ b/blueprintcompiler/language/attributes.py @@ -32,17 +32,6 @@ class BaseAttribute(AstNode): def name(self): return self.tokens["name"] - def emit_xml(self, xml: XmlEmitter): - value = self.children[Value][0] - attrs = { self.attr_name: self.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 diff --git a/blueprintcompiler/language/common.py b/blueprintcompiler/language/common.py index 108c43a..f6a8f8e 100644 --- a/blueprintcompiler/language/common.py +++ b/blueprintcompiler/language/common.py @@ -27,7 +27,6 @@ from ..decompiler import DecompileCtx, decompiler from ..gir import StringType, BoolType, IntType, FloatType, GirType, Enumeration from ..lsp_utils import Completion, CompletionItemKind, SemanticToken, SemanticTokenType from ..parse_tree import * -from ..xml_emitter import XmlEmitter OBJECT_CONTENT_HOOKS = AnyOf() diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index a684b39..29df93e 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -27,9 +27,6 @@ expr = Pratt() class Expr(AstNode): grammar = expr - def emit_xml(self, xml: XmlEmitter): - self.children[-1].emit_xml(xml) - class InfixExpr(AstNode): @property @@ -41,19 +38,17 @@ class InfixExpr(AstNode): class IdentExpr(AstNode): grammar = UseIdent("ident") - def emit_xml(self, xml: XmlEmitter): - xml.start_tag("constant") - xml.put_text(self.tokens["ident"]) - xml.end_tag() + @property + def ident(self) -> str: + return self.tokens["ident"] class LookupOp(InfixExpr): grammar = [".", UseIdent("property")] - def emit_xml(self, xml: XmlEmitter): - xml.start_tag("lookup", name=self.tokens["property"]) - self.lhs.emit_xml(xml) - xml.end_tag() + @property + def property_name(self) -> str: + return self.tokens["property"] expr.children = [ diff --git a/blueprintcompiler/language/gobject_object.py b/blueprintcompiler/language/gobject_object.py index 9a78667..711795c 100644 --- a/blueprintcompiler/language/gobject_object.py +++ b/blueprintcompiler/language/gobject_object.py @@ -33,10 +33,6 @@ class ObjectContent(AstNode): def gir_class(self): return self.parent.gir_class - def emit_xml(self, xml: XmlEmitter): - for x in self.children: - x.emit_xml(xml) - class Object(AstNode): grammar: T.Any = [ ConcreteClassName, @@ -44,9 +40,21 @@ class Object(AstNode): ObjectContent, ] + @property + def id(self) -> str: + return self.tokens["id"] + + @property + def class_name(self) -> ClassName | None: + return self.children[ClassName][0] + + @property + def content(self) -> ObjectContent: + return self.children[ObjectContent][0] + @property def gir_class(self): - return self.children[ClassName][0].gir_type + return self.class_name.gir_type @cached_property def action_widgets(self) -> T.List[ResponseId]: @@ -62,28 +70,6 @@ class Object(AstNode): if child.response_id ] - def emit_start_tag(self, xml: XmlEmitter): - xml.start_tag("object", **{ - "class": self.children[ClassName][0].glib_type_name, - "id": self.tokens["id"], - }) - - def emit_xml(self, xml: XmlEmitter): - self.emit_start_tag(xml) - - 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) diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index e5d753b..4d47e80 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -133,44 +133,3 @@ class Property(AstNode): 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: - if self.tokens["binding"]: - xml.start_tag("binding", **props) - for x in self.children: - x.emit_xml(xml) - xml.end_tag() - else: - xml.put_self_closing("property", **props); - else: - xml.start_tag("property", **props) - value.emit_xml(xml) - xml.end_tag() diff --git a/blueprintcompiler/language/gobject_signal.py b/blueprintcompiler/language/gobject_signal.py index a519dee..ba1b48b 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -40,6 +40,30 @@ class Signal(AstNode): )), ) + @property + def name(self) -> str: + return self.tokens["name"] + + @property + def detail_name(self) -> str | None: + return self.tokens["detail_name"] + + @property + def handler(self) -> str: + return self.tokens["handler"] + + @property + def object_id(self) -> str | None: + return self.tokens["object"] + + @property + def is_swapped(self) -> bool: + return self.tokens["swapped"] or False + + @property + def is_after(self) -> bool: + return self.tokens["after"] or False + @property def gir_signal(self): @@ -89,19 +113,6 @@ class Signal(AstNode): 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 "" diff --git a/blueprintcompiler/language/gtk_a11y.py b/blueprintcompiler/language/gtk_a11y.py index 57bd6af..cab72e0 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -167,12 +167,6 @@ class A11y(AstNode): def unique_in_parent(self): self.validate_unique_in_parent("Duplicate accessibility block") - 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], diff --git a/blueprintcompiler/language/gtk_combo_box_text.py b/blueprintcompiler/language/gtk_combo_box_text.py index ecee31f..31d2d08 100644 --- a/blueprintcompiler/language/gtk_combo_box_text.py +++ b/blueprintcompiler/language/gtk_combo_box_text.py @@ -60,12 +60,6 @@ class Items(AstNode): def unique_in_parent(self): self.validate_unique_in_parent("Duplicate items block") - 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], diff --git a/blueprintcompiler/language/gtk_file_filter.py b/blueprintcompiler/language/gtk_file_filter.py index 39e563e..4419311 100644 --- a/blueprintcompiler/language/gtk_file_filter.py +++ b/blueprintcompiler/language/gtk_file_filter.py @@ -39,18 +39,9 @@ class Filters(AstNode): ) wrapped_validator(self) - 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() + pass def create_node(tag_name: str, singular: str): diff --git a/blueprintcompiler/language/gtk_layout.py b/blueprintcompiler/language/gtk_layout.py index 8ccc136..b52f7bc 100644 --- a/blueprintcompiler/language/gtk_layout.py +++ b/blueprintcompiler/language/gtk_layout.py @@ -64,12 +64,6 @@ class Layout(AstNode): def unique_in_parent(self): self.validate_unique_in_parent("Duplicate layout block") - 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], diff --git a/blueprintcompiler/language/gtk_menu.py b/blueprintcompiler/language/gtk_menu.py index 2c4131a..355997a 100644 --- a/blueprintcompiler/language/gtk_menu.py +++ b/blueprintcompiler/language/gtk_menu.py @@ -19,22 +19,26 @@ import typing as T +from blueprintcompiler.language.values import Value + from .attributes import BaseAttribute from .gobject_object import Object, ObjectContent 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() - +class Menu(AstNode): @property def gir_class(self): return self.root.gir.namespaces["Gtk"].lookup_type("Gio.Menu") + @property + def id(self) -> str: + return self.tokens["id"] + + @property + def tag(self) -> str: + return self.tokens["tag"] + class MenuAttribute(BaseAttribute): tag_name = "attribute" @@ -43,6 +47,10 @@ class MenuAttribute(BaseAttribute): def value_type(self): return None + @property + def value(self) -> Value: + return self.children[Value][0] + menu_contents = Sequence() diff --git a/blueprintcompiler/language/gtk_size_group.py b/blueprintcompiler/language/gtk_size_group.py index de766ed..eb56043 100644 --- a/blueprintcompiler/language/gtk_size_group.py +++ b/blueprintcompiler/language/gtk_size_group.py @@ -39,9 +39,6 @@ class Widget(AstNode): 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 = [ @@ -59,12 +56,6 @@ class Widgets(AstNode): def unique_in_parent(self): self.validate_unique_in_parent("Duplicate widgets block") - 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], diff --git a/blueprintcompiler/language/gtk_string_list.py b/blueprintcompiler/language/gtk_string_list.py index 4f19190..e7eb0f8 100644 --- a/blueprintcompiler/language/gtk_string_list.py +++ b/blueprintcompiler/language/gtk_string_list.py @@ -31,13 +31,6 @@ class Item(AstNode): 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 = [ @@ -55,12 +48,6 @@ class Strings(AstNode): def unique_in_parent(self): self.validate_unique_in_parent("Duplicate strings block") - 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], diff --git a/blueprintcompiler/language/gtk_styles.py b/blueprintcompiler/language/gtk_styles.py index f93ddc0..644d8a3 100644 --- a/blueprintcompiler/language/gtk_styles.py +++ b/blueprintcompiler/language/gtk_styles.py @@ -25,9 +25,6 @@ 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 = [ @@ -45,12 +42,6 @@ class Styles(AstNode): def unique_in_parent(self): self.validate_unique_in_parent("Duplicate styles block") - 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], diff --git a/blueprintcompiler/language/gtkbuilder_child.py b/blueprintcompiler/language/gtkbuilder_child.py index 72843a1..71efe19 100644 --- a/blueprintcompiler/language/gtkbuilder_child.py +++ b/blueprintcompiler/language/gtkbuilder_child.py @@ -41,6 +41,10 @@ class Child(AstNode): Object, ] + @property + def object(self) -> Object: + return self.children[Object][0] + @validate() def parent_can_have_child(self): if gir_class := self.parent.gir_class: @@ -70,17 +74,6 @@ class Child(AstNode): 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): diff --git a/blueprintcompiler/language/gtkbuilder_template.py b/blueprintcompiler/language/gtkbuilder_template.py index 6ab6c5a..35ef4f2 100644 --- a/blueprintcompiler/language/gtkbuilder_template.py +++ b/blueprintcompiler/language/gtkbuilder_template.py @@ -34,28 +34,27 @@ class Template(Object): ObjectContent, ] + @property + def id(self) -> str: + return self.tokens["id"] + + @property + def class_name(self) -> ClassName | None: + if len(self.children[ClassName]): + return self.children[ClassName][0] + else: + return None + @property def gir_class(self): # Templates might not have a parent class defined - if len(self.children[ClassName]): - return self.children[ClassName][0].gir_type + if class_name := self.class_name: + return class_name.gir_type @validate("id") def unique_in_parent(self): self.validate_unique_in_parent(f"Only one template may be defined per file, but this file contains {len(self.parent.children[Template])}",) - def emit_start_tag(self, xml: XmlEmitter): - if len(self.children[ClassName]): - parent = self.children[ClassName][0].glib_type_name - else: - parent = None - - xml.start_tag( - "template", - **{"class": self.tokens["id"]}, - parent=parent - ) - @decompiler("template") def decompile_template(ctx: DecompileCtx, gir, klass, parent="Widget"): diff --git a/blueprintcompiler/language/imports.py b/blueprintcompiler/language/imports.py index 823baec..f0fe3df 100644 --- a/blueprintcompiler/language/imports.py +++ b/blueprintcompiler/language/imports.py @@ -61,10 +61,6 @@ class GtkDirective(AstNode): 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", @@ -82,6 +78,3 @@ class Import(AstNode): return gir.get_namespace(self.tokens["namespace"], self.tokens["version"]) except CompileError: return None - - def emit_xml(self, xml): - pass diff --git a/blueprintcompiler/language/response_id.py b/blueprintcompiler/language/response_id.py index 7dce2f2..745c73f 100644 --- a/blueprintcompiler/language/response_id.py +++ b/blueprintcompiler/language/response_id.py @@ -120,6 +120,14 @@ class ResponseId(AstNode): if widget.tokens["is_default"]: raise CompileError("Default response is already set") + @property + def response_id(self) -> str: + return self.tokens["response_id"] + + @property + def is_default(self) -> bool: + return self.tokens["is_default"] or False + @property def widget_id(self) -> str: """Get action widget ID.""" @@ -128,25 +136,3 @@ class ResponseId(AstNode): _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() diff --git a/blueprintcompiler/language/types.py b/blueprintcompiler/language/types.py index 0d6b38c..8f39cbf 100644 --- a/blueprintcompiler/language/types.py +++ b/blueprintcompiler/language/types.py @@ -76,9 +76,6 @@ class TypeName(AstNode): if self.gir_type: return self.gir_type.doc - def emit_xml(self, xml: XmlEmitter): - pass - class ClassName(TypeName): @validate("namespace", "class_name") diff --git a/blueprintcompiler/language/ui.py b/blueprintcompiler/language/ui.py index e0d5dcd..5a9c4fb 100644 --- a/blueprintcompiler/language/ui.py +++ b/blueprintcompiler/language/ui.py @@ -86,10 +86,3 @@ class UI(AstNode): 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() diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 44e28ba..5030774 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -46,14 +46,12 @@ class TranslatedStringValue(Value): ) @property - def attrs(self): - attrs = { "translatable": "true" } - if "context" in self.tokens: - attrs["context"] = self.tokens["context"] - return attrs + def string(self) -> str: + return self.tokens["value"] - def emit_xml(self, xml: XmlEmitter): - xml.put_text(self.tokens["value"]) + @property + def context(self) -> str | None: + return self.tokens["context"] class TypeValue(Value): @@ -68,9 +66,6 @@ class TypeValue(Value): def type_name(self): return self.children[TypeName][0] - def emit_xml(self, xml: XmlEmitter): - xml.put_text(self.type_name.glib_type_name) - @validate() def validate_for_type(self): type = self.parent.value_type @@ -81,8 +76,9 @@ class TypeValue(Value): class QuotedValue(Value): grammar = UseQuoted("value") - def emit_xml(self, xml: XmlEmitter): - xml.put_text(self.tokens["value"]) + @property + def value(self) -> str: + return self.tokens["value"] @validate() def validate_for_type(self): @@ -119,8 +115,9 @@ class QuotedValue(Value): class NumberValue(Value): grammar = UseNumber("value") - def emit_xml(self, xml: XmlEmitter): - xml.put_text(self.tokens["value"]) + @property + def value(self) -> int | float: + return self.tokens["value"] @validate() def validate_for_type(self): @@ -179,19 +176,10 @@ class FlagsValue(Value): if type is not None and 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 diff --git a/blueprintcompiler/main.py b/blueprintcompiler/main.py index 9f2f4c2..124654d 100644 --- a/blueprintcompiler/main.py +++ b/blueprintcompiler/main.py @@ -25,7 +25,7 @@ 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 +from .outputs import XmlOutput VERSION = "uninstalled" LIBDIR = None @@ -141,7 +141,9 @@ class BlueprintApp: if len(ast.errors): raise MultipleErrors(ast.errors) - return ast.generate(), warnings + formatter = XmlOutput() + + return formatter.emit(ast), warnings def main(version, libdir): diff --git a/blueprintcompiler/outputs/__init__.py b/blueprintcompiler/outputs/__init__.py new file mode 100644 index 0000000..e3054a3 --- /dev/null +++ b/blueprintcompiler/outputs/__init__.py @@ -0,0 +1,7 @@ +from ..language import UI + +class OutputFormat: + def emit(self, ui: UI) -> str: + raise NotImplementedError() + +from .xml import XmlOutput diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py new file mode 100644 index 0000000..2cbb425 --- /dev/null +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -0,0 +1,272 @@ +from .. import OutputFormat +from ...language import * +from .xml_emitter import XmlEmitter + + +class XmlOutput(OutputFormat): + def emit(self, ui: UI) -> str: + xml = XmlEmitter() + self._emit_ui(ui, xml) + return xml.result + + def _emit_ui(self, ui: UI, xml: XmlEmitter): + xml.start_tag("interface") + + for x in ui.children: + if isinstance(x, GtkDirective): + self._emit_gtk_directive(x, xml) + elif isinstance(x, Import): + pass + elif isinstance(x, Template): + self._emit_template(x, xml) + elif isinstance(x, Object): + self._emit_object(x, xml) + elif isinstance(x, Menu): + self._emit_menu(x, xml) + else: + raise CompilerBugError() + + xml.end_tag() + + def _emit_gtk_directive(self, gtk: GtkDirective, xml: XmlEmitter): + xml.put_self_closing("requires", lib="gtk", version=gtk.gir_namespace.version) + + def _emit_template(self, template: Template, xml: XmlEmitter): + xml.start_tag("template", **{"class": template.id}, parent=template.class_name) + self._emit_object_or_template(template, xml) + xml.end_tag() + + def _emit_object(self, obj: Object, xml: XmlEmitter): + xml.start_tag( + "object", + **{"class": obj.class_name}, + id=obj.id, + ) + self._emit_object_or_template(obj, xml) + xml.end_tag() + + def _emit_object_or_template(self, obj: Object | Template, xml: XmlEmitter): + for child in obj.content.children: + if isinstance(child, Property): + self._emit_property(child, xml) + elif isinstance(child, Signal): + self._emit_signal(child, xml) + elif isinstance(child, Child): + self._emit_child(child, xml) + else: + self._emit_extensions(child, xml) + + # List action widgets + action_widgets = obj.action_widgets + if action_widgets: + xml.start_tag("action-widgets") + for action_widget in action_widgets: + xml.start_tag( + "action-widget", + response=action_widget.response_id, + default=action_widget.is_default or None, + ) + xml.put_text(action_widget.widget_id) + xml.end_tag() + xml.end_tag() + + def _emit_menu(self, menu: Menu, xml: XmlEmitter): + xml.start_tag(menu.tag, id=menu.id) + for child in menu.children: + if isinstance(child, Menu): + self._emit_menu(child, xml) + elif isinstance(child, MenuAttribute): + self._emit_attribute("attribute", "name", child.name, child.value, xml) + else: + raise CompilerBugError() + xml.end_tag() + + def _emit_property(self, property: Property, xml: XmlEmitter): + values = property.children[Value] + value = values[0] if len(values) == 1 else None + + bind_flags = [] + if property.tokens["bind_source"] and not property.tokens["no_sync_create"]: + bind_flags.append("sync-create") + if property.tokens["inverted"]: + bind_flags.append("invert-boolean") + if property.tokens["bidirectional"]: + bind_flags.append("bidirectional") + bind_flags_str = "|".join(bind_flags) or None + + props = { + "name": property.tokens["name"], + "bind-source": property.tokens["bind_source"], + "bind-property": property.tokens["bind_property"], + "bind-flags": bind_flags_str, + } + + if isinstance(value, TranslatedStringValue): + xml.start_tag("property", **props, **self._translated_string_attrs(value)) + xml.put_text(value.string) + xml.end_tag() + elif len(property.children[Object]) == 1: + xml.start_tag("property", **props) + self._emit_object(property.children[Object][0], xml) + xml.end_tag() + elif value is None: + if property.tokens["binding"]: + xml.start_tag("binding", **props) + self._emit_expression(property.children[Expr][0], xml) + xml.end_tag() + else: + xml.put_self_closing("property", **props) + else: + xml.start_tag("property", **props) + self._emit_value(value, xml) + xml.end_tag() + + def _translated_string_attrs( + self, translated: TranslatedStringValue + ) -> T.Dict[str, str | None]: + return { + "translatable": "true", + "context": translated.context, + } + + def _emit_signal(self, signal: Signal, xml: XmlEmitter): + name = signal.name + if signal.detail_name: + name += "::" + signal.detail_name + xml.put_self_closing( + "signal", + name=name, + handler=signal.handler, + swapped=signal.is_swapped or None, + object=signal.object_id, + ) + + def _emit_child(self, child: Child, xml: XmlEmitter): + child_type = internal_child = None + + if child.tokens["internal_child"]: + internal_child = child.tokens["child_type"] + else: + child_type = child.tokens["child_type"] + + xml.start_tag("child", type=child_type, internal_child=internal_child) + self._emit_object(child.object, xml) + xml.end_tag() + + def _emit_value(self, value: Value, xml: XmlEmitter): + if isinstance(value, IdentValue): + if isinstance(value.parent.value_type, gir.Enumeration): + xml.put_text( + value.parent.value_type.members[value.tokens["value"]].nick + ) + else: + xml.put_text(value.tokens["value"]) + elif isinstance(value, QuotedValue) or isinstance(value, NumberValue): + xml.put_text(value.value) + elif isinstance(value, FlagsValue): + xml.put_text("|".join([flag.tokens["value"] for flag in value.children])) + elif isinstance(value, TranslatedStringValue): + raise CompilerBugError("translated values must be handled in the parent") + elif isinstance(value, TypeValue): + xml.put_text(value.type_name.glib_type_name) + else: + raise CompilerBugError() + + def _emit_expression(self, expression: Expr, xml: XmlEmitter): + self._emit_expression_part(expression.children[-1], xml) + + def _emit_expression_part(self, expression, xml: XmlEmitter): + if isinstance(expression, IdentExpr): + self._emit_ident_expr(expression, xml) + elif isinstance(expression, LookupOp): + self._emit_lookup_op(expression, xml) + elif isinstance(expression, Expr): + self._emit_expression(expression, xml) + else: + raise CompilerBugError() + + def _emit_ident_expr(self, expr: IdentExpr, xml: XmlEmitter): + xml.start_tag("constant") + xml.put_text(expr.ident) + xml.end_tag() + + def _emit_lookup_op(self, expr: LookupOp, xml: XmlEmitter): + xml.start_tag("lookup", name=expr.property_name) + self._emit_expression_part(expr.lhs, xml) + xml.end_tag() + + def _emit_attribute( + self, tag: str, attr: str, name: str, value: Value, xml: XmlEmitter + ): + attrs = {attr: name} + + if isinstance(value, TranslatedStringValue): + xml.start_tag(tag, **attrs, **self._translated_string_attrs(value)) + xml.put_text(value.string) + xml.end_tag() + else: + xml.start_tag(tag, **attrs) + self._emit_value(value, xml) + xml.end_tag() + + def _emit_extensions(self, extension, xml: XmlEmitter): + if isinstance(extension, A11y): + xml.start_tag("accessibility") + for child in extension.children: + self._emit_attribute( + child.tag_name, "name", child.name, child.children[Value][0], xml + ) + xml.end_tag() + + elif isinstance(extension, Filters): + xml.start_tag(extension.tokens["tag_name"]) + for child in extension.children: + xml.start_tag(child.tokens["tag_name"]) + xml.put_text(child.tokens["name"]) + xml.end_tag() + xml.end_tag() + + elif isinstance(extension, Items): + xml.start_tag("items") + for child in extension.children: + self._emit_attribute( + "item", "id", child.name, child.children[Value][0], xml + ) + xml.end_tag() + + elif isinstance(extension, Layout): + xml.start_tag("layout") + for child in extension.children: + self._emit_attribute( + "property", "name", child.name, child.children[Value][0], xml + ) + xml.end_tag() + + elif isinstance(extension, Strings): + xml.start_tag("items") + for child in extension.children: + value = child.children[Value][0] + if isinstance(value, TranslatedStringValue): + xml.start_tag("item", **self._translated_string_attrs(value)) + xml.put_text(value.string) + xml.end_tag() + else: + xml.start_tag("item") + self._emit_value(value, xml) + xml.end_tag() + xml.end_tag() + + elif isinstance(extension, Styles): + xml.start_tag("style") + for child in extension.children: + xml.put_self_closing("class", name=child.tokens["name"]) + xml.end_tag() + + elif isinstance(extension, Widgets): + xml.start_tag("widgets") + for child in extension.children: + xml.put_self_closing("widget", name=child.tokens["name"]) + xml.end_tag() + + else: + raise CompilerBugError() diff --git a/blueprintcompiler/xml_emitter.py b/blueprintcompiler/outputs/xml/xml_emitter.py similarity index 86% rename from blueprintcompiler/xml_emitter.py rename to blueprintcompiler/outputs/xml/xml_emitter.py index d92d1bd..def58b5 100644 --- a/blueprintcompiler/xml_emitter.py +++ b/blueprintcompiler/outputs/xml/xml_emitter.py @@ -17,9 +17,10 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later - from xml.sax import saxutils -from . import gir + +from blueprintcompiler.gir import GirType +from blueprintcompiler.language.types import ClassName class XmlEmitter: @@ -29,7 +30,7 @@ class XmlEmitter: self._tag_stack = [] self._needs_newline = False - def start_tag(self, tag, **attrs): + def start_tag(self, tag, **attrs: str | GirType | ClassName | bool | None): self._indent() self.result += f"<{tag}" for key, val in attrs.items(): @@ -55,7 +56,7 @@ class XmlEmitter: self.result += f"" self._needs_newline = True - def put_text(self, text): + def put_text(self, text: str | int | float): self.result += saxutils.escape(str(text)) self._needs_newline = False @@ -64,7 +65,9 @@ class XmlEmitter: self.result += "\n" + " " * (self.indent * len(self._tag_stack)) def _to_string(self, val): - if isinstance(val, gir.GirType): + if isinstance(val, GirType): + return val.glib_type_name + elif isinstance(val, ClassName): return val.glib_type_name else: return str(val) diff --git a/tests/fuzz.py b/tests/fuzz.py index 4f7c879..1ebd02d 100644 --- a/tests/fuzz.py +++ b/tests/fuzz.py @@ -1,6 +1,8 @@ import os, sys from pythonfuzz.main import PythonFuzz +from blueprintcompiler.outputs.xml import XmlOutput + sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) from blueprintcompiler import tokenizer, parser, decompiler, gir @@ -17,8 +19,9 @@ def fuzz(buf): tokens = tokenizer.tokenize(blueprint) ast, errors, warnings = parser.parse(tokens) + xml = XmlOutput() if errors is None and len(ast.errors) == 0: - actual = ast.generate() + xml.emit(ast) except CompilerBugError as e: raise e except PrintableError: diff --git a/tests/samples/signal.ui b/tests/samples/signal.ui index 52740fe..b361d69 100644 --- a/tests/samples/signal.ui +++ b/tests/samples/signal.ui @@ -5,7 +5,7 @@ - + diff --git a/tests/test_samples.py b/tests/test_samples.py index b5d3e3e..5f0d9e5 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -28,6 +28,7 @@ from blueprintcompiler.completions import complete from blueprintcompiler.errors import PrintableError, MultipleErrors, CompileError from blueprintcompiler.tokenizer import Token, TokenType, tokenize from blueprintcompiler import utils +from blueprintcompiler.outputs.xml import XmlOutput class TestSamples(unittest.TestCase): @@ -56,7 +57,8 @@ class TestSamples(unittest.TestCase): if len(warnings): raise MultipleErrors(warnings) - actual = ast.generate() + xml = XmlOutput() + actual = xml.emit(ast) if actual.strip() != expected.strip(): # pragma: no cover diff = difflib.unified_diff(expected.splitlines(), actual.splitlines()) print("\n".join(diff))