oficina / docs Public

app: fix round 1 -- revert shell rewrite, gate the attribute grammar at the source

Reverts shellAttributeGrammar: rewriting rastrillo's vendored shell
bytes to satisfy a rendered-page test was the wrong fix. The test
contradicted the brief's own rule that rastrillo's output is "theirs
and expected, ours must not" -- and a string substitution over a
dependency's markup fails silently the day rastrillo's shell text
changes, which it is slated to before 1.0.

Fix is at the source instead: TestOurTemplatesUseTheAttributeGrammar
(render_test.go) walks this app's own embedded views/*.html and fails
on any class="rst-*" byte in a template this app authors -- correct by
construction, since rastrillo's own partials and shell are never in
that tree. Proved it bites by planting one and watching it fail, then
reverting. Caught a real false positive along the way: list.html's own
header comment described the convention in prose that literally
contained the banned substring; reworded it.

list_test.go's rendered-page check stays as a secondary confirmation,
now allowlisting rastrillo's two known, named shell occurrences
(rst-skip, rst-page) instead of asserting zero unconditionally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011F5CivVm4CmL9ByfSSAaXA
Paul Campbell pushed by paul@keymail.dev c4389b8449886d3f35ef4d58160a894888a89d33 parent dae98da
4 files changed, +89 −33
  • internal/docs/list_test.go +24 −2
  • internal/docs/render.go +1 −26
  • internal/docs/render_test.go +56 −0
  • internal/docs/views/list.html +8 −5
