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:
+102
-7
@@ -119,17 +119,11 @@ type Query struct {
|
||||
// Run applies q, newest first, with undated bundles after dated ones and ties broken by key so the same
|
||||
// query always answers in the same order.
|
||||
func (s *Site) Run(q Query) []Bundle {
|
||||
seen := map[string]bool{}
|
||||
var out []Bundle
|
||||
for kl := range s.byKeyLang {
|
||||
key, _, found := strings.Cut(kl, "\x00")
|
||||
if !found || seen[key] {
|
||||
continue
|
||||
}
|
||||
for _, key := range s.keys() {
|
||||
if q.Section != "" && !strings.HasPrefix(key, q.Section+"/") {
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
b, _, ok := s.Lookup(key, q.Lang)
|
||||
if !ok || !b.hasTag(q.Tag) {
|
||||
continue
|
||||
@@ -148,6 +142,107 @@ func (s *Site) Run(q Query) []Bundle {
|
||||
return out
|
||||
}
|
||||
|
||||
// keys lists every bundle key once, sorted, so nothing iterating the index depends on map order.
|
||||
func (s *Site) keys() []string {
|
||||
seen := make(map[string]bool, len(s.byKeyLang))
|
||||
out := make([]string, 0, len(s.byKeyLang))
|
||||
for kl := range s.byKeyLang {
|
||||
key, _, found := strings.Cut(kl, "\x00")
|
||||
if !found || seen[key] {
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
out = append(out, key)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// Sequence is the reading order a bundle participates in: a series and its members.
|
||||
type Sequence struct {
|
||||
// Series is the landing bundle — comics/the-long-monsoon for its chapters.
|
||||
Series Bundle
|
||||
// Members are the series' bundles in reading order.
|
||||
Members []Bundle
|
||||
// Index is the 1-based position of the bundle asked about, and zero when that bundle is the landing
|
||||
// page itself.
|
||||
Index int
|
||||
}
|
||||
|
||||
// Sequence resolves the series a bundle participates in, reporting false when it is in none.
|
||||
//
|
||||
// Membership is structural (ADR-0033): a bundle with bundles nested under it is a series landing page and
|
||||
// they are its members, and a bundle nested under another is a member of that one. A landing page inside
|
||||
// another series reports its own members rather than its siblings, since the deeper series is what the
|
||||
// page is about.
|
||||
//
|
||||
// Members resolve through the language fallback chain, so a chapter missing in this language still holds
|
||||
// its place in the reading order rather than breaking prev/next (ADR-0009). `draft` is not honoured
|
||||
// because no bundle carries it yet (content-model.md).
|
||||
func (s *Site) Sequence(key, lang string) (Sequence, bool) {
|
||||
if members := s.members(key, lang); len(members) > 0 {
|
||||
// key names a bundle: members are the bundles nested under it.
|
||||
landing, _, _ := s.Lookup(key, lang)
|
||||
return Sequence{Series: landing, Members: members}, true
|
||||
}
|
||||
series, nested := s.parent(key)
|
||||
if !nested {
|
||||
return Sequence{}, false
|
||||
}
|
||||
landing, _, _ := s.Lookup(series, lang) // parent only names bundles
|
||||
seq := Sequence{Series: landing, Members: s.members(series, lang)}
|
||||
for i, m := range seq.Members {
|
||||
if m.Key == key {
|
||||
seq.Index = i + 1
|
||||
}
|
||||
}
|
||||
return seq, true
|
||||
}
|
||||
|
||||
// parent is the nearest ancestor of key that is itself a bundle, in any language.
|
||||
func (s *Site) parent(key string) (string, bool) {
|
||||
for i := strings.LastIndex(key, "/"); i > 0; i = strings.LastIndex(key[:i], "/") {
|
||||
if ancestor := key[:i]; s.has(ancestor) {
|
||||
return ancestor, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// has reports whether a key exists in any language.
|
||||
func (s *Site) has(key string) bool {
|
||||
_, _, ok := s.Lookup(key, "")
|
||||
return ok
|
||||
}
|
||||
|
||||
// members are the bundles whose nearest bundle ancestor is series, in reading order.
|
||||
//
|
||||
// order ascending where it is set, then by key: a member without order sorts after every member carrying
|
||||
// one, mirroring how an undated bundle sorts after dated ones (ADR-0033).
|
||||
func (s *Site) members(series, lang string) []Bundle {
|
||||
var out []Bundle
|
||||
for _, key := range s.keys() {
|
||||
if p, nested := s.parent(key); !nested || p != series {
|
||||
continue
|
||||
}
|
||||
if b, _, ok := s.Lookup(key, lang); ok {
|
||||
out = append(out, b)
|
||||
}
|
||||
}
|
||||
sort.SliceStable(out, func(i, j int) bool {
|
||||
a, b := out[i], out[j]
|
||||
switch {
|
||||
case (a.Order == 0) != (b.Order == 0):
|
||||
return b.Order == 0
|
||||
case a.Order != b.Order:
|
||||
return a.Order < b.Order
|
||||
default:
|
||||
return a.Key < b.Key
|
||||
}
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
// hasTag reports whether the bundle carries a tag slug. An empty slug matches everything.
|
||||
func (b Bundle) hasTag(slug string) bool {
|
||||
if slug == "" {
|
||||
|
||||
Reference in New Issue
Block a user