From 7cf1fcc6085e25ca6e4d7172597c9380c061320f Mon Sep 17 00:00:00 2001 From: Juan-Pablo Scaletti Date: Wed, 10 Jul 2024 14:44:04 -0500 Subject: [PATCH] Fix Issue#79: comments after meta declarations --- pyproject.toml | 2 +- src/jinjax/catalog.py | 12 ++++++------ src/jinjax/component.py | 40 ++++++++++++++++++++++++++-------------- tests/test_component.py | 30 ++++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 21 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a2c4e59..7fd092e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "jinjax" -version = "0.42" +version = "0.43" description = "Replace your HTML templates with Python server-Side components" authors = ["Juan-Pablo Scaletti "] license = "MIT" diff --git a/src/jinjax/catalog.py b/src/jinjax/catalog.py index fa50ba6..5a8088f 100644 --- a/src/jinjax/catalog.py +++ b/src/jinjax/catalog.py @@ -20,8 +20,8 @@ ALLOWED_EXTENSIONS = (".css", ".js", ".mjs") DEFAULT_PREFIX = "" DEFAULT_EXTENSION = ".jinja" -PROP_ATTRS = "attrs" -PROP_CONTENT = "content" +ARGS_ATTRS = "attrs" +ARGS_CONTENT = "content" class Catalog: @@ -217,19 +217,19 @@ def irender( attrs = attrs.as_dict if isinstance(attrs, HTMLAttrs) else attrs attrs.update(kw) kw = attrs - props, extra = component.filter_args(kw) + args, extra = component.filter_args(kw) try: - props[PROP_ATTRS] = HTMLAttrs(extra) + args[ARGS_ATTRS] = HTMLAttrs(extra) except Exception as exc: raise InvalidArgument( f"The arguments of the component <{component.name}>" f"were parsed incorrectly as:\n {str(kw)}" ) from exc - props[PROP_CONTENT] = Markup( + args[ARGS_CONTENT] = Markup( content if content or not caller else caller().strip() ) - return component.render(**props) + return component.render(**args) def get_middleware( self, diff --git a/src/jinjax/component.py b/src/jinjax/component.py index c83637e..7bdb176 100644 --- a/src/jinjax/component.py +++ b/src/jinjax/component.py @@ -18,14 +18,22 @@ if t.TYPE_CHECKING: from typing_extensions import Self +RX_COMMA = re.compile(r"\s*,\s*") -RX_PROPS_START = re.compile(r"{#-?\s*def\s+") -RX_COMMENTS = re.compile(r"\n\s*#[^\n]*") +RX_ARGS_START = re.compile(r"{#-?\s*def\s+") RX_CSS_START = re.compile(r"{#-?\s*css\s+") RX_JS_START = re.compile(r"{#-?\s*js\s+") -RX_COMMA = re.compile(r"\s*,\s*") + +# This regexp matches the meta declarations (`{#def .. #}``, `{#css .. #}``, +# and `{#js .. #}`) and regular Jinja comments AT THE BEGINNING of the components source. +# You can also have comments inside the declarations. RX_META_HEADER = re.compile(r"^(\s*{#.*?#})+", re.DOTALL) +# This regexep matches comments (everything after a `#`) +# Used to remove them from inside meta declarations +RX_INTER_COMMENTS = re.compile(r"\s*#[^\n]*") + + ALLOWED_NAMES_IN_EXPRESSION_VALUES = { "len": len, "max": max, @@ -157,32 +165,36 @@ def load_metadata(self, source: str) -> None: if not match: return - header = RX_COMMENTS.sub("", match.group(0)).replace("\n", " ") + header = match.group(0) # Reversed because I will use `header.pop()` - header = header.split("#}")[::-1] + header = header.split("#}")[:-1][::-1] def_found = False while header: - line = header.pop().strip(" -") - expr = self.read_metadata_line(line, RX_PROPS_START) + item = header.pop().strip(" -\n") + + expr = self.read_metadata_item(item, RX_ARGS_START) if expr: if def_found: raise DuplicateDefDeclaration(self.name) + expr = RX_INTER_COMMENTS.sub("", expr).replace("\n", " ") self.required, self.optional = self.parse_args_expr(expr) def_found = True continue - expr = self.read_metadata_line(line, RX_CSS_START) + expr = self.read_metadata_item(item, RX_CSS_START) if expr: + expr = RX_INTER_COMMENTS.sub("", expr).replace("\n", " ") self.css = [*self.css, *self.parse_files_expr(expr)] continue - expr = self.read_metadata_line(line, RX_JS_START) + expr = self.read_metadata_item(item, RX_JS_START) if expr: + expr = RX_INTER_COMMENTS.sub("", expr).replace("\n", " ") self.js = [*self.js, *self.parse_files_expr(expr)] continue - def read_metadata_line(self, source: str, rx_start: re.Pattern) -> str: + def read_metadata_item(self, source: str, rx_start: re.Pattern) -> str: start = rx_start.match(source) if not start: return "" @@ -224,17 +236,17 @@ def parse_files_expr(self, expr: str) -> list[str]: def filter_args( self, kw: dict[str, t.Any] ) -> tuple[dict[str, t.Any], dict[str, t.Any]]: - props = {} + args = {} for key in self.required: if key not in kw: raise MissingRequiredArgument(self.name, key) - props[key] = kw.pop(key) + args[key] = kw.pop(key) for key in self.optional: - props[key] = kw.pop(key, self.optional[key]) + args[key] = kw.pop(key, self.optional[key]) extra = kw.copy() - return props, extra + return args, extra def render(self, **kwargs): assert self.tmpl, f"Component {self.name} has no template" diff --git a/tests/test_component.py b/tests/test_component.py index f4fabd5..9961b21 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -283,3 +283,33 @@ def test_comments_in_args_decl(): "layer_class": None, "layer_height": 4, } + + +def test_comment_after_args_decl(): + com = Component( + name="Test.jinja", + source=""" +{# def + arg, +#} + +{# + Some comment. +#} +Hi +""".strip()) + assert com.required == ["arg"] + assert com.optional == {} + + +def test_fake_decl(): + com = Component( + name="Test.jinja", + source=""" +{# definitely not an args decl! #} +{# def arg #} +{# jsadfghkl are letters #} +{# csssssss #} +""".strip()) + assert com.required == ["arg"] + assert com.optional == {}