|
|
@@ -0,0 +1,91 @@
|
|
|
+#!/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)
|