From 7bf5697c71526a770689ad325d5501b9baef5b9c Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Fri, 30 Oct 2026 20:50:38 -0400 Subject: [PATCH] fix(podcasts): library switcher leaked literal "continue" and the podcasts option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The D13 podcast exclusion (9a148c23) was written as a templ `if !IsBookLibrary(...) { continue }` — but templ markup control flow has NO continue STATEMENT: codegen emitted the word as a TEXT node (visible junk between options on every page's library subbar) and rendered the podcasts option anyway, defeating the exclusion. The real browser sweep caught what every Go/API test structurally cannot: generated markup that renders wrong yet compiles clean. Rewritten as an inverted guard (the idiomatic templ skip). New render pin TestLibrarySwitcherExcludesPodcasts fails on both halves of the original leak — the literal text AND the presence of the podcasts option — so this codegen gotcha class stays tripped-wired. --- templates/library_switcher.templ | 25 +-- templates/library_switcher_templ.go | 272 ++++++++++++++-------------- templates/partials_render_test.go | 24 +++ 3 files changed, 170 insertions(+), 151 deletions(-) diff --git a/templates/library_switcher.templ b/templates/library_switcher.templ index 29d79fae..81e93ad7 100644 --- a/templates/library_switcher.templ +++ b/templates/library_switcher.templ @@ -30,19 +30,22 @@ templ LibrarySwitcher(libData []LibraryData, currentLibraryID string, title stri // in the book-library switcher — feeds surface via // /podcasts only. Presentation-layer filter so every // switcher consumer is correct at once. - if !IsBookLibrary(lib.TypeName) { - continue - } - if lib.Offline { - if lib.ID == currentLibraryID { - + // (templ markup control flow has NO `continue` + // statement — codegen emits it as literal TEXT and + // renders the option anyway, browser-sweep finding + // 2026-10-30; keep the skip as an inverted guard.) + if IsBookLibrary(lib.TypeName) { + if lib.Offline { + if lib.ID == currentLibraryID { + + } else { + + } + } else if lib.ID == currentLibraryID { + } else { - + } - } else if lib.ID == currentLibraryID { - - } else { - } } diff --git a/templates/library_switcher_templ.go b/templates/library_switcher_templ.go index 84412ca4..d58d007c 100644 --- a/templates/library_switcher_templ.go +++ b/templates/library_switcher_templ.go @@ -82,201 +82,193 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, title strin return templ_7745c5c3_Err } for _, lib := range libData { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if !IsBookLibrary(lib.TypeName) { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "continue") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if lib.Offline { - if lib.ID == currentLibraryID { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } else if lib.ID == currentLibraryID { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, ")") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, ")") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - } else if lib.ID == currentLibraryID { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -287,7 +279,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, title strin } } if len(actions) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -297,12 +289,12 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, title strin return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -331,7 +323,7 @@ func DashboardActions() templ.Component { templ_7745c5c3_Var16 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/templates/partials_render_test.go b/templates/partials_render_test.go index e5866594..42bccbfd 100644 --- a/templates/partials_render_test.go +++ b/templates/partials_render_test.go @@ -106,3 +106,27 @@ func TestUnlinkedBooksPageRender(t *testing.T) { } } } + +// The library switcher's D13 podcast exclusion must hold at the +// presentation layer — and the templ `continue` statement leak (codegen +// emits it as literal TEXT, browser sweep 2026-10-30) must never return. +func TestLibrarySwitcherExcludesPodcasts(t *testing.T) { + var b strings.Builder + libs := []LibraryData{ + {ID: "l1", Name: "Ebooks", TypeName: "books", MediaCount: 5}, + {ID: "l2", Name: "Podcasts", TypeName: "podcasts", MediaCount: 235}, + } + if err := LibrarySwitcher(libs, "l1", "", nil).Render(context.Background(), &b); err != nil { + t.Fatalf("LibrarySwitcher: %v", err) + } + out := b.String() + if strings.Contains(out, "Podcasts (235)") { + t.Error("switcher leaked the podcasts library into the book switcher") + } + if strings.Contains(out, "continue") { + t.Error("switcher rendered the literal text `continue` (templ has no continue statement)") + } + if !strings.Contains(out, "Ebooks (5)") { + t.Error("switcher missing the books library") + } +}