wapdown/README.md
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

160 lines
6.9 KiB
Markdown

# 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
```
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
```bash
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:
```markdown
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:
```bash
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.**
```markdown
---
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
```bash
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:
```bash
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