The README advertised a Codeberg URL, but the repository lives at code.randogoth.com. Verified that the new URL resolves over HTTPS, which is what `uv tool install --from git+...` needs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
160 lines
6.9 KiB
Markdown
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://code.randogoth.com/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 |
|
|
| `` | `<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
|