Files
bdeshiandClaude Opus 5 30c26bd1ac resolve relative links against the disk, serve them as addresses
Item 2 of the order of work. An author writes `../day-01.en.md` — the path an
editor preview resolves — and the engine emits `/posts/day-01/`.

The larger effect is durability. Resolution goes through key → route, and a slug
moves the route while never moving the key (ADR-0035), so a relative link survives
a rename that a hand-written /posts/a-better-name/ does not. The demo proves it:
`../renamed-thing.en.md` renders as href="/posts/a-better-name/" — the author wrote
the filename and got the slugged address.

This is the engine altering authored markup, which ADR-0045 polices, so the test
that matters is what it declines to touch. Fourteen cases must survive exactly as
written: an absolute URL, a scheme-relative URL, mailto:, tel:, a root-relative
path, a bare fragment, a bare query, a name climbing out of content/, and every
relative path whose extension is not .md. That last line is what keeps cover.jpg
working — a bundle's assets already resolve because its URL mirrors its directory,
so rewriting them would break what works. Nine rewrite cases sit beside them.

Key derivation goes through content.KeyFromName, exported for this: the
language-suffix rule is the part that would drift between two copies, so it lives
in one place while the five lines of joining are duplicated in check.

khosra check now reports a relative .md link resolving to no bundle, as fatal —
verified by mistyping one and watching exit 1. Only the .md form: an extensionless
relative path may be an asset, and a checker that calls a working link broken gets
ignored wholesale.

Two debts this change paid rather than deferred.

render.go reached the file-length advisory, so theme parsing moved to theme.go —
414 and 105 lines, one topic each, since parsing runs per rebuild and rendering
runs per request. Not a _helpers.go shard.

And the demo's coverage test bound its renderer with a *copy* of the rebuilder's
wiring, so it missed this feature entirely while the real binary served it
correctly. Navigation had already drifted the same way. Both now call one bind(),
which is exactly what ADR-0072 was written about — and the test failing is the only
reason the copy was found.

Extensions 7 → 8. Core 3020 → 3049 of 3400: the seam is ~20 lines, the feature is
in ext where it belongs.

18 files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 16:44:48 +06:00

26 KiB

Surface

Generated by scripts/surface.sh — do not edit. Regenerate with make surface.

Every top-level declaration in the engine, with its line. Read this before opening a source file: it answers "where does X live" and "what is in this package" without the bodies. What each file is for lives in state.md; why it is that way lives in decisions.md.

cmd/khosra — 414 lines + 348 test

