From 2ca4bb9573fde8303501807d6166baa13d73c322 Mon Sep 17 00:00:00 2001 From: Juro Oravec Date: Thu, 25 Jun 2026 10:51:33 +0200 Subject: [PATCH 1/2] release: 1.3.1 Bump version to 1.3.1 and add CHANGELOG entry for the multiline plain-string SyntaxError fix (PR #37). NOTE: This commit only carries the release metadata (version + CHANGELOG). It must sit on top of a `main` that already contains the PR #37 fix (crates/djc-template-parser/src/tag_compiler.rs). Merge PR #37 first, then apply/rebase this commit, then tag `1.3.1` to trigger the PyPI publish. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 19 +++++++++++++++++++ pyproject.toml | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1570e63..bcbbee6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Release notes +## v1.3.1 + +### Fix + +- Fix `SyntaxError` when a tag value was a multiline plain string (a quoted + string that spans multiple lines and contains no `{{ }}` template + expressions), e.g. an Alpine.js or hyperscript handler: + + ```django + {% component "small_button" + _="on click + set replyForm to closest
" + %}{% endcomponent %} + ``` + + Literal newlines are now escaped when the string is compiled to a Python + literal, so the value stays a valid single-line literal. Regression from + v1.3.0. ([#37](https://github.com/django-components/djc-core/pull/37)) + ## v1.3.0 Drop support for Python 3.8 and 3.9. diff --git a/pyproject.toml b/pyproject.toml index 6c1c948..8b28316 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "djc_core" -version = "1.3.0" +version = "1.3.1" requires-python = ">=3.10, <4.0" description = "Core library for django-components written in Rust." keywords = ["django", "components", "html"] From 3ab0e6dbd47a2bf687b6b165c03ee5386d0e2bc3 Mon Sep 17 00:00:00 2001 From: Juro Oravec Date: Thu, 25 Jun 2026 11:10:44 +0200 Subject: [PATCH 2/2] test: add multiline plain-string regression test Add a compile test for a multiline plain-string tag value to the source-of-truth Python test file. The file previously had no such case, so the regression was only covered by the Rust test added in PR #37. This is the regression test for the PR #37 fix: it asserts that `{% component key="on click\n..." %}` compiles to a valid Python literal instead of raising `SyntaxError`. It therefore PASSES only once the PR #37 fix is present on this branch (parse + expected_tag already pass on 1.3.0; only the compile step fails pre-fix). Mirrors the same case added to django-components' tests/test_tag_parser.py. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/test_template_parser__tag.py | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/test_template_parser__tag.py b/tests/test_template_parser__tag.py index 64dabd7..a85a1fb 100644 --- a/tests/test_template_parser__tag.py +++ b/tests/test_template_parser__tag.py @@ -1817,6 +1817,39 @@ def test_string_simple_as_kwarg(self): assert args == [] assert kwargs == [("key", "world")] + def test_string_multiline_value(self): + # A quoted string value that spans multiple lines but contains no + # `{{ }}` expressions (e.g. an Alpine.js or hyperscript handler) must + # compile to a valid single-line Python literal, with the newline + # escaped, instead of raising `SyntaxError`. Regression from 1.3.0 + # (PR #37). + tag_content = '{% component key="on click\n set x to 1" %}' + tag = parse_tag(tag_content) + + expected_tag = GenericTag( + token=token(tag_content, 0, 1, 1), + name=token("component", 3, 1, 4), + attrs=[ + TagAttr( + token=token('key="on click\n set x to 1"', 13, 1, 14), + key=token("key", 13, 1, 14), + value=plain_string_value('"on click\n set x to 1"', 17, 1, 18, None), + is_flag=False, + ), + ], + is_self_closing=False, + used_variables=[], + assigned_variables=[], + ) + + assert tag == expected_tag + + tag_func = _simple_compile_tag(tag, tag_content) + args, kwargs = tag_func({}) + + assert args == [] + assert kwargs == [("key", "on click\n set x to 1")] + def test_string_with_filter(self): tag_content = "{% component 'hello'|upper %}" tag = parse_tag(tag_content)