give the author three controls the engine was deciding alone
`include: merge` in frontmatter splices a bundle's includes before the parse, so
a page assembled from several files is one document: one footnote list at its
end, numbered straight through, and an abbreviation defined anywhere reaching
every part. Moving the rendered block afterwards would have meant editing
goldmark's own markup; handing the parser one source gets the right answer from
it instead. Without the flag nothing changes — each fragment stays its own
document with namespaced ids, so no existing content re-renders.
Heading ids are unique under either model. Merging gets that free, because one
parse means one id set, but embedding did not: three `## Description`s across a
page and its fragments produced three identical anchors, and every link to them
landed on the first. A nested parse now shares the parent's id set, so the
second becomes #description-1 — goldmark's own suffixing, reaching across files
because they finally share the set it counts in.
Auditing for other policies the author could not reach found two more.
A heading may declare its anchor: `## Title {#stable-anchor}`. This is the one
that mattered most and nobody had asked for it — a derived id changes when the
text does, so rewording a heading silently broke every link to that anchor,
which is indefensible in an engine whose first value is that published addresses
are permanent.
`::toc{depth=2}` shortens a contents list, because a theme cannot know per page
how deep is useful and the author can.
Deliberately not added: a typographer toggle, a per-picture "do not resample",
icon overrides. No second user for any of them.
The hand-copied wiring in example_test.go drifted for the third time this
session — Compose this time, after the dialect and notation — each caught by a
demo case rather than by the copy. The latent row is now marked due, with what
moving the list would require.
This commit is contained in:
@@ -42,6 +42,9 @@ type Renderer struct {
|
||||
sections func() []string
|
||||
// siteFS is kept only so Refresh can reparse what New parsed.
|
||||
siteFS fs.FS
|
||||
// compose may rewrite a body before it is parsed, for a bundle that asks its includes to be merged
|
||||
// (ADR-0066). Set at wiring time like sections, and never called otherwise.
|
||||
compose func(src []byte, origin Origin) []byte
|
||||
}
|
||||
|
||||
// parsedTheme is one snapshot of the theme: the sets a request executes, and the stylesheet the shell inlines.
|
||||
@@ -154,7 +157,7 @@ func New(siteFS fs.FS, settings content.Settings, extend func(Partial) []goldmar
|
||||
// Heading IDs are a parser option rather than an extension, and they are the engine's half of a table of
|
||||
// contents: the anchor has to exist before a theme can link to it (ADR-0058).
|
||||
r.md = goldmark.New(goldmark.WithExtensions(extensions...),
|
||||
goldmark.WithParserOptions(parser.WithAutoHeadingID()),
|
||||
goldmark.WithParserOptions(parser.WithAutoHeadingID(), parser.WithHeadingAttribute()),
|
||||
goldmark.WithRendererOptions(html.WithUnsafe()))
|
||||
return r, nil
|
||||
}
|
||||
@@ -218,6 +221,12 @@ func (r *Renderer) absolute(path string) string {
|
||||
// sections exist. A callback rather than a slice, because content changes and a copy would go stale.
|
||||
func (r *Renderer) Navigation(sections func() []string) { r.sections = sections }
|
||||
|
||||
// Compose registers the rewrite a merging bundle's body goes through before it is parsed (ADR-0066).
|
||||
//
|
||||
// A seam rather than a call, for the same reason extend is one: only cmd knows which features exist, and
|
||||
// splicing source files together is a feature's work, not the renderer's.
|
||||
func (r *Renderer) Compose(rewrite func(src []byte, origin Origin) []byte) { r.compose = rewrite }
|
||||
|
||||
// Refresh reparses the theme and swaps it in, so a running server picks up an edited template the same way it
|
||||
// picks up edited content (ADR-0055). Called once per rebuild, off the request path.
|
||||
//
|
||||
@@ -341,9 +350,16 @@ func (r *Renderer) Bundle(b content.Bundle, served string, variants []string, se
|
||||
// The parse carries which bundle it is, so a feature can resolve a path in a call against the bundle's
|
||||
// own directory (ADR-0031: through the rooted filesystem, never a joined path).
|
||||
pc := parser.NewContext()
|
||||
WithOrigin(pc, Origin{Dir: path.Dir(b.Path), Files: r.files})
|
||||
origin := Origin{Dir: path.Dir(b.Path), Files: r.files}
|
||||
WithOrigin(pc, origin)
|
||||
// `include: merge` asks for one document rather than a page of embedded ones, so the fragments are
|
||||
// spliced in before the parse and their footnotes, abbreviations and headings become the page's (ADR-0066).
|
||||
source := b.Body
|
||||
if kind, _ := b.Extra["include"].(string); kind == "merge" && r.compose != nil {
|
||||
source = r.compose(source, origin)
|
||||
}
|
||||
var body bytes.Buffer
|
||||
if err := r.md.Convert(b.Body, &body, parser.WithContext(pc)); err != nil {
|
||||
if err := r.md.Convert(source, &body, parser.WithContext(pc)); err != nil {
|
||||
return nil, fmt.Errorf("markdown %s: %w", b.Path, err)
|
||||
}
|
||||
title := b.Title
|
||||
|
||||
Reference in New Issue
Block a user