feat: render wml decks with paginated card sub-documents

This commit is contained in:
randogoth 2026-10-04 21:04:49 +03:00
parent 36b868e094
commit 1de4674b09
17 changed files with 1519 additions and 5 deletions

View file

@ -8,7 +8,7 @@ A Rust re-implementation of [smolweb](https://code.randogoth.com/randogoth/smolw
## Status
It serves. HTTP, Spartan and Nex are working, with gemtext, XHTML-MP and HTML output and virtual hosting across all three.
It serves. HTTP, Spartan and Nex are working across five output formats, with virtual hosting on all three. Gemini and Gopher are the remaining protocols.
| Area | State |
| --- | --- |
@ -19,7 +19,7 @@ It serves. HTTP, Spartan and Nex are working, with gemtext, XHTML-MP and HTML ou
| HTTP, Spartan and Nex listeners, virtual hosting | implemented |
| Fixed-width text output (Nex, later Gopher) | implemented |
| FIGlet banners and hyphenation, behind features | implemented |
| WML and XHTML-MP decks | planned |
| WML decks and card sub-documents, behind a feature | implemented |
| Gemini and Gopher | planned |
## Configuration
@ -105,6 +105,8 @@ Include and art targets resolve relative to the file holding the reference and m
Configuration keys are not carried over verbatim either: md2txt accepts three spellings of `paragraph_spacing`, and its `cache_control` is named for the header it lands in rather than the value it holds, which here is `max_age`. Its `text` and `nex` renderers differ only in heading decoration and in whether links are inlined or numbered — and the numbered form never writes the reference list its numbers point at, so there is one text renderer here rather than two. Tables, which both of its text renderers drop entirely, are rendered as aligned columns.
wapdown's own parser produces neither tables nor lists, so its WML decks render a bullet list as literal text and drop tables entirely; both come out as real markup here, lists as marked lines and tables as `<table>`, which WML has.
smolweb inherits five invented directives from its two renderer libraries: `{.card}`, `{.include}`, `![[ ]]`, `#[label](art.txt)` and MultiMarkdown attribute lists (`{: .center}`) — two incompatible brace grammars, with art alignment expressible two different ways in one line. None of it appeared in real content, so itsybitsy expresses the same capabilities with standard constructs instead. Parsing once rather than once per library also means the text formats gain setext headings and the WAP formats gain tables and ASCII art, none of which their original parser handled.
## Output formats
@ -117,6 +119,7 @@ Each format is a crate implementing one trait, compiled in behind a cargo featur
| `xhtmlmp` | `application/vnd.wap.xhtml+xml` | Well-formed XML with the Mobile Profile doctype |
| `html` | `text/html` | The same markup without the XML declaration |
| `text` | `text/plain` | Wrapped to 80 columns, because Nex and Gopher clients do not wrap |
| `wml` | `text/vnd.wap.wml` | WAP 1.x decks, paginated to a per-card byte budget (feature `wml`) |
HTTP negotiates between them from `Accept`. Only a literal media type counts as a match, so a browser's `*/*` can never be read as willingness to receive a WAP format; `?format=<id>` overrides negotiation outright, and naming an id the listener does not serve is a bad request rather than a silent fallback.
@ -128,6 +131,7 @@ Two decorative capabilities for the text format are off by default, because most
| Feature | Adds | Cost |
| --- | --- | --- |
| `wml` | WML 1.3 decks for WAP 1.x handsets | +37 KiB |
| `figlet` | `h1_style = "figlet"` banners, fonts `small` and `standard` | +60 KiB |
| `hyphenation` | `hyphenate = true`, English patterns | +127 KiB |
| `hyphenation-all` | every language the pattern crate carries | +3.0 MiB |
@ -138,6 +142,23 @@ cargo build --release --features "figlet hyphenation"
A banner that will not fit the line, names a font this build lacks, or is asked for by a build without `figlet` falls back to the level's underline — a heading that cannot be decorated should not be lost. An unknown font name is logged, since that is almost always a typo. Likewise `hyphenate` has no effect without the feature, and an unknown `hyphen_lang` leaves the text unhyphenated; a ragged right edge is the plain-text convention anyway.
## Card sub-documents
WML is the one output that paginates: a WAP 1.x handset has a hard per-card byte budget and refuses a deck that exceeds it. So a long page becomes a chain of screens, and a page with dividers becomes a menu card linking to the rest.
With `deck_per_card`, each card also gets its own URL under the page's:
| Request | WML client | Any other format |
| --- | --- | --- |
| `/trail` | the menu deck | the whole page |
| `/trail/weather` | that card's own deck | redirects to `/trail` |
| `/trail/nonexistent` | redirects to `/trail` | redirects to `/trail` |
| `/about/whatever` | not found | not found |
That URL space belongs to the format that claims it. A format which does not address a sub-document redirects to the parent rather than substituting something else, and a page with no dividers has no such URLs at all — `/about/whatever` is not a sub-document just because `/about` exists. Nex and Gopher have no redirect status, so they resolve to the parent's content directly instead of bouncing.
The deck keys are `split_level`, `split_on_rule`, `max_card_bytes`, `menu`, `menu_style`, `deck_per_card`, `template_nav`, `nav_next_label`, `nav_prev_label`, `nav_back_label`, `home_label` and `images`.
## URLs
Links should be root-relative and extensionless (`[about](/about)`, not `about.md`), so the same link resolves identically from every protocol.