rastrillo / fiesta Public

Move classSpelling into check, and drop the keep entry staticcheck does not need

Review of Task 3, Important 1, Minor 5, Minor 6 and Minor 7.

Ruling (b) named classSpelling among the helpers this task moves,
and it was left out. It is now in check/rig_browser_test.go with
openTagInFixture and rstAttrInFixture, verbatim from SEED's
ui/markup_v3_browser_test.go:485 except that it reads
fiesta.TokensCSS. What it reaches in the untagged
ui/markup_v3_test.go (attributeFor, classInSelector, classInMarkup,
stripCSSComments) is in check/markuphelp_test.go, untagged, as
canonColours was done. The markup-v3 and card ports both delete it
from the files they copy.

serveScript checks the .js suffix before it asks the lookup for the
file: cheaper, and the refusal reads first.

The keep entry for canonColours is gone: make staticcheck runs with
-tags browser only, where computed reaches it, and staticcheck
agrees (exit 0 without the entry).

TestTheDepthFixtureServesItsStylesheets, seen red: with the theme
route reading fiesta.ThemeCSS("no-such-theme") in a scratch copy it
fails on "GET /theme.css answered 500, want 200", "--rst-radius is
empty on the page; the theme did not apply" and the harness's HTTP
500 problem. Reverted; green again.
Human

Prompt

Task 3 fix round 1: the review at task-3-review.md, items Important 1 and Minor 1 to 8, as the coordinator ruled them.

From this commit's own message. The prompt ledger has no entries for it — which is what a squashed, rebased or amended commit looks like, and what every commit looks like in a repo without a ledger.

Summary

Adds classSpelling with its two fixture regexes to check/rig_browser_test.go and its untagged dependencies (attributeFor, classInSelector, classInMarkup, stripCSSComments) to check/markuphelp_test.go; checks the .js suffix before the script lookup; removes the canonColours keep entry.
Agent notes
The F10 list in the task report now names classSpelling and its four untagged dependencies. The rig keep list gained classSpelling; the controller tells later tasks which entries to delete.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev b4f0664483743b78811e29429da609c895b07245 parent 3079cd5
3 files changed, +125 −9
  • check/colourhelp_test.go +0 −7
  • check/markuphelp_test.go +39 −0
  • check/rig_browser_test.go +86 −2
