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:
@@ -121,6 +121,100 @@ func mustScan(t *testing.T, fsys fstest.MapFS) []Bundle {
|
||||
return b
|
||||
}
|
||||
|
||||
// seriesFS is a series joined structurally: the landing bundle plus four chapters nested under it, two
|
||||
// carrying order and two not, and one outsider that must never join.
|
||||
func seriesFS() fstest.MapFS {
|
||||
return fstest.MapFS{
|
||||
"content/comics/the-long-monsoon/_index.md": {Data: []byte("---\ntitle: The Long Monsoon\n---\n")},
|
||||
"content/comics/the-long-monsoon/first-rain.md": {Data: []byte("---\ntitle: First Rain\norder: 10\n---\n")},
|
||||
"content/comics/the-long-monsoon/the-flood.md": {Data: []byte("---\ntitle: The Flood\norder: 20\n---\n")},
|
||||
"content/comics/the-long-monsoon/appendix-a.md": {Data: []byte("---\ntitle: Appendix A\n---\n")},
|
||||
"content/comics/the-long-monsoon/appendix-b.bn.md": {Data: []byte("---\ntitle: পরিশিষ্ট খ\n---\n")},
|
||||
"content/comics/the-long-monsoon/appendix-b.md": {Data: []byte("---\ntitle: Appendix B\n---\n")},
|
||||
"content/comics/elsewhere.md": {Data: []byte("---\ntitle: Elsewhere\n---\n")},
|
||||
}
|
||||
}
|
||||
|
||||
func TestSequenceMembershipIsStructural(t *testing.T) {
|
||||
site := NewSite(mustScan(t, seriesFS()))
|
||||
seq, ok := site.Sequence("comics/the-long-monsoon/the-flood", "en")
|
||||
if !ok {
|
||||
t.Fatal("a bundle nested under a landing bundle is a member (ADR-0033)")
|
||||
}
|
||||
if seq.Series.Title != "The Long Monsoon" {
|
||||
t.Errorf("series = %q, want the enclosing landing bundle", seq.Series.Title)
|
||||
}
|
||||
var titles []string
|
||||
for _, m := range seq.Members {
|
||||
titles = append(titles, m.Title)
|
||||
}
|
||||
want := []string{"First Rain", "The Flood", "Appendix A", "Appendix B"}
|
||||
if len(titles) != len(want) {
|
||||
t.Fatalf("members = %v, want %v", titles, want)
|
||||
}
|
||||
for i := range want {
|
||||
if titles[i] != want[i] {
|
||||
t.Fatalf("members = %v, want %v — ordered first, then by name", titles, want)
|
||||
}
|
||||
}
|
||||
if seq.Index != 2 {
|
||||
t.Errorf("index = %d, want 2", seq.Index)
|
||||
}
|
||||
if _, ok := site.Sequence("comics/elsewhere", "en"); ok {
|
||||
t.Error("a bundle beside the series is not in it")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSequenceOnTheLandingPageListsItsMembers(t *testing.T) {
|
||||
site := NewSite(mustScan(t, seriesFS()))
|
||||
seq, ok := site.Sequence("comics/the-long-monsoon", "en")
|
||||
if !ok {
|
||||
t.Fatal("a bundle with bundles nested under it is a series landing page")
|
||||
}
|
||||
if seq.Index != 0 {
|
||||
t.Errorf("index = %d, want 0: the landing page holds no position in its own series", seq.Index)
|
||||
}
|
||||
if len(seq.Members) != 4 {
|
||||
t.Errorf("members = %d, want 4", len(seq.Members))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSequenceMembersFollowTheLanguageFallback(t *testing.T) {
|
||||
site := NewSite(mustScan(t, seriesFS()))
|
||||
seq, ok := site.Sequence("comics/the-long-monsoon/the-flood", "bn")
|
||||
if !ok {
|
||||
t.Fatal("the series must resolve in every language")
|
||||
}
|
||||
if len(seq.Members) != 4 {
|
||||
t.Fatalf("members = %d, want 4: a chapter missing in Bengali still holds its place", len(seq.Members))
|
||||
}
|
||||
if got := seq.Members[3].Title; got != "পরিশিষ্ট খ" {
|
||||
t.Errorf("last member = %q, want the Bengali variant where one exists", got)
|
||||
}
|
||||
if got := seq.Members[1].Title; got != "The Flood" {
|
||||
t.Errorf("member without a Bengali variant = %q, want the English fallback", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNestedSeriesReportsItsOwnMembers(t *testing.T) {
|
||||
fsys := seriesFS()
|
||||
fsys["content/comics/_index.md"] = &fstest.MapFile{Data: []byte("---\ntitle: Comics\n---\n")}
|
||||
site := NewSite(mustScan(t, fsys))
|
||||
seq, ok := site.Sequence("comics/the-long-monsoon", "en")
|
||||
if !ok {
|
||||
t.Fatal("still a series")
|
||||
}
|
||||
if len(seq.Members) != 4 || seq.Series.Title != "The Long Monsoon" {
|
||||
t.Errorf("a landing page inside another series reports its own members, got %d under %q",
|
||||
len(seq.Members), seq.Series.Title)
|
||||
}
|
||||
// The outsider's nearest bundle ancestor is now the section landing, so it joins that sequence.
|
||||
seq, ok = site.Sequence("comics/elsewhere", "en")
|
||||
if !ok || seq.Series.Title != "Comics" {
|
||||
t.Errorf("nearest ancestor should win, got %q ok=%v", seq.Series.Title, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryFiltersByTagAndSection(t *testing.T) {
|
||||
fsys := fstest.MapFS{
|
||||
"content/posts/a.md": {Data: []byte("---\ntitle: A\ndate: 2026-01-03\ntags: [Monsoon, prose]\n---\n")},
|
||||
|
||||
Reference in New Issue
Block a user