oficina / calendar Public

Serve Calendar theme from shared Estilo assets

Paul Campbell pushed by paul@keymail.dev 040eb25d1e8963b50e22c0e5344e4b4b764f9b67 parent 1a6dcd0
9 files changed, +106 −203
  • Makefile +5 −0
  • go.mod +2 −1
  • go.sum +5 −10
  • internal/calendar/asset_theme.go +17 −0
  • internal/calendar/render.go +1 −1
  • internal/calendar/static/theme.css +0 −187
  • internal/calendar/theme_test.go +52 −0
  • internal/calendartest/vendored_test.go +15 −0
  • internal/guardstest/guards_test.go +9 −4

CI passed — run details

diff --git a/Makefile b/Makefile
index 86ae38b..93e632b 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,10 @@
APP := calendar
+private_comma := ,
+export GOPRIVATE := $(if $(GOPRIVATE),$(GOPRIVATE)$(private_comma))amadan.net/oficina/estilo.git
+export GONOPROXY := $(if $(GONOPROXY),$(GONOPROXY)$(private_comma))amadan.net/oficina/estilo.git
+export GONOSUMDB := $(if $(GONOSUMDB),$(GONOSUMDB)$(private_comma))amadan.net/oficina/estilo.git
+
# What `carlos ship -target` defaults to, and therefore what a release
# must be built for. Building for your own machine and shipping that is a
# silent architecture mismatch: the binary uploads fine and fails to exec
diff --git a/go.mod b/go.mod
index 776316c..81ffa62 100644
--- a/go.mod
+++ b/go.mod
@@ -3,9 +3,11 @@ module amadan.net/oficina/calendar
go 1.25.0
require (
+ amadan.net/oficina/estilo.git v0.0.0-20260908001419-672a447da335
github.com/carlosframework/rastrillo v0.23.0
github.com/chromedp/chromedp v0.14.2
github.com/go-chi/chi/v5 v5.3.2
+ golang.org/x/crypto v0.55.0
gorm.io/gorm v1.31.2
)
@@ -25,7 +27,6 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
- golang.org/x/crypto v0.55.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.41.0 // indirect
gorm.io/plugin/dbresolver v1.6.2 // indirect
diff --git a/go.sum b/go.sum
index 9e6296c..2c562f5 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,5 @@
+amadan.net/oficina/estilo.git v0.0.0-20260908001419-672a447da335 h1:YogZKYINrPNXRfPedXxOIDbGCMY4ff7yAdohYphEWMg=
+amadan.net/oficina/estilo.git v0.0.0-20260908001419-672a447da335/go.mod h1:0R45/vCUAT+kpS7S6K/ZxBdE8f7YxFN4HT3HyDJ3nPY=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/carlosframework/rastrillo v0.23.0 h1:tKAAlwPt4Pg/rkqk7o6jvV12bbZ0r34oEVpuOlKfX7w=
@@ -48,24 +50,17 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
-golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
-golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
-golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
-golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
+golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
-golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
-golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
-golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
-golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
-golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
+golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
diff --git a/internal/calendar/asset_theme.go b/internal/calendar/asset_theme.go
new file mode 100644
index 0000000..91a9ef9
--- /dev/null
+++ b/internal/calendar/asset_theme.go
@@ -0,0 +1,17 @@
+package calendar
+
+import (
+ "io/fs"
+ "testing/fstest"
+
+ "amadan.net/oficina/estilo.git/theme"
+)
+
+type sharedThemeFS struct{ fs.FS }
+
+func (f sharedThemeFS) Open(name string) (fs.File, error) {
+ if name == "static/theme.css" {
+ return fstest.MapFS{name: &fstest.MapFile{Data: theme.CSS(), Mode: 0444}}.Open(name)
+ }
+ return f.FS.Open(name)
+}
diff --git a/internal/calendar/render.go b/internal/calendar/render.go
index 2eab125..df528e6 100644
--- a/internal/calendar/render.go
+++ b/internal/calendar/render.go
@@ -24,7 +24,7 @@ var appFS embed.FS
// assets fingerprints static/: {{asset "static/tokens.css"}} in a
// template returns a content-hashed URL, served immutable-cacheable by
// the handler New mounts.
-var assets = rastrillo.NewAssets(appFS)
+var assets = rastrillo.NewAssets(sharedThemeFS{appFS})
// pageNames is every page this app renders. Kept as a list rather than
// discovered from the filesystem so a template that is never rendered
diff --git a/internal/calendar/static/theme.css b/internal/calendar/static/theme.css
deleted file mode 100644
index 5da4384..0000000
--- a/internal/calendar/static/theme.css
+++ /dev/null
@@ -1,187 +0,0 @@
-/* day — rastrillo's default theme: an everyday blue on white and grey.
- Structure lives in tokens.css; this file is colour, type and shape.
- rastrillo new writes it once as static/theme.css.
-
- The look: the ordinary, personable interface. A white page, a plain
- grey text scale, one friendly blue that means "this does something",
- and the conventional green/amber/red for status. Nothing here is
- trying to be memorable — day is the theme an app ships on while it
- works out what it wants to look like, and the one that never gets in
- the way of the content. Semantic status colours are functional and
- never the accent, so a pill can never be mistaken for a link.
-
- ── One block, both schemes ───────────────────────────────────────────
- Every token is declared once, as light-dark(<light>, <dark>), under a
- single :root that sets color-scheme: light dark. The browser picks the
- half that matches the resolved scheme, so there is no second copy of
- the palette to keep in sync and no way to author dark half-done. The
- two rules at the bottom are the whole toggle: setting color-scheme on
- :root[data-theme=…] re-resolves every light-dark() in the file at
- once, in both directions, which is why an explicit choice beats the OS
- without restating a single colour.
-
- Tokens with one value in both schemes — the font stack, the radii —
- are written plain, because light-dark(x, x) says nothing twice.
-
- light-dark() is a colour function: it may only stand where a <color>
- is expected. So a shadow token wraps its colour in it and writes the
- geometry once, outside the call — `0 8px 24px light-dark(a, b)`, never
- `light-dark(0 8px 24px a, …)`, which no browser would accept as a
- box-shadow.
-
- ── The shape axis ────────────────────────────────────────────────────
- Radius and shadow are the theme's now, not tokens.css's: a theme that
- can only change colour cannot change how an app feels. day is soft and
- unremarkable — 8px cards, 6px controls, shadows with a wide blur and
- little alpha, the depth of paper resting on paper.
-
- ── WCAG 2.2 AA, measured ─────────────────────────────────────────────
- Normal text needs 4.5:1; large text and UI component boundaries need
- 3:1. Every pair below was computed from these exact hex values, in
- both schemes. If you change a token, recompute its rows.
-
- LIGHT ratio requirement
- --rst-text on --rst-surface 16.91 4.5:1 text
- --rst-text on --rst-bg 16.91 4.5:1 text
- --rst-text on --rst-surface-2 15.77 4.5:1 text
- --rst-text on --rst-accent-soft 15.21 4.5:1 text
- --rst-text-muted on --rst-surface 7.56 4.5:1 text
- --rst-text-muted on --rst-bg 7.56 4.5:1 text
- --rst-text-muted on --rst-surface-2 7.05 4.5:1 text
- --rst-text-muted on --rst-accent-soft 6.80 4.5:1 text
- --rst-text-faint on --rst-surface 5.29 4.5:1 text
- --rst-text-faint on --rst-bg 5.29 4.5:1 text
- --rst-text-faint on --rst-surface-2 4.93 4.5:1 text
- --rst-text-faint on --rst-accent-soft 4.76 4.5:1 text
- --rst-accent on --rst-surface 5.28 4.5:1 text + focus ring
- --rst-accent on --rst-bg 5.28 4.5:1 text + focus ring
- --rst-accent on --rst-surface-2 4.93 4.5:1 text
- --rst-accent on --rst-accent-soft 4.75 4.5:1 text
- --rst-on-accent on --rst-accent 5.28 4.5:1 button label
- --rst-on-accent on --rst-accent-strong 7.18 4.5:1 button label, hover
- --rst-line-strong on --rst-surface 3.63 3:1 control border
- --rst-line-strong on --rst-bg 3.63 3:1 control border
- --rst-line-strong on --rst-surface-2 3.39 3:1 control border
- pill neutral fg on its own tint 9.02 4.5:1 text
- pill positive fg on its own tint 6.49 4.5:1 text
- pill warning fg on its own tint 6.15 4.5:1 text
- pill negative fg on its own tint 5.30 4.5:1 text
- --rst-on-accent on negative fg (fill) 6.47 4.5:1 danger button label
-
- DARK ratio requirement
- --rst-text on --rst-surface 13.74 4.5:1 text
- --rst-text on --rst-bg 15.32 4.5:1 text
- --rst-text on --rst-surface-2 14.63 4.5:1 text
- --rst-text on --rst-accent-soft 13.02 4.5:1 text
- --rst-text-muted on --rst-surface 7.56 4.5:1 text
- --rst-text-muted on --rst-bg 8.43 4.5:1 text
- --rst-text-muted on --rst-surface-2 8.05 4.5:1 text
- --rst-text-muted on --rst-accent-soft 7.16 4.5:1 text
- --rst-text-faint on --rst-surface 6.33 4.5:1 text
- --rst-text-faint on --rst-bg 7.06 4.5:1 text
- --rst-text-faint on --rst-surface-2 6.74 4.5:1 text
- --rst-text-faint on --rst-accent-soft 6.00 4.5:1 text
- --rst-accent on --rst-surface 6.95 4.5:1 text + focus ring
- --rst-accent on --rst-bg 7.75 4.5:1 text + focus ring
- --rst-accent on --rst-surface-2 7.40 4.5:1 text
- --rst-accent on --rst-accent-soft 6.59 4.5:1 text
- --rst-on-accent on --rst-accent 7.86 4.5:1 button label
- --rst-on-accent on --rst-accent-strong 10.29 4.5:1 button label, hover
- --rst-line-strong on --rst-surface 4.26 3:1 control border
- --rst-line-strong on --rst-bg 4.75 3:1 control border
- --rst-line-strong on --rst-surface-2 4.53 3:1 control border
- pill neutral fg on its own tint 8.44 4.5:1 text
- pill positive fg on its own tint 10.18 4.5:1 text
- pill warning fg on its own tint 9.62 4.5:1 text
- pill negative fg on its own tint 7.85 4.5:1 text
- --rst-on-accent on negative fg (fill) 8.99 4.5:1 danger button label
-
- The tightest pair in the set is --rst-accent on --rst-accent-soft in
- light, at 4.75:1 against a 4.5:1 floor. Put a token on a surface not
- listed above and you are guessing; recompute first.
-
- --rst-line is a hairline divider between rows, not a control border or
- a meaningful graphic, so 1.4.11's 3:1 does not apply to it. Control
- borders use --rst-line-strong, which does clear 3:1.
-
- --rst-header-rule is not in the table either, and deliberately so
- (design doc §6-v2.2): the page header's rule is decoration, not
- structure — the heading's size, weight and spacing carry the
- structure — so no contrast floor applies to it and it is not in
- ui/contrast_test.go's pair table. Do not argue it up to 3:1.
-
- Pagination chips are the one deliberate exception: they are bordered
- with --rst-line rather than --rst-line-strong, because a page strip
- ringed in hard outlines shouts louder than the list it belongs to.
- 1.4.11 is satisfied without that border — each chip's meaning is
- carried by its text label at 4.5:1 or better, and the current page
- adds a filled background, a heavier weight and aria-current on top.
-
- --rst-shadow-pop, --rst-shadow-knob, --rst-shadow-lift and
- --rst-overlay are not in the table: they paint a translucent shadow or
- scrim over whatever sits underneath, not text over a fixed
- background, so a contrast ratio does not apply. The scrim deepens in
- dark, matching --rst-shadow-pop's own jump: black reads lighter
- against a dark backdrop, so the overlay needs more alpha to still
- read as a dimming layer.
- ──────────────────────────────────────────────────────────────────── */
-
-:root {
- color-scheme: light dark;
-
- /* Type: the family only. Sizes and spacing are structural and stay in
- tokens.css. */
- --rst-font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-
- /* Shape: soft, ordinary, unremarkable. */
- --rst-radius: 8px;
- --rst-radius-sm: 6px;
- --rst-radius-pill: 999px;
-
- /* Surfaces and lines */
- --rst-bg: light-dark(#ffffff, #111418);
- --rst-surface: light-dark(#ffffff, #1a1f26);
- --rst-surface-2: light-dark(#f6f7f8, #15191f);
- --rst-line: light-dark(#e5e7eb, #2a3038);
- --rst-line-strong: light-dark(#7f8792, #79828e);
-
- /* The page header's rule, derived rather than authored: the accent
- folded into the line at 18%, so a theme that changes its accent
- gets a matching rule with no second value to keep in step. day's
- is barely there — a hairline you would call grey until you put it
- beside plain's. Decorative: it carries no contrast floor. */
- --rst-header-rule: color-mix(in oklab, var(--rst-accent) 18%, var(--rst-line));
-
- /* Text */
- --rst-text: light-dark(#1a1d21, #e8eaed);
- --rst-text-muted: light-dark(#4b5563, #a8b0ba);
- --rst-text-faint: light-dark(#656c78, #98a1ac);
-
- /* Accent */
- --rst-accent: light-dark(#2464e0, #7ba7ff);
- --rst-accent-strong: light-dark(#1a4fc0, #a3c0ff);
- --rst-accent-soft: light-dark(#eef3fe, #17233a);
- --rst-on-accent: light-dark(#ffffff, #0b1220);
-
- /* Status tones */
- --rst-tone-neutral-fg: light-dark(#374151, #c3c9d1);
- --rst-tone-neutral-bg: light-dark(#eef0f2, #262c34);
- --rst-tone-positive-fg: light-dark(#166534, #6ee7a8);
- --rst-tone-positive-bg: light-dark(#dcfce7, #10281c);
- --rst-tone-warning-fg: light-dark(#854d0e, #f0c46a);
- --rst-tone-warning-bg: light-dark(#fef3c7, #2c2110);
- --rst-tone-negative-fg: light-dark(#b91c1c, #f79aa0);
- --rst-tone-negative-bg: light-dark(#fee2e2, #331819);
-
- /* Depth */
- --rst-shadow-pop: 0 8px 24px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.5));
- --rst-shadow-knob: 0 1px 2px rgba(0, 0, 0, 0.3);
- --rst-shadow-lift: 0 1px 2px rgba(0, 0, 0, 0.08);
- --rst-overlay: light-dark(rgba(0, 0, 0, 0.45), rgba(0, 0, 0, 0.6));
-}
-
-/* The whole toggle. Setting color-scheme re-resolves every light-dark()
- above, so an explicit choice beats the OS in both directions without
- restating a colour. */
-:root[data-theme="light"] { color-scheme: light; }
-:root[data-theme="dark"] { color-scheme: dark; }
diff --git a/internal/calendar/theme_test.go b/internal/calendar/theme_test.go
new file mode 100644
index 0000000..08bd7e9
--- /dev/null
+++ b/internal/calendar/theme_test.go
@@ -0,0 +1,52 @@
+package calendar
+
+import (
+ "bytes"
+ "net/http"
+ "net/http/httptest"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "testing"
+
+ "amadan.net/oficina/estilo.git/theme"
+)
+
+func TestSharedThemeThroughRenderedAssetRoute(t *testing.T) {
+ a, err := New(Config{DBPath: filepath.Join(t.TempDir(), "calendar.db"), Env: "development"})
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { a.Close() })
+ get := func(path string) *httptest.ResponseRecorder {
+ w := httptest.NewRecorder()
+ a.Handler().ServeHTTP(w, httptest.NewRequest(http.MethodGet, "http://localhost:8080"+path, nil))
+ return w
+ }
+ page := get("/signin")
+ if page.Code != http.StatusOK {
+ t.Fatalf("signin status %d", page.Code)
+ }
+ match := regexp.MustCompile(`/static/theme\.[0-9a-f]{16}\.css`).FindString(page.Body.String())
+ if match == "" {
+ t.Fatal("rendered page lacks a fingerprinted theme")
+ }
+ for _, tc := range []struct {
+ path string
+ cache string
+ }{{match, "public, max-age=31536000, immutable"}, {"/static/theme.css", "no-cache"}, {"/static/theme.0000000000000000.css", "no-cache"}} {
+ w := get(tc.path)
+ if w.Code != http.StatusOK || !bytes.Equal(w.Body.Bytes(), theme.CSS()) {
+ t.Fatalf("theme route %s did not serve shared bytes: %d", tc.path, w.Code)
+ }
+ if !strings.HasPrefix(w.Header().Get("Content-Type"), "text/css") {
+ t.Fatal("theme has wrong content type")
+ }
+ if w.Header().Get("Cache-Control") != tc.cache {
+ t.Fatal("theme cache policy does not match fingerprint")
+ }
+ }
+ if !bytes.Contains(theme.CSS(), []byte("--rst-header-rule:")) {
+ t.Fatal("Calendar header token disappeared")
+ }
+}
diff --git a/internal/calendartest/vendored_test.go b/internal/calendartest/vendored_test.go
index ef5dc34..a232932 100644
--- a/internal/calendartest/vendored_test.go
+++ b/internal/calendartest/vendored_test.go
@@ -6,6 +6,7 @@ import (
"path/filepath"
"testing"
+ "amadan.net/oficina/estilo.git/theme"
"github.com/carlosframework/rastrillo/ui"
)
@@ -39,6 +40,20 @@ func TestVendoredAssetsMatchTheLibrary(t *testing.T) {
t.Fatalf("unknown theme %q", vendoredTheme)
}
for name, lib := range assets {
+ if name == "theme.css" {
+ body := func(css []byte) []byte {
+ if bytes.HasPrefix(css, []byte("/*")) {
+ if end := bytes.Index(css, []byte("*/")); end >= 0 {
+ return css[end+2:]
+ }
+ }
+ return css
+ }
+ if !bytes.Equal(body(theme.CSS()), body(lib)) {
+ t.Fatal("shared theme differs from the selected framework day theme")
+ }
+ continue
+ }
if vendoredIsMine[name] {
continue
}
diff --git a/internal/guardstest/guards_test.go b/internal/guardstest/guards_test.go
index f4d37d5..601eb1f 100644
--- a/internal/guardstest/guards_test.go
+++ b/internal/guardstest/guards_test.go
@@ -18,6 +18,8 @@ import (
"runtime"
"strings"
"testing"
+
+ "amadan.net/oficina/estilo.git/theme"
)
// repoRoot is found from this file's own path, never from the working
@@ -75,7 +77,6 @@ var lineCapExempt = map[string]string{
"internal/calendar/static/rastrillo.js": "vendored from rastrillo; pinned byte-for-byte",
"internal/calendar/static/select.js": "vendored from rastrillo; pinned byte-for-byte",
"internal/calendar/static/tokens.css": "vendored from rastrillo; pinned byte-for-byte",
- "internal/calendar/static/theme.css": "vendored from rastrillo; pinned byte-for-byte",
}
func TestNoFileExceedsTheLineCap(t *testing.T) {
@@ -183,9 +184,13 @@ func TestEveryTokenWeUseIsDefined(t *testing.T) {
declared := map[string]bool{}
for _, name := range []string{"tokens.css", "theme.css"} {
- b, err := os.ReadFile(filepath.Join(static, name))
- if err != nil {
- t.Fatalf("read %s: %v", name, err)
+ b := theme.CSS()
+ if name != "theme.css" {
+ var err error
+ b, err = os.ReadFile(filepath.Join(static, name))
+ if err != nil {
+ t.Fatalf("read %s: %v", name, err)
+ }
}
for _, m := range regexp.MustCompile(`(--rst-[A-Za-z0-9-]+)\s*:`).FindAllStringSubmatch(string(b), -1) {
declared[m[1]] = true