feat: DTD checker script

This commit is contained in:
randogoth 2026-10-11 08:07:32 +03:00
parent e5bdc3e373
commit 63bbf4d496

102
dtd/check_dtd.py Normal file
View file

@ -0,0 +1,102 @@
#!/usr/bin/env python
"""Check mews-0.1.dtd against the XHTML Mobile 1.2 driver and the spec.
The Mews DTD is maintained by hand. It cannot be generated by stripping
xhtml-mobile12.dtd: that file is a driver which declares no elements (the
declarations live in the W3C and OMA module files it points to), DTD
attribute lists only ever add, a re-declared element is an error rather
than an override, and the rules that define Mews (the head model of
SPEC.md 3.3, the body variant classes of 5.3, the ban on nested tables
of 4.2, and lang and dir) do not exist in XHTML-MP to be kept.
This script verifies what the driver and the spec can check:
- the driver is the expected XHTML Mobile 1.2 driver, with event
attributes included and bidi ignored, so lang and dir are Mews
extensions (SPEC.md 3);
- the DTD declares exactly the elements SPEC.md 4.1 permits;
- the DTD keeps the driver's pre content model verbatim;
- the DTD keeps the li value and ol start attributes the driver adds.
Run with: uv run check_dtd.py
"""
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent
DRIVER_PATH = ROOT / "xhtml-mobile12.dtd"
DTD_PATH = ROOT / "mews-0.1.dtd"
DRIVER_FPI = "-//OMA//DTD XHTML Mobile 1.2//EN"
# The Mews element set (SPEC.md 4.1).
KEPT_ELEMENTS = (
"html head title body "
"h1 h2 h3 h4 h5 h6 "
"p blockquote pre address hr "
"em strong b i code kbd samp var q cite abbr dfn br "
"a "
"ul ol li dl dt dd "
"table caption tr th td "
"img "
"form fieldset label input select option optgroup textarea "
"meta link"
).split()
def entity(text, name):
match = re.search(r"<!ENTITY\s+%\s*" + re.escape(name) + r'\s+"([^"]*)"', text)
return match.group(1).strip() if match else None
def pre_content(text):
match = re.search(r'<!ENTITY\s+%\s*pre\.content\s+"(.*?)"\s*>', text, re.DOTALL)
return re.sub(r"\s+", " ", match.group(1)) if match else None
def attlist(text, element):
match = re.search(r"<!ATTLIST\s+" + element + r"\s+(.*?)\n>", text, re.DOTALL)
return match.group(1) if match else None
def main():
driver = DRIVER_PATH.read_text(encoding="utf-8")
dtd = DTD_PATH.read_text(encoding="utf-8")
errors = []
def check(condition, message):
if not condition:
errors.append(message)
check(entity(driver, "XHTML.version") == DRIVER_FPI, "driver is not XHTML Mobile 1.2")
check(
entity(driver, "xhtml-events.module") == "INCLUDE",
"driver must include event attributes; Mews drops them",
)
check(
entity(driver, "XHTML.bidi") == "IGNORE",
"driver must ignore bidi, so dir is a Mews extension",
)
declared = set(re.findall(r"<!ELEMENT\s+([\w:-]+)", dtd))
expected = set(KEPT_ELEMENTS)
check(
declared == expected,
f"elements differ from SPEC.md 4.1: {sorted(declared ^ expected)}",
)
check(pre_content(dtd) == pre_content(driver), "pre content model differs from the driver's")
check("value" in (attlist(dtd, "li") or ""), "li is missing the driver's value attribute")
check("start" in (attlist(dtd, "ol") or ""), "ol is missing the driver's start attribute")
if errors:
for message in errors:
print(f"error: {message}", file=sys.stderr)
sys.exit(1)
print(f"{DTD_PATH.name} matches the driver and SPEC.md 4.1 ({len(expected)} elements)")
if __name__ == "__main__":
main()