- rename the two libraries: writeonce-framework -> writeonce-serve
(`use serve`), wo-html -> writeonce-view (`use view`). Names say the
ROLE now; every sample, script, gate and live doc follows
- stories/specs/plans keep the old names: they are dated records, and
both library READMEs carry a "renamed 2026-08-25" note
- serve/http/files.wo: StaticFiles { dir, max_bytes } — traversal
refused not normalised, extension content types, attachment
disposition for archives. Lifted out of the shop, which had said in
a comment that it belonged in the framework
- shop drops its private copy and mounts the framework's
- site: /dl/*path over $WO_DIST (default ./dist), 16 MiB ceiling
- /install gains supported systems — Linux x86-64, glibc >= 2.38,
not musl — read off `file` and the binaries' GLIBC_ symbol
versions, not off a wish list; plus GitHub release as primary,
/dl as mirror, and the sha256 verify step
- site-accept: 17 -> 21 checks (supported systems, gzip download with
a binary-safe probe, checksum, /dl traversal 404)
Verified on 192.168.0.165: the real 960,820-byte tarball downloads
as application/gzip and its sha256 matches the published digest.
Gates: oop-accept MET, site 21/0, web-app 46/0, fibers 10/0,
db-actor 8/0; shop rebuilt and its /assets served by the framework.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
108 lines
4.6 KiB
Markdown
108 lines
4.6 KiB
Markdown
# writeonce-view — server-rendered HTML as plain Text
|
|
> Renamed 2026-08-25: this library was `wo-html`, imported as `use html`.
|
|
> Stories, specs and plans dated before that still say the old name — they
|
|
> are dated records and were left as written.
|
|
|
|
A view library, not a framework and not a template engine. Everything in
|
|
it is a pure function or a class with a `render()`; nothing here opens a
|
|
socket, reads a file, or touches the database.
|
|
|
|
```
|
|
[deps]
|
|
view = { git = "https://github.com/shoneyj/writeonce-view", rev = "v0.1.0" }
|
|
```
|
|
|
|
## The four layers
|
|
|
|
| layer | what it is |
|
|
| --- | --- |
|
|
| `esc()` | HTML-escape `& < > "`. A `{{ }}` hole in a raw text literal compiles to a call to this, so display data is escaped by construction; calling it by hand is the fallback, not the norm |
|
|
| `el()`, `link()`, `card()`, `form_post()`, … | element builders — `el("h1", "text-3xl font-bold", t)` |
|
|
| `Component` / `Layout` / `render_all()` | the view unit and its composition |
|
|
| `tw_css()` / `page()` | a hand-written Tailwind-style utility sheet, inlined into one self-contained document — no CDN, no build step, no JS |
|
|
|
|
## Components
|
|
|
|
A component is a class with fields and `fn render() -> Text`. Nothing
|
|
declares that it implements `Component` — satisfaction is **structural**,
|
|
exactly like the framework's `Handler`. Its fields ARE its inputs; the
|
|
language has no closures, so a field is the capture.
|
|
|
|
```
|
|
class ProductCard {
|
|
sku: Text
|
|
name: Text
|
|
fn render() -> Text {
|
|
return `
|
|
<div class="card">
|
|
<h3><a href="/p/{{ self.sku }}">{{ self.name }}</a></h3>
|
|
</div>`;
|
|
}
|
|
}
|
|
```
|
|
|
|
Composition is nesting — a parent holds children and calls their render:
|
|
|
|
```
|
|
pub class ProductListPage {
|
|
cards: multi Component
|
|
fn render() -> Text {
|
|
return `<div class="grid">${render_all(self.cards)}</div>`;
|
|
}
|
|
}
|
|
```
|
|
|
|
`multi Component` holds a heterogeneous list **directly**; no wrapper
|
|
record is needed (the framework's `Mw`/`Aw` wrappers are not a language
|
|
requirement). `render_all(cs)` renders children in order.
|
|
|
|
`Layout { title, head, nav, content, footer }` is content projection —
|
|
Angular's `<ng-content>` with the slots as ordinary pre-rendered `Text`.
|
|
The caller passes `child.render()`, a raw literal, or a builder's output;
|
|
the layout never learns which, which is precisely why it never needs the
|
|
child's type. A page wanting a fixed-width column wraps its content
|
|
before handing it over — deliberately no container knob here. `head` is
|
|
the one slot that is not body markup: a favicon link or a meta tag has
|
|
nowhere else to go, and `""` is the ordinary value (`page()` passes it
|
|
for you).
|
|
|
|
`Layout` renders through `page()`, so it inlines the utility sheet. An
|
|
app that links a real stylesheet instead writes its own two-slot shell
|
|
component — `docs/examples/shop/layout/app.wo` is that case, and it is a
|
|
component like any other.
|
|
|
|
## The MVC seam
|
|
|
|
| | where it lives |
|
|
| --- | --- |
|
|
| **Model** | `@table` rows, queried in the HANDLER. This library contains no `from … select` anywhere and must not grow one |
|
|
| **View** | components: fields in, Text out. No hidden state, no globals — a page is byte-deterministic from its fields |
|
|
| **Controller** | the framework's `Handler`: it queries, fills the component's fields, and answers `ok_html(c.render())` |
|
|
|
|
`ok_html` is the **framework's** (`framework/http`, beside `ok_text` and
|
|
`ok_json`): a status line plus a content-type is transport, not
|
|
rendering, so writeonce-view never learns what a `Resp` is.
|
|
|
|
The seam is what makes a view testable without a server and a query
|
|
testable without markup. Breaking it looks like one convenience — a
|
|
component that queries "just this once" — and costs both.
|
|
|
|
## Deliberately absent
|
|
|
|
Client-side anything (change detection, event bindings, two-way binding,
|
|
SPA routing, hydration): the no-JS posture stands, and interactivity is
|
|
form round trips. A runtime template engine: reflection-free means an
|
|
untyped `map<Text, Text>`, so templates compile to code at build time or
|
|
they do not exist. Dependency injection: components are data-in,
|
|
Text-out. Structural directives (`w:if` / `w:for`): the language's own
|
|
`if` and `for` compose literals, and no sample has yet proven the need
|
|
for a second control-flow dialect. Scoped CSS: the utility sheet stays
|
|
one static string until the pain is measured.
|
|
|
|
## Consumers
|
|
|
|
- [`docs/examples/site`](../site) — the tutorial site: `Layout` +
|
|
a `ChapterNav` component reused on the homepage and every chapter page.
|
|
Gated by `just site`.
|
|
- [`docs/examples/shop`](../shop/README.md) — the program template: its
|
|
own `AppShell` component, page components holding child components.
|