rastrillo / fiesta Public

Read a component out of the row its example shows it in

The examples are about to show components inside the row they sit in:
a dropdown in a section head or a list toolbar, the language and
scheme menus in the column's footer row. Three readers take an
example's markup as the bare component, and a row around it would
break each: the gallery's header lifts scheme-menu/standalone as its
own scheme menu, check's dropdown drives box the dropdown idiom on its
own, and the golden tests Rastrillo will write compare its partials
with the examples. A pattern over the bytes would end a dropdown at the
</details> of the menu group nested inside it.

example.Element returns the one element carrying an attribute, start
tag to matching end tag, exactly as written, and example.Contents what
lies between its tags. Both use the tokenizer only and count nested
elements of the carrier's own tag. Each is an error, naming the
attribute, when nothing carries it, when two elements do at the
outermost level (the second would go unseen), and when the element is
never closed (truncated markup would read as a smaller component). A
carrier inside another counts once, as the outer one. The package's
interface only gains the two functions.

The gallery's header now lifts Element(markup, "rst-scheme"); on
today's state, whose body is that element, the output is unchanged.
check's dropdownSample is Element of the dropdown idiom's markup.
TestDropdownExclusivityIsNative, TestNestedMenuGroupNeverSharesItsParentsName,
TestADropdownOpensUnderItsOwnSummary and
TestADropdownWithoutAnchorsOrScriptsStaysOnScreen are adapted
preservation tests: their input is the dropdown element rather than
the whole idiom state, which today also holds the filter chip; their
assertions are unchanged.

New-behaviour tests seen red against stubs returning "" and no error:
TestElementReturnsTheCarrierAsWritten, TestContentsReturnsWhatLiesInside,
TestElementReadsAVoidElement, TestElementRefusesAMissingCarrier,
TestElementRefusesTwoCarriers, TestElementRefusesAnUnclosedCarrier and
TestANestedCarrierCountsOnce. TestTheHeaderLiftsOnlyTheSchemeMenu,
which renders the gallery with the standalone state wrapped in a
<footer rst-page-foot>, was red against the old schemeMenu: the bar's
scheme menu sat in the footer it had lifted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 7b71b5b515ad02129a20676832a286cf30978f31 parent 8ee2d35
7 files changed, +259 −8
  • check/css_test.go +2 −2
  • check/dropdown_browser_test.go +2 −2
  • check/markuphelp_test.go +13 −0
  • example/example.go +114 −0
  • example/example_test.go +77 −0
  • internal/gallery/gallery.go +11 −4
  • internal/gallery/gallery_test.go +40 −0

CI passed — run details

CI log
=== gofmt ===
=== no-rastrillo ===
mkdir -p "/var/lib/amadan-runner/w4/repos/rastrillo__fiesta/wt/fiesta-1-1/.build/tmp"
go test -run '^(TestFiestaNeverImportsRastrillo|TestThePublicPackagesLinkNoBrowser)$' -count=1 .
go: downloading go1.26.10 (linux/arm64)
ok  	amadan.net/rastrillo/fiesta	0.998s
=== size ===
go test ./internal/build -run '^TestTheOneLinkIsUnder25KiBGzipped$' -count=1 -v
=== RUN   TestTheOneLinkIsUnder25KiBGzipped
    build_test.go:138: fiesta-plain.css: 152856 bytes, 22303 bytes gzipped (21.8 KiB of the 25.0 KiB limit)
    build_test.go:138: fiesta-signal.css: 152951 bytes, 22414 bytes gzipped (21.9 KiB of the 25.0 KiB limit)
    build_test.go:138: fiesta.css: 155770 bytes, 23018 bytes gzipped (22.5 KiB of the 25.0 KiB limit)
