rastrillo / fiesta Public

Read the themes in check with the declaration reader

Every gate that reads a theme read its text with patterns, and none
took the comments out first.

- themeTokens cut the first ":root {" to its first } and read
  --rst-* declarations with a pattern, so in
  :root { --rst-x: 1; /* --rst-x: 2; */ } the contrast gates measured
  2. It now reads the theme's top-level :root rules with
  declarationsOf, and day's relative-colour block by its context,
  which replaces the pattern that cut it out by its indentation.
- The scheme-number gate found the toggle rules and the dark-number
  blocks by searching the text, so a toggle quoted in a string
  counted for the real one the file no longer had. It now sorts each
  declaration by the rule and at-rule it sits in and refuses one that
  is in none of the allowed places. It returns the dark numbers, so
  pressNumber and the day test read them instead of searching block
  text again.
- The embedded-theme test, the both-schemes test and themePropSet
  counted and read declarations with patterns over the whole file;
  they read declarations now.
- splitLightDark read a comma inside a string as the split:
  light-dark("a, b") gave "a and b". It reads with cssSpan.
- The gallery's highlight-colour gate read each theme's :root the
  same way as themeTokens did. It moves into check, beside the other
  contrast gates, where it reads view.css with leafRules and the
  themes with themeTokens.

On the three shipped themes the old and new readers give the same
tokens in both schemes, the same token sets and the same dark
numbers.

Red: the commented declaration, the quoted light-dark() and the
toggle in a string, fed to the old readers, read wrongly; each is
now a control in TestDerivedColourEvaluatorReadsDaysShapes or
TestSchemeNumberGateRejectsWhatItMustReject. Mutations, each red and
reverted: no relative-colour override (T1), splitLightDark without
the scanner (T2), toggles counted from the text (T3), declarations
outside the allowed rules let through (T4), the two number blocks not
compared (T5).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 9797a6fdd41b695c96ebff90c4b66a710bb99d2d parent 072d79e
5 files changed, +261 −220
  • check/contrast_face_test.go +2 −4
  • check/contrast_test.go +110 −23
  • check/css_test.go +128 −92
  • check/derived_test.go +21 −21
  • internal/gallery/gallery_test.go +0 −80

CI passed — run details

CI log
=== gofmt ===
=== no-rastrillo ===
go test -run '^(TestFiestaNeverImportsRastrillo|TestThePublicPackagesLinkNoBrowser)$' -count=1 .
ok  	amadan.net/rastrillo/fiesta	0.155s
=== size ===
go test ./internal/build -run '^TestTheOneLinkIsUnder25KiBGzipped$' -count=1 -v
=== RUN   TestTheOneLinkIsUnder25KiBGzipped
    build_test.go:138: fiesta-plain.css: 158347 bytes, 23104 bytes gzipped (22.6 KiB of the 25.0 KiB limit)
    build_test.go:138: fiesta-signal.css: 158442 bytes, 23217 bytes gzipped (22.7 KiB of the 25.0 KiB limit)
    build_test.go:138: fiesta.css: 161261 bytes, 23823 bytes gzipped (23.3 KiB of the 25.0 KiB limit)
--- PASS: TestTheOneLinkIsUnder25KiBGzipped (0.02s)
PASS
ok  	amadan.net/rastrillo/fiesta/internal/build	0.026s
=== history ===
history: 50 commits; release tags: v1.0.0
=== suite ===
--- shard 0 of 48 ---
==> shard 0 of 48: make root
go build ./...
go vet ./...
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh root 'internal/gallery@3 cmd/fiesta@2 internal/www@2'
ok  	amadan.net/rastrillo/fiesta	4.585s
ok  	amadan.net/rastrillo/fiesta/check	8.716s
ok  	amadan.net/rastrillo/fiesta/example	0.208s
?   	amadan.net/rastrillo/fiesta/hack/seed/english	[no test files]
ok  	amadan.net/rastrillo/fiesta/internal/axe	0.214s
ok  	amadan.net/rastrillo/fiesta/internal/build	1.536s
ok  	amadan.net/rastrillo/fiesta/internal/codeview	0.022s
?   	amadan.net/rastrillo/fiesta/internal/harness	[no test files]
ok  	amadan.net/rastrillo/fiesta/internal/markup	0.012s
ok  	amadan.net/rastrillo/fiesta/internal/nodetest	0.604s
ok  	amadan.net/rastrillo/fiesta/internal/rowrule	0.014s
ok  	amadan.net/rastrillo/fiesta/internal/site	3.571s
ok  	amadan.net/rastrillo/fiesta/internal/tokens	0.049s
suite: root took 15s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m11.170000s 0m2.030000s
--- shard 1 of 48 ---
==> shard 1 of 48: make test.internal.gallery~1of3
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'internal.gallery~1of3'
gotest: slice 1 of 3 of ./internal/gallery/ runs 36 of 108 test tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	66.208s
suite: test.internal.gallery~1of3 took 71s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m49.780000s 0m1.220000s
--- shard 2 of 48 ---
==> shard 2 of 48: make test.internal.gallery~2of3
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'internal.gallery~2of3'
gotest: slice 2 of 3 of ./internal/gallery/ runs 36 of 108 test tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	70.320s
suite: test.internal.gallery~2of3 took 75s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m52.960000s 0m1.590000s
--- shard 3 of 48 ---
==> shard 3 of 48: make test.internal.gallery~3of3
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'internal.gallery~3of3'
gotest: slice 3 of 3 of ./internal/gallery/ runs 36 of 108 test tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	63.211s
suite: test.internal.gallery~3of3 took 67s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m46.680000s 0m1.380000s
--- shard 4 of 48 ---
==> shard 4 of 48: make test.cmd.fiesta~1of2
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'cmd.fiesta~1of2'
gotest: slice 1 of 2 of ./cmd/fiesta/ runs 7 of 12 test tests
ok  	amadan.net/rastrillo/fiesta/cmd/fiesta	54.191s
suite: test.cmd.fiesta~1of2 took 55s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m32.850000s 0m2.130000s
--- shard 5 of 48 ---
==> shard 5 of 48: make test.cmd.fiesta~2of2
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'cmd.fiesta~2of2'
gotest: slice 2 of 2 of ./cmd/fiesta/ runs 5 of 12 test tests
ok  	amadan.net/rastrillo/fiesta/cmd/fiesta	56.597s
suite: test.cmd.fiesta~2of2 took 58s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m35.340000s 0m2.330000s
--- shard 6 of 48 ---
==> shard 6 of 48: make test.internal.www~1of2
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'internal.www~1of2'
gotest: slice 1 of 2 of ./internal/www/ runs 11 of 25 test tests
ok  	amadan.net/rastrillo/fiesta/internal/www	65.903s
suite: test.internal.www~1of2 took 68s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m46.890000s 0m1.570000s
--- shard 7 of 48 ---
==> shard 7 of 48: make test.internal.www~2of2
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'internal.www~2of2'
gotest: slice 2 of 2 of ./internal/www/ runs 14 of 25 test tests
ok  	amadan.net/rastrillo/fiesta/internal/www	64.074s
suite: test.internal.www~2of2 took 66s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m44.520000s 0m1.570000s
--- shard 8 of 48 ---
==> shard 8 of 48: make staticcheck
go run honnef.co/go/tools/cmd/staticcheck@v0.8.2 -tags browser ./...
suite: staticcheck took 10s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m14.200000s 0m0.940000s
--- shard 9 of 48 ---
==> shard 9 of 48: make govulncheck
go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 ./...
=== Symbol Results ===

