Separate output into its own module

This commit is contained in:
James Westman 2022-10-14 21:04:37 -05:00
parent 8cf793023d
commit a24f16109f
No known key found for this signature in database
GPG key ID: CE2DBA0ADB654EA6
33 changed files with 407 additions and 291 deletions

View file

@ -18,7 +18,7 @@ build:
- ninja -C _build docs/en - ninja -C _build docs/en
- git clone https://gitlab.gnome.org/jwestman/blueprint-regression-tests.git - git clone https://gitlab.gnome.org/jwestman/blueprint-regression-tests.git
- cd blueprint-regression-tests - cd blueprint-regression-tests
- git checkout d14b95b6c1fc0cddd4b0ad21d224b05edee2d01f - git checkout 94613f275efc810610768d5ee8b2aec28392c3e8
- ./test.sh - ./test.sh
- cd .. - cd ..
coverage: '/TOTAL.*\s([.\d]+)%/' coverage: '/TOTAL.*\s([.\d]+)%/'

View file

@ -23,7 +23,6 @@ import typing as T
from .errors import * from .errors import *
from .lsp_utils import SemanticToken from .lsp_utils import SemanticToken
from .xml_emitter import XmlEmitter
class Children: class Children:
@ -96,16 +95,6 @@ class AstNode:
if isinstance(item, attr_type): if isinstance(item, attr_type):
yield name, item 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]: def get_docs(self, idx: int) -> T.Optional[str]:
for name, attr in self._attrs_by_type(Docs): for name, attr in self._attrs_by_type(Docs):
if attr.token_name: if attr.token_name:

View file

@ -28,11 +28,11 @@ from gi.repository import GIRepository # type: ignore
from .errors import CompileError, CompilerBugError from .errors import CompileError, CompilerBugError
from . import typelib, xml_reader from . import typelib, xml_reader
_namespace_cache = {} _namespace_cache: T.Dict[str, "Namespace"] = {}
_xml_cache = {} _xml_cache = {}
def get_namespace(namespace, version): def get_namespace(namespace, version) -> "Namespace":
search_paths = GIRepository.Repository.get_search_path() search_paths = GIRepository.Repository.get_search_path()
filename = f"{namespace}-{version}.typelib" filename = f"{namespace}-{version}.typelib"
@ -518,11 +518,11 @@ class Namespace(GirNode):
return get_xml(self.name, self.version).get_elements("namespace")[0] return get_xml(self.name, self.version).get_elements("namespace")[0]
@cached_property @cached_property
def name(self): def name(self) -> str:
return self.tl.HEADER_NAMESPACE return self.tl.HEADER_NAMESPACE
@cached_property @cached_property
def version(self): def version(self) -> str:
return self.tl.HEADER_NSVERSION return self.tl.HEADER_NSVERSION
@property @property

View file

@ -23,6 +23,7 @@ import difflib
import os import os
from . import decompiler, tokenizer, parser from . import decompiler, tokenizer, parser
from .outputs.xml import XmlOutput
from .errors import MultipleErrors, PrintableError from .errors import MultipleErrors, PrintableError
from .utils import Colors from .utils import Colors
@ -57,7 +58,8 @@ def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]:
if len(ast.errors): if len(ast.errors):
raise MultipleErrors(ast.errors) raise MultipleErrors(ast.errors)
ast.generate() output = XmlOutput()
output.emit(ast)
except PrintableError as e: except PrintableError as e:
e.pretty_print(out_file, decompiled) e.pretty_print(out_file, decompiled)

View file

@ -1,16 +1,13 @@
""" Contains all the syntax beyond basic objects, properties, signal, and
templates. """
from .attributes import BaseAttribute, BaseTypedAttribute from .attributes import BaseAttribute, BaseTypedAttribute
from .expression import Expr from .expression import IdentExpr, LookupOp, Expr
from .gobject_object import Object, ObjectContent from .gobject_object import Object, ObjectContent
from .gobject_property import Property from .gobject_property import Property
from .gobject_signal import Signal from .gobject_signal import Signal
from .gtk_a11y import A11y from .gtk_a11y import A11y
from .gtk_combo_box_text import Items 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_layout import Layout
from .gtk_menu import menu from .gtk_menu import menu, Menu, MenuAttribute
from .gtk_size_group import Widgets from .gtk_size_group import Widgets
from .gtk_string_list import Strings from .gtk_string_list import Strings
from .gtk_styles import Styles from .gtk_styles import Styles
@ -18,7 +15,8 @@ from .gtkbuilder_child import Child
from .gtkbuilder_template import Template from .gtkbuilder_template import Template
from .imports import GtkDirective, Import from .imports import GtkDirective, Import
from .ui import UI 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 * from .common import *

