diff --git a/README.md b/README.md index 93514f0..71a4dd2 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ uv tool install --from git+https://codeberg.org/randogoth/md2txt/ md2txt ## CLI -- `md2txt` – converts Markdown into plain text with extensive formatting support. It ships with the default `markdown` parser and `text` renderer plugins, registers optional `micron`, `ama`, and `gemini` renderers for Micron/Ancient Machine Book or Gemtext output, and exposes the core pipeline so you can add your own parser or renderer modules: +- `md2txt` – converts Markdown into plain text with extensive formatting support. It ships with the default `markdown` parser and `text` renderer plugins, registers optional `micron`, `ama`, `gemini`, and `wml` renderers for Micron/Ancient Machine Book, Gemtext, or WML (WAP) output, and exposes the core pipeline so you can add your own parser or renderer modules: - FIGlet-rendered headings. - Emphasis styles converted to spaced or delimited characters. - Optional alignment and margin controls via HTML `

` attributes or MultiMarkdown attribute blocks (e.g. `{:.center margin=20px}`) applied as leading spaces. @@ -36,6 +36,7 @@ md2txt input.md --width 72 # override column width md2txt input.md --renderer micron # emit Micron-formatted output for NomadNet md2txt input.md --renderer ama # emit AMA markup for Ancient Machine Book md2txt input.md --renderer gemini # emit Gemtext for Gemini capsules +md2txt input.md --renderer wml # emit a single-card WML deck for WAP browsers md2txt input.md --renderer-option width=68 # pass column width to a renderer ``` @@ -122,6 +123,40 @@ Inline or block-level ASCII art can be embedded directly in Markdown: Generated text uses CRLF line endings to maintain DOS compatibility. +## WML Decks + +The `wml` renderer converts Markdown into a [WML](https://en.wikipedia.org/wiki/Wireless_Markup_Language) deck for old WAP browsers. WML screens are idiomatically short and button/link-navigated rather than one long scrolling page, so unlike the other renderers, `wml` can split a document into multiple ``s — but only when you ask it to; a plain document with no options stays a single card. + +Two independent, both-optional ways to define card boundaries: + +- **Manual dividers** — insert a `{.card}` or `{.card Title}` line wherever you want a new card to start, regardless of heading structure: + + ```markdown + Intro shown on the menu card... + + {.card Weather} + Cold and clear... + + {.card Trail Status} + - North Loop: open + ``` + +- **Heading-level splitting** — opt in with `--renderer-option split_level=N` to start a new card at every heading of level `N` (e.g. `split_level=2` for the common one-H1-title-plus-H2-sections pattern). Ignored if the document contains any `{.card}` markers. + +When a document resolves to two or more cards, `wml` builds a "menu" card by default — the classic WAP-portal pattern: a list of button/link choices leading to each section, each with a Back control (`--renderer-option menu=false` switches to a linear book instead, chaining cards with Next/Back). A section whose content would still be too large for one card even after splitting is automatically paginated further (`--renderer-option max_card_bytes=N`, default 1400 — a nod to classic WAP 1.x per-deck cache limits; `0` disables it). + +| Renderer option | Default | Meaning | +| --- | --- | --- | +| `title` | *(none)* | Overall deck title, used on the menu card (or the single card, if never split). | +| `split_level` | `0` (disabled) | Heading level that starts a new card. Ignored if `{.card}` markers are present. | +| `max_card_bytes` | `1400` | Byte safety-net per card; `0` disables. | +| `menu` | `true` | Hub-and-spoke menu card vs. linear Next/Back chaining (`menu=false`). | +| `nav_next_label` | `More` | Label for forward navigation (linear mode / overflow pagination). | +| `nav_prev_label` | `Prev` | Label for backward navigation between overflow pagination parts. | +| `nav_back_label` | `Back` | Label for backward navigation from a section back to the menu/previous section. | + +WML 1.3 is the only supported DTD (fixed by design, not a flag). Since WML uses `$` for variable substitution, a literal `$` in your Markdown is escaped to `$$` in the output automatically. Images pass through as `...` referencing the original path/URL — no WBMP conversion is performed. + ## Plugin Architecture The conversion pipeline lives in `src/md2txt/conversion/core.py` and is exposed via `run_conversion`. It is designed around lightweight factories: @@ -149,7 +184,7 @@ register_renderer("ansi", my_renderer_factory) Once registered (for example in a small module that imports `md2txt.cli`), the new plugins are available via `--parser my-markdown` or `--renderer ansi`. The CLI lists registered plugin names in `--help`, and the helper functions `available_parsers()` / `available_renderers()` return the sorted names if you need to build higher-level tooling. -The shared preprocessing helpers—YAML frontmatter parsing, recursive include expansion, and ASCII art sentinels—also live in `src/md2txt/conversion/core.py`, allowing alternate front-ends to reuse exactly the same behaviour without duplicating code. +The shared preprocessing helpers—YAML frontmatter parsing, recursive include expansion, ASCII art sentinels, and pipe-table sentinels—also live in `src/md2txt/conversion/core.py`, allowing alternate front-ends to reuse exactly the same behaviour without duplicating code. --- diff --git a/examples/wml-manual.md b/examples/wml-manual.md new file mode 100644 index 0000000..9efb7c4 --- /dev/null +++ b/examples/wml-manual.md @@ -0,0 +1,14 @@ +Live updates for the ridge trail network. Reception is spotty past the tree line, check before you go. + +{.card Weather} +Cold and clear. Wind: **15 mph** gusting from the *northwest*. Permit fee: $5. + +{.card Trail Status} +- North Loop: open +- South Loop: closed +- Summit Spur: open, ice above 2000m + +{.card Contact} +Ranger station: [call dispatch](tel:+15555550123) + +![Trail map](https://example.com/map.png) diff --git a/examples/wml.md b/examples/wml.md new file mode 100644 index 0000000..92f2ae9 --- /dev/null +++ b/examples/wml.md @@ -0,0 +1,19 @@ +# Trail Conditions + +Live updates for the ridge trail network. Reception is spotty past the tree line, check before you go. + +## Weather + +Cold and clear. Wind: **15 mph** gusting from the *northwest*. Permit fee: $5. + +## Trail Status + +- North Loop: open +- South Loop: closed +- Summit Spur: open, ice above 2000m + +## Contact + +Ranger station: [call dispatch](tel:+15555550123) + +![Trail map](https://example.com/map.png) diff --git a/examples/wml.wml b/examples/wml.wml new file mode 100644 index 0000000..c28de82 --- /dev/null +++ b/examples/wml.wml @@ -0,0 +1,17 @@ + + + + +

