fix(shop): views markup-first within today's grammar
- render() bodies: one HTML line per 'h = h ..' statement, single-
quoted attributes, ${} holes, esc() on data — el() chains gone
- discovered + recorded gap #3: no multi-line expressions or literals
(leading/trailing .. and paren grouping all reject at NEWLINE) —
exactly the tax story 37's raw literal deletes
- 37-target comment blocks dropped (bodies now self-explanatory; the
README states the delta); rebuilt + full buy-flow re-smoked (178.0
total, stock 12->10, 409, traversal 404)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
5c48f4ba77
commit
e2511fc461
7 changed files with 97 additions and 159 deletions
|
|
@ -29,7 +29,7 @@ it and restart: the orders are still there (WAL replay).
|
|||
| `product_page/`, `orders/` | one view module per feature + its root controller file | feature folders |
|
||||
| `static_files/controller.wo` | `/assets/*` from disk, traversal-safe, typed | `angular.json` assets |
|
||||
| `assets/style.css` | ONE real stylesheet, sectioned per feature | the `.scss` files |
|
||||
| the `-- 37 target:` block above each `render()` | STORY 37's TARGET — an in-class raw template literal ({{ }} auto-escaped + type-checked, {!! !!} raw slots) replacing the hand-built body below it | Vue `<template>` (in-SFC) |
|
||||
| the `render()` bodies | markup-first TODAY: one HTML line per `h = h ..` statement, `${}` holes; story 37 collapses each body to ONE raw literal with `{{ }}` auto-escaped typed holes | Vue `<template>` (in-SFC) |
|
||||
| `main.wo` | bootstrap: seed, routes, serve — nothing else | `app-routing.module.ts` + `main.ts` |
|
||||
|
||||
Separation is compiler-enforced where the language allows it today:
|
||||
|
|
@ -39,16 +39,17 @@ sit in the root module beside `types.wo`, because of gap #1 below.
|
|||
|
||||
## What is deliberately different (doctrine)
|
||||
|
||||
- **Templates compile or they don't exist (story 37).** The target is
|
||||
an IN-CLASS raw template literal: `render()` returns one multi-line
|
||||
literal whose `{{ expr }}` holes are auto-escaped, compile-time-
|
||||
checked interpolations (typo'd field = compile error); `{!! !!}` is
|
||||
the raw slot for prebuilt fragments; structural control stays the
|
||||
language's `if`/`for` composing literals. Every `render()` here
|
||||
carries that target as the `-- 37 target:` comment above it — the
|
||||
body below is today's hand-lowering. NO template engine runs at
|
||||
request time — ever. Styles stay a real CSS file, served statically
|
||||
(there is no scss preprocessor).
|
||||
- **Templates compile or they don't exist (story 37).** The views are
|
||||
markup-first within today's language: one HTML line per `h = h ..`
|
||||
statement (single-quoted attributes — no escape noise), `${}` holes,
|
||||
data through `esc()`. Story 37's raw template literal removes exactly
|
||||
the two taxes you can see: the `h = h ..` prefix on every line (the
|
||||
language has NO multi-line expression or literal — recorded gap #3,
|
||||
proven while writing this file) and the explicit `esc()` calls
|
||||
(`{{ }}` auto-escapes and type-checks; `{!! !!}` is the raw slot).
|
||||
Same markup, less ceremony. NO template engine runs at request time —
|
||||
ever. Styles stay a real CSS file, served statically (there is no
|
||||
scss preprocessor).
|
||||
- **No closures, no DI.** A view is a class with fields + `render()`;
|
||||
a controller is a class satisfying `Handler`. Capture = a field.
|
||||
- **No sessions/cart yet.** Buying is per-product (qty → order). A cart
|
||||
|
|
@ -71,7 +72,7 @@ sit in the root module beside `types.wo`, because of gap #1 below.
|
|||
1. `types.wo` — the entire persistence layer is 20 lines.
|
||||
2. `orders.controller.wo` — the whole buying flow (validate, stock
|
||||
check, decrement, durable insert, render) with no framework magic.
|
||||
3. `orders/view.wo` — each `-- 37 target:` literal vs the hand-built
|
||||
body under it. The pair is the referendum: the literal is what
|
||||
writing a view will feel like, the body is what it costs today.
|
||||
3. `orders/view.wo` — the referendum: is HTML-per-line with `${}`
|
||||
holes readable enough today, and is 37's delta (delete every
|
||||
`h = h ..` prefix and `esc()` call) worth a compiler slice?
|
||||
4. `main.wo` — the app at a glance: five routes, one middleware, serve.
|
||||
|
|
|
|||
|
|
@ -1,36 +1,22 @@
|
|||
-- layout/app.wo — the app shell (app.html's analog): one full document
|
||||
-- wrapping every page with the shared header and footer. Styles are NOT
|
||||
-- inlined here — the shop links /assets/style.css, a real stylesheet
|
||||
-- served by the static_files controller, so view markup and styling
|
||||
-- stay separate files exactly as the template promises.
|
||||
-- inlined — pages link /assets/style.css (served by static_files), so
|
||||
-- markup and styling stay separate files.
|
||||
use framework/http
|
||||
use html
|
||||
|
||||
-- 37 target:
|
||||
-- return `
|
||||
-- <!doctype html><html>
|
||||
-- <head>
|
||||
-- <meta charset="utf-8">
|
||||
-- <meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
-- <title>{{ title }}</title>
|
||||
-- <link rel="stylesheet" href="/assets/style.css">
|
||||
-- </head>
|
||||
-- <body>
|
||||
-- {!! header() !!}
|
||||
-- <main class="site-main">{!! content !!}</main>
|
||||
-- {!! footer() !!}
|
||||
-- </body></html>`;
|
||||
pub fn app_shell(title: Text, content: Text) -> Text {
|
||||
let d = "<!doctype html><html><head><meta charset=\"utf-8\">";
|
||||
d = d .. "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">";
|
||||
d = d .. "<title>${esc(title)}</title>";
|
||||
d = d .. "<link rel=\"stylesheet\" href=\"/assets/style.css\">";
|
||||
d = d .. "</head><body>";
|
||||
d = d .. header();
|
||||
d = d .. el("main", "site-main", content);
|
||||
d = d .. footer();
|
||||
d = d .. "</body></html>";
|
||||
return d;
|
||||
let h = "<!doctype html><html><head>";
|
||||
h = h .. "<meta charset='utf-8'>";
|
||||
h = h .. "<meta name='viewport' content='width=device-width,initial-scale=1'>";
|
||||
h = h .. "<title>${esc(title)}</title>";
|
||||
h = h .. "<link rel='stylesheet' href='/assets/style.css'>";
|
||||
h = h .. "</head><body>";
|
||||
h = h .. header();
|
||||
h = h .. "<main class='site-main'>${content}</main>";
|
||||
h = h .. footer();
|
||||
h = h .. "</body></html>";
|
||||
return h;
|
||||
}
|
||||
|
||||
-- The one transport helper every controller shares: a 200 HTML Resp.
|
||||
|
|
@ -41,8 +27,10 @@ pub fn ok_html(body: Text) -> Resp {
|
|||
}
|
||||
|
||||
pub fn html_error(status: Int, title: Text, msg: Text) -> Resp {
|
||||
let content = el("h1", "", esc(title)) .. el("p", "", esc(msg)) .. el("p", "", link("/", "", "Back to products"));
|
||||
let c = "<h1>${esc(title)}</h1>";
|
||||
c = c .. "<p>${esc(msg)}</p>";
|
||||
c = c .. "<p><a href='/'>Back to products</a></p>";
|
||||
let h: map<Text, Text> = {};
|
||||
h["content-type"] = "text/html; charset=utf-8";
|
||||
return Resp { status: status, headers: h, body: app_shell("shop — ${title}", content) };
|
||||
return Resp { status: status, headers: h, body: app_shell("shop — ${title}", c) };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
-- layout/footer.wo — the footer fragment (footer.html's analog).
|
||||
use html
|
||||
|
||||
-- 37 target:
|
||||
-- return `<footer class="site-footer">writeonce shop — one binary: server, database, these pages.</footer>`;
|
||||
pub fn footer() -> Text {
|
||||
return el("footer", "site-footer", "writeonce shop — one binary: server, database, these pages.");
|
||||
return "<footer class='site-footer'>writeonce shop — one binary: server, database, these pages.</footer>";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,8 @@
|
|||
-- layout/header.wo — the header fragment (header.html's analog).
|
||||
use html
|
||||
|
||||
-- 37 target:
|
||||
-- return `
|
||||
-- <header class="site-header">
|
||||
-- <a href="/" class="brand">writeonce shop</a>
|
||||
-- <nav class="site-nav"><a href="/">Products</a> <a href="/orders">Orders</a></nav>
|
||||
-- </header>`;
|
||||
pub fn header() -> Text {
|
||||
let brand = link("/", "brand", "writeonce shop");
|
||||
let nav = link("/", "", "Products") .. link("/orders", "", "Orders");
|
||||
return el("header", "site-header", brand .. el("nav", "site-nav", nav));
|
||||
let h = "<header class='site-header'>";
|
||||
h = h .. "<a href='/' class='brand'>writeonce shop</a>";
|
||||
h = h .. "<nav class='site-nav'><a href='/'>Products</a> <a href='/orders'>Orders</a></nav>";
|
||||
h = h .. "</header>";
|
||||
return h;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
-- orders/view.wo — the confirmation page and the orders table. The
|
||||
-- comment above each render() is the STORY-37 TARGET (raw template
|
||||
-- literal, {{ }} auto-escaped); the body is today's hand-lowering.
|
||||
-- orders/view.wo — the confirmation page and the orders table.
|
||||
-- Markup-first, one HTML line per statement; see product_list/view.wo
|
||||
-- for the convention and the story-37 endgame.
|
||||
use html
|
||||
|
||||
pub class OrderOk {
|
||||
|
|
@ -8,18 +8,13 @@ pub class OrderOk {
|
|||
qty: Int
|
||||
total: Float
|
||||
|
||||
-- 37 target:
|
||||
-- return `
|
||||
-- <div class="card">
|
||||
-- <h1>Order placed</h1>
|
||||
-- <p>{{ self.qty }} × {{ self.name }} — total € {{ self.total }}</p>
|
||||
-- <p><a href="/orders">See all orders</a> · <a href="/">Keep shopping</a></p>
|
||||
-- </div>`;
|
||||
fn render() -> Text {
|
||||
let head = el("h1", "", "Order placed");
|
||||
let what = el("p", "", "${self.qty} × ${esc(self.name)} — total € ${self.total}");
|
||||
let links = el("p", "", link("/orders", "", "See all orders") .. " · " .. link("/", "", "Keep shopping"));
|
||||
return el("div", "card", head .. what .. links);
|
||||
let h = "<div class='card'>";
|
||||
h = h .. "<h1>Order placed</h1>";
|
||||
h = h .. "<p>${self.qty} × ${esc(self.name)} — total € ${self.total}</p>";
|
||||
h = h .. "<p><a href='/orders'>See all orders</a> · <a href='/'>Keep shopping</a></p>";
|
||||
h = h .. "</div>";
|
||||
return h;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -29,44 +24,30 @@ pub class OrderRow {
|
|||
total: Float
|
||||
status: Text
|
||||
|
||||
-- 37 target:
|
||||
-- return `
|
||||
-- <tr>
|
||||
-- <td>{{ self.name }}</td>
|
||||
-- <td>{{ self.qty }}</td>
|
||||
-- <td>€ {{ self.total }}</td>
|
||||
-- <td>{{ self.status }}</td>
|
||||
-- </tr>`;
|
||||
fn render() -> Text {
|
||||
let cells = el("td", "", esc(self.name));
|
||||
cells = cells .. el("td", "", "${self.qty}");
|
||||
cells = cells .. el("td", "", "€ ${self.total}");
|
||||
cells = cells .. el("td", "", esc(self.status));
|
||||
return el("tr", "", cells);
|
||||
let h = "<tr>";
|
||||
h = h .. "<td>${esc(self.name)}</td>";
|
||||
h = h .. "<td>${self.qty}</td>";
|
||||
h = h .. "<td>€ ${self.total}</td>";
|
||||
h = h .. "<td>${esc(self.status)}</td>";
|
||||
h = h .. "</tr>";
|
||||
return h;
|
||||
}
|
||||
}
|
||||
|
||||
pub class OrdersPage {
|
||||
rows: Text
|
||||
|
||||
-- 37 target:
|
||||
-- if self.rows == "" {
|
||||
-- return `<h1>Orders</h1>
|
||||
-- <p>No orders yet — <a href="/">go buy something</a>.</p>`;
|
||||
-- }
|
||||
-- return `
|
||||
-- <h1>Orders</h1>
|
||||
-- <table class="orders">
|
||||
-- <tr><th>Product</th><th>Qty</th><th>Total</th><th>Status</th></tr>
|
||||
-- {!! self.rows !!}
|
||||
-- </table>`;
|
||||
fn render() -> Text {
|
||||
let head = el("h1", "", "Orders");
|
||||
let h = "<h1>Orders</h1>";
|
||||
if self.rows == "" {
|
||||
return head .. el("p", "", "No orders yet — " .. link("/", "", "go buy something") .. ".");
|
||||
h = h .. "<p>No orders yet — <a href='/'>go buy something</a>.</p>";
|
||||
return h;
|
||||
}
|
||||
let thead = el("tr", "", el("th", "", "Product") .. el("th", "", "Qty") .. el("th", "", "Total") .. el("th", "", "Status"));
|
||||
let table = el("table", "orders", thead .. self.rows);
|
||||
return head .. table;
|
||||
h = h .. "<table class='orders'>";
|
||||
h = h .. "<tr><th>Product</th><th>Qty</th><th>Total</th><th>Status</th></tr>";
|
||||
h = h .. "${self.rows}";
|
||||
h = h .. "</table>";
|
||||
return h;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
-- product_list/view.wo — the VIEW. Classes with `fn render() -> Text`:
|
||||
-- fields are exactly the values displayed. No queries here — the
|
||||
-- controller fills the fields.
|
||||
-- product_list/view.wo — the VIEW. Classes with `fn render() -> Text`;
|
||||
-- fields are exactly the values displayed, bodies are MARKUP-FIRST: one
|
||||
-- HTML line per statement (single-quoted attributes — no escape noise),
|
||||
-- ${} holes, data through esc().
|
||||
--
|
||||
-- Each render() carries its STORY-37 TARGET above it: a raw multi-line
|
||||
-- template literal whose {{ expr }} holes are auto-escaped, compile-
|
||||
-- time-checked interpolations (a typo'd field = compile error). The
|
||||
-- body below is today's hand-lowering of exactly that literal.
|
||||
-- The `h = h ..` prefix on every line is the language's cost today:
|
||||
-- statements are single-line and there is no multi-line literal. Story
|
||||
-- 37's raw template literal deletes exactly that prefix and the esc()
|
||||
-- calls ({{ }} auto-escapes and type-checks) — the markup stays as-is.
|
||||
use html
|
||||
|
||||
pub class ProductCard {
|
||||
|
|
@ -14,36 +15,26 @@ pub class ProductCard {
|
|||
price: Float
|
||||
stock: Int
|
||||
|
||||
-- 37 target:
|
||||
-- let tail = `<p class="stock">{{ self.stock }} in stock</p>`;
|
||||
-- if self.stock == 0 { tail = `<p class="stock out">sold out</p>`; }
|
||||
-- return `
|
||||
-- <div class="card">
|
||||
-- <h3><a href="/p/{{ self.sku }}">{{ self.name }}</a></h3>
|
||||
-- <p class="price">€ {{ self.price }}</p>
|
||||
-- {!! tail !!}
|
||||
-- </div>`;
|
||||
fn render() -> Text {
|
||||
let h = el("h3", "", link("/p/${esc(self.sku)}", "", esc(self.name)));
|
||||
let price = el("p", "price", "€ ${self.price}");
|
||||
let stock = el("p", "stock", "${self.stock} in stock");
|
||||
if self.stock == 0 {
|
||||
stock = el("p", "stock out", "sold out");
|
||||
let h = "<div class='card'>";
|
||||
h = h .. "<h3><a href='/p/${esc(self.sku)}'>${esc(self.name)}</a></h3>";
|
||||
h = h .. "<p class='price'>€ ${self.price}</p>";
|
||||
if self.stock > 0 {
|
||||
h = h .. "<p class='stock'>${self.stock} in stock</p>";
|
||||
} else {
|
||||
h = h .. "<p class='stock out'>sold out</p>";
|
||||
}
|
||||
return el("div", "card", h .. price .. stock);
|
||||
h = h .. "</div>";
|
||||
return h;
|
||||
}
|
||||
}
|
||||
|
||||
pub class ProductListPage {
|
||||
cards: Text
|
||||
|
||||
-- 37 target:
|
||||
-- return `
|
||||
-- <h1>Products</h1>
|
||||
-- <div class="grid">{!! self.cards !!}</div>`;
|
||||
fn render() -> Text {
|
||||
let head = el("h1", "", "Products");
|
||||
let grid = el("div", "grid", self.cards);
|
||||
return head .. grid;
|
||||
let h = "<h1>Products</h1>";
|
||||
h = h .. "<div class='grid'>${self.cards}</div>";
|
||||
return h;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
-- product_page/view.wo — the product detail view with the order form.
|
||||
-- The comment above render() is the STORY-37 TARGET (raw template
|
||||
-- literal, {{ }} auto-escaped); the body is today's hand-lowering.
|
||||
-- Markup-first, one HTML line per statement; see product_list/view.wo
|
||||
-- for the convention and the story-37 endgame.
|
||||
use html
|
||||
|
||||
pub class ProductPage {
|
||||
|
|
@ -9,35 +9,22 @@ pub class ProductPage {
|
|||
price: Float
|
||||
stock: Int
|
||||
|
||||
-- 37 target:
|
||||
-- let action = `
|
||||
-- <form method="POST" action="/orders/{{ self.sku }}">
|
||||
-- <label>Quantity</label>
|
||||
-- <input type="text" name="qty" value="1" class="field">
|
||||
-- <button type="submit" class="btn">Buy</button>
|
||||
-- </form>`;
|
||||
-- if self.stock == 0 { action = `<p class="stock out">sold out</p>`; }
|
||||
-- return `
|
||||
-- <div class="card detail">
|
||||
-- <h1>{{ self.name }}</h1>
|
||||
-- <p class="price">€ {{ self.price }}</p>
|
||||
-- <p class="stock">{{ self.stock }} in stock</p>
|
||||
-- {!! action !!}
|
||||
-- </div>
|
||||
-- <p><a href="/">← all products</a></p>`;
|
||||
fn render() -> Text {
|
||||
let head = el("h1", "", esc(self.name));
|
||||
let price = el("p", "price", "€ ${self.price}");
|
||||
let stock = el("p", "stock", "${self.stock} in stock");
|
||||
let form = "";
|
||||
let h = "<div class='card detail'>";
|
||||
h = h .. "<h1>${esc(self.name)}</h1>";
|
||||
h = h .. "<p class='price'>€ ${self.price}</p>";
|
||||
h = h .. "<p class='stock'>${self.stock} in stock</p>";
|
||||
if self.stock > 0 {
|
||||
let qty = text_input("qty", "1");
|
||||
let buy = submit_btn("Buy");
|
||||
form = form_post("/orders/${esc(self.sku)}", el("label", "", "Quantity") .. qty .. buy);
|
||||
h = h .. "<form method='POST' action='/orders/${esc(self.sku)}'>";
|
||||
h = h .. "<label>Quantity</label>";
|
||||
h = h .. "<input type='text' name='qty' value='1' class='field'>";
|
||||
h = h .. "<button type='submit' class='btn'>Buy</button>";
|
||||
h = h .. "</form>";
|
||||
} else {
|
||||
form = el("p", "stock out", "sold out");
|
||||
h = h .. "<p class='stock out'>sold out</p>";
|
||||
}
|
||||
let back = el("p", "", link("/", "", "← all products"));
|
||||
return el("div", "card detail", head .. price .. stock .. form) .. back;
|
||||
h = h .. "</div>";
|
||||
h = h .. "<p><a href='/'>← all products</a></p>";
|
||||
return h;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue