104 lines
5 KiB
Markdown
104 lines
5 KiB
Markdown
# 🕸 itsybitsy
|
|
|
|
[](LICENSE) [](https://ai-declaration.md)
|
|
|
|
Serve folders of Markdown to different domains over HTTP, [Spartan](https://portal.mozz.us/gemini/spartan.mozz.us/) and [Nex](https://nightfall.city/nex/info/specification.txt), rendered on the fly with no build step.
|
|
|
|
A Rust re-implementation of [smolweb](https://code.randogoth.com/randogoth/smolweb), which serves one folder from one process with its output formats hardcoded and its configuration in per-file frontmatter. itsybitsy changes all three: one process serves many domains, each output format is a separate crate behind a cargo feature, and configuration lives in a TOML file per content directory.
|
|
|
|
## Status
|
|
|
|
Early. Configuration, path resolution and Markdown parsing are implemented and tested; nothing is served yet.
|
|
|
|
| Area | State |
|
|
| --- | --- |
|
|
| Server configuration, virtual hosts, `--check` | implemented |
|
|
| Path resolution, traversal containment, live reload | implemented |
|
|
| Markdown parsing, includes, ASCII art, card breaks | implemented |
|
|
| gemtext and HTML output | planned |
|
|
| HTTP, Spartan and Nex listeners | planned |
|
|
| 80-column text and Nex output | planned |
|
|
| WML and XHTML-MP decks | planned |
|
|
| Gemini and Gopher | planned |
|
|
|
|
## Configuration
|
|
|
|
Two files. A server file names the virtual hosts and the listeners:
|
|
|
|
```toml
|
|
version = 1
|
|
|
|
[site.smol]
|
|
root = "/srv/smol/content"
|
|
hosts = ["smol.place", "www.smol.place"]
|
|
|
|
[listener.web]
|
|
protocol = "http"
|
|
bind = "0.0.0.0:8080"
|
|
formats = ["xhtmlmp", "html"]
|
|
default_site = "smol"
|
|
```
|
|
|
|
A `.itsybitsy.toml` in any content directory says how the Markdown in **that directory** renders. There is no inheritance: a subdirectory without its own file uses the built-in defaults rather than its parent's. Repetition in a deep tree is the price of never having to look elsewhere to know how a page renders.
|
|
|
|
```toml
|
|
[defaults]
|
|
cache_control = 3600
|
|
|
|
[page."index.md"]
|
|
title = "Notes"
|
|
cache_control = 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.
|
|
|
|
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.
|
|
|
|
`--check` validates the configuration and reports the routing it resolved, without binding a port:
|
|
|
|
```console
|
|
$ itsybitsy --config /etc/itsybitsy.toml --check
|
|
sites:
|
|
smol: /srv/smol/content
|
|
hosts:
|
|
smol.place -> smol
|
|
www.smol.place -> smol
|
|
listeners:
|
|
web: http on 0.0.0.0:8080 [xhtmlmp, html] by host, default smol
|
|
formats rendered per page: html, xhtmlmp
|
|
```
|
|
|
|
Everything the schema cannot express is checked here rather than on first request: a format no enabled feature provides, two sites claiming one hostname, a content root that does not exist, a server file sitting inside a content root where it would be served.
|
|
|
|
## Markdown
|
|
|
|
Markdown is parsed once per document into one representation that every output format reads, rather than once per format. Four directives sit outside CommonMark and are recognised before parsing, each occupying a whole line:
|
|
|
|
| Directive | Means |
|
|
| --- | --- |
|
|
| `{.include path}` or `![[path]]` | splice that file's lines in here |
|
|
| `#[label](art.txt)` | a verbatim block read from that file |
|
|
| `{.card Title}` | a divider for formats that paginate; nothing for the rest |
|
|
|
|
Include and art targets resolve relative to the including file and must stay inside the content root. Expansion is bounded on three axes — nesting depth, total lines, and total bytes — because a cycle check alone does not stop a long chain, and neither stops a diamond where two branches include the same file without ever repeating one on a single path.
|
|
|
|
## URLs
|
|
|
|
Links should be root-relative and extensionless (`[about](/about)`, not `about.md`), so the same link resolves identically from every protocol.
|
|
|
|
| Request | Serves |
|
|
| --- | --- |
|
|
| `/` | `index.md` |
|
|
| `/foo` | `foo.md`, else `foo/index.md` |
|
|
| `/foo.md` | redirects to `/foo` |
|
|
| `/img.png` | the file itself, by media type |
|
|
|
|
Nothing outside the content root is reachable. A request target is percent-decoded before it is normalised, so an encoded `..` becomes a real one and gets clamped at the root rather than quietly matching nothing; any path component beginning with a dot is refused outright; and whatever survives is canonicalised and required to still be inside the root, which is what defeats a symlink pointing out of it. There are no generated directory listings — only `index.md`.
|
|
|
|
## Development
|
|
|
|
```bash
|
|
devbox run check # fmt --check, clippy -D warnings, test
|
|
devbox run test
|
|
devbox run build
|
|
```
|