diff --git a/src/ytdl_sub/script/functions/__init__.py b/src/ytdl_sub/script/functions/__init__.py index 00235002..5247143e 100644 --- a/src/ytdl_sub/script/functions/__init__.py +++ b/src/ytdl_sub/script/functions/__init__.py @@ -1,9 +1,10 @@ from typing import Optional +from ytdl_sub.script.functions.array_functions import ArrayFunctions from ytdl_sub.script.functions.numeric_functions import NumericFunctions from ytdl_sub.script.functions.special_functions import SpecialFunctions from ytdl_sub.script.functions.string_functions import StringFunctions -class Functions(StringFunctions, NumericFunctions, SpecialFunctions): +class Functions(StringFunctions, NumericFunctions, SpecialFunctions, ArrayFunctions): pass diff --git a/src/ytdl_sub/script/functions/array_functions.py b/src/ytdl_sub/script/functions/array_functions.py new file mode 100644 index 00000000..b75a6ade --- /dev/null +++ b/src/ytdl_sub/script/functions/array_functions.py @@ -0,0 +1,14 @@ +from typing import List + +from ytdl_sub.script.types.resolvable import Array +from ytdl_sub.script.types.resolvable import Resolvable + + +class ArrayFunctions: + @staticmethod + def extend(*arrays: Array) -> Array: + output: List[Resolvable] = [] + for array in arrays: + output.extend(array.value) + + return Array(output) diff --git a/src/ytdl_sub/script/parser.py b/src/ytdl_sub/script/parser.py index 9516191f..f11185a4 100644 --- a/src/ytdl_sub/script/parser.py +++ b/src/ytdl_sub/script/parser.py @@ -4,6 +4,7 @@ from typing import Optional from ytdl_sub.script.syntax_tree import SyntaxTree from ytdl_sub.script.types.function import ArgumentType from ytdl_sub.script.types.function import Function +from ytdl_sub.script.types.resolvable import Array from ytdl_sub.script.types.resolvable import Boolean from ytdl_sub.script.types.resolvable import Float from ytdl_sub.script.types.resolvable import Integer @@ -48,7 +49,7 @@ class _Parser: if ch.isspace() and not var_name: self._pos += 1 continue - if ch in ["}", ",", ")"] or ch.isspace(): + if ch in ["}", ",", ")", "]"] or ch.isspace(): break is_lower = ch.isascii() and ch.islower() @@ -115,6 +116,9 @@ class _Parser: return Boolean(value=False) if self._read(increment_pos=False) in ["'", '"']: return self._parse_string() + if self._read(increment_pos=False) == "[": + self._pos += 1 + return self._parse_array() if self._read(increment_pos=False).isascii() and self._read(increment_pos=False).islower(): return self._parse_variable() raise StringFormattingException( @@ -122,7 +126,7 @@ class _Parser: "string, boolean, or variable without brackets" ) - def _parse_function_args(self) -> List[ArgumentType]: + def _parse_args(self, breaking_char: str = ")") -> List[ArgumentType]: """ Begin parsing function args after the first ``(``, i.e. ``function_name(`` """ @@ -131,7 +135,7 @@ class _Parser: arguments: List[ArgumentType] = [] while ch := self._read(increment_pos=False): - if ch == ")": + if ch == breaking_char: break if ch.isspace(): @@ -162,7 +166,22 @@ class _Parser: if ch != "(": function_name += ch else: - function_args = self._parse_function_args() + function_args = self._parse_args() + + raise StringFormattingException("Invalid function") + + def _parse_array(self) -> Array: + """ + Begin parsing an array after reading the first ``[`` + """ + function_args: List[String | Variable | "Function"] = [] + + while ch := self._read(increment_pos=False): + if ch == "]": + self._pos += 1 + return Array(value=function_args) + else: + function_args = self._parse_args(breaking_char="]") raise StringFormattingException("Invalid function") @@ -193,6 +212,9 @@ class _Parser: if ch1 == "%": self._pos += 1 self._ast.append(self._parse_function()) + elif ch1 == "[": + self._pos += 1 + self._ast.append(self._parse_array()) else: self._ast.append(self._parse_variable()) else: diff --git a/src/ytdl_sub/script/types/function.py b/src/ytdl_sub/script/types/function.py index 6f274d19..b3367819 100644 --- a/src/ytdl_sub/script/types/function.py +++ b/src/ytdl_sub/script/types/function.py @@ -15,6 +15,7 @@ from typing import final from typing import get_origin from ytdl_sub.script.functions import Functions +from ytdl_sub.script.types.resolvable import Array from ytdl_sub.script.types.resolvable import Boolean from ytdl_sub.script.types.resolvable import Float from ytdl_sub.script.types.resolvable import Integer @@ -26,7 +27,7 @@ from ytdl_sub.script.types.resolvable import String from ytdl_sub.script.types.variable import Variable from ytdl_sub.utils.exceptions import StringFormattingException -ArgumentType = Union[Integer, Float, String, Boolean, Variable, "Function"] +ArgumentType = Union[Integer, Float, String, Boolean, Variable, "Function", Array] @dataclass(frozen=True) diff --git a/src/ytdl_sub/script/types/resolvable.py b/src/ytdl_sub/script/types/resolvable.py index f7e1b813..9ccc63b3 100644 --- a/src/ytdl_sub/script/types/resolvable.py +++ b/src/ytdl_sub/script/types/resolvable.py @@ -61,13 +61,8 @@ class String(ResolvableT[str]): @dataclass(frozen=True) -class _List(Resolvable, Generic[T], ABC): - value: List[T] +class Array(Resolvable): + value: List[Resolvable] def __str__(self) -> str: - return f"[{', '.join([str(val) for val in self.value])}]" - - -@dataclass(frozen=True) -class StringList(_List[String]): - pass + return f"[{', '.join([val.value for val in self.value])}]" diff --git a/tests/unit/script/test_parser.py b/tests/unit/script/test_parser.py index e304f09f..9788243a 100644 --- a/tests/unit/script/test_parser.py +++ b/tests/unit/script/test_parser.py @@ -28,6 +28,13 @@ class TestParser: ] ) + def test_array(self): + parsed = parse("hello {['elem1', 'elem2']}") + parsed_empty = parse("hello {[]}") + parsed_with_var = parse("hello {['elem1', variable_name]}") + parsed_extend = parse("hi {%extend(['elem1', 'elem2'], ['elem3'], [], ['elem4'])}") + assert False + def test_conditional(self): parsed = parse("hello {%if(True, 'hi', 3.4)}") assert parsed == SyntaxTree(