docs/content-model.md opens with "Engine specification". It is also where the rule lives that a leading underscore makes a file unaddressable — and the human who owns this site did not know that rule, because nothing in this repository is addressed to an author. Twelve documents named docs/ while being exclusively about building the parser is a signpost pointing at the wrong room. Naming the directory for its audience makes the gap visible instead of hiding it. docs/ is now reserved and deliberately absent: an empty docs/ is an honest statement that end-user documentation does not exist, where docs/ full of parser specs was a claim that it did. HARNESS.md stays at the root. Root holds the three entry points — README.md for a human, CLAUDE.md for an agent, HARNESS.md for whoever maintains the machine — and harness/README.md is the map of the directory, so moving the guide inside would have collided with it for nothing. Mechanical and wide: 100 path references across 24 files. Every verify.sh gate that names a doc by path, the directory lists the dangling-path and ADR-number gates scan, surface.sh's output target, the Makefile, CLAUDE.md's read order, the skill, four commands, and two Go package comments. A first pass with a shell loop silently edited only four files and the rest still said docs/; the fix was to write the file list out and check the remaining count was zero rather than trust the loop's exit status. No rule, threshold, gate or obligation moved — this is a rename, and the gates demonstrated it twice: they stayed green on the new paths, and the ADR-number gate caught ADR-0082 before the entry existed. Deferred, both on the human's call: the end-user documentation site itself, which wants its own decision about where it lives and whether its claims are gated; and moving examples/ under docs/, since demo-site is a live site root that verify.sh, the coverage test and make demo all point at, and moving it would couple a rename to a design nobody has made. 31 files, +146/-106. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
193 lines
7.9 KiB
Go
193 lines
7.9 KiB
Go
// The types in this file are the theme contract in Go: what a template receives, nothing about how it is
|
|
// produced. Split from render.go when that file crossed the size warning — the seam was already here, since
|
|
// harness/theme-contract.md describes exactly this and nothing else.
|
|
|
|
package render
|
|
|
|
import (
|
|
"html/template"
|
|
"io/fs"
|
|
"time"
|
|
|
|
"khosra/internal/content"
|
|
)
|
|
|
|
// head is what every kind of page shares: the document shell the base template needs. Absence is the
|
|
// zero value — a template reads what exists and never fails on a missing field (invariant 1).
|
|
type head struct {
|
|
// Title may be empty for a bundle; a listing always has one.
|
|
Title string
|
|
// Lang is the locale being served.
|
|
Lang string
|
|
// Canonical is the permalink of what was actually served, which differs from the URL requested when
|
|
// the fallback chain supplied another language (ADR-0009).
|
|
Canonical string
|
|
// Alternates lists every language this key exists in, for hreflang.
|
|
Alternates []Alternate
|
|
// Style is the reference theme's stylesheet, inlined so a bare site root needs no asset route.
|
|
Style template.CSS
|
|
// Site is what the site declared about itself in site.yaml (ADR-0039). Zero when it declared nothing.
|
|
Site content.Settings
|
|
// Sections are the site's sections, for navigation: only .Title and .URL are set. Empty until the engine is
|
|
// told where to find them, which cmd does at wiring time (ADR-0049).
|
|
Sections []Item
|
|
}
|
|
|
|
// Page is one bundle rendered.
|
|
type Page struct {
|
|
head
|
|
// Key is the bundle's identity, useful for building links.
|
|
Key string
|
|
// HTML is the rendered body.
|
|
HTML template.HTML
|
|
// Extra carries every frontmatter key the parser does not name (ADR-0002).
|
|
Extra map[string]any
|
|
// Sequence is the series this page sits in, nil when it sits in none.
|
|
Sequence *Sequence
|
|
// Tags are this bundle's own terms, each with the URL of its listing. Empty when it carries none.
|
|
Tags []Item
|
|
// ExtrasURL links this bundle's supporting files, empty when it has none — so a theme can offer them
|
|
// without guessing whether they exist (ADR-0047).
|
|
ExtrasURL string
|
|
// Assets is the theme's own markup for everything this page's shortcodes and `use:` asked for, already
|
|
// rendered and deduplicated. Emit it in `<head>`. Empty for a page that asked for nothing, which is
|
|
// nearly all of them (ADR-0079).
|
|
Assets template.HTML
|
|
// Styles and Scripts are URLs of this bundle's own CSS and JS, built by the engine because a theme must
|
|
// not construct an address. Empty unless frontmatter named files beside the body.
|
|
Styles, Scripts []string
|
|
}
|
|
|
|
// Sequence is a series as a page sees it: its members in reading order, and where this page is in them
|
|
// (ADR-0033).
|
|
type Sequence struct {
|
|
// Title is the series' title, empty when the landing page omits one; URL is its permalink.
|
|
Title, URL string
|
|
// Members are every entry in reading order — ascending, unlike a dated listing.
|
|
Members []Item
|
|
// Index is this page's 1-based position, zero when this page is the series landing itself. Count is
|
|
// how many members there are.
|
|
Index, Count int
|
|
// Prev and Next are the neighbours in reading order, nil at the ends and on the landing page. Prev is
|
|
// the *earlier* entry, the opposite sense of a listing's PrevURL.
|
|
Prev, Next *Item
|
|
// First and Last are the ends of the series, set whenever it has members.
|
|
First, Last *Item
|
|
}
|
|
|
|
// Extras is a bundle's supporting files, browsable (ADR-0047).
|
|
type Extras struct {
|
|
head
|
|
// Bundle is the page these files belong to, so a theme has its title and a way back.
|
|
Bundle Item
|
|
// Entries is the whole tree, sorted by path.
|
|
Entries []content.Entry
|
|
// Selected is the entry the URL named, nil on the bare listing.
|
|
Selected *Selected
|
|
}
|
|
|
|
// Selected is the entry a request picked out of the tree.
|
|
type Selected struct {
|
|
content.Entry
|
|
// HTML is the rendered file for a markdown or text entry, empty for anything else.
|
|
HTML template.HTML
|
|
// RawURL always works: it is how a theme embeds an image, links a PDF, or offers a download.
|
|
RawURL string
|
|
}
|
|
|
|
// List is a collection page: the result of a Query, one page of it.
|
|
type List struct {
|
|
head
|
|
// Items are the entries on this page, in the Query's order.
|
|
Items []Item
|
|
// Page is 1-based; Pages is the total, at least 1 even when empty.
|
|
Page, Pages int
|
|
// PrevURL and NextURL are empty at the ends. Newer is "prev" because the order is newest first.
|
|
PrevURL, NextURL string
|
|
// Groups is the same entries partitioned by section, set alongside Items for a tag listing. Both shapes
|
|
// are offered because which one to show is the theme's decision, not the engine's (ADR-0046).
|
|
Groups []Group
|
|
}
|
|
|
|
// Group is a named run of entries within a listing.
|
|
type Group struct {
|
|
Name string
|
|
Items []Item
|
|
}
|
|
|
|
// Item is one entry in a listing.
|
|
type Item struct {
|
|
Title string
|
|
Key string
|
|
URL string
|
|
Date time.Time
|
|
// Section is the entry's top-level section, so a flat listing can label where an entry came from without
|
|
// the engine deciding that it must be grouped (ADR-0046).
|
|
Section string
|
|
}
|
|
|
|
// Alternate is one language a bundle exists in.
|
|
type Alternate struct {
|
|
Lang string
|
|
// URL is absolute when the site declares a base, because hreflang is read by machines that resolve nothing
|
|
// against the page.
|
|
URL string
|
|
// Path is the same address, root-relative — what a *visible* language link wants. Found by looking at a
|
|
// rendered page: using URL sent a reader from a local server to the canonical host (ADR-0049).
|
|
Path string
|
|
}
|
|
|
|
// Partial renders a named fragment. A feature under internal/ext is handed one of these at wiring time,
|
|
// because markup belongs to the theme and a feature must not write any (ADR-0036).
|
|
type Partial func(name string, data Fragment) ([]byte, error)
|
|
|
|
// Fragment is what a fragment template receives (ADR-0037, widened by ADR-0042).
|
|
type Fragment struct {
|
|
// Args are the call's key="value" pairs, exactly as written. Escaping is the template's.
|
|
Args map[string]string
|
|
// Pictures are what the engine gathered rather than the author wrote: one for a figure, many for a
|
|
// gallery, none when the call names nothing a picture. Kept apart from Args so a supplied value can never
|
|
// be mistaken for an authored one.
|
|
Pictures []Picture
|
|
// Lang is the language being served, so a fragment can localise its own words through `t` (ADR-0067).
|
|
Lang string
|
|
// Body is a container call's content, already rendered. Empty for a leaf call (ADR-0064).
|
|
Body template.HTML
|
|
// Headings are the document's headings, for a call that builds a table of contents (ADR-0065).
|
|
Headings []Heading
|
|
}
|
|
|
|
// Heading is one heading in the document, with the id an anchor links to (ADR-0065).
|
|
type Heading struct {
|
|
Level int
|
|
Text, ID string
|
|
}
|
|
|
|
// Picture is one image a fragment can render (ADR-0042).
|
|
type Picture struct {
|
|
// Src is the author's own file, relative to the bundle. A browser that ignores Srcset still gets the
|
|
// picture that was put there.
|
|
Src string
|
|
// Srcset offers the derivatives, closed by the original at its own width; empty when the picture is
|
|
// already small enough that no derivative was worth making.
|
|
Srcset string
|
|
// Width and Height are the original's intrinsic size, so a page can reserve the box before the bytes
|
|
// arrive. Zero when the file could not be read.
|
|
Width, Height int
|
|
}
|
|
|
|
// Origin tells a feature which bundle is being rendered, so a path in a call can resolve relative to it.
|
|
//
|
|
// Features read it from the parser context with OriginFrom. It carries the site's fs.FS rather than a
|
|
// directory name alone, because every read goes through the rooted filesystem and never a joined path
|
|
// (ADR-0031).
|
|
type Origin struct {
|
|
// Dir is the bundle's directory, relative to the site root: "content/comics/the-long-monsoon".
|
|
Dir string
|
|
// Files is the site root. Nil when the renderer was built without one, in which case a feature that
|
|
// needs files degrades rather than guessing.
|
|
Files fs.FS
|
|
// Lang is the language of the variant being rendered, so a feature can hand it to a fragment (ADR-0067).
|
|
Lang string
|
|
}
|