diff --git a/gtkblueprinttool/ast.py b/gtkblueprinttool/ast.py index 6f53e90..a5073e5 100644 --- a/gtkblueprinttool/ast.py +++ b/gtkblueprinttool/ast.py @@ -266,14 +266,23 @@ class Child(AstNode): class ObjectContent(AstNode): child_type = "object_content" - def __init__(self, properties=[], signals=[], children=[]): + def __init__(self, properties=[], signals=[], children=[], style=[]): super().__init__() self.properties = properties self.signals = signals self.children = children + self.style = style + + @validate() + def only_one_style_class(self): + if len(self.style) > 1: + raise CompileError( + f"Only one style directive allowed per object, but this object contains {len(self.style)}", + start=self.style[1].group.start, + ) def emit_xml(self, xml: XmlEmitter): - for x in [*self.properties, *self.signals, *self.children]: + for x in [*self.properties, *self.signals, *self.children, *self.style]: x.emit_xml(xml) @@ -390,3 +399,28 @@ class Signal(AstNode): if self.detail_name: name += "::" + self.detail_name xml.put_self_closing("signal", name=name, handler=self.handler, swapped="true" if self.swapped else None) + + +class Style(AstNode): + child_type = "style" + + def __init__(self, style_classes=None): + super().__init__() + self.style_classes = style_classes or [] + + def emit_xml(self, xml: XmlEmitter): + xml.start_tag("style") + for style in self.style_classes: + style.emit_xml(xml) + xml.end_tag() + + +class StyleClass(AstNode): + child_type = "style_classes" + + def __init__(self, name): + super().__init__() + self.name = name + + def emit_xml(self, xml): + xml.put_self_closing("class", name=self.name) diff --git a/gtkblueprinttool/parse_tree.py b/gtkblueprinttool/parse_tree.py index 571d8c3..f799925 100644 --- a/gtkblueprinttool/parse_tree.py +++ b/gtkblueprinttool/parse_tree.py @@ -319,6 +319,19 @@ class ZeroOrMore(ParseNode): return True +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 = child + self.delimiter = 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. """ @@ -362,6 +375,9 @@ class OpenParen(StaticToken): class CloseParen(StaticToken): token_type = TokenType.CLOSE_PAREN +class Comma(StaticToken): + token_type = TokenType.COMMA + class Op(ParseNode): """ ParseNode that matches the given operator. """ diff --git a/gtkblueprinttool/parser.py b/gtkblueprinttool/parser.py index f61d15e..ce39057 100644 --- a/gtkblueprinttool/parser.py +++ b/gtkblueprinttool/parser.py @@ -137,11 +137,27 @@ def parse(tokens) -> ast.UI: ) ) + style = Group( + ast.Style, + Sequence( + Keyword("style"), + Delimited( + Group( + ast.StyleClass, + UseQuoted("name") + ), + Comma(), + ), + StmtEnd(), + ) + ) + object_content = Group( ast.ObjectContent, Sequence( OpenBlock(), ZeroOrMore(AnyOf( + style, property, binding, signal, diff --git a/tests/test_parser.py b/tests/test_parser.py index a2e635d..cc957f3 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -45,6 +45,7 @@ class TestParser(unittest.TestCase): } Label { + style "dim-label", "my-class"; label: "Text"; notify::visible => on_notify_visible(); } @@ -109,3 +110,7 @@ class TestParser(unittest.TestCase): self.assertEqual(signal.handler, "on_notify_visible") self.assertEqual(signal.detail_name, "visible") self.assertFalse(signal.swapped) + self.assertEqual(len(obj.object_content.style), 1) + style = obj.object_content.style[0] + self.assertEqual(len(style.style_classes), 2) + self.assertEqual([s.name for s in style.style_classes], ["dim-label", "my-class"])