diff --git a/blueprintcompiler/decompiler.py b/blueprintcompiler/decompiler.py index 483d6f6..845a88d 100644 --- a/blueprintcompiler/decompiler.py +++ b/blueprintcompiler/decompiler.py @@ -17,8 +17,8 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later -import re import typing as T +from collections import defaultdict from dataclasses import dataclass from enum import Enum @@ -30,7 +30,7 @@ from .xml_reader import Element, parse, parse_string __all__ = ["decompile"] -_DECOMPILERS: T.Dict = {} +_DECOMPILERS: dict[str, list] = defaultdict(list) _CLOSING = { "{": "}", "[": "]", @@ -54,24 +54,25 @@ class DecompileCtx: def __init__(self) -> None: self._result: str = "" self.gir = GirContext() - self._indent: int = 0 self._blocks_need_end: T.List[str] = [] self._last_line_type: LineType = LineType.NONE self.template_class: T.Optional[str] = None + self._obj_type_stack: list[T.Optional[GirType]] = [] + self._node_stack: list[Element] = [] self.gir.add_namespace(get_namespace("Gtk", "4.0")) @property def result(self) -> str: - imports = "\n".join( + import_lines = sorted( [ f"using {ns} {namespace.version};" for ns, namespace in self.gir.namespaces.items() + if ns != "Gtk" ] ) - full_string = imports + "\n" + self._result - formatted_string = formatter.format(full_string) - return formatted_string + full_string = "\n".join(["using Gtk 4.0;", *import_lines]) + self._result + return formatter.format(full_string) def type_by_cname(self, cname: str) -> T.Optional[GirType]: if type := self.gir.get_type_by_cname(cname): @@ -90,10 +91,26 @@ class DecompileCtx: def start_block(self) -> None: self._blocks_need_end.append("") + self._obj_type_stack.append(None) def end_block(self) -> None: if close := self._blocks_need_end.pop(): self.print(close) + self._obj_type_stack.pop() + + @property + def current_obj_type(self) -> T.Optional[GirType]: + return next((x for x in reversed(self._obj_type_stack) if x is not None), None) + + def push_obj_type(self, type: T.Optional[GirType]) -> None: + self._obj_type_stack[-1] = type + + @property + def current_node(self) -> T.Optional[Element]: + if len(self._node_stack) == 0: + return None + else: + return self._node_stack[-1] def end_block_with(self, text: str) -> None: self._blocks_need_end[-1] = text @@ -105,7 +122,7 @@ class DecompileCtx: if len(self._blocks_need_end): self._blocks_need_end[-1] = _CLOSING[line[-1]] - def print_attribute(self, name: str, value: str, type: GirType) -> None: + def print_value(self, value: str, type: T.Optional[GirType]) -> None: def get_enum_name(value): for member in type.members.values(): if ( @@ -117,12 +134,14 @@ class DecompileCtx: return value.replace("-", "_") if type is None: - self.print(f"{name}: {escape_quote(value)};") + self.print(f"{escape_quote(value)}") elif type.assignable_to(FloatType()): - self.print(f"{name}: {value};") + self.print(str(value)) elif type.assignable_to(BoolType()): val = truthy(value) - self.print(f"{name}: {'true' if val else 'false'};") + self.print("true" if val else "false") + elif type.assignable_to(ArrayType(StringType())): + self.print(f"[{', '.join([escape_quote(x) for x in value.split('\n')])}]") elif ( type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Pixbuf")) or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Texture")) @@ -136,30 +155,42 @@ class DecompileCtx: self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutTrigger") ) ): - self.print(f"{name}: {escape_quote(value)};") + self.print(f"{escape_quote(value)}") elif value == self.template_class: - self.print(f"{name}: template;") + self.print("template") elif type.assignable_to( self.gir.namespaces["Gtk"].lookup_type("GObject.Object") - ): - self.print(f"{name}: {value};") + ) or isinstance(type, Interface): + self.print(value) elif isinstance(type, Bitfield): flags = [get_enum_name(flag) for flag in value.split("|")] - self.print(f"{name}: {' | '.join(flags)};") + self.print(" | ".join(flags)) elif isinstance(type, Enumeration): - self.print(f"{name}: {get_enum_name(value)};") + self.print(get_enum_name(value)) + elif isinstance(type, TypeType): + if t := self.type_by_cname(value): + self.print(f"typeof<{full_name(t)}>") + else: + self.print(f"typeof<${value}>") else: - self.print(f"{name}: {escape_quote(value)};") + self.print(f"{escape_quote(value)}") + + def print_attribute(self, name: str, value: str, type: GirType) -> None: + self.print(f"{name}: ") + self.print_value(value, type) + self.print(";") -def _decompile_element( +def decompile_element( ctx: DecompileCtx, gir: T.Optional[GirContext], xml: Element ) -> None: try: - decompiler = _DECOMPILERS.get(xml.tag) - if decompiler is None: + decompilers = [d for d in _DECOMPILERS[xml.tag] if d._filter(ctx)] + if len(decompilers) == 0: raise UnsupportedError(f"unsupported XML tag: <{xml.tag}>") + decompiler = decompilers[0] + args: T.Dict[str, T.Optional[str]] = { canon(name): value for name, value in xml.attrs.items() } @@ -169,13 +200,16 @@ def _decompile_element( else: args["cdata"] = xml.cdata + ctx._node_stack.append(xml) ctx.start_block() gir = decompiler(ctx, gir, **args) - for child in xml.children: - _decompile_element(ctx, gir, child) + if not decompiler._skip_children: + for child in xml.children: + decompile_element(ctx, gir, child) ctx.end_block() + ctx._node_stack.pop() except UnsupportedError as e: raise e @@ -187,7 +221,7 @@ def decompile(data: str) -> str: ctx = DecompileCtx() xml = parse(data) - _decompile_element(ctx, None, xml) + decompile_element(ctx, None, xml) return ctx.result @@ -196,7 +230,7 @@ def decompile_string(data): ctx = DecompileCtx() xml = parse_string(data) - _decompile_element(ctx, None, xml) + decompile_element(ctx, None, xml) return ctx.result @@ -212,7 +246,7 @@ def truthy(string: str) -> bool: return string.lower() in ["yes", "true", "t", "y", "1"] -def full_name(gir) -> str: +def full_name(gir: GirType) -> str: return gir.name if gir.full_name.startswith("Gtk.") else gir.full_name @@ -223,17 +257,43 @@ def lookup_by_cname(gir, cname: str) -> T.Optional[GirType]: return gir.get_containing(Repository).get_type_by_cname(cname) -def decompiler(tag, cdata=False): +def decompiler( + tag, + cdata=False, + parent_type: T.Optional[str] = None, + parent_tag: T.Optional[str] = None, + skip_children=False, +): def decorator(func): func._cdata = cdata - _DECOMPILERS[tag] = func + func._skip_children = skip_children + + def filter(ctx): + if parent_type is not None: + if ( + ctx.current_obj_type is None + or ctx.current_obj_type.full_name != parent_type + ): + return False + + if parent_tag is not None: + if not any(x.tag == parent_tag for x in ctx._node_stack): + return False + + return True + + func._filter = filter + + _DECOMPILERS[tag].append(func) return func return decorator @decompiler("interface") -def decompile_interface(ctx, gir): +def decompile_interface(ctx, gir, domain=None): + if domain is not None: + ctx.print(f"translation-domain {escape_quote(domain)};") return gir @@ -284,7 +344,7 @@ def decompile_property( ctx.print(f"/* Translators: {comments} */") if cdata is None: - ctx.print(f"{name}: ", False) + ctx.print(f"{name}: ") ctx.end_block_with(";") elif bind_source: flags = "" @@ -295,6 +355,10 @@ def decompile_property( flags += " inverted" if "bidirectional" in bind_flags: flags += " bidirectional" + + if bind_source == ctx.template_class: + bind_source = "template" + ctx.print(f"{name}: bind {bind_source}.{bind_property}{flags};") elif truthy(translatable): comments, translatable = decompile_translatable( diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index 9bcde22..30a5eaa 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -904,6 +904,10 @@ class Namespace(GirNode): def get_type_by_cname(self, cname: str) -> T.Optional[GirType]: """Gets a type from this namespace by its C name.""" + for basic in _BASIC_TYPES.values(): + if basic.glib_type_name == cname: + return basic() + for item in self.entries.values(): if ( hasattr(item, "cname") diff --git a/blueprintcompiler/language/binding.py b/blueprintcompiler/language/binding.py index 86beec0..3b9af97 100644 --- a/blueprintcompiler/language/binding.py +++ b/blueprintcompiler/language/binding.py @@ -107,3 +107,9 @@ class SimpleBinding: no_sync_create: bool = False bidirectional: bool = False inverted: bool = False + + +@decompiler("binding") +def decompile_binding(ctx: DecompileCtx, gir: gir.GirContext, name: str): + ctx.end_block_with(";") + ctx.print(f"{name}: bind ") diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index 4c13b21..117fd30 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -18,9 +18,9 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +from ..decompiler import decompile_element from .common import * from .contexts import ScopeCtx, ValueTypeCtx -from .gtkbuilder_template import Template from .types import TypeName expr = Sequence() @@ -274,3 +274,69 @@ expr.children = [ AnyOf(ClosureExpr, LiteralExpr, ["(", Expression, ")"]), ZeroOrMore(AnyOf(LookupOp, CastExpr)), ] + + +@decompiler("lookup", skip_children=True, cdata=True) +def decompile_lookup( + ctx: DecompileCtx, gir: gir.GirContext, cdata: str, name: str, type: str +): + if t := ctx.type_by_cname(type): + type = decompile.full_name(t) + else: + type = "$" + type + + assert ctx.current_node is not None + + constant = None + if len(ctx.current_node.children) == 0: + constant = cdata + elif ( + len(ctx.current_node.children) == 1 + and ctx.current_node.children[0].tag == "constant" + ): + constant = ctx.current_node.children[0].cdata + + if constant is not None: + if constant == ctx.template_class: + ctx.print("template." + name) + else: + ctx.print(constant + "." + name) + return + else: + for child in ctx.current_node.children: + decompile.decompile_element(ctx, gir, child) + + ctx.print(f" as <{type}>.{name}") + + +@decompiler("constant", cdata=True) +def decompile_constant( + ctx: DecompileCtx, gir: gir.GirContext, cdata: str, type: T.Optional[str] = None +): + if type is None: + if cdata == ctx.template_class: + ctx.print("template") + else: + ctx.print(cdata) + else: + ctx.print_value(cdata, ctx.type_by_cname(type)) + + +@decompiler("closure", skip_children=True) +def decompile_closure(ctx: DecompileCtx, gir: gir.GirContext, function: str, type: str): + if t := ctx.type_by_cname(type): + type = decompile.full_name(t) + else: + type = "$" + type + + ctx.print(f"${function}(") + + assert ctx.current_node is not None + for i, node in enumerate(ctx.current_node.children): + decompile_element(ctx, gir, node) + + assert ctx.current_node is not None + if i < len(ctx.current_node.children) - 1: + ctx.print(", ") + + ctx.end_block_with(f") as <{type}>") diff --git a/blueprintcompiler/language/gobject_object.py b/blueprintcompiler/language/gobject_object.py index 2fdbb6a..54cb297 100644 --- a/blueprintcompiler/language/gobject_object.py +++ b/blueprintcompiler/language/gobject_object.py @@ -115,7 +115,7 @@ def validate_parent_type(node, ns: str, name: str, err_msg: str): @decompiler("object") -def decompile_object(ctx, gir, klass, id=None): +def decompile_object(ctx: DecompileCtx, 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 @@ -124,4 +124,5 @@ def decompile_object(ctx, gir, klass, id=None): ctx.print(f"{klass_name} {{") else: ctx.print(f"{klass_name} {id} {{") + ctx.push_obj_type(gir_class) return gir_class diff --git a/blueprintcompiler/language/gtk_combo_box_text.py b/blueprintcompiler/language/gtk_combo_box_text.py index e514e4a..3a0d7c7 100644 --- a/blueprintcompiler/language/gtk_combo_box_text.py +++ b/blueprintcompiler/language/gtk_combo_box_text.py @@ -89,3 +89,29 @@ class ExtComboBoxItems(AstNode): ) def items_completer(lsp, ast_node, match_variables): yield Completion("items", CompletionItemKind.Snippet, snippet="items [$0]") + + +@decompiler("items", parent_type="Gtk.ComboBoxText") +def decompile_items(ctx: DecompileCtx, gir: gir.GirContext): + ctx.print("items [") + + +@decompiler("item", parent_type="Gtk.ComboBoxText", cdata=True) +def decompile_item( + ctx: DecompileCtx, + gir: gir.GirContext, + cdata: str, + id: T.Optional[str] = None, + translatable="false", + comments=None, + context=None, +): + comments, translatable = decompile_translatable( + cdata, translatable, context, comments + ) + if comments: + ctx.print(comments) + if id: + ctx.print(f"{id}: ") + ctx.print(translatable) + ctx.print(",") diff --git a/blueprintcompiler/language/gtk_menu.py b/blueprintcompiler/language/gtk_menu.py index 4b940af..a77484a 100644 --- a/blueprintcompiler/language/gtk_menu.py +++ b/blueprintcompiler/language/gtk_menu.py @@ -266,7 +266,7 @@ def decompile_submenu(ctx, gir, id=None): ctx.print("submenu {") -@decompiler("item") +@decompiler("item", parent_tag="menu") def decompile_item(ctx, gir, id=None): if id: ctx.print(f"item {id} {{") diff --git a/blueprintcompiler/language/gtk_size_group.py b/blueprintcompiler/language/gtk_size_group.py index baa4c30..0945e69 100644 --- a/blueprintcompiler/language/gtk_size_group.py +++ b/blueprintcompiler/language/gtk_size_group.py @@ -96,3 +96,13 @@ class ExtSizeGroupWidgets(AstNode): ) def size_group_completer(lsp, ast_node, match_variables): yield Completion("widgets", CompletionItemKind.Snippet, snippet="widgets [$0]") + + +@decompiler("widgets") +def size_group_decompiler(ctx, gir: gir.GirContext): + ctx.print("widgets [") + + +@decompiler("widget") +def widget_decompiler(ctx, gir: gir.GirContext, name: str): + ctx.print(name + ",") diff --git a/blueprintcompiler/language/gtk_string_list.py b/blueprintcompiler/language/gtk_string_list.py index be01262..36a01f6 100644 --- a/blueprintcompiler/language/gtk_string_list.py +++ b/blueprintcompiler/language/gtk_string_list.py @@ -73,3 +73,25 @@ class ExtStringListStrings(AstNode): ) def strings_completer(lsp, ast_node, match_variables): yield Completion("strings", CompletionItemKind.Snippet, snippet="strings [$0]") + + +@decompiler("items", parent_type="Gtk.StringList") +def decompile_strings(ctx: DecompileCtx, gir: gir.GirContext): + ctx.print("strings [") + + +@decompiler("item", cdata=True, parent_type="Gtk.StringList") +def decompile_item( + ctx: DecompileCtx, + gir: gir.GirContext, + translatable="false", + comments=None, + context=None, + cdata=None, +): + comments, translatable = decompile_translatable( + cdata, translatable, context, comments + ) + if comments is not None: + ctx.print(comments) + ctx.print(translatable + ",") diff --git a/blueprintcompiler/utils.py b/blueprintcompiler/utils.py index 6f2bd40..ea8102e 100644 --- a/blueprintcompiler/utils.py +++ b/blueprintcompiler/utils.py @@ -109,14 +109,14 @@ class UnescapeError(Exception): def escape_quote(string: str) -> str: return ( - "'" + '"' + ( string.replace("\\", "\\\\") - .replace("'", "\\'") + .replace('"', '\\"') .replace("\n", "\\n") .replace("\t", "\\t") ) - + "'" + + '"' ) diff --git a/tests/samples/accessibility.blp b/tests/samples/accessibility.blp index ec563b4..e9f7fc1 100644 --- a/tests/samples/accessibility.blp +++ b/tests/samples/accessibility.blp @@ -7,5 +7,5 @@ Gtk.Box { checked: true; } } -Gtk.Label my_label {} +Gtk.Label my_label {} diff --git a/tests/samples/accessibility_dec.blp b/tests/samples/accessibility_dec.blp index 4a370cb..9a2643c 100644 --- a/tests/samples/accessibility_dec.blp +++ b/tests/samples/accessibility_dec.blp @@ -2,7 +2,7 @@ using Gtk 4.0; Box { accessibility { - label: _('Hello, world!'); + label: _("Hello, world!"); labelled-by: my_label; checked: true; } diff --git a/tests/samples/action_widgets.blp b/tests/samples/action_widgets.blp index 2d4d6ae..98601f2 100644 --- a/tests/samples/action_widgets.blp +++ b/tests/samples/action_widgets.blp @@ -1,25 +1,25 @@ using Gtk 4.0; Dialog { - [action response=cancel] - Button cancel_button { - label: _("Cancel"); - } + [action response=cancel] + Button cancel_button { + label: _("Cancel"); + } - [action response=9] - Button custom_response_button { - label: _("Reinstall Windows"); - } + [action response=9] + Button custom_response_button { + label: _("Reinstall Windows"); + } - [action response=ok default] - Button ok_button { - label: _("Ok"); - } + [action response=ok default] + Button ok_button { + label: _("Ok"); + } } InfoBar { - [action response=ok] - Button ok_info_button { - label: _("Ok"); - } + [action response=ok] + Button ok_info_button { + label: _("Ok"); + } } diff --git a/tests/samples/adw_alertdialog_responses.blp b/tests/samples/adw_alertdialog_responses.blp index 6680aa6..eefe951 100644 --- a/tests/samples/adw_alertdialog_responses.blp +++ b/tests/samples/adw_alertdialog_responses.blp @@ -3,8 +3,8 @@ using Adw 1; Adw.AlertDialog { responses [ - cancel: _('Cancel'), - discard: _('Discard') destructive, - save: 'Save' suggested disabled, + cancel: _("Cancel"), + discard: _("Discard") destructive, + save: "Save" suggested disabled, ] -} \ No newline at end of file +} diff --git a/tests/samples/adw_messagedialog_responses.blp b/tests/samples/adw_messagedialog_responses.blp index be8c03b..5b35842 100644 --- a/tests/samples/adw_messagedialog_responses.blp +++ b/tests/samples/adw_messagedialog_responses.blp @@ -3,8 +3,8 @@ using Adw 1; Adw.MessageDialog { responses [ - cancel: _('Cancel'), - discard: _('Discard') destructive, - save: 'Save' suggested disabled, + cancel: _("Cancel"), + discard: _("Discard") destructive, + save: "Save" suggested disabled, ] -} \ No newline at end of file +} diff --git a/tests/samples/expr_closure.blp b/tests/samples/expr_closure.blp index 81c3f2c..a67403e 100644 --- a/tests/samples/expr_closure.blp +++ b/tests/samples/expr_closure.blp @@ -2,4 +2,4 @@ using Gtk 4.0; Label my-label { label: bind ($my-closure(my-label.margin-bottom)) as ; -} \ No newline at end of file +} diff --git a/tests/samples/expr_closure_args.blp b/tests/samples/expr_closure_args.blp index 5699094..136fb0c 100644 --- a/tests/samples/expr_closure_args.blp +++ b/tests/samples/expr_closure_args.blp @@ -1,5 +1,5 @@ using Gtk 4.0; Label { - label: bind $my-closure (true, 10, "Hello") as ; -} \ No newline at end of file + label: bind $my-closure(true, 10, "Hello") as ; +} diff --git a/tests/samples/expr_closure_dec.blp b/tests/samples/expr_closure_dec.blp new file mode 100644 index 0000000..ff6069e --- /dev/null +++ b/tests/samples/expr_closure_dec.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Label my-label { + label: bind $my-closure(my-label.margin-bottom) as ; +} diff --git a/tests/samples/expr_lookup.blp b/tests/samples/expr_lookup.blp index 12e2de1..679f600 100644 --- a/tests/samples/expr_lookup.blp +++ b/tests/samples/expr_lookup.blp @@ -5,5 +5,5 @@ Overlay { } Label { - label: bind (label.parent) as .child as