- layout/logo.wo: the mark as inline SVG — dark tile, two-stroke "W" (white then accent blue). One source: nav brand + /favicon.svg - favicon/controller.wo: GET /favicon.svg, image/svg+xml, day cache - install/: GET /install — toolchain tarball, PATH, verify, first project, build/run, adding a dep. Copy from the real install README - packages/: GET /packages + /packages/:name — catalogue with the [deps] line, what each library gives you, and a usage snippet. Index cards are child components (multi Component) - wo-html: page_head(title, head, body) and a `head` slot on Layout — a favicon link or meta tag had nowhere else to go; page() passes "" - header: Install/Tutorial/Packages/GitHub, brand shows the mark - main.wo: SITE_HOST picks the interface (loopback default), bound address printed at startup - site-accept: 11 -> 17 checks (install, packages x2, 404, favicon, inline logo) Verified on 192.168.0.165:8080 — every route, favicon bytes, and the mark rasterised at 256px and 32px. Gates: oop-accept MET, site 17/0, web-app 46/0, fibers 10/0, db-actor 8/0; shop rebuilt clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| html.wo | ||
| README.md | ||
| wo.toml | ||
wo-html — server-rendered HTML as plain Text
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]
html = { git = "https://github.com/shoneyj/wo-html", 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 wo-html 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— the tutorial site:Layout+ aChapterNavcomponent reused on the homepage and every chapter page. Gated byjust site.docs/examples/shop— the program template: its ownAppShellcomponent, page components holding child components.