Trail Conditions

+

Live updates for the ridge trail network. Reception is spotty past the tree line, check before you go.

+

Weather

+

Cold and clear. Wind: 15 mph gusting from the northwest. Permit fee: $$5.

+

Trail Status

+

- North Loop: open

+

- South Loop: closed

+

- Summit Spur: open, ice above 2000m

+

Contact

+

Ranger station: call dispatch

+

Trail map

+ + diff --git a/src/md2txt/cli.py b/src/md2txt/cli.py index b8c9d3e..0951d78 100644 --- a/src/md2txt/cli.py +++ b/src/md2txt/cli.py @@ -25,6 +25,7 @@ from .renderers.text import TextRenderer from .renderers import ama # noqa: F401 # register AMA renderer plugin from .renderers import gemini # noqa: F401 # register Gemini renderer plugin from .renderers import micron # noqa: F401 # register Micron renderer plugin +from .renderers import wml # noqa: F401 # register WML renderer plugin def _split_option(token: str) -> Tuple[str, str]: diff --git a/src/md2txt/conversion/core.py b/src/md2txt/conversion/core.py index f283bff..9656673 100644 --- a/src/md2txt/conversion/core.py +++ b/src/md2txt/conversion/core.py @@ -17,6 +17,8 @@ ASCII_BLOCK_PATTERN = re.compile( ASCII_INLINE_PATTERN = re.compile(r"#\[(?P