diff --git a/blueprintcompiler/decompiler.py b/blueprintcompiler/decompiler.py index 98e00af..2663a09 100644 --- a/blueprintcompiler/decompiler.py +++ b/blueprintcompiler/decompiler.py @@ -23,7 +23,7 @@ from dataclasses import dataclass from enum import Enum from .gir import * -from .utils import Colors +from .utils import Colors, escape_quote from .xml_reader import Element, parse, parse_string __all__ = ["decompile"] @@ -138,7 +138,7 @@ class DecompileCtx: return value.replace("-", "_") if type is None: - self.print(f'{name}: "{escape_quote(value)}";') + self.print(f"{name}: {escape_quote(value)};") elif type.assignable_to(FloatType()): self.print(f"{name}: {value};") elif type.assignable_to(BoolType()): @@ -157,7 +157,7 @@ class DecompileCtx: self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutTrigger") ) ): - self.print(f'{name}: "{escape_quote(value)}";') + self.print(f"{name}: {escape_quote(value)};") elif value == self.template_class: self.print(f"{name}: template;") elif type.assignable_to( @@ -170,7 +170,7 @@ class DecompileCtx: elif isinstance(type, Enumeration): self.print(f"{name}: {get_enum_name(value)};") else: - self.print(f'{name}: "{escape_quote(value)}";') + self.print(f"{name}: {escape_quote(value)};") def _decompile_element( @@ -253,15 +253,6 @@ def decompiler(tag, cdata=False): return decorator -def escape_quote(string: str) -> str: - return ( - string.replace("\\", "\\\\") - .replace("'", "\\'") - .replace('"', '\\"') - .replace("\n", "\\n") - ) - - @decompiler("interface") def decompile_interface(ctx, gir): return gir @@ -289,11 +280,11 @@ def decompile_translatable( comments = f"/* Translators: {comments} */" if context is not None: - return comments, f'C_("{escape_quote(context)}", "{escape_quote(string)}")' + return comments, f"C_({escape_quote(context)}, {escape_quote(string)})" else: - return comments, f'_("{escape_quote(string)}")' + return comments, f"_({escape_quote(string)})" else: - return comments, f'"{escape_quote(string)}"' + return comments, f"{escape_quote(string)}" @decompiler("property", cdata=True) @@ -334,7 +325,7 @@ def decompile_property( ctx.print(comments) ctx.print(f"{name}: {translatable};") elif gir is None or gir.properties.get(name) is None: - ctx.print(f'{name}: "{escape_quote(cdata)}";') + ctx.print(f"{name}: {escape_quote(cdata)};") else: ctx.print_attribute(name, cdata, gir.properties.get(name).type) return gir diff --git a/blueprintcompiler/language/gtk_a11y.py b/blueprintcompiler/language/gtk_a11y.py index 2ad8097..a4c3415 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -231,7 +231,7 @@ def decompile_relation(ctx, gir, name, cdata): @decompiler("state", cdata=True) def decompile_state(ctx, gir, name, cdata, translatable="false"): if decompile.truthy(translatable): - ctx.print(f'{name}: _("{escape_quote(cdata)}");') + ctx.print(f"{name}: _({escape_quote(cdata)});") else: ctx.print_attribute(name, cdata, get_types(ctx.gir).get(name)) diff --git a/blueprintcompiler/language/gtk_file_filter.py b/blueprintcompiler/language/gtk_file_filter.py index 8dcbcb8..482d6e1 100644 --- a/blueprintcompiler/language/gtk_file_filter.py +++ b/blueprintcompiler/language/gtk_file_filter.py @@ -119,7 +119,7 @@ def decompile_mime_types(ctx, gir): @decompiler("mime-type", cdata=True) def decompile_mime_type(ctx, gir, cdata): - ctx.print(f'"{cdata}",') + ctx.print(f"{escape_quote(cdata)},") @decompiler("patterns") @@ -129,7 +129,7 @@ def decompile_patterns(ctx, gir): @decompiler("pattern", cdata=True) def decompile_pattern(ctx, gir, cdata): - ctx.print(f'"{cdata}",') + ctx.print(f"{escape_quote(cdata)},") @decompiler("suffixes") @@ -139,4 +139,4 @@ def decompile_suffixes(ctx, gir): @decompiler("suffix", cdata=True) def decompile_suffix(ctx, gir, cdata): - ctx.print(f'"{cdata}",') + ctx.print(f"{escape_quote(cdata)},") diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index 8f3ef31..fff6e4a 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -22,6 +22,7 @@ import typing as T from enum import Enum +from . import utils from .ast_utils import AstNode from .errors import ( CompileError, @@ -573,14 +574,19 @@ class UseQuoted(ParseNode): if token.type != TokenType.QUOTED: return False - string = ( - str(token)[1:-1] - .replace("\\n", "\n") - .replace('\\"', '"') - .replace("\\\\", "\\") - .replace("\\'", "'") - ) - ctx.set_group_val(self.key, string, token) + unescaped = None + + try: + unescaped = utils.unescape_quote(str(token)) + except utils.UnescapeError as e: + start = ctx.tokens[ctx.index - 1].start + range = Range(start + e.start, start + e.end, ctx.text) + ctx.errors.append( + CompileError(f"Invalid escape sequence '{range.text}'", range) + ) + + ctx.set_group_val(self.key, unescaped, token) + return True diff --git a/blueprintcompiler/utils.py b/blueprintcompiler/utils.py index 4c4b44a..25803d7 100644 --- a/blueprintcompiler/utils.py +++ b/blueprintcompiler/utils.py @@ -18,6 +18,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import typing as T +from dataclasses import dataclass class Colors: @@ -98,3 +99,57 @@ def idxs_to_range(start: int, end: int, text: str): "character": end_c, }, } + + +@dataclass +class UnescapeError(Exception): + start: int + end: int + + +def escape_quote(string: str) -> str: + return ( + "'" + + ( + string.replace("\\", "\\\\") + .replace("'", "\\'") + .replace("\n", "\\n") + .replace("\t", "\\t") + ) + + "'" + ) + + +def unescape_quote(string: str) -> str: + string = string[1:-1] + + REPLACEMENTS = { + "\\": "\\", + "n": "\n", + "t": "\t", + '"': '"', + "'": "'", + } + + result = "" + i = 0 + while i < len(string): + c = string[i] + if c == "\\": + i += 1 + + if i >= len(string): + from .errors import CompilerBugError + + raise CompilerBugError() + + if r := REPLACEMENTS.get(string[i]): + result += r + else: + raise UnescapeError(i, i + 2) + else: + result += c + + i += 1 + + return result diff --git a/tests/sample_errors/bad_escape_sequence.blp b/tests/sample_errors/bad_escape_sequence.blp new file mode 100644 index 0000000..4b109c6 --- /dev/null +++ b/tests/sample_errors/bad_escape_sequence.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Label { + label: '***** \f *****'; +} diff --git a/tests/sample_errors/bad_escape_sequence.err b/tests/sample_errors/bad_escape_sequence.err new file mode 100644 index 0000000..e4ec183 --- /dev/null +++ b/tests/sample_errors/bad_escape_sequence.err @@ -0,0 +1 @@ +4,17,2,Invalid escape sequence '\f' \ No newline at end of file diff --git a/tests/samples/accessibility_dec.blp b/tests/samples/accessibility_dec.blp index a603e25..eedb6cd 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/file_filter.blp b/tests/samples/file_filter.blp index 3a52c3f..e9866d9 100644 --- a/tests/samples/file_filter.blp +++ b/tests/samples/file_filter.blp @@ -1,18 +1,18 @@ using Gtk 4.0; FileFilter { - name: "File Filter Name"; + name: 'File Filter Name'; mime-types [ - "text/plain", - "image/ *", + 'text/plain', + 'image/ *', ] patterns [ - "*.txt", + '*.txt', ] suffixes [ - "png", + 'png', ] } diff --git a/tests/samples/layout_dec.blp b/tests/samples/layout_dec.blp index b0e66e0..3754fe1 100644 --- a/tests/samples/layout_dec.blp +++ b/tests/samples/layout_dec.blp @@ -3,8 +3,8 @@ using Gtk 4.0; Grid { Label { layout { - column: "0"; - row: "1"; + column: '0'; + row: '1'; } } } diff --git a/tests/samples/menu_dec.blp b/tests/samples/menu_dec.blp index 14b4df3..44c244f 100644 --- a/tests/samples/menu_dec.blp +++ b/tests/samples/menu_dec.blp @@ -3,26 +3,26 @@ using Gtk 4.0; menu my-menu { submenu { section { - label: "test section"; + label: 'test section'; } item { - label: C_("context", "test translated item"); + label: C_('context', 'test translated item'); } item { - label: "test item shorthand 1"; + label: 'test item shorthand 1'; } item { - label: "test item shorthand 2"; - action: "app.test-action"; + label: 'test item shorthand 2'; + action: 'app.test-action'; } item { - label: "test item shorthand 3"; - action: "app.test-action"; - icon: "test-symbolic"; + label: 'test item shorthand 3'; + action: 'app.test-action'; + icon: 'test-symbolic'; } } } diff --git a/tests/samples/responses.blp b/tests/samples/responses.blp index d7032a7..be8c03b 100644 --- a/tests/samples/responses.blp +++ b/tests/samples/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/scale_marks.blp b/tests/samples/scale_marks.blp index a766cfa..f75930c 100644 --- a/tests/samples/scale_marks.blp +++ b/tests/samples/scale_marks.blp @@ -3,7 +3,7 @@ using Gtk 4.0; Scale { marks [ mark (-1, bottom), - mark (0, top, _("Hello, world!")), + mark (0, top, _('Hello, world!')), mark (2), ] } diff --git a/tests/samples/strings.blp b/tests/samples/strings.blp index ef237ae..08bb418 100644 --- a/tests/samples/strings.blp +++ b/tests/samples/strings.blp @@ -1,5 +1,5 @@ using Gtk 4.0; Label { - label: "Test 1 2 3\n & 4 \"5\' 6"; + label: "\\\\'Test 1 2 3\n & 4 \"5\' 6 \t"; } diff --git a/tests/samples/strings.ui b/tests/samples/strings.ui index 1dea963..e7fef2e 100644 --- a/tests/samples/strings.ui +++ b/tests/samples/strings.ui @@ -7,7 +7,7 @@ corresponding .blp file and regenerate this file with blueprint-compiler. - Test 1 2 3 - & 4 "5' 6 + \\'Test 1 2 3 + & 4 "5' 6 diff --git a/tests/samples/strings_dec.blp b/tests/samples/strings_dec.blp new file mode 100644 index 0000000..576ddb4 --- /dev/null +++ b/tests/samples/strings_dec.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Label { + label: '\\\\\'Test 1 2 3\n & 4 "5\' 6 \t'; +} diff --git a/tests/samples/template.blp b/tests/samples/template.blp index fa2f041..fb019d3 100644 --- a/tests/samples/template.blp +++ b/tests/samples/template.blp @@ -1,7 +1,7 @@ using Gtk 4.0; template $TestTemplate : ApplicationWindow { - test-property: "Hello, world"; + test-property: 'Hello, world'; test-signal => $on_test_signal(); } diff --git a/tests/samples/translated.blp b/tests/samples/translated.blp index c7d4cd7..8c837d7 100644 --- a/tests/samples/translated.blp +++ b/tests/samples/translated.blp @@ -1,9 +1,9 @@ using Gtk 4.0; Label { - label: _("Hello, world!"); + label: _('Hello, world!'); } Label { - label: C_("translation context", "Hello"); + label: C_('translation context', 'Hello'); } diff --git a/tests/samples/unchecked_class_dec.blp b/tests/samples/unchecked_class_dec.blp index dbbfe14..d9df875 100644 --- a/tests/samples/unchecked_class_dec.blp +++ b/tests/samples/unchecked_class_dec.blp @@ -2,6 +2,6 @@ using Gtk 4.0; $MyComponent component { $MyComponent2 { - flags-value: "a|b"; + flags-value: 'a|b'; } } diff --git a/tests/test_samples.py b/tests/test_samples.py index 226e762..a91a594 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -229,7 +229,7 @@ class TestSamples(unittest.TestCase): self.assert_decompile("responses") self.assert_decompile("scale_marks") self.assert_decompile("signal") - self.assert_decompile("strings") + self.assert_decompile("strings_dec") self.assert_decompile("style_dec") self.assert_decompile("template") self.assert_decompile("translated")