--- PASS: TestTheOneLinkIsUnder25KiBGzipped (0.02s)
PASS
ok  	amadan.net/rastrillo/fiesta/internal/build	0.025s
=== history ===
history: a shallow clone; fetching the whole history and the release tags
history: 8 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	3.746s
ok  	amadan.net/rastrillo/fiesta/check	5.971s
ok  	amadan.net/rastrillo/fiesta/example	0.279s
?   	amadan.net/rastrillo/fiesta/hack/seed/english	[no test files]
ok  	amadan.net/rastrillo/fiesta/internal/axe	0.143s
ok  	amadan.net/rastrillo/fiesta/internal/build	1.512s
ok  	amadan.net/rastrillo/fiesta/internal/codeview	0.007s
?   	amadan.net/rastrillo/fiesta/internal/harness	[no test files]
ok  	amadan.net/rastrillo/fiesta/internal/markup	0.008s
ok  	amadan.net/rastrillo/fiesta/internal/nodetest	0.522s
ok  	amadan.net/rastrillo/fiesta/internal/site	4.085s
ok  	amadan.net/rastrillo/fiesta/internal/tokens	0.033s
suite: root took 41s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m21.850000s 0m4.490000s
--- 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 35 of 105 test tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	71.453s
suite: test.internal.gallery~1of3 took 95s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m52.630000s 0m2.000000s
--- 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 35 of 105 test tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	69.198s
suite: test.internal.gallery~2of3 took 92s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m50.690000s 0m2.010000s
--- 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 35 of 105 test tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	67.792s
suite: test.internal.gallery~3of3 took 91s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m48.280000s 0m2.040000s
--- 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	58.147s
suite: test.cmd.fiesta~1of2 took 81s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m36.250000s 0m2.840000s
--- 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	59.225s
suite: test.cmd.fiesta~2of2 took 82s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m38.530000s 0m2.990000s
--- 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	66.275s
suite: test.internal.www~1of2 took 96s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
1m1.700000s 0m3.910000s
--- 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	63.265s
suite: test.internal.www~2of2 took 94s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m59.620000s 0m3.960000s
--- shard 8 of 48 ---
==> shard 8 of 48: make staticcheck
go run honnef.co/go/tools/cmd/staticcheck@v0.8.2 -tags browser ./...
go: downloading honnef.co/go/tools v0.8.2
go: downloading golang.org/x/tools v0.51.0
go: downloading github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c
go: downloading golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678
go: downloading golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa
suite: staticcheck took 48s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m45.310000s 0m5.990000s
--- shard 9 of 48 ---
==> shard 9 of 48: make govulncheck
go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 ./...
go: downloading golang.org/x/vuln v1.8.0
go: downloading golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518
go: downloading golang.org/x/mod v0.41.0
go: downloading golang.org/x/tools v0.50.0
go: downloading golang.org/x/sync v0.23.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 37s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m18.130000s 0m2.480000s
--- 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' .
go: downloading github.com/zricethezav/gitleaks/v8 v8.30.1
go: downloading github.com/Masterminds/sprig/v3 v3.3.0
go: downloading github.com/charmbracelet/lipgloss v0.5.0
go: downloading github.com/fatih/semgroup v1.2.0
go: downloading github.com/gitleaks/go-gitdiff v0.9.1
go: downloading github.com/h2non/filetype v1.1.3
go: downloading github.com/mholt/archives v0.1.2
go: downloading github.com/rs/zerolog v1.33.0
go: downloading github.com/spf13/cobra v1.9.1
go: downloading github.com/spf13/viper v1.19.0
go: downloading golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa
go: downloading github.com/BobuSumisu/aho-corasick v1.0.3
go: downloading github.com/hashicorp/go-version v1.7.0
go: downloading github.com/muesli/termenv v0.15.1
go: downloading github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68
go: downloading github.com/lucasb-eyer/go-colorful v1.2.0
go: downloading golang.org/x/sync v0.11.0
go: downloading github.com/mattn/go-runewidth v0.0.14
go: downloading dario.cat/mergo v1.0.1
go: downloading github.com/Masterminds/goutils v1.1.1
go: downloading github.com/Masterminds/semver/v3 v3.3.0
go: downloading github.com/google/uuid v1.6.0
go: downloading github.com/huandu/xstrings v1.5.0
go: downloading github.com/mitchellh/copystructure v1.2.0
go: downloading github.com/shopspring/decimal v1.4.0
go: downloading github.com/spf13/cast v1.7.1
go: downloading golang.org/x/crypto v0.35.0
go: downloading github.com/klauspost/compress v1.17.11
go: downloading github.com/STARRY-S/zip v0.2.1
go: downloading github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3
go: downloading github.com/bodgit/sevenzip v1.6.0
go: downloading github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707
go: downloading github.com/klauspost/pgzip v1.2.6
go: downloading github.com/minio/minlz v1.0.0
go: downloading github.com/nwaples/rardecode/v2 v2.1.0
go: downloading github.com/pierrec/lz4/v4 v4.1.21
go: downloading github.com/sorairolake/lzip-go v0.3.5
go: downloading github.com/therootcompany/xz v1.0.1
go: downloading github.com/ulikunitz/xz v0.5.12
go: downloading golang.org/x/text v0.22.0
go: downloading github.com/mattn/go-colorable v0.1.14
go: downloading github.com/fsnotify/fsnotify v1.8.0
go: downloading github.com/mitchellh/mapstructure v1.5.0
go: downloading github.com/sagikazarmark/slog-shim v0.1.0
go: downloading github.com/spf13/afero v1.12.0
go: downloading github.com/spf13/pflag v1.0.6
go: downloading github.com/rivo/uniseg v0.2.0
go: downloading github.com/mitchellh/reflectwalk v1.0.2
go: downloading github.com/aymanbagabas/go-osc52/v2 v2.0.1
go: downloading github.com/mattn/go-isatty v0.0.20
go: downloading golang.org/x/sys v0.30.0
go: downloading github.com/subosito/gotenv v1.6.0
go: downloading github.com/hashicorp/hcl v1.0.0
go: downloading gopkg.in/ini.v1 v1.67.0
go: downloading github.com/magiconair/properties v1.8.9
go: downloading github.com/pelletier/go-toml/v2 v2.2.3
go: downloading gopkg.in/yaml.v3 v3.0.1
go: downloading github.com/bodgit/plumbing v1.3.0
go: downloading github.com/bodgit/windows v1.0.1
go: downloading github.com/hashicorp/go-multierror v1.1.1
go: downloading go4.org v0.0.0-20230225012048-214862532bf5
go: downloading github.com/hashicorp/golang-lru/v2 v2.0.7
go: downloading github.com/hashicorp/errwrap v1.1.0
12:08AM INF Unknown SCM platform. Use --platform to include links in findings. host=amadan.net
12:08AM INF 8 commits scanned.
12:08AM INF scanned ~8122312 bytes (8.12 MB) in 2.9s
12:08AM INF no leaks found
suite: gitleaks took 39s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m30.810000s 0m3.850000s
--- shard 11 of 48 ---
==> shard 11 of 48: make browser.internal.harness
./hack/gotest.sh browser 'internal.harness'
gotest: on ip-172-31-24-2, 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	5.849s
suite: browser.internal.harness took 39s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m18.190000s 0m3.830000s
--- shard 12 of 48 ---
==> shard 12 of 48: make browser.check~1of13
./hack/gotest.sh browser 'check~1of13'
gotest: on ip-172-31-24-2, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 1 of 13 of ./check/ runs 6 of 196 browser tests
ok  	amadan.net/rastrillo/fiesta/check	107.656s
suite: browser.check~1of13 took 146s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
1m17.850000s 0m18.130000s
--- shard 13 of 48 ---
==> shard 13 of 48: make browser.check~2of13
./hack/gotest.sh browser 'check~2of13'
gotest: on ip-172-31-24-2, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 2 of 13 of ./check/ runs 10 of 196 browser tests
ok  	amadan.net/rastrillo/fiesta/check	100.126s
suite: browser.check~2of13 took 139s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
1m4.550000s 0m17.900000s
--- shard 14 of 48 ---
==> shard 14 of 48: make browser.check~3of13
./hack/gotest.sh browser 'check~3of13'
gotest: on ip-172-31-24-2, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 3 of 13 of ./check/ runs 11 of 196 browser tests
ok  	amadan.net/rastrillo/fiesta/check	71.769s
suite: browser.check~3of13 took 110s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m57.650000s 0m11.780000s
--- shard 15 of 48 ---
==> shard 15 of 48: make browser.check~4of13
./hack/gotest.sh browser 'check~4of13'
gotest: on ip-172-31-24-2, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 4 of 13 of ./check/ runs 11 of 196 browser tests
ok  	amadan.net/rastrillo/fiesta/check	76.573s
suite: browser.check~4of13 took 115s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
1m0.570000s 0m14.210000s
--- shard 16 of 48 ---
==> shard 16 of 48: make browser.check~5of13
./hack/gotest.sh browser 'check~5of13'
gotest: on ip-172-31-24-2, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 5 of 13 of ./check/ runs 16 of 196 browser tests
ok  	amadan.net/rastrillo/fiesta/check	47.647s
suite: browser.check~5of13 took 87s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m36.030000s 0m9.190000s
--- shard 17 of 48 ---
==> shard 17 of 48: make browser.check~6of13
./hack/gotest.sh browser 'check~6of13'
gotest: on ip-172-31-24-2, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 6 of 13 of ./check/ runs 16 of 196 browser tests
ok  	amadan.net/rastrillo/fiesta/check	98.136s
suite: browser.check~6of13 took 137s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
1m5.910000s 0m18.310000s
--- shard 18 of 48 ---
==> shard 18 of 48: make browser.check~7of13
./hack/gotest.sh browser 'check~7of13'
gotest: on ip-172-31-24-2, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 7 of 13 of ./check/ runs 16 of 196 browser tests
ok  	amadan.net/rastrillo/fiesta/check	93.853s
suite: browser.check~7of13 took 132s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
1m4.920000s 0m16.740000s
--- shard 19 of 48 ---
==> shard 19 of 48: make browser.check~8of13
./hack/gotest.sh browser 'check~8of13'
gotest: on ip-172-31-24-2, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 8 of 13 of ./check/ runs 17 of 196 browser tests
ok  	amadan.net/rastrillo/fiesta/check	93.770s
suite: browser.check~8of13 took 132s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
1m3.500000s 0m18.430000s
--- shard 20 of 48 ---
==> shard 20 of 48: make browser.check~9of13
./hack/gotest.sh browser 'check~9of13'
gotest: on ip-172-31-24-2, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 9 of 13 of ./check/ runs 18 of 196 browser tests
ok  	amadan.net/rastrillo/fiesta/check	92.079s
suite: browser.check~9of13 took 131s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
1m8.110000s 0m21.680000s
--- shard 21 of 48 ---
==> shard 21 of 48: make browser.check~10of13
./hack/gotest.sh browser 'check~10of13'
gotest: on ip-172-31-24-2, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 10 of 13 of ./check/ runs 18 of 196 browser tests
ok  	amadan.net/rastrillo/fiesta/check	60.315s
suite: browser.check~10of13 took 99s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m50.890000s 0m13.160000s
--- shard 22 of 48 ---
==> shard 22 of 48: make browser.check~11of13
./hack/gotest.sh browser 'check~11of13'
gotest: on ip-172-31-24-2, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 11 of 13 of ./check/ runs 19 of 196 browser tests
ok  	amadan.net/rastrillo/fiesta/check	48.591s
suite: browser.check~11of13 took 87s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m37.190000s 0m10.290000s
--- shard 23 of 48 ---
==> shard 23 of 48: make browser.check~12of13
./hack/gotest.sh browser 'check~12of13'
gotest: on ip-172-31-24-2, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 12 of 13 of ./check/ runs 19 of 196 browser tests
ok  	amadan.net/rastrillo/fiesta/check	55.699s
suite: browser.check~12of13 took 95s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m43.440000s 0m12.540000s
--- shard 24 of 48 ---
==> shard 24 of 48: make browser.check~13of13
./hack/gotest.sh browser 'check~13of13'
gotest: on ip-172-31-24-2, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 13 of 13 of ./check/ runs 19 of 196 browser tests
ok  	amadan.net/rastrillo/fiesta/check	97.334s
suite: browser.check~13of13 took 136s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
1m7.740000s 0m22.000000s
--- 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-24-2, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 1 of 18 of ./internal/gallery/ runs 6 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	35.428s
suite: browser.internal.gallery~1of18 took 74s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m35.090000s 0m6.280000s
--- 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-24-2, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 2 of 18 of ./internal/gallery/ runs 6 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	52.359s
suite: browser.internal.gallery~2of18 took 90s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m34.960000s 0m6.660000s
--- 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-24-2, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 3 of 18 of ./internal/gallery/ runs 6 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	61.728s
suite: browser.internal.gallery~3of18 took 100s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m35.350000s 0m7.180000s
--- 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-24-2, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 4 of 18 of ./internal/gallery/ runs 6 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	60.379s
suite: browser.internal.gallery~4of18 took 99s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m37.930000s 0m8.340000s
--- 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-24-2, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 5 of 18 of ./internal/gallery/ runs 6 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	62.978s
suite: browser.internal.gallery~5of18 took 101s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m36.190000s 0m7.540000s
--- 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-24-2, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 6 of 18 of ./internal/gallery/ runs 6 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	57.131s
suite: browser.internal.gallery~6of18 took 95s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m35.880000s 0m7.000000s
--- 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-24-2, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 7 of 18 of ./internal/gallery/ runs 5 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	56.528s
suite: browser.internal.gallery~7of18 took 94s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m32.700000s 0m5.460000s
--- 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-24-2, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 8 of 18 of ./internal/gallery/ runs 5 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	60.418s
suite: browser.internal.gallery~8of18 took 99s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m37.450000s 0m8.270000s
--- 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-24-2, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 9 of 18 of ./internal/gallery/ runs 5 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	66.750s
suite: browser.internal.gallery~9of18 took 105s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m40.210000s 0m9.770000s
--- 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-24-2, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 10 of 18 of ./internal/gallery/ runs 2 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	61.924s
suite: browser.internal.gallery~10of18 took 99s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m33.510000s 0m5.640000s
--- 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-24-2, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 11 of 18 of ./internal/gallery/ runs 2 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	61.754s
suite: browser.internal.gallery~11of18 took 99s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m34.100000s 0m5.700000s
--- 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-24-2, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 12 of 18 of ./internal/gallery/ runs 2 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	66.236s
suite: browser.internal.gallery~12of18 took 104s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m36.240000s 0m7.390000s
--- 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-24-2, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 13 of 18 of ./internal/gallery/ runs 3 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	68.440s
suite: browser.internal.gallery~13of18 took 106s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m37.980000s 0m7.910000s
--- 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-24-2, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 14 of 18 of ./internal/gallery/ runs 4 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	70.179s
suite: browser.internal.gallery~14of18 took 108s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m36.710000s 0m7.830000s
--- 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-24-2, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 15 of 18 of ./internal/gallery/ runs 4 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	69.767s
suite: browser.internal.gallery~15of18 took 108s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m38.620000s 0m8.330000s
--- 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-24-2, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 16 of 18 of ./internal/gallery/ runs 5 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	65.288s
suite: browser.internal.gallery~16of18 took 102s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m36.460000s 0m7.070000s
--- 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-24-2, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 17 of 18 of ./internal/gallery/ runs 5 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	67.639s
suite: browser.internal.gallery~17of18 took 106s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m40.790000s 0m9.470000s
--- 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-24-2, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 18 of 18 of ./internal/gallery/ runs 5 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	68.867s
suite: browser.internal.gallery~18of18 took 106s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m37.150000s 0m8.040000s
--- 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-24-2, 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.734s
suite: browser.internal.gallery.history took 69s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m41.050000s 0m5.140000s
--- 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-24-2, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 1 of 4 of ./internal/www/ runs 4 of 22 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/www	70.936s
suite: browser.internal.www~1of4 took 106s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m51.680000s 0m5.410000s
--- 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-24-2, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 2 of 4 of ./internal/www/ runs 6 of 22 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/www	68.170s
suite: browser.internal.www~2of4 took 104s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
1m4.300000s 0m5.300000s
--- 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-24-2, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 3 of 4 of ./internal/www/ runs 6 of 22 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/www	83.749s
suite: browser.internal.www~3of4 took 118s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
1m11.610000s 0m5.920000s
--- 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-24-2, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 4 of 4 of ./internal/www/ runs 6 of 22 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/www	78.439s
suite: browser.internal.www~4of4 took 114s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
1m4.190000s 0m5.640000s
diff --git a/check/css_test.go b/check/css_test.go
index 2bd03f7..69555e0 100644
--- a/check/css_test.go
+++ b/check/css_test.go
@@ -635,7 +635,7 @@ func TestJobStatusPartialClassesAreStyled(t *testing.T) {
// <details name> attribute, not JavaScript — this pins both halves of
// that promise.
func TestDropdownExclusivityIsNative(t *testing.T) {
- sample := idiomMarkup(t, "dropdown")
+ sample := dropdownSample(t)
if !strings.Contains(sample, `<details rst-dropdown name=`) {
t.Errorf("dropdown sample's outer <details> carries no name attribute: %s", sample)
}
@@ -660,7 +660,7 @@ var detailsNamePattern = regexp.MustCompile(`<details [^>]*name="([^"]*)"`)
// pointer. The nested sample must carry a name of its own, and it must
// not be the parent's.
func TestNestedMenuGroupNeverSharesItsParentsName(t *testing.T) {
- sample := idiomMarkup(t, "dropdown")
+ sample := dropdownSample(t)
names := detailsNamePattern.FindAllStringSubmatch(sample, -1)
if len(names) != 2 {
t.Fatalf("expected two named <details> in the dropdown sample, found %d: %s", len(names), sample)
diff --git a/check/dropdown_browser_test.go b/check/dropdown_browser_test.go
index d75a108..1c97d98 100644
--- a/check/dropdown_browser_test.go
+++ b/check/dropdown_browser_test.go
@@ -53,7 +53,7 @@ const withoutAnchorSupport = `(() => { const s = CSS.supports.bind(CSS); CSS.sup
// browser with neither (TestADropdownWithoutAnchorsOrScriptsStaysOnScreen).
func TestADropdownOpensUnderItsOwnSummary(t *testing.T) {
partial := fixture(t, "dropdown_browser_test.go", "TestADropdownOpensUnderItsOwnSummary", "partial")
- sample := idiomMarkup(t, "dropdown")
+ sample := dropdownSample(t)
box := func(style, menu string) string {
return `<style>#box { padding: 1rem; } ` + style + `</style><div id="box">` + menu + `</div>`
}
@@ -163,7 +163,7 @@ func TestADropdownWithoutAnchorsOrScriptsStaysOnScreen(t *testing.T) {
`<link rel="stylesheet" href="/tokens-noanchor.css"><link rel="stylesheet" href="/theme.css"></head><body><div id="box" style="padding: 1rem">` + menu + `</div></body></html>`
}
pages := map[string]string{}
- for name, menu := range map[string]string{"partial": partial, "sample": idiomMarkup(t, "dropdown")} {
+ for name, menu := range map[string]string{"partial": partial, "sample": dropdownSample(t)} {
pages["/"+name] = bare("ltr", menu)
pages["/rtl/"+name] = bare("rtl", menu)
}
diff --git a/check/markuphelp_test.go b/check/markuphelp_test.go
index 2686784..859d54a 100644
--- a/check/markuphelp_test.go
+++ b/check/markuphelp_test.go
@@ -38,6 +38,19 @@ func idiomMarkup(t *testing.T, key string) string {
return markup
}
+// dropdownSample is the dropdown idiom's menu alone, out of whatever
+// row its example shows it in: the drives that box a dropdown on its
+// own and the checks on its <details> want the component, and a row
+// around it would put a heading and a toolbar in their boxes too.
+func dropdownSample(t *testing.T) string {
+ t.Helper()
+ menu, err := example.Element(idiomMarkup(t, "dropdown"), "rst-dropdown")
+ if err != nil {
+ t.Fatal(err)
+ }
+ return menu
+}
+
// defaultMarkup reads the state examples.json names as the default of
// the example called name: the whole document for a layout, the body
// for anything else.
diff --git a/example/example.go b/example/example.go
index 1aa1e97..e9529f8 100644
--- a/example/example.go
+++ b/example/example.go
@@ -181,6 +181,120 @@ func body(doc string) (start, end int, err error) {
}
}
+// Element returns the one element in markup that carries attribute,
+// from its start tag to its matching end tag, exactly as written.
+//
+// The examples show a component inside the row it sits in, and a
+// reader that wants the bare component (the gallery's header, a drive
+// that boxes a dropdown on its own, a golden test against a partial)
+// would otherwise take the row with it. It is an error when no element
+// carries attribute, when two do at the outermost level (the second
+// would go unseen), and when the element is never closed (markup cut
+// short would otherwise read as a smaller component). A carrier inside
+// another counts once, as the outer one.
+func Element(markup, attribute string) (string, error) {
+ c, err := carrier(markup, attribute)
+ if err != nil {
+ return "", err
+ }
+ return markup[c.start:c.end], nil
+}
+
+// Contents returns what lies between that element's start and end tags,
+// exactly as written. A void element has none, and is an error, since
+// the caller asked for a container.
+func Contents(markup, attribute string) (string, error) {
+ c, err := carrier(markup, attribute)
+ if err != nil {
+ return "", err
+ }
+ if c.void {
+ return "", fmt.Errorf("example: the element carrying %s is a void element and holds nothing", attribute)
+ }
+ return markup[c.open:c.close], nil
+}
+
+// span is where one element lies in its markup: start to end is the
+// whole element, open to close what its tags enclose.
+type span struct {
+ start, open, close, end int
+ void bool
+}
+
+// voidElements have no end tag, so the element is its start tag alone;
+// counting depth for them would wait for a </input> that never comes.
+var voidElements = map[string]bool{
+ "area": true, "base": true, "br": true, "col": true, "embed": true, "hr": true, "img": true,
+ "input": true, "link": true, "meta": true, "source": true, "track": true, "wbr": true,
+}
+
+// carrier finds the one outermost element carrying attribute. Only
+// elements of the carrier's own tag name are counted on the way to its
+// end tag: a <details rst-menu-group> inside a dropdown's panel closes
+// with a </details> that is not the dropdown's.
+func carrier(markup, attribute string) (span, error) {
+ z := html.NewTokenizer(strings.NewReader(markup))
+ offset := 0
+ var found span
+ count, depth := 0, 0
+ tag := ""
+ for {
+ tt := z.Next()
+ raw := len(z.Raw())
+ switch tt {
+ case html.ErrorToken:
+ switch {
+ case depth > 0:
+ return span{}, fmt.Errorf("example: the element carrying %s is never closed", attribute)
+ case count == 0:
+ return span{}, fmt.Errorf("example: no element carries %s", attribute)
+ }
+ return found, nil
+ case html.StartTagToken, html.SelfClosingTagToken:
+ name, more := z.TagName()
+ if depth > 0 {
+ if string(name) == tag && tt == html.StartTagToken {
+ depth++
+ }
+ break
+ }
+ if !hasAttr(z, more, attribute) {
+ break
+ }
+ if count++; count > 1 {
+ return span{}, fmt.Errorf("example: more than one element carries %s", attribute)
+ }
+ tag = string(name)
+ found = span{start: offset, open: offset + raw}
+ if tt == html.SelfClosingTagToken || voidElements[tag] {
+ found.close, found.end, found.void = offset+raw, offset+raw, true
+ break
+ }
+ depth = 1
+ case html.EndTagToken:
+ if name, _ := z.TagName(); depth > 0 && string(name) == tag {
+ if depth--; depth == 0 {
+ found.close, found.end = offset, offset+raw
+ }
+ }
+ }
+ offset += raw
+ }
+}
+
+// hasAttr reports whether the start tag the tokenizer is on carries
+// the attribute name.
+func hasAttr(z *html.Tokenizer, more bool, name string) bool {
+ for more {
+ var k []byte
+ k, _, more = z.TagAttr()
+ if string(k) == name {
+ return true
+ }
+ }
+ return false
+}
+
const (
slotOpen = "<!-- fiesta:slot -->"
slotClose = "<!-- /fiesta:slot -->"
diff --git a/example/example_test.go b/example/example_test.go
index 8d6a8d4..f1c3ba3 100644
--- a/example/example_test.go
+++ b/example/example_test.go
@@ -646,3 +646,80 @@ func TestAllWalksTheManifestInOrder(t *testing.T) {
t.Errorf("All returned %d pieces in this order:\n%v\nwant %d:\n%v", len(got), got, len(want), want)
}
}
+
+// ── Reading one component out of its row ─────────────────────────────
+
+// rowed is a dropdown in a section head, the shape the reworked states
+// take: a heading, then the menu, whose panel holds a nested <details>
+// that a reader counting the wrong tag would end the menu on.
+const (
+ rowedMenu = `<details rst-dropdown name="a"><summary>S</summary><div rst-dropdown-menu><details rst-menu-group><summary>G</summary></details></div></details>`
+ rowed = `<div rst-box-head><h2>Posts</h2>` + rowedMenu + `</div>`
+)
+
+func TestElementReturnsTheCarrierAsWritten(t *testing.T) {
+ got, err := example.Element(rowed, "rst-dropdown")
+ if err != nil {
+ t.Fatalf("Element: %v", err)
+ }
+ if got != rowedMenu {
+ t.Fatalf("Element returned\n%s\nwant\n%s", got, rowedMenu)
+ }
+}
+
+func TestContentsReturnsWhatLiesInside(t *testing.T) {
+ got, err := example.Contents(rowed, "rst-box-head")
+ if err != nil {
+ t.Fatalf("Contents: %v", err)
+ }
+ if want := `<h2>Posts</h2>` + rowedMenu; got != want {
+ t.Fatalf("Contents returned\n%s\nwant\n%s", got, want)
+ }
+}
+
+func TestElementReadsAVoidElement(t *testing.T) {
+ got, err := example.Element(`<p><input rst-input name="q"></p>`, "rst-input")
+ if err != nil {
+ t.Fatalf("Element: %v", err)
+ }
+ if want := `<input rst-input name="q">`; got != want {
+ t.Fatalf("Element returned %q, want %q", got, want)
+ }
+}
+
+// refusal holds one call to an error that names the attribute, since
+// an error that does not say which component was missing sends its
+// reader through every row on the page.
+func refusal(t *testing.T, markup, why string) {
+ t.Helper()
+ got, err := example.Element(markup, "rst-dropdown")
+ if err == nil {
+ t.Fatalf("Element of %s returned %q and no error", why, got)
+ }
+ if !strings.Contains(err.Error(), "rst-dropdown") {
+ t.Errorf("Element of %s: the error %q does not name rst-dropdown", why, err)
+ }
+}
+
+func TestElementRefusesAMissingCarrier(t *testing.T) {
+ refusal(t, `<div rst-box-head><h2>Posts</h2></div>`, "markup with no dropdown")
+}
+
+func TestElementRefusesTwoCarriers(t *testing.T) {
+ refusal(t, `<div rst-box-head><h2>Posts</h2>`+rowedMenu+rowedMenu+`</div>`, "two sibling dropdowns")
+}
+
+func TestElementRefusesAnUnclosedCarrier(t *testing.T) {
+ refusal(t, `<div rst-box-head><h2>Posts</h2><details rst-dropdown name="a"><summary>S</summary>`, "a dropdown cut short")
+}
+
+func TestANestedCarrierCountsOnce(t *testing.T) {
+ outer := `<details rst-dropdown name="o"><summary>O</summary><div rst-dropdown-menu>` + rowedMenu + `</div></details>`
+ got, err := example.Element(`<div rst-box-head>`+outer+`</div>`, "rst-dropdown")
+ if err != nil {
+ t.Fatalf("Element: %v", err)
+ }
+ if got != outer {
+ t.Fatalf("Element returned\n%s\nwant the outer dropdown\n%s", got, outer)
+ }
+}
diff --git a/internal/gallery/gallery.go b/internal/gallery/gallery.go
index e9fe3af..c7b08d1 100644
--- a/internal/gallery/gallery.go
+++ b/internal/gallery/gallery.go
@@ -305,9 +305,10 @@ var (
)
// schemeMenu reads the header's scheme menu from the scheme-menu
-// example's standalone state: its markup, the scripts its head asks
-// for, and the attribute on its <html> that tells fiesta.js to
-// remember the choice, which every gallery page carries too.
+// example's standalone state: the [rst-scheme] element of its markup,
+// whatever row holds it, the scripts its head asks for, and the
+// attribute on its <html> that tells fiesta.js to remember the choice,
+// which every gallery page carries too.
//
// The state ships with one scheme marked current; the gallery starts
// on System, with no scheme stored, so the mark is moved to the System
@@ -331,10 +332,16 @@ func (s *site) schemeMenu() error {
for _, m := range scriptSrc.FindAllStringSubmatch(doc, -1) {
s.schemeScripts = append(s.schemeScripts, m[1])
}
- markup, err := example.Markup(s.src, name, state)
+ body, err := example.Markup(s.src, name, state)
if err != nil {
return err
}
+ // The menu alone: a state that shows it in a row (the column's
+ // footer) would otherwise put that row inside the gallery's bar.
+ markup, err := example.Element(body, "rst-scheme")
+ if err != nil {
+ return fmt.Errorf("%s/%s: %w", name, state, err)
+ }
if !ariaCurrent.MatchString(markup) {
return fmt.Errorf("%s/%s marks no scheme current", name, state)
}
diff --git a/internal/gallery/gallery_test.go b/internal/gallery/gallery_test.go
index 5103f9c..9a0231e 100644
--- a/internal/gallery/gallery_test.go
+++ b/internal/gallery/gallery_test.go
@@ -7,6 +7,7 @@ import (
"strings"
"sync"
"testing"
+ "testing/fstest"
xhtml "golang.org/x/net/html"
@@ -446,3 +447,42 @@ func TestHighlightColoursPassAgainstTheCodeBackground(t *testing.T) {
}
}
}
+
+// 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
+// <footer rst-page-foot> would put the page's foot row inside the
+// gallery's top bar, styled as a foot. The tree is fiesta.FS() with the
+// standalone state's body wrapped in that row, rendered for Day in
+// English, which is all the header needs.
+func TestTheHeaderLiftsOnlyTheSchemeMenu(t *testing.T) {
+ fsys := copyFS(t, fiesta.FS())
+ const file = "examples/scheme-menu/standalone.html"
+ doc := string(fsys[file].Data)
+ wrapped := strings.Replace(strings.Replace(doc, "<body>\n", "<body>\n<footer rst-page-foot>\n", 1), "\n</body>", "\n</footer>\n</body>", 1)
+ if strings.Count(wrapped, "rst-page-foot") != 1 || !strings.Contains(wrapped, "</footer>\n</body>") {
+ t.Fatalf("%s no longer has the <body> lines this test wraps", file)
+ }
+ fsys[file] = &fstest.MapFile{Data: []byte(wrapped)}
+ s := newTestSite(t, fsys, "")
+ if err := s.theme("day", "en"); err != nil {
+ t.Fatalf("rendering day/en: %v", err)
+ }
+ page, ok := s.out["day/en/index.html"]
+ if !ok {
+ t.Fatal("the render wrote no day/en/index.html")
+ }
+ bar, ok := parse(t, page).first(func(n node) bool { return n.hasClass("ds-top__controls") })
+ if !ok {
+ t.Fatal("day/en/index.html has no .ds-top__controls bar")
+ }
+ menu, ok := bar.first(func(n node) bool { return hasAttr(n, "rst-scheme") })
+ switch {
+ case !ok:
+ t.Fatal("the bar holds no [rst-scheme] element")
+ case menu.n.Data != "details" || !hasAttr(menu, "rst-dropdown"):
+ t.Errorf("the bar's [rst-scheme] element is a <%s>, want <details rst-dropdown rst-scheme>", menu.n.Data)
+ case hasAttr(node{menu.n.Parent}, "rst-page-foot"):
+ t.Errorf("the bar's scheme menu sits in a <%s rst-page-foot>: the header lifted the example's row with it", menu.n.Parent.Data)
+ }
+}