View file

@ -32,17 +32,6 @@ class BaseAttribute(AstNode):
def name(self): def name(self):
return self.tokens["name"] 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): class BaseTypedAttribute(BaseAttribute):
""" A BaseAttribute whose parent has a value_type property that can assist """ A BaseAttribute whose parent has a value_type property that can assist

View file

@ -27,7 +27,6 @@ from ..decompiler import DecompileCtx, decompiler
from ..gir import StringType, BoolType, IntType, FloatType, GirType, Enumeration from ..gir import StringType, BoolType, IntType, FloatType, GirType, Enumeration
from ..lsp_utils import Completion, CompletionItemKind, SemanticToken, SemanticTokenType from ..lsp_utils import Completion, CompletionItemKind, SemanticToken, SemanticTokenType
from ..parse_tree import * from ..parse_tree import *
from ..xml_emitter import XmlEmitter
OBJECT_CONTENT_HOOKS = AnyOf() OBJECT_CONTENT_HOOKS = AnyOf()

View file

@ -27,9 +27,6 @@ expr = Pratt()
class Expr(AstNode): class Expr(AstNode):
grammar = expr grammar = expr
def emit_xml(self, xml: XmlEmitter):
self.children[-1].emit_xml(xml)
class InfixExpr(AstNode): class InfixExpr(AstNode):
@property @property
@ -41,19 +38,17 @@ class InfixExpr(AstNode):
class IdentExpr(AstNode): class IdentExpr(AstNode):
grammar = UseIdent("ident") grammar = UseIdent("ident")
def emit_xml(self, xml: XmlEmitter): @property
xml.start_tag("constant") def ident(self) -> str:
xml.put_text(self.tokens["ident"]) return self.tokens["ident"]
xml.end_tag()
class LookupOp(InfixExpr): class LookupOp(InfixExpr):
grammar = [".", UseIdent("property")] grammar = [".", UseIdent("property")]
def emit_xml(self, xml: XmlEmitter): @property
xml.start_tag("lookup", name=self.tokens["property"]) def property_name(self) -> str:
self.lhs.emit_xml(xml) return self.tokens["property"]
xml.end_tag()
expr.children = [ expr.children = [

View file

@ -33,10 +33,6 @@ class ObjectContent(AstNode):
def gir_class(self): def gir_class(self):
return self.parent.gir_class return self.parent.gir_class
def emit_xml(self, xml: XmlEmitter):
for x in self.children:
x.emit_xml(xml)
class Object(AstNode): class Object(AstNode):
grammar: T.Any = [ grammar: T.Any = [
ConcreteClassName, ConcreteClassName,
@ -44,9 +40,21 @@ class Object(AstNode):
ObjectContent, 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 @property
def gir_class(self): def gir_class(self):
return self.children[ClassName][0].gir_type return self.class_name.gir_type
@cached_property @cached_property
def action_widgets(self) -> T.List[ResponseId]: def action_widgets(self) -> T.List[ResponseId]:
@ -62,28 +70,6 @@ class Object(AstNode):
if child.response_id 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): def validate_parent_type(node, ns: str, name: str, err_msg: str):
parent = node.root.gir.get_type(name, ns) parent = node.root.gir.get_type(name, ns)

View file

@ -133,44 +133,3 @@ class Property(AstNode):
def property_docs(self): def property_docs(self):
if self.gir_property is not None: if self.gir_property is not None:
return self.gir_property.doc 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()

View file

@ -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 @property
def gir_signal(self): def gir_signal(self):
@ -89,19 +113,6 @@ class Signal(AstNode):
return self.gir_signal.doc 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") @decompiler("signal")
def decompile_signal(ctx, gir, name, handler, swapped="false", object=None): def decompile_signal(ctx, gir, name, handler, swapped="false", object=None):
object_name = object or "" object_name = object or ""

View file

@ -167,12 +167,6 @@ class A11y(AstNode):
def unique_in_parent(self): def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate accessibility block") 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( @completer(
applies_in=[ObjectContent], applies_in=[ObjectContent],

View file

@ -60,12 +60,6 @@ class Items(AstNode):
def unique_in_parent(self): def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate items block") 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( @completer(
applies_in=[ObjectContent], applies_in=[ObjectContent],

View file

@ -39,18 +39,9 @@ class Filters(AstNode):
) )
wrapped_validator(self) 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): class FilterString(AstNode):
def emit_xml(self, xml): pass
xml.start_tag(self.tokens["tag_name"])
xml.put_text(self.tokens["name"])
xml.end_tag()
def create_node(tag_name: str, singular: str): def create_node(tag_name: str, singular: str):

