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:
@@ -320,6 +320,98 @@ func TestStaticFilesAreServedAndDirectoriesAreNot(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func seriesHandler(t *testing.T) http.Handler {
|
||||
t.Helper()
|
||||
fsys := fstest.MapFS{
|
||||
"content/comics/the-long-monsoon/_index.md": {Data: []byte("---\ntitle: The Long Monsoon\n---\nA series.\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/aftermath.md": {Data: []byte("---\ntitle: Aftermath\norder: 30\n---\n")},
|
||||
"content/pages/about.md": {Data: []byte("---\ntitle: About\n---\nx\n")},
|
||||
}
|
||||
bundles, err := content.Scan(fsys)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r, err := render.New(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return Handler(content.NewSite(bundles), r, nil)
|
||||
}
|
||||
|
||||
func TestSequenceNavigationLinksNeighbours(t *testing.T) {
|
||||
h := seriesHandler(t)
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/comics/the-long-monsoon/the-flood/", nil))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("got %d, want 200", rec.Code)
|
||||
}
|
||||
body := rec.Body.String()
|
||||
for _, want := range []string{
|
||||
`rel="prev" href="/comics/the-long-monsoon/first-rain/"`,
|
||||
`rel="next" href="/comics/the-long-monsoon/aftermath/"`,
|
||||
`href="/comics/the-long-monsoon/">The Long Monsoon</a> 2 of 3`,
|
||||
} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("missing %q — prev is the *earlier* chapter:\n%s", want, body)
|
||||
}
|
||||
}
|
||||
nav := body[strings.Index(body, `<nav class="sequence">`):]
|
||||
if strings.Contains(nav[:strings.Index(nav, "</nav>")], "the-flood") {
|
||||
t.Error("the nav should link its neighbours and the series, never the chapter it is on")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSequenceEndsHaveNoNeighbourBeyondThem(t *testing.T) {
|
||||
h := seriesHandler(t)
|
||||
for path, absent := range map[string]string{
|
||||
"/comics/the-long-monsoon/first-rain/": `rel="prev"`,
|
||||
"/comics/the-long-monsoon/aftermath/": `rel="next"`,
|
||||
} {
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("GET %s = %d, want 200", path, rec.Code)
|
||||
}
|
||||
if strings.Contains(rec.Body.String(), absent) {
|
||||
t.Errorf("GET %s should not carry %s at the end of a series", path, absent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeriesLandingPageListsChaptersInOrder(t *testing.T) {
|
||||
h := seriesHandler(t)
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/comics/the-long-monsoon/", nil))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("got %d, want 200", rec.Code)
|
||||
}
|
||||
body := rec.Body.String()
|
||||
if !strings.Contains(body, "A series.") {
|
||||
t.Error("the landing page is still a bundle and renders its own body")
|
||||
}
|
||||
first, second, third := strings.Index(body, "First Rain"), strings.Index(body, "The Flood"), strings.Index(body, "Aftermath")
|
||||
if first < 0 || second < first || third < second {
|
||||
t.Errorf("the archive must read in sequence order, not newest first:\n%s", body)
|
||||
}
|
||||
if strings.Contains(body, `rel="prev"`) || strings.Contains(body, `rel="next"`) {
|
||||
t.Error("a landing page holds no position of its own, so it has no neighbours")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPagesOutsideASeriesGetNoSequence(t *testing.T) {
|
||||
h := seriesHandler(t)
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/pages/about/", nil))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("got %d, want 200", rec.Code)
|
||||
}
|
||||
if body := rec.Body.String(); strings.Contains(body, "sequence") {
|
||||
t.Errorf("an unrelated page must render no sequence nav:\n%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func tagHandler(t *testing.T) http.Handler {
|
||||
t.Helper()
|
||||
fsys := fstest.MapFS{
|
||||
|
||||
Reference in New Issue
Block a user