Extract the WML exporter from md2txt into wapdown
md2txt is a suite of minimal text-based markup exporters, and every other
renderer in it (text, micron, ama, gemini, nex) is a straight transform of
a document into a flat text format. The WML renderer is neither: it emits
an XML tree, and it restructures the document rather than transforming it
-- splitting one file into many <card>s, inventing navigation between
them, building a menu card that exists in no source document, and
paginating on a byte budget. That is why it needed levers no sibling
renderer needed, and why it alone forced a CARD_BREAK block kind and a
{.card} directive into the shared parser that every other renderer
ignores.
wapdown is that renderer given a repository of its own, where
restructuring a document into a navigable deck is the point.
Ported: the Markdown parser, block event models, and pipeline core, each
trimmed to what a deck compiler actually uses (no FIGlet, no hyphenation,
no ASCII-art includes, no text-layout frontmatter), plus the eight inline
regexes the renderer had been borrowing from a 1039-line text renderer.
The plugin registry comes along so other WAP-era targets can register
alongside wml later. Zero runtime dependencies.
Config is now first-class: every lever is an argparse flag with
validation and a frontmatter equivalent, layered CLI > frontmatter >
default, replacing the generic --renderer-option KEY=VALUE passthrough.
Five defects found while reading the code, each with a regression test:
- Link and image URLs were shredded by the emphasis patterns, which ran
before LINK_RE: a path like /v1_2_3/ became /v1<i>2</i>3/ inside the
href. Links and images are now resolved and stashed first.
- <table> was emitted as a direct child of <card>. The WML 1.3 content
model is (onevent*, timer?, (do | p | pre)*), so it must sit in a <p>.
- Link labels carried emphasis, but <a> is declared (#PCDATA | br | img)*
-- there is nowhere valid for a <b> inside one. Markers are now stripped.
- The menu card was built directly and never byte-packed, so the one card
most likely to be large was the one card exempt from the budget.
- Output was written with CRLF line endings. WML is XML served over HTTP.
New, WAP-idiomatic rather than Markdown-idiomatic:
- deck-level <template> hoisting nav that would otherwise repeat per card
- <select> menus, picked with a keypad digit instead of scrolled to
- --deck-per-card, one file per section cross-linked by filename, which
is the real answer to per-deck cache limits
- <head> metadata: Cache-Control max-age and <access>
- a Home options softkey, which <prev/> alone cannot provide
- an image policy (keep/alt/drop) for the WBMP-only reality of WAP 1.x
browsers -- no conversion is performed
186 tests, where neither repo had any. Golden files pin whole-document
output; every deck is validated against the real WML 1.3 DTD, vendored at
tests/dtd/wml13.dtd. Rendering examples/trail.md still matches md2txt's
committed output byte for byte, modulo the CRLF fix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
b83e24e2f7
51 changed files with 4016 additions and 0 deletions
55
tests/test_golden.py
Normal file
55
tests/test_golden.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
"""Byte-for-byte comparison against committed reference output.
|
||||
|
||||
These catch unintended drift in whole-document output, which unit tests on
|
||||
individual constructs cannot. When a change is deliberate, regenerate with
|
||||
`uv run python tests/regenerate_golden.py` and review the resulting diff.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from xml.etree import ElementTree
|
||||
|
||||
import pytest
|
||||
|
||||
from conftest import EXAMPLES, GOLDEN, render_file
|
||||
from golden_cases import CASES, MULTI_CASES
|
||||
|
||||
|
||||
REGENERATE_HINT = (
|
||||
"Output changed. If that was intended, run "
|
||||
"`uv run python tests/regenerate_golden.py` and review the diff."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", sorted(CASES))
|
||||
def test_matches_golden(name):
|
||||
source, options = CASES[name]
|
||||
golden = GOLDEN / f"{name}.wml"
|
||||
assert golden.exists(), f"missing golden for '{name}'; {REGENERATE_HINT}"
|
||||
actual = render_file(EXAMPLES / source, **options).text
|
||||
assert actual == golden.read_text(encoding="utf-8"), REGENERATE_HINT
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", sorted(MULTI_CASES))
|
||||
def test_matches_golden_directory(name):
|
||||
source, options = MULTI_CASES[name]
|
||||
directory = GOLDEN / name
|
||||
assert directory.is_dir(), f"missing golden dir for '{name}'; {REGENERATE_HINT}"
|
||||
documents = {
|
||||
doc.name or "index.wml": doc.text
|
||||
for doc in render_file(EXAMPLES / source, **options).documents
|
||||
}
|
||||
expected = {path.name: path.read_text(encoding="utf-8") for path in directory.iterdir()}
|
||||
assert sorted(documents) == sorted(expected), REGENERATE_HINT
|
||||
for filename, text in documents.items():
|
||||
assert text == expected[filename], f"{filename}: {REGENERATE_HINT}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", sorted(GOLDEN.glob("**/*.wml")), ids=lambda p: p.name)
|
||||
def test_golden_files_are_well_formed(path):
|
||||
ElementTree.fromstring(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def test_every_example_has_a_case():
|
||||
"""A new examples/*.md without a golden case would go unverified."""
|
||||
covered = {source for source, _ in list(CASES.values()) + list(MULTI_CASES.values())}
|
||||
assert {path.name for path in EXAMPLES.glob("*.md")} == covered
|
||||
Loading…
Add table
Add a link
Reference in a new issue