Skip to content

Commit

Permalink
Fix Issue#79: comments after meta declarations
Browse files Browse the repository at this point in the history
  • Loading branch information
jpsca committed Jul 10, 2024
1 parent 78ab068 commit 7cf1fcc
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 21 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>"]
license = "MIT"
Expand Down
12 changes: 6 additions & 6 deletions src/jinjax/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down
40 changes: 26 additions & 14 deletions src/jinjax/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 ""
Expand Down Expand Up @@ -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"
Expand Down
30 changes: 30 additions & 0 deletions tests/test_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 == {}

0 comments on commit 7cf1fcc

Please sign in to comment.