fix: use single quotes for x-data attributes with tojson

The Jinja |tojson filter outputs JSON with double quotes. When used
inside a double-quoted HTML attribute, these quotes break the attribute
parsing causing "expected expression, got '}'" errors.

Solution: Use single quotes for x-data attributes so JSON double quotes
don't conflict:
  <div x-data='languageSelector("fr", {{ langs|tojson }})'>

Updated:
- language_selector.html macro (all 3 variants)
- shop/base.html language selector
- LANG-002 and LANG-003 architecture rules documentation
- Validator to detect double-quoted x-data with tojson

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-13 23:05:09 +01:00
parent 9920430b9e
commit ea64ff8eae
4 changed files with 29 additions and 27 deletions

View File

@@ -48,12 +48,12 @@ language_rules:
... ...
}"> }">
RIGHT (function with tojson - NO safe in HTML attributes): RIGHT (function call with single quotes for x-data attribute):
<div x-data="languageSelector('{{ lang }}', {{ langs|tojson }})"> <div x-data='languageSelector("{{ lang }}", {{ langs|tojson }})'>
NOTE: Use |tojson (without |safe) in HTML attributes so quotes IMPORTANT: Use SINGLE QUOTES for the x-data attribute value because
become &quot; which the browser correctly decodes. Using |safe |tojson outputs double quotes in JSON arrays. Single quotes on the
would output raw quotes that break the attribute parsing. outside prevent the JSON double quotes from breaking HTML parsing.
The languageSelector function must be defined in: The languageSelector function must be defined in:
- static/shop/js/shop-layout.js (for storefront) - static/shop/js/shop-layout.js (for storefront)
@@ -75,9 +75,12 @@ language_rules:
When passing Python lists to JavaScript, use tojson appropriately: When passing Python lists to JavaScript, use tojson appropriately:
IN HTML ATTRIBUTES (x-data, data-*, etc.): IN HTML ATTRIBUTES (x-data, data-*, etc.):
Use |tojson WITHOUT |safe - quotes become &quot; which browsers decode: Use SINGLE QUOTES for the attribute and |tojson for the array:
<div x-data="languageSelector('fr', {{ langs|tojson }})"> <div x-data='languageSelector("fr", {{ langs|tojson }})'>
<!-- Renders: x-data="languageSelector('fr', [&quot;fr&quot;, &quot;de&quot;])" --> <!-- Renders: x-data='languageSelector("fr", ["fr", "de"])' -->
The single quotes on x-data prevent JSON double quotes from
breaking the HTML attribute parsing.
IN <script> BLOCKS: IN <script> BLOCKS:
Use |tojson|safe - raw JSON is valid inside script tags: Use |tojson|safe - raw JSON is valid inside script tags:
@@ -85,13 +88,9 @@ language_rules:
const languages = {{ vendor.storefront_languages|tojson|safe }}; const languages = {{ vendor.storefront_languages|tojson|safe }};
</script> </script>
WRONG (raw output - Python syntax not valid JS): WRONG (double quotes with tojson - JSON quotes break the attribute):
languages: {{ vendor.storefront_languages }} <div x-data="init({{ data|tojson }})">
<!-- Outputs: ['fr', 'de'] - single quotes invalid in JSON --> <!-- Outputs: x-data="init(["a", "b"])" - breaks at first [ -->
WRONG (|safe in HTML attributes - breaks attribute parsing):
<div x-data="init({{ data|tojson|safe }})">
<!-- Raw quotes break the attribute -->
pattern: pattern:
file_pattern: "app/templates/**/*.html" file_pattern: "app/templates/**/*.html"
required_with_jinja_array: required_with_jinja_array:

View File

@@ -57,8 +57,9 @@
{% set current = current_language if current_language in langs else langs[0] %} {% set current = current_language if current_language in langs else langs[0] %}
{% set positions = {'left': 'left-0', 'right': 'right-0'} %} {% set positions = {'left': 'left-0', 'right': 'right-0'} %}
{# Uses languageSelector() function per LANG-002 architecture rule #} {# Uses languageSelector() function per LANG-002 architecture rule #}
{# Use single quotes for x-data so JSON double quotes don't break the attribute #}
<div <div
x-data="languageSelector('{{ current }}', {{ langs | tojson }})" x-data='languageSelector("{{ current }}", {{ langs | tojson }})'
class="relative inline-block" class="relative inline-block"
> >
<button <button
@@ -120,8 +121,9 @@
{% set current = current_language if current_language in langs else langs[0] %} {% set current = current_language if current_language in langs else langs[0] %}
{% set positions = {'left': 'left-0', 'right': 'right-0'} %} {% set positions = {'left': 'left-0', 'right': 'right-0'} %}
{# Uses languageSelector() function per LANG-002 architecture rule #} {# Uses languageSelector() function per LANG-002 architecture rule #}
{# Use single quotes for x-data so JSON double quotes don't break the attribute #}
<div <div
x-data="languageSelector('{{ current }}', {{ langs | tojson }})" x-data='languageSelector("{{ current }}", {{ langs | tojson }})'
class="relative" class="relative"
> >
<button <button
@@ -178,8 +180,9 @@
{% set langs = enabled_languages[:2] if enabled_languages else ['fr', 'de'] %} {% set langs = enabled_languages[:2] if enabled_languages else ['fr', 'de'] %}
{% set current = current_language if current_language in langs else langs[0] %} {% set current = current_language if current_language in langs else langs[0] %}
{# Uses languageSelector() function per LANG-002 architecture rule #} {# Uses languageSelector() function per LANG-002 architecture rule #}
{# Use single quotes for x-data so JSON double quotes don't break the attribute #}
<div <div
x-data="languageSelector('{{ current }}', {{ langs | tojson }})" x-data='languageSelector("{{ current }}", {{ langs | tojson }})'
class="inline-flex items-center gap-1 p-1 bg-gray-100 dark:bg-gray-700 rounded-lg" class="inline-flex items-center gap-1 p-1 bg-gray-100 dark:bg-gray-700 rounded-lg"
> >
<template x-for="lang in languages" :key="lang"> <template x-for="lang in languages" :key="lang">

View File

@@ -136,7 +136,7 @@
{# Language Selector #} {# Language Selector #}
{% set enabled_langs = vendor.storefront_languages if vendor and vendor.storefront_languages else ['fr', 'de', 'en'] %} {% set enabled_langs = vendor.storefront_languages if vendor and vendor.storefront_languages else ['fr', 'de', 'en'] %}
{% if enabled_langs|length > 1 %} {% if enabled_langs|length > 1 %}
<div class="relative" x-data="languageSelector('{{ request.state.language|default('fr') }}', {{ enabled_langs|tojson }})"> <div class="relative" x-data='languageSelector("{{ request.state.language|default("fr") }}", {{ enabled_langs|tojson }})'>
<button <button
@click="isLangOpen = !isLangOpen" @click="isLangOpen = !isLangOpen"
@click.outside="isLangOpen = false" @click.outside="isLangOpen = false"

View File

@@ -2809,22 +2809,22 @@ class ArchitectureValidator:
line_number=i, line_number=i,
message="Complex language selector should use languageSelector() function", message="Complex language selector should use languageSelector() function",
context=line.strip()[:80], context=line.strip()[:80],
suggestion="Use: x-data=\"languageSelector('{{ lang }}', {{ langs|tojson }})\"", suggestion="Use: x-data='languageSelector(\"{{ lang }}\", {{ langs|tojson }})'",
) )
# LANG-003: Check for tojson|safe in HTML attributes (breaks parsing) # LANG-003: Check for double-quoted x-data with tojson (breaks parsing)
# In x-data attributes, |tojson WITHOUT |safe is correct (quotes become &quot;) # Double quotes + tojson outputs raw JSON double quotes that break HTML
# In <script> blocks, |tojson|safe is correct (raw JSON is valid) # Use single quotes for x-data when using tojson
if "|tojson|safe" in line and "x-data=" in line: if 'x-data="' in line and "|tojson" in line:
self._add_violation( self._add_violation(
rule_id="LANG-003", rule_id="LANG-003",
rule_name="Do not use tojson|safe in HTML attributes", rule_name="Use single quotes for x-data with tojson",
severity=Severity.ERROR, severity=Severity.ERROR,
file_path=file_path, file_path=file_path,
line_number=i, line_number=i,
message="|tojson|safe in x-data breaks HTML attribute parsing", message="Double-quoted x-data with |tojson breaks HTML (JSON has double quotes)",
context=line.strip()[:80], context=line.strip()[:80],
suggestion="Remove |safe: {{ variable|tojson }} - quotes become &quot; which browsers decode", suggestion="Use single quotes: x-data='func({{ data|tojson }})'",
) )
except Exception: except Exception:
pass # Skip files that can't be read pass # Skip files that can't be read