feat: render fixed-width text for nex

This commit is contained in:
randogoth 2026-10-04 20:27:29 +03:00
parent 2f38b8b929
commit 9e1cfed284
19 changed files with 1160 additions and 44 deletions

View file

@ -17,7 +17,7 @@ It serves. HTTP, Spartan and Nex are working, with gemtext, XHTML-MP and HTML ou
| Markdown parsing, includes, ASCII art, card breaks | implemented |
| gemtext, XHTML-MP and HTML output | implemented |
| HTTP, Spartan and Nex listeners, virtual hosting | implemented |
| 80-column text and Nex output | planned |
| Fixed-width text output (Nex, later Gopher) | implemented |
| WML and XHTML-MP decks | planned |
| Gemini and Gopher | planned |
@ -51,14 +51,18 @@ A `.itsybitsy.toml` in any content directory says how the Markdown in **that dir
```toml
[defaults]
cache_control = 3600
max_age = 3600
margin_left = 2
h1_style = "underline:="
[page."index.md"]
title = "Notes"
cache_control = 300
max_age = 300
```
`[defaults]` applies to every Markdown file in the directory; a `[page."name.md"]` table overrides it for one file. `title` is per-page by nature, so setting it in `[defaults]` is an error; left unset, it is derived from the page's first level-1 heading. Each renderer's own keys arrive with that renderer. Editing the file re-renders that directory's pages on the next request, and a file that fails to parse makes them error rather than silently falling back to defaults.
`[defaults]` applies to every Markdown file in the directory; a `[page."name.md"]` table overrides it for one file. `title` is per-page by nature, so setting it in `[defaults]` is an error; left unset, it is derived from the page's first level-1 heading.
The fixed-width text format reads `margin_left`, `margin_right`, `paragraph_spacing`, `h1_style` through `h6_style`, `blockquote_bars`, `list_indent`, `code_block_line_numbers` and `wrap_code_blocks`. A heading style is `underline`, `underline:<char>`, `markers` or `plain`; by default the first three levels are underlined with `=`, `-` and `~`, and deeper ones keep their `#` markers, since underline characters run out before heading levels do. Editing the file re-renders that directory's pages on the next request, and a file that fails to parse makes them error rather than silently falling back to defaults.
Because the name begins with a dot, the same rule that keeps `.secret.md` out of the URL space keeps this file out of it too.
@ -98,6 +102,8 @@ Include and art targets resolve relative to the file holding the reference and m
### Differences from smolweb
Configuration keys are not carried over verbatim either: md2txt accepts three spellings of `paragraph_spacing`, and its `cache_control` is named for the header it lands in rather than the value it holds, which here is `max_age`. Its `text` and `nex` renderers differ only in heading decoration and in whether links are inlined or numbered — and the numbered form never writes the reference list its numbers point at, so there is one text renderer here rather than two. Tables, which both of its text renderers drop entirely, are rendered as aligned columns.
smolweb inherits five invented directives from its two renderer libraries: `{.card}`, `{.include}`, `![[ ]]`, `#[label](art.txt)` and MultiMarkdown attribute lists (`{: .center}`) — two incompatible brace grammars, with art alignment expressible two different ways in one line. None of it appeared in real content, so itsybitsy expresses the same capabilities with standard constructs instead. Parsing once rather than once per library also means the text formats gain setext headings and the WAP formats gain tables and ASCII art, none of which their original parser handled.
## Output formats
@ -109,6 +115,7 @@ Each format is a crate implementing one trait, compiled in behind a cargo featur
| `gemtext` | `text/gemini` | Unwrapped; Gemini and Spartan clients wrap for themselves |
| `xhtmlmp` | `application/vnd.wap.xhtml+xml` | Well-formed XML with the Mobile Profile doctype |
| `html` | `text/html` | The same markup without the XML declaration |
| `text` | `text/plain` | Wrapped to 80 columns, because Nex and Gopher clients do not wrap |
HTTP negotiates between them from `Accept`. Only a literal media type counts as a match, so a browser's `*/*` can never be read as willingness to receive a WAP format; `?format=<id>` overrides negotiation outright, and naming an id the listener does not serve is a bad request rather than a silent fallback.