diff --git a/internal/docs/list_test.go b/internal/docs/list_test.go
index f1d7a4a..21def7a 100644
--- a/internal/docs/list_test.go
+++ b/internal/docs/list_test.go
@@ -4,12 +4,32 @@ import (
"context"
"net/http"
"net/http/httptest"
+ "regexp"
"strings"
"testing"
"github.com/carlosframework/docs/internal/docs"
)
+// classVocabPattern finds every class="rst-*" attribute on a rendered
+// page. shellClassVocab allowlists the two rastrillo's own vendored
+// column shell hard-codes outside any block this app can override —
+// the skip link (class="rst-skip") and the <main> wrapper
+// (class="rst-page"), ui/layouts/column.html in rastrillo v0.21.0.
+// That is rastrillo's output, not this app's markup, and is not
+// something render_test.go's TestOurTemplatesUseTheAttributeGrammar
+// — the test that actually gates this app's own templates — ever
+// sees, since it walks only this app's own viewsFS. This rendered-page
+// check exists as a secondary, real-page confirmation that nothing
+// else on the page slipped in the class spelling; it is not the test
+// this app depends on to catch a class in a template it authors.
+var classVocabPattern = regexp.MustCompile(`class="rst-([a-zA-Z-]+)"`)
+
+var shellClassVocab = map[string]bool{
+ "skip": true,
+ "page": true,
+}
+
func TestListShowsDocuments(t *testing.T) {
app, _ := docs.NewForTest(t)
ctx := context.Background()
@@ -28,8 +48,10 @@ func TestListShowsDocuments(t *testing.T) {
if !strings.Contains(body, "Board minutes") {
t.Error("the document's title is not on the page")
}
- if strings.Contains(body, `class="rst-`) {
- t.Error("class vocabulary found — this app writes the attribute grammar (Global Constraints)")
+ for _, m := range classVocabPattern.FindAllStringSubmatch(body, -1) {
+ if !shellClassVocab[m[1]] {
+ t.Errorf("class vocabulary found (class=%q) outside rastrillo's own shell — this app writes the attribute grammar (Global Constraints)", `rst-`+m[1])
+ }
}
}
diff --git a/internal/docs/render.go b/internal/docs/render.go
index c74e447..8883a29 100644
--- a/internal/docs/render.go
+++ b/internal/docs/render.go
@@ -7,7 +7,6 @@ import (
"html/template"
"io/fs"
"net/http"
- "strings"
"testing/fstest"
"github.com/carlosframework/rastrillo"
@@ -111,7 +110,7 @@ func newRenderer() (*renderer, error) {
if !ok {
return nil, fmt.Errorf("docs: rastrillo ui: shell %q not shipped", shellLayout)
}
- base, err = base.Parse(string(shellAttributeGrammar(layout)))
+ base, err = base.Parse(string(layout))
if err != nil {
return nil, fmt.Errorf("docs: parse rastrillo shell %q: %w", shellLayout, err)
}
@@ -131,30 +130,6 @@ func newRenderer() (*renderer, error) {
return &renderer{pages: pages, assets: assets}, nil
}
-// shellAttributeGrammar rewrites the two class="rst-*" spots
-// rastrillo's vendored column shell hard-codes — the skip link and
-// the <main> wrapper — into their bare-attribute twins, [rst-skip]
-// and [rst-page]. Neither is behind an overridable {{block}}, so an
-// app cannot reach them by redefining a shell block the way it can
-// "brand" or "nav": this is the one place the shell's own bytes have
-// to be edited instead.
-//
-// This changes no styling. tokens.css's header comment ("TWO
-// SPELLINGS, ON PURPOSE, FOR ONE RELEASE") pairs every rst- class with
-// an attribute selector of equal specificity precisely so a caller can
-// do this — ui_test.go's TestEveryClassSelectorHasAnAttributeTwin
-// holds both the pairing and the specificity upstream. Skipping this
-// step would leave two class="rst-*" occurrences on every page this
-// app serves, regardless of what markup Task 3 and its neighbors write
-// themselves — this app's own "never class=\"rst-*\"" rule has to
-// cover the shell, not just the content each page owns.
-func shellAttributeGrammar(layout []byte) []byte {
- s := string(layout)
- s = strings.ReplaceAll(s, `class="rst-skip"`, `rst-skip`)
- s = strings.ReplaceAll(s, `class="rst-page"`, `rst-page`)
- return []byte(s)
-}
-
// page renders the named page's "layout" template — its own content
// wrapped in rastrillo's shell — into w.
func (rd *renderer) page(w http.ResponseWriter, r *http.Request, name string, data any) error {
diff --git a/internal/docs/render_test.go b/internal/docs/render_test.go
new file mode 100644
index 0000000..e6bcc77
--- /dev/null
+++ b/internal/docs/render_test.go
@@ -0,0 +1,56 @@
+package docs
+
+import (
+ "io/fs"
+ "strings"
+ "testing"
+)
+
+// TestOurTemplatesUseTheAttributeGrammar holds the markup constraint
+// at the source, not the rendered page: every HTML file this app
+// authors under views/ (embedded as viewsFS) must write the bare
+// rst-* attribute grammar (<div rst-box>), never class="rst-*".
+//
+// This is deliberately a package-internal test, not a docs_test one,
+// because it needs viewsFS itself — walking our own template sources
+// rather than a rendered response is the whole point: rastrillo's own
+// partials and shell emit class="rst-*", and that is their output,
+// not this app's markup, so it must never appear in this walk in the
+// first place, rather than being screened out after the fact.
+//
+// This replaces what used to be a rendered-page assertion
+// ("no class=\"rst-\" anywhere in the response body"), which could
+// not tell the two apart: rastrillo's column shell hard-codes
+// class="rst-skip" and class="rst-page" outside any block this app
+// can override, so that blanket check failed on every page this app
+// serves regardless of what its own templates wrote — see git history
+// on this file for the reverted fix (rewriting rastrillo's shell
+// bytes at parse time) and why it was wrong: a string substitution
+// over a dependency's markup fails silently the day rastrillo changes
+// those two spots, which it has already said it will before 1.0.
+// Checking our own sources instead needs no such rewrite, is correct
+// (rastrillo's vendored output is not ours to hold to our own
+// convention), and stays stable across a rastrillo upgrade that
+// changes its shell's own spelling, because it does not depend on
+// rastrillo's internal template text at all.
+func TestOurTemplatesUseTheAttributeGrammar(t *testing.T) {
+ err := fs.WalkDir(viewsFS, ".", func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if d.IsDir() {
+ return nil
+ }
+ b, err := fs.ReadFile(viewsFS, path)
+ if err != nil {
+ return err
+ }
+ if strings.Contains(string(b), `class="rst-`) {
+ t.Errorf("%s: class vocabulary found — this app writes the attribute grammar (Global Constraints)", path)
+ }
+ return nil
+ })
+ if err != nil {
+ t.Fatalf("walk viewsFS: %v", err)
+ }
+}
diff --git a/internal/docs/views/list.html b/internal/docs/views/list.html
index b7d2c1b..c84292f 100644
--- a/internal/docs/views/list.html
+++ b/internal/docs/views/list.html
@@ -3,11 +3,14 @@
through rastrillo.T, EmptyTitle/EmptyBody the same; a row's Title
and When are the document's own data and are printed as-is.
- Writes the bare rst-* attribute grammar throughout, never
- class="rst-*" — this app's whole-screen markup convention (Task
- 1's home page set it; render.go's shellAttributeGrammar carries
- it into the two spots rastrillo's own shell hard-codes). The
- three [docs-*] selectors below are this app's own vocabulary, not
+ Writes the bare rst-* attribute grammar throughout — the rst-
+ vocabulary always as a bare attribute, never as a class — which is
+ this app's whole-screen markup convention (Task 1's home page set
+ it). render_test.go's TestOurTemplatesUseTheAttributeGrammar walks
+ this file's own bytes to hold that; rastrillo's vendored shell and
+ partials keep their own class spelling, which is their output, not
+ this app's, and is out of that test's reach on purpose. The three
+ [docs-*] selectors below are this app's own vocabulary, not
rastrillo's — SKILL.md §7's "ui is rastrillo's vocabulary, not
yours" is why they are not spelled rst-*, and they carry their
own tiny stylesheet here rather than a static/ file this