View file

@ -64,12 +64,6 @@ class Layout(AstNode):
def unique_in_parent(self): def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate layout block") 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( @completer(
applies_in=[ObjectContent], applies_in=[ObjectContent],

View file

@ -19,22 +19,26 @@
import typing as T import typing as T
from blueprintcompiler.language.values import Value
from .attributes import BaseAttribute from .attributes import BaseAttribute
from .gobject_object import Object, ObjectContent from .gobject_object import Object, ObjectContent
from .common import * from .common import *
class Menu(Object): class Menu(AstNode):
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 @property
def gir_class(self): def gir_class(self):
return self.root.gir.namespaces["Gtk"].lookup_type("Gio.Menu") 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): class MenuAttribute(BaseAttribute):
tag_name = "attribute" tag_name = "attribute"
@ -43,6 +47,10 @@ class MenuAttribute(BaseAttribute):
def value_type(self): def value_type(self):
return None return None
@property
def value(self) -> Value:
return self.children[Value][0]
menu_contents = Sequence() menu_contents = Sequence()

View file

@ -39,9 +39,6 @@ class Widget(AstNode):
f"Cannot assign {object.gir_class.full_name} to {type.full_name}" 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): class Widgets(AstNode):
grammar = [ grammar = [
@ -59,12 +56,6 @@ class Widgets(AstNode):
def unique_in_parent(self): def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate widgets block") 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( @completer(
applies_in=[ObjectContent], applies_in=[ObjectContent],

View file

@ -31,13 +31,6 @@ class Item(AstNode):
def value_type(self): def value_type(self):
return StringType() 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): class Strings(AstNode):
grammar = [ grammar = [
@ -55,12 +48,6 @@ class Strings(AstNode):
def unique_in_parent(self): def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate strings block") 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( @completer(
applies_in=[ObjectContent], applies_in=[ObjectContent],

View file

@ -25,9 +25,6 @@ from .common import *
class StyleClass(AstNode): class StyleClass(AstNode):
grammar = UseQuoted("name") grammar = UseQuoted("name")
def emit_xml(self, xml):
xml.put_self_closing("class", name=self.tokens["name"])
class Styles(AstNode): class Styles(AstNode):
grammar = [ grammar = [
@ -45,12 +42,6 @@ class Styles(AstNode):
def unique_in_parent(self): def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate styles block") 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( @completer(
applies_in=[ObjectContent], applies_in=[ObjectContent],

View file

@ -41,6 +41,10 @@ class Child(AstNode):
Object, Object,
] ]
@property
def object(self) -> Object:
return self.children[Object][0]
@validate() @validate()
def parent_can_have_child(self): def parent_can_have_child(self):
if gir_class := self.parent.gir_class: if gir_class := self.parent.gir_class:
@ -70,17 +74,6 @@ class Child(AstNode):
else: else:
return None 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") @decompiler("child")
def decompile_child(ctx, gir, type=None, internal_child=None): def decompile_child(ctx, gir, type=None, internal_child=None):

View file

@ -34,28 +34,27 @@ class Template(Object):
ObjectContent, 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 @property
def gir_class(self): def gir_class(self):
# Templates might not have a parent class defined # Templates might not have a parent class defined
if len(self.children[ClassName]): if class_name := self.class_name:
return self.children[ClassName][0].gir_type return class_name.gir_type
@validate("id") @validate("id")
def unique_in_parent(self): 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])}",) 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") @decompiler("template")
def decompile_template(ctx: DecompileCtx, gir, klass, parent="Widget"): def decompile_template(ctx: DecompileCtx, gir, klass, parent="Widget"):

View file