diff --git a/check/colourhelp_test.go b/check/colourhelp_test.go
index 6026b26..e6203f8 100644
--- a/check/colourhelp_test.go
+++ b/check/colourhelp_test.go
@@ -13,13 +13,6 @@ import (
// beside (evalColour, rgba, accentSupportsBlock) is ported with the
// gate that uses it.
-// Landed ahead of its callers, which the gates and drives that follow
-// bring with them: computed() reads through it under the browser tag,
-// and the derived-token gate reads it untagged. Until the untagged one
-// lands, this is the use staticcheck's U1000 asks for; delete it with
-// that port.
-var _ = canonColours
-
// oklchClip renders OKLCh as eight-bit sRGB the way Chromium paints an
// out-of-gamut colour: each linear channel clipped to 0..1. Lightness
// and chroma are both kept, so a clipped colour is brighter than the
diff --git a/check/markuphelp_test.go b/check/markuphelp_test.go
new file mode 100644
index 0000000..8fd0bb0
--- /dev/null
+++ b/check/markuphelp_test.go
@@ -0,0 +1,39 @@
+package check
+
+import (
+ "regexp"
+ "strings"
+
+ "amadan.net/rastrillo/fiesta/internal/markup"
+)
+
+// What classSpelling (rig_browser_test.go) reaches in rastrillo's
+// untagged ui/markup_v3_test.go, verbatim: the class-to-attribute
+// lookup, the two regexes that find a class in a selector and in
+// markup, and the comment stripper. The CSS reader beside them
+// (parseCSSRules, splitSelectorList, specificity) is ported with the
+// gate that uses it.
+
+func attributeFor(class string) (name, variant string, ok bool) { return markup.Attribute(class) }
+
+var (
+ classInSelector = regexp.MustCompile(`\.(rst-[A-Za-z0-9_-]+)`)
+ classInMarkup = regexp.MustCompile(`class="([^"]*)"`)
+)
+
+func stripCSSComments(s string) string {
+ var b strings.Builder
+ for {
+ k := strings.Index(s, "/*")
+ if k < 0 {
+ b.WriteString(s)
+ return b.String()
+ }
+ b.WriteString(s[:k])
+ j := strings.Index(s[k+2:], "*/")
+ if j < 0 {
+ return b.String()
+ }
+ s = s[k+2+j+2:]
+ }
+}
diff --git a/check/rig_browser_test.go b/check/rig_browser_test.go
index cebb2f5..3bcf3ed 100644
--- a/check/rig_browser_test.go
+++ b/check/rig_browser_test.go
@@ -78,8 +78,12 @@ func sizingMuxWith(t *testing.T, pages map[string]string, scripts func(name stri
w.Write(svg)
})
serveScript := func(w http.ResponseWriter, r *http.Request, name string) {
+ if !strings.HasSuffix(name, ".js") {
+ http.NotFound(w, r)
+ return
+ }
body, ok := scripts(name)
- if !ok || !strings.HasSuffix(name, ".js") {
+ if !ok {
http.NotFound(w, r)
return
}
@@ -492,6 +496,86 @@ func RequireAnchorPositioning(t *testing.T, ctx context.Context, where, selector
}
}
+// ── The other spelling (ui/markup_v3_browser_test.go) ────────────────
+
+// classSpelling translates markup the other way — rst- attributes back
+// into a class list — with tokens.css's own class list as the key. That
+// key is unambiguous because TestNoTwoClassesWantTheSameAttribute
+// holds: no two classes want one attribute, so no attribute has two
+// classes to choose between.
+//
+// It exists because of the flip. The partials and the styleguide hand
+// this drive attribute markup now, and a fixture half in each spelling
+// would compare identical halves and prove nothing about them.
+func classSpelling(t *testing.T, html string) string {
+ t.Helper()
+ byAttr := map[string]string{}
+ byVariant := map[string]string{}
+ for _, m := range classInSelector.FindAllStringSubmatch(stripCSSComments(string(fiesta.TokensCSS())), -1) {
+ class := m[1]
+ name, variant, ok := attributeFor(class)
+ if !ok {
+ continue
+ }
+ if variant == "" {
+ byAttr[name] = class
+ continue
+ }
+ byVariant[name+"~="+variant] = class
+ }
+
+ out := openTagInFixture.ReplaceAllStringFunc(html, func(tag string) string {
+ // The class attribute comes out of the tag before the scan: its
+ // tokens follow whitespace exactly as an attribute does, and the
+ // seven utilities that live there must survive the round trip.
+ kept := ""
+ if m := classInMarkup.FindStringSubmatch(tag); m != nil {
+ kept = m[1]
+ }
+ stripped := classInMarkup.ReplaceAllString(tag, "")
+
+ var classes []string
+ rewritten := rstAttrInFixture.ReplaceAllStringFunc(stripped, func(m string) string {
+ g := rstAttrInFixture.FindStringSubmatch(m)
+ name, value := g[1], g[2]
+ if name == "rst-tone" {
+ return ` data-tone="` + value + `"`
+ }
+ base, known := byAttr[name]
+ if !known {
+ t.Errorf("the fixture writes [%s], which tokens.css has no class for — the round trip cannot be checked", name)
+ return m
+ }
+ classes = append(classes, base)
+ for _, variant := range strings.Fields(value) {
+ v, ok := byVariant[name+"~="+variant]
+ if !ok {
+ t.Errorf("the fixture writes %s=%q, which tokens.css has no class for", name, variant)
+ continue
+ }
+ classes = append(classes, v)
+ }
+ return ""
+ })
+ if kept != "" {
+ classes = append(classes, strings.Fields(kept)...)
+ }
+ if len(classes) == 0 {
+ return tag
+ }
+ // After the tag name, so the class lands where an author would
+ // have written it rather than at the end of the attribute list.
+ i := strings.IndexAny(rewritten, " \t\n>")
+ return rewritten[:i] + ` class="` + strings.Join(classes, " ") + `"` + rewritten[i:]
+ })
+ return out
+}
+
+var (
+ openTagInFixture = regexp.MustCompile(`<[a-zA-Z][^>]*>`)
+ rstAttrInFixture = regexp.MustCompile(`(?:^|\s)(rst-[a-z0-9-]+)(?:="([^"]*)")?`)
+)
+
// ── Landed ahead of their drives ─────────────────────────────────────
// The helpers above arrived before the drives that call them, which
@@ -501,7 +585,7 @@ func RequireAnchorPositioning(t *testing.T, ctx context.Context, where, selector
var _ = []any{
sizingRig, requirePointer, tokensJS, typeReading{}, fontsJS, fontReading{},
still, forceState, computed, rect, open, pressFallbackRig,
- probe, readTargets, settleUntil,
+ probe, readTargets, settleUntil, classSpelling,
}
// ── The fixture server's own test ────────────────────────────────────