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>
2026-09-23 09:27:49 +03:00
# wapdown
Compile Markdown into a [WML ](https://en.wikipedia.org/wiki/Wireless_Markup_Language ) deck for WAP browsers.
```bash
wapdown trail.md -o trail.wml
```
Write a document the ordinary way; get back valid WML 1.3 that a 2001 handset can actually navigate.
## Why this is not just another text exporter
Most Markdown converters transform a document: same content, different markup, one file in and one file out. A WML deck is not that shape. A WAP screen is a few lines tall, scrolling is painful, and the input device is a numeric keypad — so a deck is a set of short **cards** joined by softkeys and menus, not one long page.
So wapdown **restructures** . It decides where cards begin, builds a menu that exists nowhere in your source, wires up Back and More controls, and paginates anything still too big for the device's cache. That is why it has more knobs than a plain text exporter needs: the knobs *are* the tool.
## Install
Install the CLI with [uv ](https://github.com/astral-sh/uv ):
```bash
uv tool install --from git+https://codeberg.org/randogoth/wapdown/ wapdown
```
Zero runtime dependencies, Python 3.13+.
## Cards
Make `---` divide cards, and stop drawing ASCII rules
WML has no horizontal rule element -- its entire %layout entity is
`<br>` -- so a thematic break was being rendered as `<p>------------</p>`,
a hardcoded twelve-dash guess at screen width that wraps into nonsense on
a narrow device and is short on a wide one. It was the one construct
where the renderer invented a width out of nothing.
A `---` already looks like a divider and reads as one in every Markdown
document ever written, so it now is one: it starts a new card, by
default. Rules are never drawn, whether or not they divide.
Thematic breaks and `{.card Title}` markers are now a single mechanism --
an explicit divider, optionally carrying a title -- and compose freely in
one document. Both still win over --split-level. A section takes its
title from its own first heading when the divider does not supply one, so
the common shape needs no titles at all:
Intro on the menu.
---
## Weather
Cold and clear.
`--no-split-on-rule` / `split_on_rule: false` opts out of the splitting;
it does not bring the dashes back.
Three edges this opened, each fixed here rather than left to bite:
- Setext headings. `Heading` over `---` is an H2 in Markdown, but the
parser was ATX-only, so it produced a paragraph plus a rule. Once a
rule divides cards, a setext document would have split at every heading
and stranded each heading as body copy on the card before it. Setext
headings are now parsed, so such a document keeps its structure.
- Empty sections. A trailing `---`, or two in a row, produced an empty
"Untitled" card and a menu entry leading to it. Sections with no
renderable content are dropped.
- The multi-card threshold was two sections, so a document with a single
divider collapsed back into one card. Any explicit divider now makes a
multi-card deck: `A --- B` asks for two screens.
Also fixes a latent frontmatter bug this made far more likely: a document
opening with `---` and no `key: value` lines had its opening swallowed as
if the break were a frontmatter fence. A fenced block with no keys in it
is not frontmatter.
222 tests. Rendering examples/trail.md still matches md2txt's committed
output byte for byte; no golden file contains an ASCII rule any more.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 09:27:49 +03:00
A document with no dividers becomes a single card. There are three ways to ask for more.
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>
2026-09-23 09:27:49 +03:00
Make `---` divide cards, and stop drawing ASCII rules
WML has no horizontal rule element -- its entire %layout entity is
`<br>` -- so a thematic break was being rendered as `<p>------------</p>`,
a hardcoded twelve-dash guess at screen width that wraps into nonsense on
a narrow device and is short on a wide one. It was the one construct
where the renderer invented a width out of nothing.
A `---` already looks like a divider and reads as one in every Markdown
document ever written, so it now is one: it starts a new card, by
default. Rules are never drawn, whether or not they divide.
Thematic breaks and `{.card Title}` markers are now a single mechanism --
an explicit divider, optionally carrying a title -- and compose freely in
one document. Both still win over --split-level. A section takes its
title from its own first heading when the divider does not supply one, so
the common shape needs no titles at all:
Intro on the menu.
---
## Weather
Cold and clear.
`--no-split-on-rule` / `split_on_rule: false` opts out of the splitting;
it does not bring the dashes back.
Three edges this opened, each fixed here rather than left to bite:
- Setext headings. `Heading` over `---` is an H2 in Markdown, but the
parser was ATX-only, so it produced a paragraph plus a rule. Once a
rule divides cards, a setext document would have split at every heading
and stranded each heading as body copy on the card before it. Setext
headings are now parsed, so such a document keeps its structure.
- Empty sections. A trailing `---`, or two in a row, produced an empty
"Untitled" card and a menu entry leading to it. Sections with no
renderable content are dropped.
- The multi-card threshold was two sections, so a document with a single
divider collapsed back into one card. Any explicit divider now makes a
multi-card deck: `A --- B` asks for two screens.
Also fixes a latent frontmatter bug this made far more likely: a document
opening with `---` and no `key: value` lines had its opening swallowed as
if the break were a frontmatter fence. A fenced block with no keys in it
is not frontmatter.
222 tests. Rendering examples/trail.md still matches md2txt's committed
output byte for byte; no golden file contains an ASCII rule any more.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 09:27:49 +03:00
**Thematic breaks** — a plain `---` starts a new card. WML has no rule element to draw (its entire layout vocabulary is `<br>` ), and a row of ASCII dashes is noise on a small screen, so the glyph is put to better use as the divider it already looks like:
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>
2026-09-23 09:27:49 +03:00
```markdown
Intro shown on the menu card...
Make `---` divide cards, and stop drawing ASCII rules
WML has no horizontal rule element -- its entire %layout entity is
`<br>` -- so a thematic break was being rendered as `<p>------------</p>`,
a hardcoded twelve-dash guess at screen width that wraps into nonsense on
a narrow device and is short on a wide one. It was the one construct
where the renderer invented a width out of nothing.
A `---` already looks like a divider and reads as one in every Markdown
document ever written, so it now is one: it starts a new card, by
default. Rules are never drawn, whether or not they divide.
Thematic breaks and `{.card Title}` markers are now a single mechanism --
an explicit divider, optionally carrying a title -- and compose freely in
one document. Both still win over --split-level. A section takes its
title from its own first heading when the divider does not supply one, so
the common shape needs no titles at all:
Intro on the menu.
---
## Weather
Cold and clear.
`--no-split-on-rule` / `split_on_rule: false` opts out of the splitting;
it does not bring the dashes back.
Three edges this opened, each fixed here rather than left to bite:
- Setext headings. `Heading` over `---` is an H2 in Markdown, but the
parser was ATX-only, so it produced a paragraph plus a rule. Once a
rule divides cards, a setext document would have split at every heading
and stranded each heading as body copy on the card before it. Setext
headings are now parsed, so such a document keeps its structure.
- Empty sections. A trailing `---`, or two in a row, produced an empty
"Untitled" card and a menu entry leading to it. Sections with no
renderable content are dropped.
- The multi-card threshold was two sections, so a document with a single
divider collapsed back into one card. Any explicit divider now makes a
multi-card deck: `A --- B` asks for two screens.
Also fixes a latent frontmatter bug this made far more likely: a document
opening with `---` and no `key: value` lines had its opening swallowed as
if the break were a frontmatter fence. A fenced block with no keys in it
is not frontmatter.
222 tests. Rendering examples/trail.md still matches md2txt's committed
output byte for byte; no golden file contains an ASCII rule any more.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 09:27:49 +03:00
---
## Weather
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>
2026-09-23 09:27:49 +03:00
Cold and clear.
Make `---` divide cards, and stop drawing ASCII rules
WML has no horizontal rule element -- its entire %layout entity is
`<br>` -- so a thematic break was being rendered as `<p>------------</p>`,
a hardcoded twelve-dash guess at screen width that wraps into nonsense on
a narrow device and is short on a wide one. It was the one construct
where the renderer invented a width out of nothing.
A `---` already looks like a divider and reads as one in every Markdown
document ever written, so it now is one: it starts a new card, by
default. Rules are never drawn, whether or not they divide.
Thematic breaks and `{.card Title}` markers are now a single mechanism --
an explicit divider, optionally carrying a title -- and compose freely in
one document. Both still win over --split-level. A section takes its
title from its own first heading when the divider does not supply one, so
the common shape needs no titles at all:
Intro on the menu.
---
## Weather
Cold and clear.
`--no-split-on-rule` / `split_on_rule: false` opts out of the splitting;
it does not bring the dashes back.
Three edges this opened, each fixed here rather than left to bite:
- Setext headings. `Heading` over `---` is an H2 in Markdown, but the
parser was ATX-only, so it produced a paragraph plus a rule. Once a
rule divides cards, a setext document would have split at every heading
and stranded each heading as body copy on the card before it. Setext
headings are now parsed, so such a document keeps its structure.
- Empty sections. A trailing `---`, or two in a row, produced an empty
"Untitled" card and a menu entry leading to it. Sections with no
renderable content are dropped.
- The multi-card threshold was two sections, so a document with a single
divider collapsed back into one card. Any explicit divider now makes a
multi-card deck: `A --- B` asks for two screens.
Also fixes a latent frontmatter bug this made far more likely: a document
opening with `---` and no `key: value` lines had its opening swallowed as
if the break were a frontmatter fence. A fenced block with no keys in it
is not frontmatter.
222 tests. Rendering examples/trail.md still matches md2txt's committed
output byte for byte; no golden file contains an ASCII rule any more.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 09:27:49 +03:00
---
## Trail Status
- North Loop: open
```
Each card takes its title from its own first heading. `***` and `___` work identically — any Markdown thematic break divides. A trailing `---` , or two in a row, adds no empty card. `--no-split-on-rule` turns this off; breaks are then ignored rather than drawn, because there is still nothing to draw them with.
`---` under a line of text is a setext heading, not a break, and is parsed as one — so a document written in that style keeps its headings instead of shattering into cards.
**Named dividers** — `{.card Title}` is the same mechanism with an explicit title, for when a section has no heading to borrow one from:
```markdown
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>
2026-09-23 09:27:49 +03:00
{.card Trail Status}
- North Loop: open
```
Make `---` divide cards, and stop drawing ASCII rules
WML has no horizontal rule element -- its entire %layout entity is
`<br>` -- so a thematic break was being rendered as `<p>------------</p>`,
a hardcoded twelve-dash guess at screen width that wraps into nonsense on
a narrow device and is short on a wide one. It was the one construct
where the renderer invented a width out of nothing.
A `---` already looks like a divider and reads as one in every Markdown
document ever written, so it now is one: it starts a new card, by
default. Rules are never drawn, whether or not they divide.
Thematic breaks and `{.card Title}` markers are now a single mechanism --
an explicit divider, optionally carrying a title -- and compose freely in
one document. Both still win over --split-level. A section takes its
title from its own first heading when the divider does not supply one, so
the common shape needs no titles at all:
Intro on the menu.
---
## Weather
Cold and clear.
`--no-split-on-rule` / `split_on_rule: false` opts out of the splitting;
it does not bring the dashes back.
Three edges this opened, each fixed here rather than left to bite:
- Setext headings. `Heading` over `---` is an H2 in Markdown, but the
parser was ATX-only, so it produced a paragraph plus a rule. Once a
rule divides cards, a setext document would have split at every heading
and stranded each heading as body copy on the card before it. Setext
headings are now parsed, so such a document keeps its structure.
- Empty sections. A trailing `---`, or two in a row, produced an empty
"Untitled" card and a menu entry leading to it. Sections with no
renderable content are dropped.
- The multi-card threshold was two sections, so a document with a single
divider collapsed back into one card. Any explicit divider now makes a
multi-card deck: `A --- B` asks for two screens.
Also fixes a latent frontmatter bug this made far more likely: a document
opening with `---` and no `key: value` lines had its opening swallowed as
if the break were a frontmatter fence. A fenced block with no keys in it
is not frontmatter.
222 tests. Rendering examples/trail.md still matches md2txt's committed
output byte for byte; no golden file contains an ASCII rule any more.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 09:27:49 +03:00
The two compose freely in one document.
**Heading-level splitting** — `--split-level 2` starts a new card at every `##` , which fits a document that is already structured by headings and has no dividers in it:
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>
2026-09-23 09:27:49 +03:00
```bash
wapdown trail.md --split-level 2
```
Make `---` divide cards, and stop drawing ASCII rules
WML has no horizontal rule element -- its entire %layout entity is
`<br>` -- so a thematic break was being rendered as `<p>------------</p>`,
a hardcoded twelve-dash guess at screen width that wraps into nonsense on
a narrow device and is short on a wide one. It was the one construct
where the renderer invented a width out of nothing.
A `---` already looks like a divider and reads as one in every Markdown
document ever written, so it now is one: it starts a new card, by
default. Rules are never drawn, whether or not they divide.
Thematic breaks and `{.card Title}` markers are now a single mechanism --
an explicit divider, optionally carrying a title -- and compose freely in
one document. Both still win over --split-level. A section takes its
title from its own first heading when the divider does not supply one, so
the common shape needs no titles at all:
Intro on the menu.
---
## Weather
Cold and clear.
`--no-split-on-rule` / `split_on_rule: false` opts out of the splitting;
it does not bring the dashes back.
Three edges this opened, each fixed here rather than left to bite:
- Setext headings. `Heading` over `---` is an H2 in Markdown, but the
parser was ATX-only, so it produced a paragraph plus a rule. Once a
rule divides cards, a setext document would have split at every heading
and stranded each heading as body copy on the card before it. Setext
headings are now parsed, so such a document keeps its structure.
- Empty sections. A trailing `---`, or two in a row, produced an empty
"Untitled" card and a menu entry leading to it. Sections with no
renderable content are dropped.
- The multi-card threshold was two sections, so a document with a single
divider collapsed back into one card. Any explicit divider now makes a
multi-card deck: `A --- B` asks for two screens.
Also fixes a latent frontmatter bug this made far more likely: a document
opening with `---` and no `key: value` lines had its opening swallowed as
if the break were a frontmatter fence. A fenced block with no keys in it
is not frontmatter.
222 tests. Rendering examples/trail.md still matches md2txt's committed
output byte for byte; no golden file contains an ASCII rule any more.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 09:27:49 +03:00
Explicit dividers win over `--split-level` when both are present: a boundary written into the document is a stronger statement of intent than a rule passed on the command line.
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>
2026-09-23 09:27:49 +03:00
## Navigation
Once a document resolves to two or more cards, wapdown builds the classic WAP-portal shape — a **menu** card listing each section, each spoke reachable in one press and returning with Back:
```bash
wapdown trail.md --split-level 2 --menu-style select
```
`--menu-style select` renders those choices as a `<select>` rather than a list of links, so the destination is picked with a digit instead of scrolled to. On a keypad that is usually the better default; it is not the default here only because it changes how the deck looks.
`--no-menu` switches to a linear book instead: cards chained with More and Back, no hub.
`--home-label Home` adds an options softkey jumping straight back to the menu from anywhere, which `<prev/>` alone cannot do since it only pops history.
Shared navigation is hoisted into a deck-level `<template>` so it is transmitted once rather than repeated on every card — a real saving when the whole deck has to fit in a cache. `--no-template-nav` turns that off.
## Size limits
WAP 1.x gateways cap how much deck a device will hold. `--max-card-bytes` (default 1400, `0` disables) is the safety net: any card still over budget after section splitting gets paginated with Prev/More. Some headroom is reserved for each card's navigation markup, which is why the effective budget is slightly below the number you pass.
When a deck as a whole is too large, paginating inside one file does not help — the file still has to be fetched whole. `--deck-per-card` writes **one `.wml` file per section** into an output directory, cross-linked by filename, so a device only ever downloads the card it asked for:
```bash
wapdown trail.md --split-level 2 --deck-per-card -o ./decks/
# decks/index.wml decks/weather.wml decks/trail-status.wml ...
```
In menu mode the hub lands in `index.wml` . In linear mode there is no hub, so the entry point is the first section's file.
## Images
wapdown performs **no image conversion** . Most WAP 1.x browsers render only WBMP, so referencing a PNG generally yields a broken-image placeholder on the device. `--images` decides what to do about that:
| Value | Behaviour |
| --- | --- |
| `keep` *(default)* | Emit `<img>` as written, whatever the format. |
| `alt` | Replace non-WBMP images with their alt text, and warn. |
| `drop` | Remove non-WBMP images entirely, and warn. |
`.wbmp` sources always pass through untouched.
## Configuration
Every option can be set on the command line or in the document's frontmatter. The frontmatter key is the long flag name with underscores; **command-line flags win** .
```markdown
---
title: Trail Conditions
split_level: 2
menu_style: select
cache_control: 0
---
# Trail Conditions
...
```
| Flag | Frontmatter | Default | Meaning |
| --- | --- | --- | --- |
| `--title` | `title` | *(none)* | Deck title, used on the menu card. |
| `--split-level` | `split_level` | `0` | Heading level that starts a new card; `0` disables. Ignored when `{.card}` markers are present. |
| `--max-card-bytes` | `max_card_bytes` | `1400` | Per-card byte safety net; `0` disables. |
| `--menu` / `--no-menu` | `menu` | `true` | Hub-and-spoke menu vs. linear More/Back chaining. |
| `--menu-style` | `menu_style` | `links` | `links` or `select` for the menu's choices. |
Make `---` divide cards, and stop drawing ASCII rules
WML has no horizontal rule element -- its entire %layout entity is
`<br>` -- so a thematic break was being rendered as `<p>------------</p>`,
a hardcoded twelve-dash guess at screen width that wraps into nonsense on
a narrow device and is short on a wide one. It was the one construct
where the renderer invented a width out of nothing.
A `---` already looks like a divider and reads as one in every Markdown
document ever written, so it now is one: it starts a new card, by
default. Rules are never drawn, whether or not they divide.
Thematic breaks and `{.card Title}` markers are now a single mechanism --
an explicit divider, optionally carrying a title -- and compose freely in
one document. Both still win over --split-level. A section takes its
title from its own first heading when the divider does not supply one, so
the common shape needs no titles at all:
Intro on the menu.
---
## Weather
Cold and clear.
`--no-split-on-rule` / `split_on_rule: false` opts out of the splitting;
it does not bring the dashes back.
Three edges this opened, each fixed here rather than left to bite:
- Setext headings. `Heading` over `---` is an H2 in Markdown, but the
parser was ATX-only, so it produced a paragraph plus a rule. Once a
rule divides cards, a setext document would have split at every heading
and stranded each heading as body copy on the card before it. Setext
headings are now parsed, so such a document keeps its structure.
- Empty sections. A trailing `---`, or two in a row, produced an empty
"Untitled" card and a menu entry leading to it. Sections with no
renderable content are dropped.
- The multi-card threshold was two sections, so a document with a single
divider collapsed back into one card. Any explicit divider now makes a
multi-card deck: `A --- B` asks for two screens.
Also fixes a latent frontmatter bug this made far more likely: a document
opening with `---` and no `key: value` lines had its opening swallowed as
if the break were a frontmatter fence. A fenced block with no keys in it
is not frontmatter.
222 tests. Rendering examples/trail.md still matches md2txt's committed
output byte for byte; no golden file contains an ASCII rule any more.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 09:27:49 +03:00
| `--split-on-rule` / `--no-split-on-rule` | `split_on_rule` | `true` | Start a new card at every thematic break. |
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>
2026-09-23 09:27:49 +03:00
| `--deck-per-card` | `deck_per_card` | `false` | Write one file per section into the output directory. |
| `--nav-next-label` | `nav_next_label` | `More` | Forward label. |
| `--nav-prev-label` | `nav_prev_label` | `Prev` | Label between pagination parts. |
| `--nav-back-label` | `nav_back_label` | `Back` | Label back to the menu or previous section. |
| `--home-label` | `home_label` | *(none)* | Adds an options softkey to the menu card. |
| `--template-nav` / `--no-template-nav` | `template_nav` | `true` | Hoist shared nav into a `<template>` . |
| `--images` | `images` | `keep` | `keep` , `alt` , or `drop` . |
| `--cache-control` | `cache_control` | *(none)* | Emit a `Cache-Control: max-age=N` meta element. |
| `--access-domain` | `access_domain` | *(none)* | Emit `<access domain=...>` . |
| `--access-path` | `access_path` | *(none)* | Emit `<access path=...>` . |
## Markdown support
| Markdown | WML |
| --- | --- |
| Headings | `<p><b><big>` for the first on a card, `<p><b>` after |
Make `---` divide cards, and stop drawing ASCII rules
WML has no horizontal rule element -- its entire %layout entity is
`<br>` -- so a thematic break was being rendered as `<p>------------</p>`,
a hardcoded twelve-dash guess at screen width that wraps into nonsense on
a narrow device and is short on a wide one. It was the one construct
where the renderer invented a width out of nothing.
A `---` already looks like a divider and reads as one in every Markdown
document ever written, so it now is one: it starts a new card, by
default. Rules are never drawn, whether or not they divide.
Thematic breaks and `{.card Title}` markers are now a single mechanism --
an explicit divider, optionally carrying a title -- and compose freely in
one document. Both still win over --split-level. A section takes its
title from its own first heading when the divider does not supply one, so
the common shape needs no titles at all:
Intro on the menu.
---
## Weather
Cold and clear.
`--no-split-on-rule` / `split_on_rule: false` opts out of the splitting;
it does not bring the dashes back.
Three edges this opened, each fixed here rather than left to bite:
- Setext headings. `Heading` over `---` is an H2 in Markdown, but the
parser was ATX-only, so it produced a paragraph plus a rule. Once a
rule divides cards, a setext document would have split at every heading
and stranded each heading as body copy on the card before it. Setext
headings are now parsed, so such a document keeps its structure.
- Empty sections. A trailing `---`, or two in a row, produced an empty
"Untitled" card and a menu entry leading to it. Sections with no
renderable content are dropped.
- The multi-card threshold was two sections, so a document with a single
divider collapsed back into one card. Any explicit divider now makes a
multi-card deck: `A --- B` asks for two screens.
Also fixes a latent frontmatter bug this made far more likely: a document
opening with `---` and no `key: value` lines had its opening swallowed as
if the break were a frontmatter fence. A fenced block with no keys in it
is not frontmatter.
222 tests. Rendering examples/trail.md still matches md2txt's committed
output byte for byte; no golden file contains an ASCII rule any more.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 09:27:49 +03:00
| Paragraphs, lists | `<p>` — WML has no list element, so bullets and numbers become literal text |
| `---` / `***` / `___` | a card divider; never drawn, as WML has no rule element |
| Setext headings (`===` / `---` underlines) | same as `#` / `##` |
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>
2026-09-23 09:27:49 +03:00
| `**bold**` , `*italic*` | `<b>` , `<i>` |
| `~~strike~~` , `` ` code` ` ` | markers stripped; WML has no equivalent |
| Links | `<a href>` |
| Images | `<img src alt>` , subject to `--images` |
| Fenced/indented code | `<p>` with `<br/>` between lines |
| Blockquotes | `<p><i>` |
| Pipe tables | `<p><table columns="N">` with a bold header row |
Make `---` divide cards, and stop drawing ASCII rules
WML has no horizontal rule element -- its entire %layout entity is
`<br>` -- so a thematic break was being rendered as `<p>------------</p>`,
a hardcoded twelve-dash guess at screen width that wraps into nonsense on
a narrow device and is short on a wide one. It was the one construct
where the renderer invented a width out of nothing.
A `---` already looks like a divider and reads as one in every Markdown
document ever written, so it now is one: it starts a new card, by
default. Rules are never drawn, whether or not they divide.
Thematic breaks and `{.card Title}` markers are now a single mechanism --
an explicit divider, optionally carrying a title -- and compose freely in
one document. Both still win over --split-level. A section takes its
title from its own first heading when the divider does not supply one, so
the common shape needs no titles at all:
Intro on the menu.
---
## Weather
Cold and clear.
`--no-split-on-rule` / `split_on_rule: false` opts out of the splitting;
it does not bring the dashes back.
Three edges this opened, each fixed here rather than left to bite:
- Setext headings. `Heading` over `---` is an H2 in Markdown, but the
parser was ATX-only, so it produced a paragraph plus a rule. Once a
rule divides cards, a setext document would have split at every heading
and stranded each heading as body copy on the card before it. Setext
headings are now parsed, so such a document keeps its structure.
- Empty sections. A trailing `---`, or two in a row, produced an empty
"Untitled" card and a menu entry leading to it. Sections with no
renderable content are dropped.
- The multi-card threshold was two sections, so a document with a single
divider collapsed back into one card. Any explicit divider now makes a
multi-card deck: `A --- B` asks for two screens.
Also fixes a latent frontmatter bug this made far more likely: a document
opening with `---` and no `key: value` lines had its opening swallowed as
if the break were a frontmatter fence. A fenced block with no keys in it
is not frontmatter.
222 tests. Rendering examples/trail.md still matches md2txt's committed
output byte for byte; no golden file contains an ASCII rule any more.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 09:27:49 +03:00
| `{.card Title}` | a card divider carrying a title |
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>
2026-09-23 09:27:49 +03:00
| `{.include file.md}` | inlined before conversion |
Two consequences of WML's own grammar are worth knowing:
- **A literal `$` is written `$$` .** WML uses `$` to introduce a variable substitution, so wapdown doubles it for you. `Permit fee: $5` comes out as `Permit fee: $$5` , which the device renders as `$5` .
- **Link labels cannot carry emphasis.** WML declares `<a>` as `(#PCDATA | br | img)*` , so there is nowhere valid to put a `<b>` inside one. `[**go** now](url)` becomes `go now` , with the markers stripped rather than translated.
WML 1.3 is the only supported DTD, by design rather than by flag.
## Serving a deck
Serve `.wml` as `text/vnd.wap.wml` — a browser that receives `text/html` will usually refuse to render it. `--cache-control 0` is worth setting for content that changes, since WAP gateways cache aggressively.
## Development
```bash
uv sync
uv run pytest # unit, golden, and well-formedness tests
uv run --extra dtd pytest # ...plus validation against the real WML 1.3 DTD
```
The DTD is vendored at `tests/dtd/wml13.dtd` so the suite never depends on fetching a twenty-year-old URL. DTD validation needs `lxml` and is skipped when it is absent.
Golden files under `tests/golden/` pin whole-document output. After an intentional renderer change:
```bash
uv run python tests/regenerate_golden.py
jj diff tests/golden # the diff is the change, stated in output
```
## Architecture
Markdown is parsed into a stream of block events, which a renderer turns into output. The pieces are registered as plugins (`src/wapdown/plugins/` ), so another WAP-era target — HDML, i-mode cHTML — can be added alongside `wml` without restructuring anything.
- `parsers/markdown.py` — Markdown to `BlockEvent` s
- `renderers/wml/inline.py` — inline markup and WML's escaping rules
- `renderers/wml/deck.py` — sectioning, byte packing, navigation topology
- `renderers/wml/renderer.py` — element emission
- `config.py` — defaults, frontmatter, and CLI layering
Unlike a streaming text renderer, the WML renderer buffers the whole event stream: card boundaries, menu choices, and nav targets all depend on knowing the document's full structure up front.
## Credit
The WML renderer began life inside [md2txt ](https://codeberg.org/randogoth/md2txt ), a suite of plain-text Markdown exporters, and was extracted here because a deck compiler is a different kind of thing from a text exporter.
## License
MIT