@ -61,10 +61,6 @@ class GtkDirective(AstNode):
return gir.get_namespace("Gtk", self.tokens["version"]) 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): class Import(AstNode):
grammar = Statement( grammar = Statement(
"using", "using",
@ -82,6 +78,3 @@ class Import(AstNode):
return gir.get_namespace(self.tokens["namespace"], self.tokens["version"]) return gir.get_namespace(self.tokens["namespace"], self.tokens["version"])
except CompileError: except CompileError:
return None return None
def emit_xml(self, xml):
pass

View file

@ -120,6 +120,14 @@ class ResponseId(AstNode):
if widget.tokens["is_default"]: if widget.tokens["is_default"]:
raise CompileError("Default response is already set") 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 @property
def widget_id(self) -> str: def widget_id(self) -> str:
"""Get action widget ID.""" """Get action widget ID."""
@ -128,25 +136,3 @@ class ResponseId(AstNode):
_object: Object = self.parent.children[Object][0] _object: Object = self.parent.children[Object][0]
return _object.tokens["id"] 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 <action-widgets> 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()

View file

@ -76,9 +76,6 @@ class TypeName(AstNode):
if self.gir_type: if self.gir_type:
return self.gir_type.doc return self.gir_type.doc
def emit_xml(self, xml: XmlEmitter):
pass
class ClassName(TypeName): class ClassName(TypeName):
@validate("namespace", "class_name") @validate("namespace", "class_name")

View file

@ -86,10 +86,3 @@ class UI(AstNode):
token = obj.group.tokens["id"] token = obj.group.tokens["id"]
raise CompileError(f"Duplicate object ID '{obj.tokens['id']}'", token.start, token.end) raise CompileError(f"Duplicate object ID '{obj.tokens['id']}'", token.start, token.end)
passed[obj.tokens["id"]] = obj 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()

View file

@ -46,14 +46,12 @@ class TranslatedStringValue(Value):
) )
@property @property
def attrs(self): def string(self) -> str:
attrs = { "translatable": "true" } return self.tokens["value"]
if "context" in self.tokens:
attrs["context"] = self.tokens["context"]
return attrs
def emit_xml(self, xml: XmlEmitter): @property
xml.put_text(self.tokens["value"]) def context(self) -> str | None:
return self.tokens["context"]
class TypeValue(Value): class TypeValue(Value):
@ -68,9 +66,6 @@ class TypeValue(Value):
def type_name(self): def type_name(self):
return self.children[TypeName][0] return self.children[TypeName][0]
def emit_xml(self, xml: XmlEmitter):
xml.put_text(self.type_name.glib_type_name)
@validate() @validate()
def validate_for_type(self): def validate_for_type(self):
type = self.parent.value_type type = self.parent.value_type
@ -81,8 +76,9 @@ class TypeValue(Value):
class QuotedValue(Value): class QuotedValue(Value):
grammar = UseQuoted("value") grammar = UseQuoted("value")
def emit_xml(self, xml: XmlEmitter): @property
xml.put_text(self.tokens["value"]) def value(self) -> str:
return self.tokens["value"]
@validate() @validate()
def validate_for_type(self): def validate_for_type(self):
@ -119,8 +115,9 @@ class QuotedValue(Value):
class NumberValue(Value): class NumberValue(Value):
grammar = UseNumber("value") grammar = UseNumber("value")
def emit_xml(self, xml: XmlEmitter): @property
xml.put_text(self.tokens["value"]) def value(self) -> int | float:
return self.tokens["value"]
@validate() @validate()
def validate_for_type(self): def validate_for_type(self):
@ -179,19 +176,10 @@ class FlagsValue(Value):
if type is not None and not isinstance(type, gir.Bitfield): if type is not None and not isinstance(type, gir.Bitfield):
raise CompileError(f"{type.full_name} is not a bitfield type") 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): class IdentValue(Value):
grammar = UseIdent("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() @validate()
def validate_for_type(self): def validate_for_type(self):
type = self.parent.value_type type = self.parent.value_type

View file

@ -25,7 +25,7 @@ from .errors import PrintableError, report_bug, MultipleErrors
from .lsp import LanguageServer from .lsp import LanguageServer
from . import parser, tokenizer, decompiler, interactive_port from . import parser, tokenizer, decompiler, interactive_port
from .utils import Colors from .utils import Colors
from .xml_emitter import XmlEmitter from .outputs import XmlOutput
VERSION = "uninstalled" VERSION = "uninstalled"
LIBDIR = None LIBDIR = None
@ -141,7 +141,9 @@ class BlueprintApp:
if len(ast.errors): if len(ast.errors):
raise MultipleErrors(ast.errors) raise MultipleErrors(ast.errors)
return ast.generate(), warnings formatter = XmlOutput()
return formatter.emit(ast), warnings
def main(version, libdir): def main(version, libdir):

View file

@ -0,0 +1,7 @@
from ..language import UI
class OutputFormat:
def emit(self, ui: UI) -> str:
raise NotImplementedError()
from .xml import XmlOutput

View file

@ -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()

View file

@ -17,9 +17,10 @@
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from xml.sax import saxutils from xml.sax import saxutils
from . import gir
from blueprintcompiler.gir import GirType
from blueprintcompiler.language.types import ClassName
class XmlEmitter: class XmlEmitter:
@ -29,7 +30,7 @@ class XmlEmitter:
self._tag_stack = [] self._tag_stack = []
self._needs_newline = False self._needs_newline = False
def start_tag(self, tag, **attrs): def start_tag(self, tag, **attrs: str | GirType | ClassName | bool | None):
self._indent() self._indent()
self.result += f"<{tag}" self.result += f"<{tag}"
for key, val in attrs.items(): for key, val in attrs.items():
@ -55,7 +56,7 @@ class XmlEmitter:
self.result += f"</{tag}>" self.result += f"</{tag}>"
self._needs_newline = True self._needs_newline = True
def put_text(self, text): def put_text(self, text: str | int | float):
self.result += saxutils.escape(str(text)) self.result += saxutils.escape(str(text))
self._needs_newline = False self._needs_newline = False
@ -64,7 +65,9 @@ class XmlEmitter:
self.result += "\n" + " " * (self.indent * len(self._tag_stack)) self.result += "\n" + " " * (self.indent * len(self._tag_stack))
def _to_string(self, val): 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 return val.glib_type_name
else: else:
return str(val) return str(val)

View file

@ -1,6 +1,8 @@
import os, sys import os, sys
from pythonfuzz.main import PythonFuzz from pythonfuzz.main import PythonFuzz
from blueprintcompiler.outputs.xml import XmlOutput
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from blueprintcompiler import tokenizer, parser, decompiler, gir from blueprintcompiler import tokenizer, parser, decompiler, gir
@ -17,8 +19,9 @@ def fuzz(buf):
tokens = tokenizer.tokenize(blueprint) tokens = tokenizer.tokenize(blueprint)
ast, errors, warnings = parser.parse(tokens) ast, errors, warnings = parser.parse(tokens)
xml = XmlOutput()
if errors is None and len(ast.errors) == 0: if errors is None and len(ast.errors) == 0:
actual = ast.generate() xml.emit(ast)
except CompilerBugError as e: except CompilerBugError as e:
raise e raise e
except PrintableError: except PrintableError:

View file

@ -5,7 +5,7 @@
<signal name="activate" handler="click" object="button"/> <signal name="activate" handler="click" object="button"/>
</object> </object>
<object class="GtkButton" id="button"> <object class="GtkButton" id="button">
<signal name="clicked" handler="on_button_clicked" swapped="true"/> <signal name="clicked" handler="on_button_clicked" swapped="True"/>
<signal name="notify::visible" handler="on_button_notify_visible"/> <signal name="notify::visible" handler="on_button_notify_visible"/>
</object> </object>
</interface> </interface>

View file

@ -28,6 +28,7 @@ from blueprintcompiler.completions import complete
from blueprintcompiler.errors import PrintableError, MultipleErrors, CompileError from blueprintcompiler.errors import PrintableError, MultipleErrors, CompileError
from blueprintcompiler.tokenizer import Token, TokenType, tokenize from blueprintcompiler.tokenizer import Token, TokenType, tokenize
from blueprintcompiler import utils from blueprintcompiler import utils
from blueprintcompiler.outputs.xml import XmlOutput
class TestSamples(unittest.TestCase): class TestSamples(unittest.TestCase):
@ -56,7 +57,8 @@ class TestSamples(unittest.TestCase):
if len(warnings): if len(warnings):
raise MultipleErrors(warnings) raise MultipleErrors(warnings)
actual = ast.generate() xml = XmlOutput()
actual = xml.emit(ast)
if actual.strip() != expected.strip(): # pragma: no cover if actual.strip() != expected.strip(): # pragma: no cover
diff = difflib.unified_diff(expected.splitlines(), actual.splitlines()) diff = difflib.unified_diff(expected.splitlines(), actual.splitlines())
print("\n".join(diff)) print("\n".join(diff))