resolve sequences from the directory tree
A bundle nested under another bundle is a member of that series (ADR-0033), so
`Site.Sequence` walks up to the nearest bundle ancestor and back down to its
members: ordered by `order` where set, then by name. Members resolve through the
language fallback, so a chapter with no Bengali variant still holds its place in
Bengali reading order instead of breaking prev/next.
One `.Sequence` field carries both shapes a theme needs. A landing page renders
`.Members` as an archive; a chapter renders `.Prev`/`.Next`, which are pointers
into `.Members` so `{{with}}` yields nothing at the ends. `Index == 0` is what
tells the two apart.
`Query` was deliberately not extended. A series ascends where `Run` descends, and
an order knob on `Query` is the config knob rule 6 bans; instead `Site.keys()`
came out so both iterate the index one way, deleting `Run`'s own dedupe map.
`draft` is not honoured: no bundle carries the field and nothing else excludes
drafts, so entry 19 adds it in both places at once. Recorded in content-model.md
rather than left implied.
state.md also corrects six inventory rows that had drifted before this change —
three LOC figures, the test total, `go.mod`, and two lines that were flatly wrong
("Dependencies: none", "goldmark is not yet imported"). The coupling gate proves
state.md changed with the code; it cannot prove the numbers are right.
This commit is contained in:
@@ -46,6 +46,25 @@ type Page struct {
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// List is a collection page: the result of a Query, one page of it.
|
||||
@@ -156,8 +175,9 @@ func readStyle(siteFS fs.FS) (template.CSS, error) {
|
||||
// Bundle renders one bundle into a complete page.
|
||||
//
|
||||
// served is the language actually chosen by the fallback chain, and variants every language the key
|
||||
// exists in; both feed canonical and hreflang, which a theme must not construct itself.
|
||||
func (r *Renderer) Bundle(b content.Bundle, served string, variants []string) ([]byte, error) {
|
||||
// exists in; both feed canonical and hreflang, which a theme must not construct itself. seq is the series
|
||||
// the bundle sits in, or nil.
|
||||
func (r *Renderer) Bundle(b content.Bundle, served string, variants []string, seq *content.Sequence) ([]byte, error) {
|
||||
var body bytes.Buffer
|
||||
if err := r.md.Convert(b.Body, &body); err != nil {
|
||||
return nil, fmt.Errorf("markdown %s: %w", b.Path, err)
|
||||
@@ -167,10 +187,11 @@ func (r *Renderer) Bundle(b content.Bundle, served string, variants []string) ([
|
||||
title = b.Key
|
||||
}
|
||||
p := Page{
|
||||
head: head{Title: title, Lang: served, Canonical: content.URL(b.Key, served), Style: r.style},
|
||||
Key: b.Key,
|
||||
HTML: template.HTML(body.String()),
|
||||
Extra: b.Extra,
|
||||
head: head{Title: title, Lang: served, Canonical: content.URL(b.Key, served), Style: r.style},
|
||||
Key: b.Key,
|
||||
HTML: template.HTML(body.String()),
|
||||
Extra: b.Extra,
|
||||
Sequence: r.sequence(seq, served),
|
||||
}
|
||||
for _, l := range variants {
|
||||
p.Alternates = append(p.Alternates, Alternate{Lang: l, URL: content.URL(b.Key, l)})
|
||||
@@ -209,6 +230,36 @@ func (r *Renderer) Tag(section, slug, lang string, all []content.Bundle, page in
|
||||
return r.execute(r.list, l, "tag "+slug)
|
||||
}
|
||||
|
||||
// sequence builds the series view for a page: its members, and the neighbours around this page.
|
||||
//
|
||||
// Neighbours are pointers into Members, so a theme reads them with `with` and gets nothing at the ends
|
||||
// rather than an empty entry that looks like a link.
|
||||
func (r *Renderer) sequence(seq *content.Sequence, lang string) *Sequence {
|
||||
if seq == nil {
|
||||
return nil
|
||||
}
|
||||
out := &Sequence{
|
||||
Title: seq.Series.Title,
|
||||
URL: content.URL(seq.Series.Key, lang),
|
||||
Index: seq.Index,
|
||||
Count: len(seq.Members),
|
||||
}
|
||||
for _, m := range seq.Members {
|
||||
out.Members = append(out.Members, r.item(m, lang))
|
||||
}
|
||||
if len(out.Members) == 0 {
|
||||
return out
|
||||
}
|
||||
out.First, out.Last = &out.Members[0], &out.Members[len(out.Members)-1]
|
||||
if seq.Index > 1 {
|
||||
out.Prev = &out.Members[seq.Index-2]
|
||||
}
|
||||
if seq.Index > 0 && seq.Index < len(out.Members) {
|
||||
out.Next = &out.Members[seq.Index]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// item is one listing entry.
|
||||
func (r *Renderer) item(b content.Bundle, lang string) Item {
|
||||
return Item{Title: b.Title, Key: b.Key, URL: content.URL(b.Key, lang), Date: b.Date}
|
||||
|
||||
Reference in New Issue
Block a user