188 lines
8.9 KiB
Markdown
188 lines
8.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
|
||
|
|
```
|
||
|
|
|
||
|
|
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
|
||
|
|
|
||
|
|
A document with no options becomes a single card. Splitting is always opt-in, and there are two ways to ask for it.
|
||
|
|
|
||
|
|
**Manual dividers** — put a `{.card}` or `{.card Title}` line wherever a new card should start:
|
||
|
|
|
||
|
|
```markdown
|
||
|
|
Intro shown on the menu card...
|
||
|
|
|
||
|
|
{.card Weather}
|
||
|
|
Cold and clear.
|
||
|
|
|
||
|
|
{.card Trail Status}
|
||
|
|
- North Loop: open
|
||
|
|
```
|
||
|
|
|
||
|
|
**Heading-level splitting** — `--split-level 2` starts a new card at every `##`, which fits the common one-H1-title-plus-H2-sections document:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
wapdown trail.md --split-level 2
|
||
|
|
```
|
||
|
|
|
||
|
|
If a document contains any `{.card}` markers, they win and `--split-level` is ignored: an explicit boundary in the document is a stronger statement of intent than a rule passed on the command line.
|
||
|
|
|
||
|
|
## 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. |
|
||
|
|
| `--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 |
|
||
|
|
| Paragraphs, lists, rules | `<p>` — WML has no list element, so bullets and numbers become literal text |
|
||
|
|
| `**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 |
|
||
|
|
| `{.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
|