check.go 45 · main.go 225 · new.go 42 · wire.go 102

  • check.go:16 func runCheck(args []string)
  • main.go:24 func main()
  • main.go:45 func runServe()
  • main.go:101 func opened(dir, base string) (fs.FS, content.Settings)
  • main.go:122 func pollInterval(dev bool, chosen time.Duration) time.Duration
  • main.go:138 func watching(fsys fs.FS, every time.Duration, rebuild func() int)
  • main.go:150 func rebuilder(fsys fs.FS, settings content.Settings, cache string, reveal bool,
  • main.go:186 func defaultCache() string
  • main.go:196 func fatal(msg string, err error)
  • main.go:210 func logging(level, format string) error
  • new.go:12 func runNew(args []string)
  • wire.go:23 func theme(siteFS fs.FS, settings content.Settings) (*render.Renderer, error)
  • wire.go:36 func extenders(partial render.Partial) []goldmark.Extender
  • wire.go:57 func bind(r *render.Renderer, site *content.Site)
  • wire.go:82 func routes(siteFS fs.FS, settings content.Settings, site func() *content.Site) map[string]http.Handler
  • wire.go:93 func claim(out map[string]http.Handler, feature string, from map[string]http.Handler)

internal/content — 1083 lines + 598 test

clock.go 12 · content.go 490 · doc.go 5 · extras.go 92 · settings.go 59 · site.go 425

  • clock.go:9 var now = time.Now
  • clock.go:12 func Now() time.Time { return now() }
  • content.go:20 const DefaultLang = "en"
  • content.go:27 type Bundle struct
  • content.go:77 func OpenSite(dir string) (fs.FS, error)
  • content.go:89 type Problem struct
  • content.go:103 func Scan(fsys fs.FS) ([]Bundle, error)
  • content.go:112 func ScanReport(fsys fs.FS) ([]Bundle, []Problem, error)
  • content.go:155 func KeyFromName(name string) (key, lang string, ok bool) { return splitName(name) }
  • content.go:158 func Parse(name string, data []byte) (Bundle, error)
  • content.go:205 func (b Bundle) Published(at time.Time) bool
  • content.go:214 func (b Bundle) Assets() (string, bool)
  • content.go:224 func stringList(v any) []string
  • content.go:246 func asTime(v any) time.Time
  • content.go:262 func asInt(v any) int
  • content.go:280 func bundleFiles(v any, where string) []string
  • content.go:292 func terms(v any) []string
  • content.go:316 func TagSlug(tag string) string
  • content.go:325 func Normalise(s string) string { return norm.NFC.String(s) }
  • content.go:331 func splitName(name string) (key, lang string, ok bool)
  • content.go:353 func isLangTag(s string) bool
  • content.go:371 func isPartial(base string) bool
  • content.go:383 func skipDir(base string) bool
  • content.go:391 func splitFrontmatter(data []byte) (front, body []byte)
  • content.go:406 func trimLeadingFence(data []byte, fence string) ([]byte, bool)
  • content.go:423 func dropCollisions(all []Bundle) ([]Bundle, []Problem)
  • content.go:445 const PerPage = 10
  • content.go:451 func URL(key, lang string) string
  • content.go:464 func TagURL(section, slug, lang string, page int) string
  • content.go:474 const DerivedPrefix = "/derived/"
  • content.go:477 func DerivedURL(name string) string { return DerivedPrefix + name }
  • content.go:480 const TagsSegment = "tags"
  • content.go:484 func PageURL(key, lang string, page int) string
  • extras.go:14 const ExtrasDir = "extras"
  • extras.go:17 type Entry struct
  • extras.go:33 func Extras(fsys fs.FS, b Bundle) []Entry
  • extras.go:67 func KindOf(name string) string
  • extras.go:86 func ExtrasURL(route, lang, entry string) string
  • settings.go:13 const SettingsFile = "site.yaml"
  • settings.go:20 type Settings struct
  • settings.go:33 func LoadSettings(fsys fs.FS) (Settings, error)
  • settings.go:54 func Absolute(base, path string) string
  • site.go:11 type Site struct
  • site.go:28 func NewSite(bundles []Bundle) *Site
  • site.go:45 func (s *Site) Reveal() { s.reveal = true }
  • site.go:52 func (s *Site) visible(b Bundle) bool
  • site.go:58 func (s *Site) Problems() []Problem { return s.problems }
  • site.go:61 func (s *Site) note(path, detail string)
  • site.go:72 func (s *Site) indexRoutes(bundles []Bundle)
  • site.go:111 func (s *Site) KeyFor(route string) (string, bool)
  • site.go:122 func (s *Site) RouteOf(key string) string
  • site.go:130 func sorted(set map[string]bool) []string
  • site.go:143 func (s *Site) indexAliases(bundles []Bundle)
  • site.go:175 func (s *Site) Alias(alias string) (string, bool)
  • site.go:184 func (s *Site) Lookup(key, lang string) (b Bundle, served string, ok bool)
  • site.go:202 func (s *Site) Variants(key string) []string
  • site.go:216 func (s *Site) HasLang(lang string) bool
  • site.go:226 func (s *Site) Len() int { return len(s.byKeyLang) }
  • site.go:232 type Query struct
  • site.go:243 func (s *Site) Run(q Query) []Bundle
  • site.go:268 func (s *Site) keys() []string
  • site.go:284 type Sequence struct
  • site.go:305 func (s *Site) Sequence(key, lang string) (Sequence, bool)
  • site.go:326 func (s *Site) parent(key string) (string, bool)
  • site.go:336 func (s *Site) has(key string) bool
  • site.go:345 func (s *Site) members(series, lang string) []Bundle
  • site.go:370 func (b Bundle) hasTag(slug string) bool
  • site.go:383 func (b Bundle) Section() string
  • site.go:395 func (s *Site) Everything() []Bundle
  • site.go:408 func (s *Site) Sections() []string

internal/ext/check — 270 lines + 168 test

check.go 262 · doc.go 8

  • check.go:16 type Finding struct
  • check.go:30 func Run(fsys fs.FS, bundles []content.Bundle, site *content.Site, problems []content.Problem) []Finding
  • check.go:53 func Fatal(found []Finding) bool
  • check.go:63 func inspect(fsys fs.FS, b content.Bundle, site *content.Site) []Finding
  • check.go:85 var figureCall = regexp.MustCompile((?m)^\s* figure\{([^}]*)\}\s*$)
  • check.go:89 var altArg = regexp.MustCompile(alt=("[^"]+"|[^"\s}]+))
  • check.go:95 func checkFigures(b content.Bundle) []Finding
  • check.go:106 var internalLink = regexp.MustCompile(\]\((/[^)\s"]*))
  • check.go:113 var bundleLink = regexp.MustCompile(\]\((\.{0,2}/?[^) \s"#?]*\.md))
  • check.go:119 func checkLinks(fsys fs.FS, b content.Bundle, site *content.Site) []Finding
  • check.go:152 func bundleAt(target string, b content.Bundle, site *content.Site) (string, bool)
  • check.go:168 func engineOwned(target string) bool
  • check.go:185 func resolves(fsys fs.FS, target string, from content.Bundle, site *content.Site) bool
  • check.go:211 func asset(fsys fs.FS, trimmed string, site *content.Site) bool
  • check.go:239 func mixedOrdering(bundles []content.Bundle, site *content.Site) []Finding

internal/ext/discover — 107 lines + 139 test

discover.go 98 · doc.go 9

  • discover.go:14 const
  • discover.go:24 func Routes(siteFS fs.FS, settings content.Settings, site func() *content.Site) map[string]http.Handler
  • discover.go:40 func robots(w http.ResponseWriter, siteFS fs.FS, base string)
  • discover.go:61 func sitemap(w http.ResponseWriter, req *http.Request, site *content.Site, base string)
  • discover.go:83 func xmlEscape(s string) string
  • discover.go:93 func writeAs(w http.ResponseWriter, contentType string, out []byte, what string)

doc.go 11 · links.go 134

  • links.go:20 const contentDir = "content"
  • links.go:23 func New() goldmark.Extender { return extension{} }
  • links.go:25 type extension struct{}
  • links.go:30 func (extension) Extend(md goldmark.Markdown)
  • links.go:34 type rewrite struct{}
  • links.go:36 func (rewrite) Transform(doc *ast.Document, reader text.Reader, pc parser.Context)
  • links.go:61 func resolve(dest string, origin render.Origin) (string, bool)
  • links.go:91 func relative(dest string) (*url.URL, bool)
  • links.go:107 func keyOf(name string) (string, bool)
  • links.go:125 func suffix(ref *url.URL) string

internal/ext/notation — 411 lines + 149 test

abbr.go 246 · doc.go 8 · notation.go 157

  • abbr.go:17 var defined = parser.NewContextKey()
  • abbr.go:21 var definitionKind = ast.NewNodeKind("AbbrDefinition")
  • abbr.go:23 type definition struct{ ast.BaseBlock }
  • abbr.go:25 func (n *definition) Kind() ast.NodeKind { return definitionKind }
  • abbr.go:27 func (n *definition) Dump(source []byte, level int) { ast.DumpHelper(n, source, level, nil, nil) }
  • abbr.go:30 var abbrKind = ast.NewNodeKind("Abbr")
  • abbr.go:32 type abbr struct
  • abbr.go:37 func (n *abbr) Kind() ast.NodeKind { return abbrKind }
  • abbr.go:39 func (n *abbr) Dump(source []byte, level int) { ast.DumpHelper(n, source, level, nil, nil) }
  • abbr.go:45 type definitionParser struct{}
  • abbr.go:47 func (definitionParser) Trigger() []byte { return []byte{'*'} }
  • abbr.go:49 func (definitionParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State)
  • abbr.go:65 func (definitionParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State
  • abbr.go:69 func (definitionParser) Close(node ast.Node, reader text.Reader, pc parser.Context) {}
  • abbr.go:71 func (definitionParser) CanInterruptParagraph() bool { return true }
  • abbr.go:73 func (definitionParser) CanAcceptIndentedLine() bool { return false }
  • abbr.go:77 func parseDefinition(line string) (term, expansion string, ok bool)
  • abbr.go:101 type expand struct{}
  • abbr.go:103 func (expand) Transform(doc *ast.Document, reader text.Reader, pc parser.Context)
  • abbr.go:136 func replace(node *ast.Text, source []byte, terms map[string]string, ordered []string)
  • abbr.go:172 func slice(seg text.Segment, from, to int) text.Segment
  • abbr.go:180 func firstMatch(body string, ordered []string) (string, int)
  • abbr.go:193 func earliest(body, term string) int
  • abbr.go:209 func whole(body string, index, length int) bool
  • abbr.go:226 func renderDefinition(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error)
  • abbr.go:232 func renderAbbr(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error)
  • abbr.go:243 func (nodeRenderer) registerAbbr(reg renderer.NodeRendererFuncRegisterer)
  • notation.go:17 var marks = []mark{
  • notation.go:27 type mark struct
  • notation.go:33 func New() goldmark.Extender { return extension{} }
  • notation.go:35 type extension struct{}
  • notation.go:41 func (extension) Extend(md goldmark.Markdown)
  • notation.go:56 var kind = ast.NewNodeKind("Notation")
  • notation.go:59 type node struct
  • notation.go:64 func (n *node) Kind() ast.NodeKind { return kind }
  • notation.go:66 func (n *node) Dump(source []byte, level int) { ast.DumpHelper(n, source, level, nil, nil) }
  • notation.go:70 type processor struct{ tag string }
  • notation.go:72 func (p processor) IsDelimiter(b byte) bool { return b == '~' || b == '=' }
  • notation.go:74 func (p processor) CanOpenCloser(opener, closer *parser.Delimiter) bool
  • notation.go:78 func (p processor) OnMatch(consumes int) ast.Node { return &node{tag p.tag} }
  • notation.go:81 type inlineParser struct{ m mark }
  • notation.go:83 func (p inlineParser) Trigger() []byte { return []byte{p.m.char} }
  • notation.go:85 func (p inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node
  • notation.go:94 func (p inlineParser) doubled(block text.Reader, line []byte, segment text.Segment, pc parser.Context) ast.Node
  • notation.go:113 func (p inlineParser) single(block text.Reader, line []byte, segment text.Segment) ast.Node
  • notation.go:142 type nodeRenderer struct{}
  • notation.go:144 func (r nodeRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer)
  • notation.go:149 func render(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error)

internal/ext/passthrough — 178 lines + 136 test

doc.go 11 · passthrough.go 167

  • passthrough.go:23 const Dir = "root"
  • passthrough.go:28 const headersFile = "_headers.yaml"
  • passthrough.go:35 const tmplSuffix = ".tmpl"
  • passthrough.go:46 func Routes(siteFS fs.FS, settings content.Settings) map[string]http.Handler
  • passthrough.go:92 func readHeaders(siteFS fs.FS) map[string]map[string]string
  • passthrough.go:109 type file struct
  • passthrough.go:117 func (f *file) ServeHTTP(w http.ResponseWriter, r *http.Request)
  • passthrough.go:157 func (f *file) render(data []byte) ([]byte, error)

internal/ext/scaffold — 102 lines + 89 test

doc.go 8 · scaffold.go 94

  • scaffold.go:23 func New(siteDir, key, lang, title string) (string, error)
  • scaffold.go:69 func frontmatter(title string) string
  • scaffold.go:76 func titleFrom(key string) string
  • scaffold.go:85 func mkdirAll(root *os.Root, dir string) error

internal/ext/shortcodes — 1333 lines + 842 test

code.go 211 · containers.go 192 · doc.go 7 · icons.go 129 · images.go 241 · memo.go 73 · shortcodes.go 385 · toc.go 95

  • code.go:27 const codeFragment = "code"
  • code.go:30 var codeKind = ast.NewNodeKind("ShortcodeCode")
  • code.go:32 type codeBlock struct
  • code.go:46 func (n *codeBlock) Kind() ast.NodeKind { return codeKind }
  • code.go:48 func (n *codeBlock) Dump(source []byte, level int) { ast.DumpHelper(n, source, level, nil, nil) }
  • code.go:54 type code struct{}
  • code.go:56 func (code) Transform(doc *ast.Document, reader text.Reader, pc parser.Context)
  • code.go:76 func readFence(f *ast.FencedCodeBlock, source []byte, origin render.Origin) *codeBlock
  • code.go:119 func fromFile(origin render.Origin, name, span string) ([]byte, int, bool)
  • code.go:144 func ranges(spec string) [][2]int
  • code.go:172 func (f fragments) renderCode(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error)
  • containers.go:22 var containerKind = ast.NewNodeKind("ShortcodeContainer")
  • containers.go:24 type container struct
  • containers.go:34 func (n *container) Kind() ast.NodeKind { return containerKind }
  • containers.go:36 func (n *container) Dump(source []byte, level int) { ast.DumpHelper(n, source, level, nil, nil) }
  • containers.go:39 type containers struct{}
  • containers.go:41 func (containers) Trigger() []byte { return []byte{' '} }
  • containers.go:43 func (containers) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State)
  • containers.go:61 func (containers) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State
  • containers.go:70 func (containers) Close(node ast.Node, reader text.Reader, pc parser.Context) {}
  • containers.go:72 func (containers) CanInterruptParagraph() bool { return true }
  • containers.go:74 func (containers) CanAcceptIndentedLine() bool { return false }
  • containers.go:80 type bodies struct
  • containers.go:84 func (b bodies) Transform(doc *ast.Document, reader text.Reader, pc parser.Context)
  • containers.go:108 func (f fragments) renderContainer(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error)
  • containers.go:136 func Merge(src []byte, origin render.Origin) []byte
  • containers.go:165 func withoutIncludes(body []byte) []byte
  • containers.go:184 func included(origin render.Origin, name string) ([]byte, error)
  • icons.go:19 const iconFragment = "icon"
  • icons.go:27 type icons struct{}
  • icons.go:29 func (icons) Trigger() []byte { return []byte{' '} }
  • icons.go:31 func (icons) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node
  • icons.go:52 var iconKind = ast.NewNodeKind("ShortcodeIcon")
  • icons.go:54 type iconNode struct
  • icons.go:61 func (n *iconNode) Kind() ast.NodeKind { return iconKind }
  • icons.go:63 func (n *iconNode) Dump(source []byte, level int) { ast.DumpHelper(n, source, level, nil, nil) }
  • icons.go:67 func (f fragments) renderIcon(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error)
  • icons.go:87 func iconName(line []byte) (string, int, bool)
  • icons.go:117 func isNameByte(c byte, first bool) bool
  • icons.go:129 func isWordRune(r rune) bool { return unicode.IsLetter(r) || unicode.IsDigit(r) }
  • images.go:30 var widths = []int{480, 960, 1440}
  • images.go:37 func Derive(siteFS fs.FS, cacheDir string) (int, error)
  • images.go:75 func derive(data []byte, name, cacheDir string) (int, error)
  • images.go:102 func scale(src image.Image, width int) image.Image
  • images.go:114 func write(file string, img image.Image, source string) error
  • images.go:132 func encode(w *os.File, img image.Image, source string) error
  • images.go:148 var inspected = newMemo(remembered)
  • images.go:155 func picture(origin render.Origin, file string) (render.Picture, bool)
  • images.go:205 func derivedName(data []byte, source string, width int) string
  • images.go:215 func lossless(source string) bool
  • images.go:225 func showable(name string) bool
  • images.go:235 func derivable(name string) bool
  • memo.go:13 const remembered = 1024
  • memo.go:21 type memo struct
  • memo.go:30 type entry struct
  • memo.go:35 func newMemo(max int) *memo
  • memo.go:40 func (m *memo) get(key string) (render.Picture, bool)
  • memo.go:52 func (m *memo) put(key string, picture render.Picture)
  • memo.go:69 func (m *memo) len() int
  • shortcodes.go:28 const
  • shortcodes.go:37 func New(partial render.Partial) goldmark.Extender
  • shortcodes.go:41 type extension struct
  • shortcodes.go:50 func (e extension) Extend(md goldmark.Markdown)
  • shortcodes.go:70 var nested = parser.NewContextKey()
  • shortcodes.go:75 var includedAs = parser.NewContextKey()
  • shortcodes.go:77 const footnoteKey = "khosra footnote-prefix"
  • shortcodes.go:85 func FootnotePrefix(n ast.Node) []byte
  • shortcodes.go:103 type includes struct
  • shortcodes.go:107 func (in includes) Transform(doc *ast.Document, reader text.Reader, pc parser.Context)
  • shortcodes.go:131 func (in includes) convert(name string, pc parser.Context) ([]byte, error)
  • shortcodes.go:165 func pending(doc *ast.Document) []*node
  • shortcodes.go:183 var kind = ast.NewNodeKind("Shortcode")
  • shortcodes.go:186 type node struct
  • shortcodes.go:205 func (n *node) Kind() ast.NodeKind { return kind }
  • shortcodes.go:207 func (n *node) Dump(source []byte, level int) { ast.DumpHelper(n, source, level, nil, nil) }
  • shortcodes.go:210 type blocks struct{}
  • shortcodes.go:212 func (blocks) Trigger() []byte { return []byte{' '} }
  • shortcodes.go:214 func (blocks) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State)
  • shortcodes.go:251 func gallery(pc parser.Context) []render.Picture
  • shortcodes.go:278 func (blocks) Continue(n ast.Node, reader text.Reader, pc parser.Context) parser.State
  • shortcodes.go:282 func (blocks) Close(n ast.Node, reader text.Reader, pc parser.Context) {}
  • shortcodes.go:284 func (blocks) CanInterruptParagraph() bool { return true }
  • shortcodes.go:286 func (blocks) CanAcceptIndentedLine() bool { return false }
  • shortcodes.go:289 type fragments struct
  • shortcodes.go:293 func (f fragments) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer)
  • shortcodes.go:304 func (f fragments) render(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error)
  • shortcodes.go:332 func parse(line, prefix string) (name string, args map[string]string, ok bool)
  • shortcodes.go:367 func argument(s string) (key, value, rest string, ok bool)
  • toc.go:16 const tocName = "toc"
  • toc.go:23 type tables struct{}
  • toc.go:25 func (tables) Transform(doc *ast.Document, reader text.Reader, pc parser.Context)
  • toc.go:53 func deeper(all []render.Heading, depth string) []render.Heading
  • toc.go:69 func headingText(heading *ast.Heading, source []byte) string
  • toc.go:84 func headingID(heading *ast.Heading) string
  • toc.go:95 var _ parser.ASTTransformer = tables{}

internal/ext/watch — 133 lines + 114 test

doc.go 8 · watch.go 125

  • watch.go:16 type Changed func()
  • watch.go:32 func Watch(fsys fs.FS, every, settle time.Duration, stop <-chan struct{}, onChange Changed)
  • watch.go:76 func Fingerprint(fsys fs.FS) string
  • watch.go:90 func record(sum hash.Hash, p string, d fs.DirEntry, err error) error
  • watch.go:113 func dropping(name string) bool

internal/render — 831 lines + 441 test

chrome.go 115 · render.go 414 · theme.go 105 · view.go 197

  • chrome.go:19 var chrome = map[string]map[string]string{
  • chrome.go:38 var months = map[string][]string{
  • chrome.go:46 var digits = map[string][]rune{
  • chrome.go:52 var funcs = template.FuncMap{"t" text, "num" numerals, "day" day}
  • chrome.go:58 func text(lang, key string, args ...string) string
  • chrome.go:78 func numerals(lang string, n int) string
  • chrome.go:87 func day(lang string, t time.Time) string
  • chrome.go:103 func localiseDigits(lang, s string) string
  • render.go:27 type Renderer struct
  • render.go:49 var originKey = parser.NewContextKey()
  • render.go:52 func OriginFrom(pc parser.Context) (Origin, bool)
  • render.go:59 func WithOrigin(pc parser.Context, origin Origin)
  • render.go:65 var callsKey = parser.NewContextKey()
  • render.go:76 func RecordCall(pc parser.Context, name string)
  • render.go:88 func callsFrom(pc parser.Context) []string
  • render.go:104 func New(siteFS fs.FS, settings content.Settings, extend func(Partial) []goldmark.Extender) (*Renderer, error)
  • render.go:134 func (r *Renderer) head(title, lang, canonical string) head
  • render.go:151 func (r *Renderer) absolute(path string) string
  • render.go:159 func (r *Renderer) Navigation(sections func() []string) { r.sections = sections }
  • render.go:165 func (r *Renderer) Links(resolve func(key, lang string) (string, bool)) { r.links = resolve }
  • render.go:171 func (r *Renderer) Compose(rewrite func(src []byte, origin Origin) []byte) { r.compose = rewrite }
  • render.go:175 func (r *Renderer) Partial(name string, data Fragment) ([]byte, error)
  • render.go:193 func (r *Renderer) assets(names []string) template.HTML
  • render.go:215 func (r *Renderer) Extras(b content.Bundle, served string, entries []content.Entry, selected *Selected) ([]byte, error)
  • render.go:236 func (r *Renderer) RenderText(kind string, data []byte) (template.HTML, error)
  • render.go:254 func (r *Renderer) Bundle(b content.Bundle, served string, variants []string, seq *content.Sequence) ([]byte, error)
  • render.go:308 func (r *Renderer) Listing(section, lang string, all []content.Bundle, page int) ([]byte, error)
  • render.go:327 func (r *Renderer) Tag(section, slug, lang string, all []content.Bundle, page int) ([]byte, error)
  • render.go:354 func (r *Renderer) sequence(seq *content.Sequence, lang string) *Sequence
  • render.go:381 func (r *Renderer) item(b content.Bundle, lang string) Item
  • render.go:386 func (r *Renderer) paginate(title, lang, canonical string, all []content.Bundle, page int, url func(int) string) (List, []content.Bundle)
  • render.go:408 func (r *Renderer) execute(set *template.Template, data any, what string) ([]byte, error)
  • theme.go:15 var themeFS embed.FS
  • theme.go:18 type parsedTheme struct
  • theme.go:34 func parseTheme(siteFS fs.FS) (*parsedTheme, error)
  • theme.go:65 func parseSet(siteFS fs.FS, names ...string) (*template.Template, error)
  • theme.go:94 func readStyle(siteFS fs.FS) (template.CSS, error)
  • view.go:17 type head struct
  • view.go:37 type Page struct
  • view.go:63 type Sequence struct
  • view.go:79 type Extras struct
  • view.go:90 type Selected struct
  • view.go:99 type List struct
  • view.go:113 type Group struct
  • view.go:119 type Item struct
  • view.go:130 type Alternate struct
  • view.go:142 type Partial func(name string, data Fragment) ([]byte, error)
  • view.go:145 type Fragment struct
  • view.go:161 type Heading struct
  • view.go:167 type Picture struct
  • view.go:184 type Origin struct

internal/web — 745 lines + 1396 test

asset.go 58 · extras.go 93 · feed.go 125 · logging.go 58 · resolve.go 170 · web.go 241

  • asset.go:22 func serveAsset(w http.ResponseWriter, req *http.Request, site *content.Site, siteFS fs.FS, res resolution) bool
  • extras.go:18 func serveExtras(w http.ResponseWriter, req *http.Request, site *content.Site, r *render.Renderer,
  • extras.go:69 func renderExtras(w http.ResponseWriter, r *render.Renderer, b content.Bundle, served string,
  • extras.go:85 func find(entries []content.Entry, want string) (content.Entry, bool)
  • feed.go:13 const feedFile = "feed.xml"
  • feed.go:17 const feedMax = 20
  • feed.go:24 type atom struct
  • feed.go:34 type atomLink struct
  • feed.go:40 type atomEntry struct
  • feed.go:52 func serveFeed(w http.ResponseWriter, req *http.Request, site *content.Site, res resolution, settings content.Settings) bool
  • feed.go:102 func dated(all []content.Bundle) []content.Bundle
  • feed.go:113 func feedTitle(settings content.Settings, res resolution) string
  • logging.go:20 func Logged(h http.Handler) http.Handler
  • logging.go:43 type recorder struct
  • logging.go:49 func (r *recorder) WriteHeader(status int)
  • logging.go:54 func (r *recorder) Write(b []byte) (int, error)
  • resolve.go:12 type resolution struct
  • resolve.go:40 func resolve(path string, site *content.Site) (resolution, bool)
  • resolve.go:102 func cutLang(key string, site *content.Site) (lang, rest, redirect string)
  • resolve.go:117 func cutExtras(key string) (bundle, entry string, ok bool)
  • resolve.go:131 func cutFeed(key string) (rest string, ok bool)
  • resolve.go:142 func cutTag(key string) (tag, section string, ok bool)
  • resolve.go:154 func cutPage(key string) (rest string, page int, ok bool)
  • web.go:19 type Snapshot struct
  • web.go:28 type Current func() *Snapshot
  • web.go:31 func Fixed(site *content.Site, theme *render.Renderer) Current
  • web.go:43 func Handler(current Current, siteFS, derivedFS fs.FS, settings content.Settings, routes map[string]http.Handler) http.Handler
  • web.go:84 func serveStatic(sub fs.FS) http.Handler
  • web.go:100 func serveListing(w http.ResponseWriter, req *http.Request, site *content.Site, r *render.Renderer, res resolution) bool
  • web.go:124 func serveTags(w http.ResponseWriter, req *http.Request, site *content.Site, r *render.Renderer, res resolution) bool
  • web.go:149 func write(w http.ResponseWriter, out []byte, what string)
  • web.go:158 func writeAs(w http.ResponseWriter, contentType string, out []byte, what string)
  • web.go:166 func serve(w http.ResponseWriter, req *http.Request, site *content.Site, r *render.Renderer, siteFS fs.FS, settings content.Settings)
  • web.go:197 func serveBundle(w http.ResponseWriter, req *http.Request, site *content.Site, r *render.Renderer,