No vulnerabilities found.

Your code is affected by 0 vulnerabilities.
This scan also found 2 vulnerabilities in packages you import and 5
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
Use '-show verbose' for more details.
suite: govulncheck took 8s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m3.030000s 0m0.380000s
--- shard 10 of 48 ---
==> shard 10 of 48: make gitleaks
go run github.com/zricethezav/gitleaks/v8@v8.30.1 git --no-banner --redact --log-opts='--full-history --diff-filter=tuxdb HEAD' .
1:08PM INF Unknown SCM platform. Use --platform to include links in findings. host=amadan.net
1:08PM INF 50 commits scanned.
1:08PM INF scanned ~9368747 bytes (9.37 MB) in 7.2s
1:08PM INF no leaks found
suite: gitleaks took 8s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m8.000000s 0m0.190000s
--- shard 11 of 48 ---
==> shard 11 of 48: make browser.internal.harness
./hack/gotest.sh browser 'internal.harness'
gotest: on ip-172-31-19-131, driving the harness's own choice (/usr/local/bin/chromium)
gotest: ./internal/harness/ runs its 10 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/harness	7.109s
suite: browser.internal.harness took 10s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m1.740000s 0m1.370000s
--- shard 12 of 48 ---
==> shard 12 of 48: make browser.check~1of13
./hack/gotest.sh browser 'check~1of13'
gotest: on ip-172-31-19-131, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 1 of 13 of ./check/ runs 17 of 228 browser tests
ok  	amadan.net/rastrillo/fiesta/check	89.192s
suite: browser.check~1of13 took 100s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m48.600000s 0m14.120000s
--- shard 13 of 48 ---
==> shard 13 of 48: make browser.check~2of13
./hack/gotest.sh browser 'check~2of13'
gotest: on ip-172-31-19-131, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 2 of 13 of ./check/ runs 17 of 228 browser tests
ok  	amadan.net/rastrillo/fiesta/check	100.126s
suite: browser.check~2of13 took 110s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m55.550000s 0m15.950000s
--- shard 14 of 48 ---
==> shard 14 of 48: make browser.check~3of13
./hack/gotest.sh browser 'check~3of13'
gotest: on ip-172-31-19-131, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 3 of 13 of ./check/ runs 16 of 228 browser tests
ok  	amadan.net/rastrillo/fiesta/check	90.561s
suite: browser.check~3of13 took 101s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m43.180000s 0m12.930000s
--- shard 15 of 48 ---
==> shard 15 of 48: make browser.check~4of13
./hack/gotest.sh browser 'check~4of13'
gotest: on ip-172-31-19-131, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 4 of 13 of ./check/ runs 16 of 228 browser tests
ok  	amadan.net/rastrillo/fiesta/check	90.939s
suite: browser.check~4of13 took 102s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m55.010000s 0m17.310000s
--- shard 16 of 48 ---
==> shard 16 of 48: make browser.check~5of13
./hack/gotest.sh browser 'check~5of13'
gotest: on ip-172-31-19-131, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 5 of 13 of ./check/ runs 18 of 228 browser tests
ok  	amadan.net/rastrillo/fiesta/check	81.240s
suite: browser.check~5of13 took 91s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m40.710000s 0m14.690000s
--- shard 17 of 48 ---
==> shard 17 of 48: make browser.check~6of13
./hack/gotest.sh browser 'check~6of13'
gotest: on ip-172-31-19-131, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 6 of 13 of ./check/ runs 18 of 228 browser tests
ok  	amadan.net/rastrillo/fiesta/check	84.933s
suite: browser.check~6of13 took 95s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m47.020000s 0m16.610000s
--- shard 18 of 48 ---
==> shard 18 of 48: make browser.check~7of13
./hack/gotest.sh browser 'check~7of13'
gotest: on ip-172-31-19-131, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 7 of 13 of ./check/ runs 18 of 228 browser tests
ok  	amadan.net/rastrillo/fiesta/check	85.081s
suite: browser.check~7of13 took 95s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m46.460000s 0m14.860000s
--- shard 19 of 48 ---
==> shard 19 of 48: make browser.check~8of13
./hack/gotest.sh browser 'check~8of13'
gotest: on ip-172-31-19-131, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 8 of 13 of ./check/ runs 18 of 228 browser tests
ok  	amadan.net/rastrillo/fiesta/check	90.349s
suite: browser.check~8of13 took 101s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m44.310000s 0m14.940000s
--- shard 20 of 48 ---
==> shard 20 of 48: make browser.check~9of13
./hack/gotest.sh browser 'check~9of13'
gotest: on ip-172-31-19-131, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 9 of 13 of ./check/ runs 18 of 228 browser tests
ok  	amadan.net/rastrillo/fiesta/check	76.979s
suite: browser.check~9of13 took 87s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m31.830000s 0m10.430000s
--- shard 21 of 48 ---
==> shard 21 of 48: make browser.check~10of13
./hack/gotest.sh browser 'check~10of13'
gotest: on ip-172-31-19-131, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 10 of 13 of ./check/ runs 18 of 228 browser tests
ok  	amadan.net/rastrillo/fiesta/check	83.298s
suite: browser.check~10of13 took 94s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m39.600000s 0m12.800000s
--- shard 22 of 48 ---
==> shard 22 of 48: make browser.check~11of13
./hack/gotest.sh browser 'check~11of13'
gotest: on ip-172-31-19-131, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 11 of 13 of ./check/ runs 18 of 228 browser tests
ok  	amadan.net/rastrillo/fiesta/check	81.889s
suite: browser.check~11of13 took 92s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m41.210000s 0m13.760000s
--- shard 23 of 48 ---
==> shard 23 of 48: make browser.check~12of13
./hack/gotest.sh browser 'check~12of13'
gotest: on ip-172-31-19-131, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 12 of 13 of ./check/ runs 18 of 228 browser tests
ok  	amadan.net/rastrillo/fiesta/check	88.043s
suite: browser.check~12of13 took 98s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m47.440000s 0m15.600000s
--- shard 24 of 48 ---
==> shard 24 of 48: make browser.check~13of13
./hack/gotest.sh browser 'check~13of13'
gotest: on ip-172-31-19-131, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 13 of 13 of ./check/ runs 18 of 228 browser tests
ok  	amadan.net/rastrillo/fiesta/check	82.592s
suite: browser.check~13of13 took 93s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m40.010000s 0m12.550000s
--- shard 25 of 48 ---
==> shard 25 of 48: make browser.internal.gallery~1of18
./hack/gotest.sh browser 'internal.gallery~1of18'
gotest: on ip-172-31-19-131, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 1 of 18 of ./internal/gallery/ runs 6 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	39.633s
suite: browser.internal.gallery~1of18 took 49s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m15.500000s 0m3.930000s
--- shard 26 of 48 ---
==> shard 26 of 48: make browser.internal.gallery~2of18
./hack/gotest.sh browser 'internal.gallery~2of18'
gotest: on ip-172-31-19-131, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 2 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	51.582s
suite: browser.internal.gallery~2of18 took 60s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m15.190000s 0m3.280000s
--- shard 27 of 48 ---
==> shard 27 of 48: make browser.internal.gallery~3of18
./hack/gotest.sh browser 'internal.gallery~3of18'
gotest: on ip-172-31-19-131, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 3 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	60.087s
suite: browser.internal.gallery~3of18 took 69s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m16.280000s 0m3.940000s
--- shard 28 of 48 ---
==> shard 28 of 48: make browser.internal.gallery~4of18
./hack/gotest.sh browser 'internal.gallery~4of18'
gotest: on ip-172-31-19-131, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 4 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	50.918s
suite: browser.internal.gallery~4of18 took 60s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m14.750000s 0m3.180000s
--- shard 29 of 48 ---
==> shard 29 of 48: make browser.internal.gallery~5of18
./hack/gotest.sh browser 'internal.gallery~5of18'
gotest: on ip-172-31-19-131, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 5 of 18 of ./internal/gallery/ runs 6 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	63.508s
suite: browser.internal.gallery~5of18 took 72s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m19.580000s 0m5.730000s
--- shard 30 of 48 ---
==> shard 30 of 48: make browser.internal.gallery~6of18
./hack/gotest.sh browser 'internal.gallery~6of18'
gotest: on ip-172-31-19-131, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 6 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	57.858s
suite: browser.internal.gallery~6of18 took 67s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m16.480000s 0m4.220000s
--- shard 31 of 48 ---
==> shard 31 of 48: make browser.internal.gallery~7of18
./hack/gotest.sh browser 'internal.gallery~7of18'
gotest: on ip-172-31-19-131, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 7 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	59.795s
suite: browser.internal.gallery~7of18 took 69s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m14.650000s 0m2.860000s
--- shard 32 of 48 ---
==> shard 32 of 48: make browser.internal.gallery~8of18
./hack/gotest.sh browser 'internal.gallery~8of18'
gotest: on ip-172-31-19-131, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 8 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	60.861s
suite: browser.internal.gallery~8of18 took 70s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m18.820000s 0m4.870000s
--- shard 33 of 48 ---
==> shard 33 of 48: make browser.internal.gallery~9of18
./hack/gotest.sh browser 'internal.gallery~9of18'
gotest: on ip-172-31-19-131, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 9 of 18 of ./internal/gallery/ runs 6 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	69.220s
suite: browser.internal.gallery~9of18 took 78s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m24.470000s 0m7.820000s
--- shard 34 of 48 ---
==> shard 34 of 48: make browser.internal.gallery~10of18
./hack/gotest.sh browser 'internal.gallery~10of18'
gotest: on ip-172-31-19-131, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 10 of 18 of ./internal/gallery/ runs 2 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	65.209s
suite: browser.internal.gallery~10of18 took 74s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m14.880000s 0m2.930000s
--- shard 35 of 48 ---
==> shard 35 of 48: make browser.internal.gallery~11of18
./hack/gotest.sh browser 'internal.gallery~11of18'
gotest: on ip-172-31-19-131, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 11 of 18 of ./internal/gallery/ runs 2 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	66.247s
suite: browser.internal.gallery~11of18 took 75s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m15.150000s 0m3.120000s
--- shard 36 of 48 ---
==> shard 36 of 48: make browser.internal.gallery~12of18
./hack/gotest.sh browser 'internal.gallery~12of18'
gotest: on ip-172-31-19-131, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 12 of 18 of ./internal/gallery/ runs 2 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	69.414s
suite: browser.internal.gallery~12of18 took 78s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m19.740000s 0m5.240000s
--- shard 37 of 48 ---
==> shard 37 of 48: make browser.internal.gallery~13of18
./hack/gotest.sh browser 'internal.gallery~13of18'
gotest: on ip-172-31-19-131, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 13 of 18 of ./internal/gallery/ runs 4 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	69.411s
suite: browser.internal.gallery~13of18 took 78s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m19.990000s 0m5.640000s
--- shard 38 of 48 ---
==> shard 38 of 48: make browser.internal.gallery~14of18
./hack/gotest.sh browser 'internal.gallery~14of18'
gotest: on ip-172-31-19-131, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 14 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	69.633s
suite: browser.internal.gallery~14of18 took 79s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m20.040000s 0m5.620000s
--- shard 39 of 48 ---
==> shard 39 of 48: make browser.internal.gallery~15of18
./hack/gotest.sh browser 'internal.gallery~15of18'
gotest: on ip-172-31-19-131, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 15 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	71.565s
suite: browser.internal.gallery~15of18 took 80s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m20.390000s 0m5.950000s
--- shard 40 of 48 ---
==> shard 40 of 48: make browser.internal.gallery~16of18
./hack/gotest.sh browser 'internal.gallery~16of18'
gotest: on ip-172-31-19-131, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 16 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	66.636s
suite: browser.internal.gallery~16of18 took 76s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m17.540000s 0m4.660000s
--- shard 41 of 48 ---
==> shard 41 of 48: make browser.internal.gallery~17of18
./hack/gotest.sh browser 'internal.gallery~17of18'
gotest: on ip-172-31-19-131, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 17 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	70.439s
suite: browser.internal.gallery~17of18 took 79s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m19.950000s 0m5.960000s
--- shard 42 of 48 ---
==> shard 42 of 48: make browser.internal.gallery~18of18
./hack/gotest.sh browser 'internal.gallery~18of18'
gotest: on ip-172-31-19-131, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 18 of 18 of ./internal/gallery/ runs 6 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	72.011s
suite: browser.internal.gallery~18of18 took 81s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m21.590000s 0m6.810000s
--- shard 43 of 48 ---
==> shard 43 of 48: make browser.internal.gallery.history
./hack/gotest.sh browser 'internal.gallery.history'
gotest: on ip-172-31-19-131, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: ./internal/gallery/history/ runs its 3 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery/history	36.310s
suite: browser.internal.gallery.history took 40s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m21.260000s 0m2.060000s
--- shard 44 of 48 ---
==> shard 44 of 48: make browser.internal.www~1of4
./hack/gotest.sh browser 'internal.www~1of4'
gotest: on ip-172-31-19-131, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 1 of 4 of ./internal/www/ runs 5 of 24 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/www	83.343s
suite: browser.internal.www~1of4 took 89s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m37.930000s 0m2.310000s
--- shard 45 of 48 ---
==> shard 45 of 48: make browser.internal.www~2of4
./hack/gotest.sh browser 'internal.www~2of4'
gotest: on ip-172-31-19-131, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 2 of 4 of ./internal/www/ runs 7 of 24 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/www	83.838s
suite: browser.internal.www~2of4 took 89s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m52.920000s 0m3.400000s
--- shard 46 of 48 ---
==> shard 46 of 48: make browser.internal.www~3of4
./hack/gotest.sh browser 'internal.www~3of4'
gotest: on ip-172-31-19-131, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 3 of 4 of ./internal/www/ runs 6 of 24 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/www	85.324s
suite: browser.internal.www~3of4 took 91s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m52.890000s 0m3.070000s
--- shard 47 of 48 ---
==> shard 47 of 48: make browser.internal.www~4of4
./hack/gotest.sh browser 'internal.www~4of4'
gotest: on ip-172-31-19-131, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 4 of 4 of ./internal/www/ runs 6 of 24 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/www	81.416s
suite: browser.internal.www~4of4 took 87s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m46.090000s 0m3.230000s
diff --git a/check/contrast_face_test.go b/check/contrast_face_test.go
index 0415955..2823e93 100644
--- a/check/contrast_face_test.go
+++ b/check/contrast_face_test.go
@@ -100,10 +100,8 @@ func pressNumber(t *testing.T, theme, scheme, name string) float64 {
v := themeTokens(t, theme)["light"][name]
if scheme == "dark" {
css, _ := fiesta.ThemeCSS(theme)
- for _, block := range schemeNumberBlocks(t, theme, string(css)) {
- if m := regexp.MustCompile(regexp.QuoteMeta(name) + `\s*:\s*([^;]+);`).FindStringSubmatch(block); m != nil {
- v = strings.TrimSpace(m[1])
- }
+ if dark, ok := schemeNumberBlocks(t, theme, string(css))[name]; ok {
+ v = dark
}
}
f, err := strconv.ParseFloat(v, 64)
diff --git a/check/contrast_test.go b/check/contrast_test.go
index 7ce2393..74c8487 100644
--- a/check/contrast_test.go
+++ b/check/contrast_test.go
@@ -2,6 +2,8 @@ package check
import (
"math"
+ "os"
+ "path/filepath"
"regexp"
"strconv"
"strings"
@@ -64,24 +66,43 @@ var colorMixSkip = map[string]bool{
// pair below names it. See the note above the pair table.
}
-// declPattern matches one custom-property declaration: "--rst-name:
-// value;". Values never contain a semicolon in this file (no font stacks
-// with quoted commas-and-semicolons, no data URIs), so splitting on ';'
-// is safe here even though it would not be for arbitrary CSS.
-var declPattern = regexp.MustCompile(`(--rst-[a-z0-9-]+)\s*:\s*([^;]+);`)
-
-// parseTokens extracts every --rst-* declaration in body into a map. Only
-// the last declaration of a given name wins, matching CSS's own "later
-// wins" rule for same-specificity declarations in one block — not that
-// a theme file redeclares a name twice in the same block today.
-func parseTokens(body string) map[string]string {
+// declaredAt is what decls set at selector in context, property by
+// property, as the cascade reads rules of one selector: the last
+// !important declaration, else the last. A value keeps a trailing
+// " !important", so a gate comparing values sees it.
+func declaredAt(decls []declaration, context, selector string) map[string]string {
out := map[string]string{}
- for _, m := range declPattern.FindAllStringSubmatch(body, -1) {
- out[m[1]] = strings.TrimSpace(m[2])
+ important := map[string]bool{}
+ for _, d := range decls {
+ if d.Context != context || d.Selector != selector || (important[d.Property] && !d.Important) {
+ continue
+ }
+ out[d.Property], important[d.Property] = fullValue(d), d.Important
}
return out
}
+// fullValue is a declaration's value with its !important, if any.
+func fullValue(d declaration) string {
+ if d.Important {
+ return d.Value + " !important"
+ }
+ return d.Value
+}
+
+// themeDeclarations reads a theme file with the declaration reader,
+// failing the test on one it refuses. A theme was read with patterns
+// over its text, which counted a declaration in a comment or a string
+// as one the browser applies.
+func themeDeclarations(t testing.TB, where, css string) []declaration {
+ t.Helper()
+ decls, err := declarationsOf(where, css)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return decls
+}
+
// splitLightDark evaluates a whole-value light-dark(<light>, <dark>)
// call, returning the two halves. It reports false for anything else —
// including a value that merely *contains* a light-dark() somewhere
@@ -99,7 +120,13 @@ func splitLightDark(v string) (light, dark string, ok bool) {
}
inner := v[len(prefix) : len(v)-1]
depth, comma := 0, -1
- for i := 0; i < len(inner); i++ {
+ // Read with cssSpan, so a comma or a bracket inside a string is
+ // text: light-dark("a, b") is one argument, not two.
+ for i := 0; i < len(inner); {
+ end, comment, err := cssSpan(inner, i)
+ if err != nil || comment {
+ return "", "", false
+ }
switch inner[i] {
case '(':
depth++
@@ -116,6 +143,7 @@ func splitLightDark(v string) (light, dark string, ok bool) {
comma = i
}
}
+ i = end
}
if depth != 0 || comma < 0 {
return "", "", false
@@ -151,28 +179,37 @@ func blockBody(t *testing.T, where, css, header string, from int) string {
return ""
}
-// themeTokens reads the one :root block a v2 theme declares and returns
-// it twice: once resolved for the light scheme, once for dark. The
-// header ":root {" cannot match the two toggle rules at the bottom of the
-// file, whose selectors are ":root[data-theme=…] {".
+// themeTokens reads the :root a v2 theme declares and returns it
+// twice: once resolved for the light scheme, once for dark.
func themeTokens(t *testing.T, theme string) map[string]map[string]string {
t.Helper()
raw, ok := fiesta.ThemeCSS(theme)
if !ok {
t.Fatalf("fiesta.ThemeCSS(%q) missing", theme)
}
- css := string(raw)
- where := "themes/" + theme + ".css"
+ return themeTokensOf(t, "themes/"+theme+".css", string(raw))
+}
- declared := parseTokens(blockBody(t, where, css, ":root {", 0))
+// themeTokensOf is themeTokens over a theme's text: the --rst-
+// properties its top-level :root rules declare, read with the
+// declaration reader, so one in a comment or a string is not one.
+func themeTokensOf(t testing.TB, where, css string) map[string]map[string]string {
+ t.Helper()
+ decls := themeDeclarations(t, where, css)
+ declared := map[string]string{}
+ for name, v := range declaredAt(decls, "", ":root") {
+ if strings.HasPrefix(name, "--rst-") {
+ declared[name] = v
+ }
+ }
if len(declared) == 0 {
t.Fatalf("%s declares no --rst- properties in its :root block", where)
}
// day redeclares its hue-derived tokens inside an @supports block
// for relative colour (derived_test.go). Those are what a current
// engine paints, so they win here, as they do in the cascade.
- if body, _ := accentSupportsBlock(css); body != "" {
- for name, v := range parseTokens(body) {
+ for name, v := range declaredAt(decls, accentSupports, ":root") {
+ if strings.HasPrefix(name, "--rst-") {
declared[name] = v
}
}
@@ -402,6 +439,56 @@ func TestThemeTokenContrastMeetsWCAG(t *testing.T) {
}
}
+// Every highlight colour of the gallery's code view reaches 4.5:1
+// against the code block's --rst-surface, in every theme and both
+// schemes, resolved from the themes' own light-dark() pairs. The axe
+// scan checks the rendered cascade; this checks every pair, including
+// the ones a page with no such token on it would never show axe. The
+// five rules are read from the gallery's view.css, the widget's half
+// of its stylesheet, where the code-block rules live.
+//
+// It lived beside the gallery and read each theme's :root with a
+// pattern, so a declaration in a comment there was measured in place
+// of the one a browser applies. Here it reads both stylesheets with
+// the declaration reader: view.css with leafRules, the themes with
+// themeTokens.
+func TestHighlightColoursPassAgainstTheCodeBackground(t *testing.T) {
+ view, err := os.ReadFile(filepath.Join("..", "internal", "gallery", "view.css"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ element := regexp.MustCompile(`^ds-[tavxs]$`)
+ token := regexp.MustCompile(`^var\((--rst-[a-z0-9-]+)\)$`)
+ pairs := map[string]string{}
+ for _, r := range leafRules(t, string(view)) {
+ if r.context != "" || !element.MatchString(r.selector) || len(r.decls) != 1 {
+ continue
+ }
+ if m := token.FindStringSubmatch(r.decls[0][1]); r.decls[0][0] == "color" && r.decls[0][2] == "" && m != nil {
+ pairs[r.selector] = m[1]
+ }
+ }
+ if len(pairs) != 5 {
+ t.Fatalf("view.css colours %d highlight elements, want 5: %v", len(pairs), pairs)
+ }
+ for _, theme := range fiesta.ThemeNames() {
+ for scheme, tok := range themeTokens(t, theme) {
+ bg := tok["--rst-surface"]
+ for el, name := range pairs {
+ fg := tok[name]
+ r, err := ContrastRatio(fg, bg)
+ if err != nil {
+ t.Errorf("%s %s %s (%s on %s): %v", theme, scheme, el, fg, bg, err)
+ continue
+ }
+ if r < 4.5 {
+ t.Errorf("%s %s: %s is %s (%s) on %s, %.2f:1, under 4.5:1", theme, scheme, el, name, fg, bg, r)
+ }
+ }
+ }
+ }
+}
+
// stageArtMix reads the colour tokens.css gives one part of the stage
// art — color-mix(in srgb, var(A) N%, var(B)) — so the check below
// measures what the stylesheet actually says rather than a copy of it.
diff --git a/check/css_test.go b/check/css_test.go
index f8aa21d..2cae5be 100644
--- a/check/css_test.go
+++ b/check/css_test.go
@@ -166,17 +166,31 @@ func TestThemeCSSIsEmbedded(t *testing.T) {
if !ok {
t.Fatalf("fiesta.ThemeCSS(%q) reports missing, but it is in fiesta.ThemeNames()", name)
}
+ // Read as declarations, so a token or a toggle that is only in
+ // a comment or a string is missing.
+ where := "themes/" + name + ".css"
+ decls := themeDeclarations(t, where, string(css))
+ root := declaredAt(decls, "", ":root")
for _, want := range []string{
"--rst-bg", "--rst-surface", "--rst-text", "--rst-accent",
"--rst-tone-positive-fg", "--rst-font", "--rst-radius", "--rst-shadow-pop",
- "color-scheme: light dark", "light-dark(",
- `:root[data-theme="dark"] { color-scheme: dark; }`,
- `:root[data-theme="light"] { color-scheme: light; }`,
} {
- if !strings.Contains(string(css), want) {
- t.Errorf("themes/%s.css is missing %q", name, want)
+ if _, ok := root[want]; !ok {
+ t.Errorf("%s declares no %s at :root", where, want)
}
}
+ if root["color-scheme"] != "light dark" {
+ t.Errorf("%s sets color-scheme %q at :root, want light dark", where, root["color-scheme"])
+ }
+ pairs := 0
+ for _, v := range root {
+ if _, _, ok := splitLightDark(v); ok {
+ pairs++
+ }
+ }
+ if pairs == 0 {
+ t.Errorf("%s declares no light-dark() pair at :root", where)
+ }
// A theme restates nothing per scheme but numbers:
// light-dark() cannot wrap a number, so the press strengths are
// written once in :root for light and again in two blocks for
@@ -184,16 +198,28 @@ func TestThemeCSSIsEmbedded(t *testing.T) {
// in both, each also declared in :root. Anything else in the
// file beyond :root and the two toggle rules is the second
// palette block light-dark() was adopted to make impossible.
+ // The toggles are read there too.
schemeNumberBlocks(t, name, string(css))
// day's relative-colour block (derived_test.go) nests a :root of
// its own; outside it the palette is one block.
- _, outside := accentSupportsBlock(string(css))
- if got := strings.Count(outside, ":root {"); got != 1 {
- t.Errorf("themes/%s.css opens %d bare :root blocks, want exactly 1", name, got)
+ bare := 0
+ for _, r := range leafRules(t, string(css)) {
+ if r.context == "" && r.selector == ":root" {
+ bare++
+ }
+ }
+ if bare != 1 {
+ t.Errorf("%s opens %d bare :root blocks, want exactly 1", where, bare)
}
- for _, prop := range []string{"--rst-bg:", "--rst-text:", "--rst-accent:", "--rst-radius:"} {
- if got := strings.Count(string(css), prop); got != 1 {
- t.Errorf("themes/%s.css declares %s %d times, want exactly 1 (one block, both schemes)", name, prop, got)
+ for _, prop := range []string{"--rst-bg", "--rst-text", "--rst-accent", "--rst-radius"} {
+ got := 0
+ for _, d := range decls {
+ if d.Property == prop {
+ got++
+ }
+ }
+ if got != 1 {
+ t.Errorf("%s declares %s: %d times, want exactly 1 (one block, both schemes)", where, prop, got)
}
}
}
@@ -271,20 +297,23 @@ func TestBothSchemesAreAuthoredInEveryTheme(t *testing.T) {
}
// The relative-colour block redeclares a handful of day's tokens
// (derived_test.go); the once-only count reads the file without
- // it, and the block may restate a name at most once.
- supports, css := accentSupportsBlock(string(raw))
- decl := regexp.MustCompile(`(--rst-[a-z0-9-]+)\s*:\s*([^;]+);`)
+ // it, and the block may restate a name at most once. Read as
+ // declarations, so one in a comment or a string is not counted.
values := map[string]string{}
- for _, m := range decl.FindAllStringSubmatch(css, -1) {
- values[m[1]] = strings.TrimSpace(m[2])
+ count, supports := map[string]int{}, map[string]int{}
+ for _, d := range themeDeclarations(t, "themes/"+name+".css", string(raw)) {
+ if d.Context == accentSupports {
+ supports[d.Property]++
+ continue
+ }
+ count[d.Property]++
+ values[d.Property] = fullValue(d)
}
for _, prop := range declaredOnce {
- // Declarations are "<prop>: value"; uses are "var(<prop>)", so the
- // trailing colon counts declarations only.
- if got := strings.Count(css, prop+":"); got != 1 {
+ if got := count[prop]; got != 1 {
t.Errorf("themes/%s.css declares %s %d times, want exactly 1", name, prop, got)
}
- if got := strings.Count(supports, prop+":"); got > 1 {
+ if got := supports[prop]; got > 1 {
t.Errorf("themes/%s.css redeclares %s %d times in its relative-colour block, want at most 1", name, prop, got)
}
}
@@ -948,7 +977,9 @@ func TestThemesDeclareIdenticalTokenSets(t *testing.T) {
}
}
-// themePropSet: every --rst-* name declared anywhere in the theme file.
+// themePropSet: every --rst-* name declared anywhere in the theme
+// file, read as declarations: a pattern over the text counted one a
+// comment mentions as declared.
func themePropSet(t *testing.T, name string) map[string]bool {
t.Helper()
css, ok := fiesta.ThemeCSS(name)
@@ -956,26 +987,28 @@ func themePropSet(t *testing.T, name string) map[string]bool {
t.Fatalf("fiesta.ThemeCSS(%q) missing", name)
}
out := map[string]bool{}
- for _, m := range regexp.MustCompile(`(--rst-[a-z0-9-]+)\s*:`).FindAllStringSubmatch(string(css), -1) {
- out[m[1]] = true
+ for _, d := range themeDeclarations(t, "themes/"+name+".css", string(css)) {
+ if strings.HasPrefix(d.Property, "--rst-") {
+ out[d.Property] = true
+ }
}
return out
}
-// schemeNumberBlocks returns the bodies of a theme's two dark-number
-// blocks, or nil when it has none, and fails the test for every problem
+// schemeNumberBlocks returns the numbers a theme restates for dark, or
+// nil when it restates none, and fails the test for every problem
// schemeNumberProblems reports.
-func schemeNumberBlocks(t *testing.T, name, css string) []string {
+func schemeNumberBlocks(t *testing.T, name, css string) map[string]string {
t.Helper()
- blocks, problems := schemeNumberProblems(name, css)
+ numbers, problems := schemeNumberProblems(name, css)
for _, p := range problems {
t.Error(p)
}
- return blocks
+ return numbers
}
// schemeNumberProblems reads a theme file against the shape a theme may take:
-// one `:root {` block; the two toggle rules
+// a :root rule; the two toggle rules
// `:root[data-theme="light"] { color-scheme: light; }` and
// `:root[data-theme="dark"] { color-scheme: dark; }`; and, together or
// not at all, `:root[data-theme="dark"] { numbers }` and
@@ -988,102 +1021,99 @@ func schemeNumberBlocks(t *testing.T, name, css string) []string {
// System on a dark OS has only the media query; one block alone hands
// the other reader the light numbers. Nothing beyond those five pieces
// may be in the file: anything else is the second palette block
-// light-dark() was adopted to make impossible.
-func schemeNumberProblems(name, css string) (blocks []string, problems []string) {
+// light-dark() was adopted to make impossible. It returns the numbers
+// restated for dark.
+//
+// It reads the file with the declaration reader and sorts each
+// declaration by the rule and at-rule it sits in. Patterns over the
+// text found a toggle or a number block quoted in a string, and took
+// it for the real one the file no longer had.
+func schemeNumberProblems(name, css string) (numbers map[string]string, problems []string) {
where := "themes/" + name + ".css"
bad := func(format string, args ...any) { problems = append(problems, where+": "+fmt.Sprintf(format, args...)) }
- stripped, err := cssWithoutComments(css)
+ decls, err := declarationsOf(where, css)
if err != nil {
bad("%v", err)
return nil, problems
}
const (
- toggleLight = `:root[data-theme="light"] { color-scheme: light; }`
- toggleDark = `:root[data-theme="dark"] { color-scheme: dark; }`
+ toggleLight = `:root[data-theme="light"]`
+ toggleDark = `:root[data-theme="dark"]`
+ darkMedia = "@media (prefers-color-scheme: dark)"
+ darkSystem = `:root:not([data-theme="light"])`
)
- if strings.Count(stripped, toggleLight) != 1 || strings.Count(stripped, toggleDark) != 1 {
- bad("does not carry the two toggle rules exactly once")
- }
- rest := strings.Replace(strings.Replace(stripped, toggleLight, "", 1), toggleDark, "", 1)
- // One more block is allowed: a relative-colour redeclaration of
- // names :root already declares (day's hue-derived fills), so that an
- // engine below the floor keeps the :root value instead of a fill
- // that resolves to nothing (derived_test.go). Nothing else may be in
- // it, and it may not introduce a name.
- supports, rest := accentSupportsBlock(rest)
- rootRe := regexp.MustCompile(`(?s):root \{(.*?)\n\}`)
- rootM := rootRe.FindStringSubmatch(rest)
- if rootM == nil {
+ root := declaredAt(decls, "", ":root")
+ if len(root) == 0 {
bad("has no :root block")
return nil, problems
}
- root := rootM[1]
- rest = strings.Replace(rest, rootM[0], "", 1)
- if supports != "" {
- declaredInRoot := parseTokens(root)
- for name := range parseTokens(supports) {
- if _, ok := declaredInRoot[name]; !ok {
- bad("redeclares %s in its relative-colour block but never declares it in :root, so an engine without relative colour has no value for it", name)
+ toggles := map[string]int{}
+ var attr, media []declaration
+ for _, d := range decls {
+ switch {
+ case d.Context == "" && d.Selector == ":root":
+ case d.Context == "" && d.Selector == toggleLight && d.Property == "color-scheme" && fullValue(d) == "light":
+ toggles["light"]++
+ case d.Context == "" && d.Selector == toggleDark && d.Property == "color-scheme" && fullValue(d) == "dark":
+ toggles["dark"]++
+ case d.Context == "" && d.Selector == toggleDark:
+ attr = append(attr, d)
+ case d.Context == darkMedia && d.Selector == darkSystem:
+ media = append(media, d)
+ // One more block is allowed: a relative-colour redeclaration of
+ // names :root already declares (day's hue-derived fills), so
+ // that an engine below the floor keeps the :root value instead
+ // of a fill that resolves to nothing (derived_test.go). Nothing
+ // else may be in it, and it may not introduce a name.
+ case d.Context == accentSupports && d.Selector == ":root":
+ if !strings.HasPrefix(d.Property, "--rst-") {
+ bad("carries something other than --rst-* declarations in its relative-colour block: %s", d)
+ } else if _, ok := root[d.Property]; !ok {
+ bad("redeclares %s in its relative-colour block but never declares it in :root, so an engine without relative colour has no value for it", d.Property)
}
- }
- if strings.TrimSpace(regexp.MustCompile(`--rst-[a-z0-9-]+\s*:[^;]+;`).ReplaceAllString(supports, "")) != "" {
- bad("carries something other than --rst-* declarations in its relative-colour block")
+ default:
+ bad("carries rules beyond :root, the toggles and the two dark-number blocks: %s", d)
}
}
- attr := regexp.MustCompile(`:root\[data-theme="dark"\] \{([^{}]*)\}`).FindAllStringSubmatch(rest, -1)
- media := regexp.MustCompile(`@media \(prefers-color-scheme: dark\) \{\s*:root:not\(\[data-theme="light"\]\) \{([^{}]*)\}\s*\}`).FindAllStringSubmatch(rest, -1)
- for _, m := range attr {
- rest = strings.Replace(rest, m[0], "", 1)
- }
- for _, m := range media {
- rest = strings.Replace(rest, m[0], "", 1)
- }
- if strings.TrimSpace(rest) != "" {
- bad("carries rules beyond :root, the toggles and the two dark-number blocks:\n%s", strings.TrimSpace(rest))
+ if toggles["light"] != 1 || toggles["dark"] != 1 {
+ bad("does not carry the two toggle rules exactly once")
}
if len(attr) == 0 && len(media) == 0 {
return nil, problems
}
- if len(attr) != 1 || len(media) != 1 {
- bad("has %d attribute and %d media dark-number blocks, want one of each or none", len(attr), len(media))
+ if len(attr) == 0 || len(media) == 0 {
+ bad("restates dark numbers in %d attribute and %d media declarations, want them in both blocks or neither", len(attr), len(media))
return nil, problems
}
- decl := regexp.MustCompile(`^(--rst-[a-z0-9-]+)\s*:\s*(.+)$`)
num := regexp.MustCompile(`^-?[0-9]*\.?[0-9]+(px|em|rem)?$`)
- inRoot := parseTokens(root)
- read := func(body string) map[string]string {
+ read := func(block []declaration) map[string]string {
out := map[string]string{}
// Every declaration is consumed by the numeric grammar, or the
// block is rejected: a stray `color: red;` must not pass because
// the reader only looked for --rst- names.
- for _, d := range strings.Split(body, ";") {
- d = strings.TrimSpace(d)
- if d == "" {
- continue
- }
- m := decl.FindStringSubmatch(d)
- if m == nil {
- bad("has %q in a scheme block; only --rst-* numbers may be restated per scheme", d)
+ for _, d := range block {
+ v := fullValue(d)
+ if !strings.HasPrefix(d.Property, "--rst-") {
+ bad("has %q in a scheme block; only --rst-* numbers may be restated per scheme", d.Property+": "+v)
continue
}
- v := strings.TrimSpace(m[2])
if !num.MatchString(v) {
- bad("restates %s as %q in a scheme block; only numbers may be restated per scheme", m[1], v)
+ bad("restates %s as %q in a scheme block; only numbers may be restated per scheme", d.Property, v)
}
- if rv, ok := inRoot[m[1]]; !ok {
- bad("restates %s for dark but never declares it in :root", m[1])
+ if rv, ok := root[d.Property]; !ok {
+ bad("restates %s for dark but never declares it in :root", d.Property)
} else if !num.MatchString(rv) {
- bad("declares %s as %q in :root; a number restated per scheme must be a number there too", m[1], rv)
+ bad("declares %s as %q in :root; a number restated per scheme must be a number there too", d.Property, rv)
}
- out[m[1]] = v
+ out[d.Property] = v
}
return out
}
- a, m := read(attr[0][1]), read(media[0][1])
+ a, m := read(attr), read(media)
if !reflect.DeepEqual(a, m) {
bad("two dark-number blocks differ: attribute %v, media %v", a, m)
}
- return []string{attr[0][1], media[0][1]}, problems
+ return a, problems
}
// TestSchemeNumberGateRejectsWhatItMustReject feeds the gate the shapes
@@ -1091,8 +1121,8 @@ func schemeNumberProblems(name, css string) (blocks []string, problems []string)
// themes is not known to refuse anything.
func TestSchemeNumberGateRejectsWhatItMustReject(t *testing.T) {
const good = ":root {\n --rst-bg: light-dark(#fff, #000);\n --rst-press-wash: 0.08;\n}\n:root[data-theme=\"light\"] { color-scheme: light; }\n:root[data-theme=\"dark\"] { color-scheme: dark; }\n:root[data-theme=\"dark\"] { --rst-press-wash: 0.18; }\n@media (prefers-color-scheme: dark) { :root:not([data-theme=\"light\"]) { --rst-press-wash: 0.18; } }\n"
- if blocks, problems := schemeNumberProblems("good", good); len(problems) != 0 || len(blocks) != 2 {
- t.Fatalf("the well-formed theme was refused: %v (%d blocks)", problems, len(blocks))
+ if numbers, problems := schemeNumberProblems("good", good); len(problems) != 0 || numbers["--rst-press-wash"] != "0.18" {
+ t.Fatalf("the well-formed theme was refused: %v (numbers %v)", problems, numbers)
}
withSupports := good + "@supports (color: oklch(from red l c h)) {\n :root {\n --rst-bg: light-dark(oklch(from #fff 0.5 c h), #000);\n }\n}\n"
if _, problems := schemeNumberProblems("good", withSupports); len(problems) != 0 {
@@ -1110,6 +1140,13 @@ func TestSchemeNumberGateRejectsWhatItMustReject(t *testing.T) {
"a supports block with a new name": good + "@supports (color: oklch(from red l c h)) {\n :root {\n --rst-new: #fff;\n }\n}\n",
"a supports block with a rule": good + "@supports (color: oklch(from red l c h)) {\n :root {\n --rst-bg: #fff; color: red;\n }\n}\n",
"a supports block for another feature": good + "@supports (color: oklch(0.5 0.1 20)) {\n :root {\n --rst-bg: #fff;\n }\n}\n",
+ // The real rule gone, and its text quoted in a string: a
+ // browser applies neither, so Dark has no color-scheme, or a
+ // reader who chose Dark gets the light numbers.
+ "a toggle only in a string": strings.Replace(strings.Replace(good, ":root[data-theme=\"dark\"] { color-scheme: dark; }\n", "", 1),
+ "--rst-press-wash: 0.08;", "--rst-press-wash: 0.08; --rst-note: ':root[data-theme=\"dark\"] { color-scheme: dark; }';", 1),
+ "a number block only in a string": strings.Replace(strings.Replace(good, ":root[data-theme=\"dark\"] { --rst-press-wash: 0.18; }\n", "", 1),
+ "--rst-press-wash: 0.08;", "--rst-press-wash: 0.08; --rst-note: ':root[data-theme=\"dark\"] { --rst-press-wash: 0.18; }';", 1),
} {
if _, problems := schemeNumberProblems("bad", css); len(problems) == 0 {
t.Errorf("%s was accepted", name)
@@ -1217,8 +1254,7 @@ func TestDayTurnsDepthOnAndTheOthersStayFlat(t *testing.T) {
}
}
css, _ := fiesta.ThemeCSS("day")
- blocks := schemeNumberBlocks(t, "day", string(css))
- if len(blocks) != 2 || !strings.Contains(blocks[0], "--rst-press-wash: 0.18;") {
+ if numbers := schemeNumberBlocks(t, "day", string(css)); numbers["--rst-press-wash"] != "0.18" {
t.Errorf("day restates no dark press strengths; want --rst-press-wash: 0.18 in both scheme blocks")
}
}
diff --git a/check/derived_test.go b/check/derived_test.go
index 5052bfe..303f81e 100644
--- a/check/derived_test.go
+++ b/check/derived_test.go
@@ -52,23 +52,13 @@ func (c rgba) String() string {
return fmt.Sprintf("rgba(%d, %d, %d, %s)", r, g, b, strconv.FormatFloat(c.a, 'f', -1, 64))
}
-// accentSupportsRe matches the one block a theme may add beyond its
-// :root, the toggles and the dark-number blocks: day's redeclaration of
-// its hue-derived tokens where the engine has relative colour syntax.
-// The test string is the exact one day.css writes; a block with another
-// condition is not this block and the structure gates refuse it.
-var accentSupportsRe = regexp.MustCompile(`(?s)@supports \(color: oklch\(from red l c h\)\) \{\s*:root \{(.*?)\n \}\n\}\n?`)
-
-// accentSupportsBlock returns the body of a theme's relative-colour
-// block, or "" when it has none, and the stylesheet with that block
-// removed, which is what the one-:root-block gates read.
-func accentSupportsBlock(css string) (body, rest string) {
- m := accentSupportsRe.FindStringSubmatchIndex(css)
- if m == nil {
- return "", css
- }
- return css[m[2]:m[3]], css[:m[0]] + css[m[1]:]
-}
+// accentSupports is the condition of the one block a theme may add
+// beyond its :root, the toggles and the dark-number blocks: day's
+// redeclaration of its hue-derived tokens where the engine has relative
+// colour syntax. It is the exact condition day.css writes, as a rule's
+// context names it; a block with another condition is not this block
+// and the structure gates refuse it.
+const accentSupports = "@supports (color: oklch(from red l c h))"
// resolvedTokens is themeTokens with every derived colour evaluated for
// its scheme. A value that is not a colour expression (a font stack, a
@@ -568,11 +558,21 @@ func TestDerivedColourEvaluatorReadsDaysShapes(t *testing.T) {
t.Errorf("%s evaluated; a shape the gate cannot read must be refused, not guessed", bad)
}
}
+ // A theme's tokens are what a browser applies: a declaration in a
+ // comment is prose, and day's relative-colour block wins where the
+ // engine reads it.
+ tok := themeTokensOf(t, "t.css", ":root { --rst-x: 1; /* --rst-x: 2; */ --rst-y: light-dark(#fff, #000); --rst-z: light-dark(\"a, b\"); }\n"+accentSupports+" {\n :root {\n --rst-y: light-dark(#eee, #111);\n }\n}\n")
+ for scheme, want := range map[string]map[string]string{
+ "light": {"--rst-x": "1", "--rst-y": "#eee", "--rst-z": `light-dark("a, b")`},
+ "dark": {"--rst-x": "1", "--rst-y": "#111", "--rst-z": `light-dark("a, b")`},
+ } {
+ for name, v := range want {
+ if got := tok[scheme][name]; got != v {
+ t.Errorf("themeTokensOf %s %s = %q, want %q", scheme, name, got, v)
+ }
+ }
+ }
if got := canonColours("radial-gradient(90px, color(srgb 0.121569 0.337255 0.839216 / 0.09), rgba(0, 0, 0, 0) 70%) oklch(0.513 0.21 263.298)"); got != "radial-gradient(90px, rgba(31, 86, 214, 0.09), rgba(0, 0, 0, 0) 70%) rgb(32, 89, 221)" {
t.Errorf("canonColours = %s", got)
}
- body, rest := accentSupportsBlock("a\n@supports (color: oklch(from red l c h)) {\n :root {\n --rst-x: 1;\n }\n}\nb")
- if !strings.Contains(body, "--rst-x: 1;") || rest != "a\nb" {
- t.Errorf("accentSupportsBlock: body %q rest %q", body, rest)
- }
}
diff --git a/internal/gallery/gallery_test.go b/internal/gallery/gallery_test.go
index 6ce9f6e..e190525 100644
--- a/internal/gallery/gallery_test.go
+++ b/internal/gallery/gallery_test.go
@@ -12,7 +12,6 @@ import (
xhtml "golang.org/x/net/html"
"amadan.net/rastrillo/fiesta"
- "amadan.net/rastrillo/fiesta/check"
"amadan.net/rastrillo/fiesta/example"
)
@@ -369,85 +368,6 @@ func TestRTLDocumentsReadRightToLeft(t *testing.T) {
}
}
-var highlightRule = regexp.MustCompile(`(?m)^(ds-[tavxs]) \{ color: var\((--rst-[a-z0-9-]+)\); \}`)
-
-// halves returns the light and dark halves of a whole-value
-// light-dark(<light>, <dark>), split at the top-level comma, or the
-// value twice when it is not one.
-func halves(v string) (light, dark string) {
- const prefix = "light-dark("
- if !strings.HasPrefix(v, prefix) || !strings.HasSuffix(v, ")") {
- return v, v
- }
- inner, depth := v[len(prefix):len(v)-1], 0
- for i := 0; i < len(inner); i++ {
- switch inner[i] {
- case '(':
- depth++
- case ')':
- depth--
- case ',':
- if depth == 0 {
- return strings.TrimSpace(inner[:i]), strings.TrimSpace(inner[i+1:])
- }
- }
- }
- return v, v
-}
-
-var declPattern = regexp.MustCompile(`(--rst-[a-z0-9-]+)\s*:\s*([^;]+);`)
-
-// Every highlight colour reaches 4.5:1 against the code block's
-// --rst-surface, in every theme and both schemes, resolved from the
-// themes' own light-dark() pairs. The axe scan checks the rendered
-// cascade; this checks every pair, including the ones a page with no
-// such token on it would never show axe. Preservation: the five lines
-// are read from view.css, the widget's half of the stylesheet, where
-// the code-block rules now live.
-func TestHighlightColoursPassAgainstTheCodeBackground(t *testing.T) {
- pairs := map[string]string{}
- for _, m := range highlightRule.FindAllStringSubmatch(string(viewCSS), -1) {
- pairs[m[1]] = m[2]
- }
- if len(pairs) != 5 {
- t.Fatalf("view.css colours %d highlight elements, want 5: %v", len(pairs), pairs)
- }
- for _, theme := range fiesta.ThemeNames() {
- raw, _ := fiesta.ThemeCSS(theme)
- css := string(raw)
- i := strings.Index(css, ":root {")
- if i < 0 {
- t.Fatalf("%s: no :root block", theme)
- }
- body := css[i:]
- if j := strings.Index(body, "}"); j >= 0 {
- body = body[:j]
- }
- values := map[string]string{}
- for _, m := range declPattern.FindAllStringSubmatch(body, -1) {
- values[m[1]] = strings.TrimSpace(m[2])
- }
- surfaceLight, surfaceDark := halves(values["--rst-surface"])
- for scheme, bg := range map[string]string{"light": surfaceLight, "dark": surfaceDark} {
- for el, token := range pairs {
- light, dark := halves(values[token])
- fg := light
- if scheme == "dark" {
- fg = dark
- }
- r, err := check.ContrastRatio(fg, bg)
- if err != nil {
- t.Errorf("%s %s %s (%s on %s): %v", theme, scheme, el, fg, bg, err)
- continue
- }
- if r < 4.5 {
- t.Errorf("%s %s: %s is %s (%s) on %s, %.2f:1, under 4.5:1", theme, scheme, el, token, fg, bg, r)
- }
- }
- }
- }
-}
-
// TestTheHeaderLiftsOnlyTheSchemeMenu: when the scheme-menu example
// shows its menu inside a row, as the footer row's states will, the
// header lifts the menu and not the row around it. A lifted