diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index b267083..d896514 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -27,7 +27,9 @@ from .values import ArrayValue, ObjectValue, Value, VariantValue class Property(AstNode): grammar = Statement( - UseIdent("name"), ":", AnyOf(Binding, VariantValue, ObjectValue, Value, ArrayValue) + UseIdent("name"), + ":", + AnyOf(Binding, VariantValue, ObjectValue, Value, ArrayValue), ) @property diff --git a/blueprintcompiler/language/gtk_menu.py b/blueprintcompiler/language/gtk_menu.py index 9e76d61..5a6627a 100644 --- a/blueprintcompiler/language/gtk_menu.py +++ b/blueprintcompiler/language/gtk_menu.py @@ -98,7 +98,7 @@ class MenuAttribute(AstNode): return self.tokens["name"] @property - def value(self) -> StringValue | VariantValue: + def value(self) -> T.Union[StringValue, VariantValue]: if len(self.children[StringValue]) > 0: return self.children[StringValue][0] elif len(self.children[VariantValue]) > 0: @@ -137,7 +137,10 @@ menu_attribute = Group( [ UseIdent("name"), ":", - Err(AnyOf(StringValue, VariantValue), "Expected string or translated string"), + Err( + AnyOf(StringValue, VariantValue), + "Expected string, translated string, or variant", + ), Match(";").expected(), ], ) diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 41a4c5e..021cf90 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -26,6 +26,12 @@ from .common import * from .contexts import ScopeCtx, ValueTypeCtx from .gobject_object import Object from .types import TypeName +from .variant import VarContent + +import gi + +gi.require_version("GLib", "2.0") +from gi.repository import GLib class Translated(AstNode): @@ -371,6 +377,7 @@ class IdentLiteral(AstNode): else: return None + class VariantValue(AstNode): grammar = [ "variant", @@ -378,8 +385,8 @@ class VariantValue(AstNode): UseQuoted("type"), ">", "(", - UseQuoted("value"), - ")" + Err(VarContent, "Invalid variant content!"), + ")", ] @property @@ -388,7 +395,7 @@ class VariantValue(AstNode): @property def var_value(self) -> str: - return self.tokens["value"] + return self.children[0].content @validate() def validate_for_type(self) -> None: @@ -404,12 +411,34 @@ class VariantValue(AstNode): raise CompileError(f"Cannot convert variant to number") elif isinstance(expected_type, gir.StringType): raise CompileError("Cannot convert variant to string") - elif isinstance(expected_type, gir.Boxed) and expected_type.full_name == "GLib.Variant": + elif ( + isinstance(expected_type, gir.Boxed) + and expected_type.full_name == "GLib.Variant" + ): pass else: raise CompileError(f"Cannot convert variant into {expected_type.full_name}") pass + @validate("type") + def validate_type(self): + if not GLib.VariantType.string_is_valid(self.var_type): + raise CompileError(f"`{self.var_type}` is not a valid variant type") + + @validate() + def validate_content(self): + if not GLib.VariantType.string_is_valid(self.var_type): + return + + try: + var_ty = GLib.VariantType.new(self.var_type) + var_val = GLib.Variant.parse(var_ty, self.var_value) + except GLib.GError as error: + raise CompileError(f"Variant did not match specified type: {error}") + + pass + + class Literal(AstNode): grammar = AnyOf( TypeLiteral, diff --git a/blueprintcompiler/language/variant.py b/blueprintcompiler/language/variant.py new file mode 100644 index 0000000..450a0c8 --- /dev/null +++ b/blueprintcompiler/language/variant.py @@ -0,0 +1,122 @@ +import typing as T + +from blueprintcompiler.gir import ArrayType +from blueprintcompiler.lsp_utils import SemanticToken + +from .common import * +from .contexts import ScopeCtx, ValueTypeCtx +from .gobject_object import Object +from .types import TypeName + +VAR_CONTENT_HOOKS: list[T.Any] = [] + + +class VarContent(AstNode): + grammar = AnyOf(*VAR_CONTENT_HOOKS) + + @property + def content(self) -> str: + return self.children[0].content + + +class VarContentBool(AstNode): + grammar = AnyOf( + [Keyword("true"), UseLiteral("value", True)], + [Keyword("false"), UseLiteral("value", False)], + ) + + @property + def content(self) -> str: + if self.tokens["value"]: + return "true" + else: + return "false" + + +class VarContentString(AstNode): + grammar = UseQuoted("value") + + @property + def content(self) -> str: + return utils.escape_quote(self.tokens["value"]) + + +class VarContentNumber(AstNode): + grammar = UseNumberText("value") + + @property + def content(self) -> str: + return self.tokens["value"] + + +class VarContentTuple(AstNode): + grammar = ["(", Delimited(VarContent, ","), ")"] + + @property + def content(self) -> str: + inner = ", ".join(child.content for child in self.children) + return f"({inner})" + + +class VarContentArray(AstNode): + grammar = ["[", Delimited(VarContent, ","), "]"] + + @property + def content(self) -> str: + inner = ", ".join(child.content for child in self.children) + return f"[{inner}]" + + +class VarContentDictEntry(AstNode): + grammar = ["{", VarContent, ",", VarContent, "}"] + + @property + def content(self): + return f"{{{self.children[0].content}, {self.children[1].content}}}" + + +class VarContentDict(AstNode): + grammar = ["{", Delimited([VarContent, ":", VarContent], ","), "}"] + + @property + def content(self) -> str: + inner = ", ".join( + f"{key.content}: {value.content}" + for (key, value) in utils.iter_batched(self.children, 2, strict=True) + ) + return f"{{{inner}}}" + + +class VarContentVariant(AstNode): + grammar = ["<", VarContent, ">"] + + @property + def content(self) -> str: + return f"<{self.children[0].content}>" + + +class VarContentMaybe(AstNode): + grammar = AnyOf( + [Keyword("just"), VarContent], + [Keyword("nothing")], + ) + + @property + def content(self) -> str: + if self.children[0] is not None: + return f"just {self.children[0].content}" + else: + return "nothing" + + +VarContent.grammar.children = [ + VarContentString, + VarContentNumber, + VarContentBool, + VarContentMaybe, + VarContentTuple, + VarContentDict, + VarContentDictEntry, + VarContentArray, + VarContentVariant, +] diff --git a/blueprintcompiler/utils.py b/blueprintcompiler/utils.py index ea8102e..b05c9cd 100644 --- a/blueprintcompiler/utils.py +++ b/blueprintcompiler/utils.py @@ -19,6 +19,7 @@ import typing as T from dataclasses import dataclass +import itertools class Colors: @@ -154,3 +155,18 @@ def unescape_quote(string: str) -> str: i += 1 return result + + +def iter_batched(iterable, n, *, strict=False): + """ + Replacement for `itertools.batched()` since the testing infrastructure + uses Python 3.9 at the moment. Copied directly off of the Python docs. + """ + # batched('ABCDEFG', 3) → ABC DEF G + if n < 1: + raise ValueError("n must be at least one") + iterator = iter(iterable) + while batch := tuple(itertools.islice(iterator, n)): + if strict and len(batch) != n: + raise ValueError("batched(): incomplete batch") + yield batch diff --git a/tests/samples/variants.blp b/tests/samples/variants.blp new file mode 100644 index 0000000..a45069a --- /dev/null +++ b/tests/samples/variants.blp @@ -0,0 +1,42 @@ +using Gtk 4.0; + +$BlueprintTestObject { + // test-one: variant<"s">("one"); + test-zero: variant<"b">(true); + test-one: variant<"s">("one"); + test-two: variant<"i">(2); + test-three: variant<"(ii)">((3, 4)); + test-four: variant<"ai">([5, 6]); + test-five: variant<"{sv}">({"key", <"value">}); + test-six: variant<"a{ss}">({ + "GLib": "2.24", + "Gtk": "4.16" + }); + test-seven: variant<"ams">([just "2", nothing]); +} + +menu test_menu { + submenu { + label: "Test menu"; + item { + label: "Option 1"; + action: "app.test_menu.set_action"; + target: variant<"y">(1); + } + item { + label: "Option 2"; + action: "app.test_menu.set_action"; + target: variant<"y">(2); + } + item { + label: "Option 3"; + action: "app.test_menu.set_action"; + target: variant<"y">(3); + } + item { + label: "Option 4"; + action: "app.test_menu.set_action"; + target: variant<"y">(4); + } + } +} \ No newline at end of file diff --git a/tests/samples/variants.ui b/tests/samples/variants.ui new file mode 100644 index 0000000..4fa0a26 --- /dev/null +++ b/tests/samples/variants.ui @@ -0,0 +1,44 @@ + + + + + + true + "one" + 2 + (3, 4) + [5, 6] + {"key", <"value">} + {"GLib": "2.24", "Gtk": "4.16"} + [just "2", nothing] + + + + Test menu + + Option 1 + app.test_menu.set_action + 1 + + + Option 2 + app.test_menu.set_action + 2 + + + Option 3 + app.test_menu.set_action + 3 + + + Option 4 + app.test_menu.set_action + 4 + + + + \ No newline at end of file diff --git a/variant-test.blp b/variant-test.blp deleted file mode 100644 index f69522f..0000000 --- a/variant-test.blp +++ /dev/null @@ -1,16 +0,0 @@ -using Gtk 4.0; - -menu root { - submenu { - name: "one"; - item { - action: "app.foo_bar"; - target: variant<"s">("\"one\""); - } - } -} - -Button { - action-name: "app.shave_yak"; - action-target: variant<"y">("8"); -} \ No newline at end of file