diff --git a/docs/source/config_reference/scripting/scripting_types.rst b/docs/source/config_reference/scripting/scripting_types.rst index 381f8cda..9275d7a1 100644 --- a/docs/source/config_reference/scripting/scripting_types.rst +++ b/docs/source/config_reference/scripting/scripting_types.rst @@ -88,6 +88,12 @@ triple-quotes can be used to avoid *closing* the String. %string("""This has both " and ' in it.""") } +If you want a plain string that contains literal curly braces, you can escape them like so: + +.. code-block:: yaml + + string_variable: "This contains \\{ literal curly braces \\}" + Integer ~~~~~~~ diff --git a/src/ytdl_sub/script/parser.py b/src/ytdl_sub/script/parser.py index 82b66bd9..a6109f4d 100644 --- a/src/ytdl_sub/script/parser.py +++ b/src/ytdl_sub/script/parser.py @@ -73,6 +73,8 @@ CUSTOM_FUNCTION_ARGUMENTS_ONLY_ARGS = InvalidSyntaxException( FUNCTION_INVALID_CHAR = InvalidSyntaxException("Invalid value when parsing a function") +BRACKET_INVALID_CHAR = InvalidSyntaxException("Invalid value within brackets") + def _UNEXPECTED_CHAR_ARGUMENT(arg_type: ParsedArgType): return InvalidSyntaxException(f"Unexpected character when parsing {arg_type.value} arguments") @@ -505,6 +507,10 @@ class _Parser: raise UNREACHABLE def _parse_main_loop(self, ch: str) -> bool: + if ch == "\\" and self._read(increment_pos=False) in {"{", "}"}: + # Escape brackets are \{ and \}, only add the second char + self._literal_str += self._read() + return True if ch == "}": if self._bracket_counter == 0: raise BRACKET_NOT_CLOSED @@ -558,9 +564,9 @@ class _Parser: elif self._bracket_counter == 0: # Only accumulate literal str if not in brackets self._literal_str += ch - else: - # Should only be possible to get here if it's a space - assert ch.isspace() + elif not ch.isspace(): + self._set_highlight_position(pos=self._pos - 1) + raise BRACKET_INVALID_CHAR return True diff --git a/tests/unit/script/test_parser.py b/tests/unit/script/test_parser.py index 196ae8f2..b40e13ca 100644 --- a/tests/unit/script/test_parser.py +++ b/tests/unit/script/test_parser.py @@ -5,6 +5,7 @@ from typing import Union import pytest from ytdl_sub.script.parser import _UNEXPECTED_CHAR_ARGUMENT +from ytdl_sub.script.parser import BRACKET_INVALID_CHAR from ytdl_sub.script.parser import BRACKET_NOT_CLOSED from ytdl_sub.script.parser import ParsedArgType from ytdl_sub.script.parser import parse @@ -192,6 +193,15 @@ class TestParser: ] ) + def test_escaped_bracket(self): + assert parse("\\{ This is escape {%string('{}')} and here \\}") == SyntaxTree( + [ + String(value="{ This is escape "), + BuiltInFunction(name="string", args=[String(value="{}")]), + String(value=" and here }"), + ] + ) + class TestParserBracketFailures: def test_bracket_open(self): @@ -208,3 +218,8 @@ class TestParserBracketFailures: match=re.escape(str(_UNEXPECTED_CHAR_ARGUMENT(ParsedArgType.MAP_KEY))), ): parse("hello {%capitalize({as_arg)}") + + @pytest.mark.parametrize("char", [")", ",", "*", "]"]) + def test_extra_char_in_brackets(self, char: str): + with pytest.raises(InvalidSyntaxException, match=re.escape(str(BRACKET_INVALID_CHAR))): + parse(f"{{ %string('hi') {char} }}")