No description
Find a file
randogoth d6361b927a Tighten the README around a Markdown-to-WML mapping table
The README explained the design before it explained the output. Someone
arriving with a document in hand had to read four sections of rationale
before learning what a heading or a table actually becomes.

The mapping now comes first and is complete: every Markdown construct
against the markup wapdown emits for it, with the WML-side reasons in a
notes column rather than in prose. Each row was checked against real
output rather than carried over from the old text.

Cut the "why this is not just another text exporter" section down to two
sentences in the intro, folded the navigation and image prose into small
tables, and dropped the separate Architecture section into the end of
Development. 210 lines to 164, 1673 words to 1176, with more of the
reference material a user actually looks things up in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 09:32:41 +03:00
examples Make --- divide cards, and stop drawing ASCII rules 2026-09-23 09:31:51 +03:00
src/wapdown Make --- divide cards, and stop drawing ASCII rules 2026-09-23 09:31:51 +03:00
tests Make --- divide cards, and stop drawing ASCII rules 2026-09-23 09:31:51 +03:00
.gitignore Extract the WML exporter from md2txt into wapdown 2026-09-23 09:27:49 +03:00
.python-version Extract the WML exporter from md2txt into wapdown 2026-09-23 09:27:49 +03:00
LICENSE Extract the WML exporter from md2txt into wapdown 2026-09-23 09:27:49 +03:00
pyproject.toml Extract the WML exporter from md2txt into wapdown 2026-09-23 09:27:49 +03:00
README.md Tighten the README around a Markdown-to-WML mapping table 2026-09-23 09:32:41 +03:00
uv.lock Extract the WML exporter from md2txt into wapdown 2026-09-23 09:27:49 +03:00

wapdown

Compile Markdown into a WML deck for WAP browsers.

wapdown trail.md -o trail.wml

A WAP screen is a few lines tall and driven by a numeric keypad, so a deck is a set of short cards joined by softkeys — not one long page. wapdown restructures a document into that shape: it decides where cards begin, builds a menu, wires up Back and More, and paginates whatever is still too big.

Install

uv tool install --from git+https://codeberg.org/randogoth/wapdown/ wapdown

Zero runtime dependencies, Python 3.13+.

Markdown → WML

Every mapping, with the markup wapdown actually emits:

Markdown WML Notes
# Heading (first on a card) <p><b><big>…</big></b></p>
## Heading (and any later heading) <p><b>…</b></p> WML has no heading element
Text over === / --- same as # / ## setext headings
Paragraph <p>…</p> the only block container WML has
- item <p>- item</p> no list element; the bullet is literal text
1. item <p>1. item</p> numbering is counted and written out
> quote <p><i>…</i></p>
--- / *** / ___ (nothing) starts a new card; WML has no rule element
{.card Title} (nothing) starts a new card, with a title
```fenced``` / 4-space indent <p>a<br/>b</p> <br/> between lines
**bold**, __bold__ <b>…</b>
*italic*, _italic_ <i>…</i>
`code` (markers stripped) no equivalent
~~strike~~ (markers stripped) no equivalent
[label](url) <a href="url">label</a> emphasis in the label is stripped
![alt](src) <img src="src" alt="alt"/> subject to --images
Pipe table <p><table columns="N">…</table></p> header row bolded
{.include file.md} (inlined before conversion)

Two quirks of WML's own grammar:

  • A literal $ becomes $$. WML uses $ for variable substitution. Permit fee: $5 is emitted as $$5 and displays as $5.
  • Link labels cannot carry emphasis. WML declares <a> as (#PCDATA | br | img)*, so [**go** now](url) becomes go now.

WML 1.3 is the only supported DTD, by design.

Cards

A document with no dividers is one card. Three ways to split it:

--- starts a new card, taking its title from the section's first heading:

Intro shown on the menu card.

---

## Weather

Cold and clear.

*** and ___ work the same. A trailing ---, or two in a row, adds no empty card. --- under text is a setext heading, not a divider.

{.card Title} is the same thing with an explicit title, for sections with no heading to borrow from. The two compose freely.

--split-level 2 starts a card at every ##, for documents structured by headings alone. Explicit dividers win when both are present.

Navigation

Two or more cards get the classic WAP portal: a menu card listing each section, each spoke returning with Back.

Flag Effect
--menu-style select menu choices as a keypad-pickable <select> instead of links
--no-menu linear book — cards chained More/Back, no hub
--home-label Home options softkey jumping straight to the menu from anywhere
--no-template-nav repeat nav on every card instead of hoisting it into <template>

Size limits

--max-card-bytes (default 1400, 0 disables) paginates any card still over budget with Prev/More. Some headroom is reserved for nav markup, so the effective budget is slightly under the number you pass.

When the whole deck is too large, paginating inside one file doesn't help — it still gets fetched whole. --deck-per-card writes one file per section, cross-linked by filename:

wapdown trail.md --deck-per-card -o ./decks/
# decks/index.wml  decks/weather.wml  decks/trail-status.wml

The hub lands in index.wml; in linear mode there is no hub, so the entry point is the first section's file.

Images

No conversion is ever performed. Most WAP 1.x browsers render only WBMP, so a PNG generally shows as a broken-image placeholder. .wbmp sources always pass through untouched.

--images Non-WBMP images
keep (default) emitted as written
alt replaced with their alt text, with a warning
drop removed, with a warning

Options

Every option works as a flag or as frontmatter — same name, underscores instead of dashes. Flags win over frontmatter.

---
title: Trail Conditions
menu_style: select
cache_control: 0
---
Flag Frontmatter Default Meaning
--title title (none) Deck title, shown on the menu card.
--split-level split_level 0 Heading level starting a new card; 0 disables.
--split-on-rule / --no-… split_on_rule true Treat --- as a card divider.
--max-card-bytes max_card_bytes 1400 Per-card byte budget; 0 disables.
--menu / --no-menu menu true Hub-and-spoke menu vs. linear chaining.
--menu-style menu_style links links or select.
--deck-per-card deck_per_card false One file per section.
--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.
--home-label home_label (none) Adds an options softkey to the menu.
--template-nav / --no-… template_nav true Hoist shared nav into <template>.
--images images keep keep, alt, or drop.
--cache-control cache_control (none) Emit Cache-Control: max-age=N.
--access-domain access_domain (none) Emit <access domain=…>.
--access-path access_path (none) Emit <access path=…>.

Serving

Serve .wml as text/vnd.wap.wml — a browser given text/html will usually refuse to render it. Set --cache-control 0 for content that changes; WAP gateways cache aggressively.

Development

uv sync
uv run pytest                  # unit, golden, well-formedness
uv run --extra dtd pytest      # ...plus validation against the real WML 1.3 DTD

The DTD is vendored at tests/dtd/wml13.dtd; validation needs lxml and is skipped without it. Golden files pin whole-document output — after an intentional change, regenerate and read the diff:

uv run python tests/regenerate_golden.py && jj diff tests/golden

Parsers and renderers are registered as plugins (src/wapdown/plugins/), so another WAP-era target such as HDML or i-mode cHTML can be added without restructuring. The renderer is split into inline.py (markup and escaping), deck.py (sectioning, packing, nav topology) and renderer.py (element emission).

License

MIT