| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091 |
- #!/usr/bin/env python3
- """Template hygiene: stm-* expressions and <script> bodies must be pure ASCII.
- Why this invariant exists (verified against the live compiled app):
- statum-mkpstm HTML-escapes the entire compiled page - including
- <script> bodies and stm-* attribute EXPRESSION values - converting
- non-ASCII characters to named entities (—, ·, ...).
- * A <script> body is raw text in HTML: the browser never entity-decodes
- it. An entity landing there reaches JavaScript source verbatim and
- renders literally ("—" as text, or worse, a syntax error).
- * An stm-* attribute value IS entity-decoded by the parser - but the
- decoded ENTITY TEXT ("—", 7 characters) is what lands in the
- expression string, not an em dash. It evaluates and renders
- literally.
- Plain element body text is unaffected (the browser decodes it once,
- correctly), so non-ASCII and entities in body content are fine and are
- deliberately NOT checked here.
- The fix is app-side (the compiler is external): keep every stm-*
- expression and every <script> body pure ASCII. Inside expressions and JS
- string literals, use JS unicode escapes ('\u2014', '\u00b7', '\u2026',
- '\u2192') to keep the exact glyph; in prose-only strings (stm-confirm
- reaches window.confirm verbatim) prefer ASCII punctuation. This test
- makes that rule permanent.
- Standalone: python3 test/test_template_ascii.py
- """
- import os
- import re
- import sys
- import unittest
- PAGES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)),
- "..", "src", "pages")
- # stm-* attributes carry Statum expression/JS values (stm-text, stm-if,
- # stm-attribute.href, stm-action, stm-confirm, stm-values, ...). All of
- # them are treated as JS or passed through raw, so all must be ASCII.
- STM_ATTR_RE = re.compile(r'\b(stm-[A-Za-z0-9_-]+)\s*=\s*"([^"]*)"')
- # <script> without a pstm-res-src (that form has no body).
- SCRIPT_RE = re.compile(r'<script\b[^>]*>(.*?)</script>', re.S)
- def non_ascii(text):
- """[(offset, char)] for every non-ASCII character in text."""
- return [(i, c) for i, c in enumerate(text) if ord(c) >= 128]
- def extract_violations(path):
- """[(kind, detail)] for every non-ASCII character in an stm-*
- attribute value or a <script> body of one template."""
- with open(path, encoding="utf-8") as fh:
- src = fh.read()
- out = []
- for m in STM_ATTR_RE.finditer(src):
- for off, ch in non_ascii(m.group(2)):
- line = src.count("\n", 0, m.start() + off) + 1
- out.append(("stm attribute %s" % m.group(1),
- "line %d: U+%04X %r" % (line, ord(ch), ch)))
- for m in SCRIPT_RE.finditer(src):
- for off, ch in non_ascii(m.group(1)):
- line = src.count("\n", 0, m.start() + off) + 1
- out.append(("script body",
- "line %d: U+%04X %r" % (line, ord(ch), ch)))
- return out
- class TemplateAsciiTest(unittest.TestCase):
- def test_stm_expressions_and_scripts_are_ascii(self):
- pages = sorted(f for f in os.listdir(PAGES_DIR) if f.endswith(".html"))
- self.assertTrue(pages, "no templates found in src/pages")
- failures = []
- for name in pages:
- for kind, detail in extract_violations(os.path.join(PAGES_DIR, name)):
- failures.append("%s (%s): %s" % (name, kind, detail))
- self.assertEqual(
- failures, [],
- "stm-* expressions and <script> bodies must be pure ASCII "
- "(statum-mkpstm entity-encodes them; the browser never decodes "
- "scripts, and decoded entity text renders literally in "
- "expressions). Use \\uXXXX escapes or ASCII punctuation.\n"
- + "\n".join(failures))
- if __name__ == "__main__":
- unittest.main(verbosity=2)
|