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>
3 files changed,
+125
−9
check/colourhelp_test.go+0 −7check/markuphelp_test.go+39 −0check/rig_browser_test.go+86 −2
Intent
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 ──────────────────────────────────── |