@rathnasgala2/template 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +204 -137
  2. package/contracts/render-policy.jcs +1 -1
  3. package/contracts/theme-styling-contract.jcs +1 -1
  4. package/package.json +4 -5
  5. package/src/core/errors.js +13 -13
  6. package/src/core/index.js +102 -66
  7. package/src/core/internal/appearance/base-layer.js +180 -0
  8. package/src/core/internal/appearance/bootstrap-script.js +11 -11
  9. package/src/core/internal/appearance/contract.js +59 -44
  10. package/src/core/internal/appearance/controller-markup.js +7 -8
  11. package/src/core/internal/appearance/styling-contract.js +132 -29
  12. package/src/core/internal/canonical-jcs.js +0 -0
  13. package/src/core/internal/content-security.js +0 -0
  14. package/src/core/internal/eleventy-render.js +52 -35
  15. package/src/core/internal/feeds.js +10 -13
  16. package/src/core/internal/fs-walk.js +2 -2
  17. package/src/core/internal/media/avif-probe.js +4 -4
  18. package/src/core/internal/media/font-policy.js +2 -2
  19. package/src/core/internal/media/jpeg-codec.js +0 -0
  20. package/src/core/internal/media/limits.js +32 -34
  21. package/src/core/internal/media/pipeline.js +4 -4
  22. package/src/core/internal/media/png-codec.js +7 -7
  23. package/src/core/internal/media/resize.js +2 -2
  24. package/src/core/internal/media/sniff.js +4 -4
  25. package/src/core/internal/media/theme-svg-sanitizer.js +278 -0
  26. package/src/core/internal/media/webp-probe.js +3 -3
  27. package/src/core/internal/messages.js +8 -8
  28. package/src/core/internal/page-kinds.js +46 -48
  29. package/src/core/internal/render-policy-content.js +57 -21
  30. package/src/core/internal/route-labels.js +5 -6
  31. package/src/core/internal/route.js +16 -20
  32. package/src/core/internal/search-index.js +12 -14
  33. package/src/core/internal/semver-range.js +84 -0
  34. package/src/core/internal/seo.js +21 -23
  35. package/src/core/internal/sitemap.js +6 -8
  36. package/src/core/internal/skeleton.js +42 -39
  37. package/src/core/internal/source-inventory.js +4 -5
  38. package/src/core/internal/text-direction.js +1 -2
  39. package/src/core/internal/theme-assets.js +294 -67
  40. package/src/core/internal/uuid.js +4 -5
  41. package/src/core/manifest.js +0 -0
  42. package/types/index.d.ts +22 -8
package/README.md CHANGED
@@ -5,14 +5,14 @@ portable author repository format. It is one of the three author-facing S2
5
5
  artifacts, alongside one `@rathnasgala2/theme-*` package and
6
6
  `@rathnasgala2/publish-action`.
7
7
 
8
- This package is JavaScript ESM on Node 24, authored without TypeScript sources
9
- (DEC-094): every export carries JSDoc types, checked by `tsc --checkJs --noEmit`
10
- against a hand-maintained `.d.ts` surface.
8
+ This package is JavaScript ESM on Node 24, authored without TypeScript sources:
9
+ every export carries JSDoc types, checked by `tsc --checkJs --noEmit` against a
10
+ hand-maintained `.d.ts` surface.
11
11
 
12
12
  ## Status
13
13
 
14
- S2-T02's repository scaffold (exact pins, `src/core/`-only layout, module-tree
15
- absence gate, quality-gate tooling) plus S2-T03's renderer adapter:
14
+ This repository's scaffold (exact pins, `src/core/`-only layout, module-tree
15
+ absence gate, quality-gate tooling) plus its renderer adapter:
16
16
  `renderPublication(buildInput, options)`, exported from `src/core/index.js`, is
17
17
  the package's one documented entry point. It:
18
18
 
@@ -29,7 +29,7 @@ the package's one documented entry point. It:
29
29
  `urn:gala:schema:artifact-manifest:2.0.0` instance describing the candidate
30
30
  output directory.
31
31
 
32
- S2-T05 adds the media pipeline under `src/core/internal/media/`, run from
32
+ The media pipeline lives under `src/core/internal/media/`, run from
33
33
  `renderPublication` after the Eleventy route listing so its output is never
34
34
  mistaken for an HTML route:
35
35
 
@@ -40,8 +40,8 @@ mistaken for an HTML route:
40
40
  entropy decode (a documented scope decision — see
41
41
  `webp-probe.js`/`avif-probe.js`). Every format shares one set of resource
42
42
  ceilings in `limits.js` (raster dimension, pixel count, decoded-buffer and
43
- source-byte caps reused from DEC-097's own closed binary-asset decoder bounds,
44
- plus this renderer's own documented per-publication ceilings).
43
+ source-byte caps reused from the theme package's own closed binary-asset
44
+ decoder bounds, plus this renderer's own documented per-publication ceilings).
45
45
  - **Metadata strip, orientation.** PNG/JPEG derivatives are always re-encoded
46
46
  from decoded pixels, never byte-copied, so no ancillary/EXIF metadata survives
47
47
  into a derivative; JPEG's EXIF `Orientation` tag is read and applied
@@ -55,7 +55,7 @@ mistaken for an HTML route:
55
55
  unconditionally as an image reference (`MEDIA_SVG_REJECTED`) — SVG is never
56
56
  processed as author media.
57
57
  - **Font policy.** `appearance.fontAssets` entries must be bounded WOFF2 (exact
58
- signature, DEC-097's 4,194,304-byte font cap); a remote font reference cannot
58
+ signature, a fixed 4,194,304-byte font cap); a remote font reference cannot
59
59
  enter `build-input` in the first place (`resolvedFile` is always a
60
60
  repository-relative path), so "no remote font is ever retrieved" holds
61
61
  structurally.
@@ -63,17 +63,16 @@ mistaken for an HTML route:
63
63
  mismatch, SVG, unrecognized format, invalid font — is a `MediaPipelineError`
64
64
  with a stable `reasonCode`, thrown before the manifest is assembled.
65
65
 
66
- `options.sourceDirectory` (new in S2-T05) is the caller-mounted, read-only
67
- repository source tree `build-input`'s `resolvedFile` references point into;
68
- every referenced file's bytes are read from there and verified against
69
- `build-input`'s declared `sourceDigest` before being decoded.
70
-
71
- S2-T04 adds the complete output-security pipeline
72
- (`src/core/internal/content-security.js`) on top of that adapter. **Division of
73
- labour** (DEC-097 section 5: normalization replaces an authored body path with a
74
- `renderableBody` carrying "the deterministic policy output/digest";
75
- `build-input.schema.json` fixes `renderableBody.bodyMediaType` as
76
- `const: "text/html"`): a `renderableBody.body` inside a validated
66
+ `options.sourceDirectory` is the caller-mounted, read-only repository source
67
+ tree `build-input`'s `resolvedFile` references point into; every referenced
68
+ file's bytes are read from there and verified against `build-input`'s declared
69
+ `sourceDigest` before being decoded.
70
+
71
+ The complete output-security pipeline (`src/core/internal/content-security.js`)
72
+ sits on top of that adapter. **Division of labour** (normalization replaces an
73
+ authored body path with a `renderableBody` carrying "the deterministic policy
74
+ output/digest"; `build-input.schema.json` fixes `renderableBody.bodyMediaType`
75
+ as `const: "text/html"`): a `renderableBody.body` inside a validated
77
76
  `build-input:2.0.0` instance is **already** render-policy-conformant HTML,
78
77
  produced once upstream of this renderer. This package therefore exposes two
79
78
  distinct public entry points:
@@ -115,23 +114,37 @@ highlighter grammar/token-class catalog) both surfaces above read from,
115
114
  generated deterministically by `scripts/generate-contracts.mjs` from
116
115
  `src/core/internal/render-policy-content.js` — the single source of truth, so
117
116
  the published contract and the enforced policy cannot drift apart. Every
118
- rendered page also carries the exact per-artifact CSP baseline as a
117
+ rendered page also carries the meta-safe CSP baseline as a
119
118
  `<meta http-equiv="Content-Security-Policy">` tag (byte-identical on every
120
119
  route, since S2 materializes no module package/configuration/output/runtime).
121
120
 
121
+ **TPL-H4 fix — meta vs. header-only directives.** The CSP specification
122
+ explicitly ignores `frame-ancestors` (and `report-uri`/`sandbox`, neither used
123
+ here) when a policy is delivered via `<meta http-equiv>`; the `<meta>` tag above
124
+ therefore never includes `frame-ancestors`, and
125
+ `contentSecurityPolicyHeaderOnlyDirectives()` (`internal/content-security.js`)
126
+ returns the one directive that is not: `frame-ancestors 'none'`. Clickjacking
127
+ protection for a published artifact is a **hosting-layer requirement**: the
128
+ adapter/deployment serving this artifact must add
129
+ `Content-Security-Policy: frame-ancestors 'none'` (or an equivalent
130
+ `X-Frame-Options` header) as a real HTTP response header. This cannot yet be
131
+ carried in `artifact-manifest:2.0.0`'s own `declarativeHeaders` field, which
132
+ `@rathnasgala2/schemas` currently fixes to `const: []`; surfacing it there is a
133
+ schema-repo change, out of this repository's scope.
134
+
122
135
  `img[src]` is restricted to repository-relative paths only in this pipeline;
123
- full manifest-coverage verification of media references is the S2-T05 media
124
- pipeline's job (immediately above). A handful of manifest fields (`builder`,
136
+ full manifest-coverage verification of media references is the media pipeline's
137
+ job (immediately above). A handful of manifest fields (`builder`,
125
138
  `sourceIdentity.repository`, `workflowIdentity`, `buildToolVersions`,
126
139
  `excludedInputs`) describe facts `build-input` does not itself carry;
127
140
  `renderPublication` accepts them as an explicit `options.provenance` bundle
128
141
  rather than fabricating them — see `src/core/manifest.js`'s module
129
- documentation. `contracts/theme-styling-contract.jcs` is S2-T12's real, closed
130
- `templateStylingContract` catalog (see the S2-T12 section below).
142
+ documentation. `contracts/theme-styling-contract.jcs` is the real, closed
143
+ `templateStylingContract` catalog (see the theme styling contract section
144
+ below).
131
145
 
132
- S2-T06 adds the core semantic skeleton, page kinds and navigation renderer on
133
- top of the S2-T04 output-security pipeline every generated page still passes
134
- through:
146
+ The core semantic skeleton, page kinds and navigation renderer sit on top of the
147
+ output-security pipeline every generated page still passes through:
135
148
 
136
149
  - **`src/core/internal/skeleton.js`** renders the one landmark contract every
137
150
  page kind shares, through its one `renderPageBody` composition: a skip link
@@ -144,13 +157,13 @@ through:
144
157
  template attribution line). Core owns nine versioned semantic slots
145
158
  (`header-actions`, `article-preamble`, `article-end`, `footer-profile`,
146
159
  `footer-auxiliary`, `account-intent`, `conversation`, `newsletter`,
147
- `edition-selector`) plus the collapsed `article-footer-ad` slot (DEC-060):
148
- every slot is one `data-gala-slot="<name>"` attribute on an otherwise empty,
149
- non-landmark `<div>`, and the collapsed slot additionally carries `hidden`. S2
160
+ `edition-selector`) plus the collapsed `article-footer-ad` slot: every slot is
161
+ one `data-gala-slot="<name>"` attribute on an otherwise empty, non-landmark
162
+ `<div>`, and the collapsed slot additionally carries `hidden`. This renderer
150
163
  has no module system, so every slot except `footer-profile` (which always
151
- wraps the publication's own footer card) and `header-actions` (S2-T07's
152
- appearance control, core content rather than a module) renders empty. This
153
- module also renders breadcrumbs and deterministic pagination controls
164
+ wraps the publication's own footer card) and `header-actions` (the appearance
165
+ control, core content rather than a module) renders empty. This module also
166
+ renders breadcrumbs and deterministic pagination controls
154
167
  (`rel="prev"`/`rel="next"`, an `aria-current="page"` status).
155
168
  - **`src/core/internal/page-kinds.js`** builds every generated page kind from a
156
169
  validated `build-input:2.0.0`: `profile` (the optional publication profile),
@@ -162,10 +175,10 @@ through:
162
175
  page per distinct authored value, plus a `/tags`/`/series` root page linking
163
176
  to each), and `archive` (one listing page per UTC calendar year, plus a
164
177
  `/archive` root page). The `error` page kind (`renderErrorPageBody`) is
165
- exported as a pure function for a future `404.html` caller (S2-T08, out of
166
- this task's scope), not wired into any generated route here. Listing page size
167
- (10), sort tie-break, and the tag/series/archive root pages are documented
168
- scoped decisions the brief text this task read does not fix verbatim.
178
+ exported as a pure function for the `404.html` caller (see below), not wired
179
+ into any generated route here. Listing page size (10), sort tie-break, and the
180
+ tag/series/archive root pages are documented scoped decisions this renderer's
181
+ own governing brief does not fix verbatim.
169
182
  - **`src/core/internal/messages.js`** is the message catalog every user-visible
170
183
  chrome string above is looked up from (never an inline literal), keyed by
171
184
  BCP-47 tag with a fixed `en` fallback — the only catalog S2 authors;
@@ -186,19 +199,19 @@ every page (a scoped decision: navigation/footer text does not flip language
186
199
  page to page); each page's own `<html lang>`/`dir` still reflects that
187
200
  individual page's own content language.
188
201
 
189
- S2-T07 adds the Light/Dark/System appearance controller
190
- (`src/core/internal/appearance/`), the only browser bootstrap S2 emits (brief S2
191
- section 3, "Module absence and CSP equality"):
202
+ The Light/Dark/System appearance controller (`src/core/internal/appearance/`) is
203
+ the only browser bootstrap this renderer emits ("Module absence and CSP
204
+ equality"):
192
205
 
193
206
  - **`contract.js`** is the one module every other appearance-related file
194
- imports every name/value from: the two DEC-097-fixed root attributes
207
+ imports every name/value from: the two fixed root attributes
195
208
  (`data-gala-publication-root` presence, `data-gala-resolved-color-mode`
196
209
  exact-value `"light"`/`"dark"`), the template-internal
197
210
  `data-gala-color-mode-selection` bookkeeping attribute, the three canonical
198
211
  mode values (`light`/`dark`/`system`), the versioned local storage key
199
212
  (`gala:appearance:color-mode:v1`), the control's `id`, the bootstrap script's
200
213
  fixed output path/media type and the `color-scheme` meta content — the
201
- contract S2-T12's published theme styling catalog targets.
214
+ contract the published theme styling catalog targets.
202
215
  - **`controller-markup.js`** renders a native `<select>` (a brief-admitted
203
216
  pattern; needs no `aria-pressed`/`aria-checked` bookkeeping of its own, unlike
204
217
  a custom button-group or radio-group would) bound to a visible `<label>`, with
@@ -225,29 +238,47 @@ section 3, "Module absence and CSP equality"):
225
238
  directory and enters it into the manifest as an ordinary `manifestAsset` row
226
239
  (`{path, byteLength, sha256}` plus its fixed
227
240
  `application/javascript; charset=utf-8` media type), after the Eleventy route
228
- listing — exactly like an S2-T05 media derivative — so it is never mistaken
229
- for an HTML route. `src/core/internal/eleventy-render.js`'s skeleton layout
230
- carries the `data-gala-publication-root` attribute, the
241
+ listing — exactly like a media derivative — so it is never mistaken for an
242
+ HTML route. `src/core/internal/eleventy-render.js`'s skeleton layout carries
243
+ the `data-gala-publication-root` attribute, the
231
244
  `<meta name="color-scheme" content="light dark">` tag and the one
232
245
  `<script src>` reference, all fixed literals from `contract.js`, so every
233
- route's `<head>` is still byte-identical (S2 materializes no module
246
+ route's `<head>` is still byte-identical (this renderer materializes no module
234
247
  package/configuration/output/runtime).
235
248
  - With JavaScript disabled, the control still renders (a native `<select>`,
236
- fully keyboard-operable and usable with no script at all) and the resolved
237
- root attribute is simply never set; `<meta name="color-scheme">` alone already
238
- carries `prefers-color-scheme` through to user-agent styling and the initial
239
- paint independent of any script.
240
-
241
- S2-T12 replaces `contracts/theme-styling-contract.jcs`'s S2-T02 scaffold
242
- placeholder with the real, closed `templateStylingContract` catalog DEC-097
243
- section 4 defines, and wires the selected theme package's stylesheets into every
244
- generated page:
249
+ fully keyboard-operable and usable with no script at all). The resolved root
250
+ attribute is server-rendered as the fixed `light` default, so a no-JS reader,
251
+ an archival crawler, or a load where the bootstrap script fails still gets a
252
+ fully themed page instead of unstyled UA-default HTML; a scripted reader's
253
+ phase 1 bootstrap always overwrites this attribute with the reader's real
254
+ stored selection/system preference before first paint.
255
+ `<meta name="color-scheme">` also carries `prefers-color-scheme` through to
256
+ user-agent styling (form controls, scrollbars) independent of any script.
257
+
258
+ **Authoring a theme:** see [`docs/theme-authoring.md`](docs/theme-authoring.md)
259
+ for the full guide — how a theme is a delta over the template-owned `gala-base`
260
+ layer, the closed 35-token catalog, the closed five-member pseudo-class catalog,
261
+ passive-asset/SVG admission, the `theme.json` digest chain, and `templateRange`.
262
+ Run `node scripts/scaffold-theme.mjs <directory>` to write a minimal,
263
+ already-conformant theme package skeleton rather than hand-typing one.
264
+
265
+ The theme styling contract replaces `contracts/theme-styling-contract.jcs`'s
266
+ original scaffold placeholder with the real, closed `templateStylingContract`
267
+ catalog this renderer's own internal styling decision defines, and wires the
268
+ selected theme package's stylesheets into every generated page:
245
269
 
246
270
  - **`src/core/internal/appearance/styling-contract.js`** is the one reviewed
247
271
  source module `scripts/generate-contracts.mjs` emits the contract from: the
248
- ordered four-layer catalog (`gala-tokens`/`gala-components`/`gala-utilities`/
249
- `gala-print`), the publication-root/resolved-palette selectors, the closed
250
- type/class/id/attribute leaf catalogs, and exactly 64 `publicThemeSlotHooks` —
272
+ ordered five-layer catalog (`gala-base`/`gala-tokens`/`gala-components`/
273
+ `gala-utilities`/`gala-print` — contract 2.1.0: `gala-base` is the template's
274
+ own layer, carrying the reset/type-scale/focus-ring defaults documented below
275
+ `internal/appearance/base-layer.js`, always ordered first so a theme's own
276
+ layers can override it), the publication-root/ resolved-palette selectors, the
277
+ closed type/class/id/attribute leaf catalogs, a closed five-member
278
+ pseudo-class catalog (`hover`, `focus-visible`, `active`, `visited`,
279
+ `disabled` — contract 2.1.0; see that module's own documentation for why each
280
+ is admitted and why the token catalog's `color-focus`/`color-link-visited` had
281
+ no reachable application before it), and exactly 64 `publicThemeSlotHooks` —
251
282
  one per catalog leaf, so every leaf a theme could validly select is a named,
252
283
  documented hook and no catalog member is orphaned. Every leaf is drawn from
253
284
  what this renderer actually renders: 28 type-selector hooks (every
@@ -255,16 +286,16 @@ generated page:
255
286
  `internal/page-kinds.js` and the markdown-it CommonMark pipeline can produce),
256
287
  15 class-selector hooks (the base Prism `.token` class plus one
257
288
  `.language-<grammar>` hook per admitted highlight grammar — fine-grained
258
- per-token-kind classes are a documented S2 scope exclusion), 2 id-selector
259
- hooks (`#main-content`, the appearance `<select>`'s fixed id) and 19
260
- attribute-value hooks (one per `data-gala-slot` value, one per the new
261
- `data-gala-page-kind` value). `catalogDigest` is
289
+ per-token-kind classes are a documented scope exclusion), 2 id-selector hooks
290
+ (`#main-content`, the appearance `<select>`'s fixed id) and 19 attribute-value
291
+ hooks (one per `data-gala-slot` value, one per the `data-gala-page-kind`
292
+ value). `catalogDigest` is
262
293
  `SHA256(UTF8("GALA-TEMPLATE-STYLING-CONTRACT-V2\0") || JCS(...))`, matching
263
- DEC-097 section 8 and the digest profile `@rathnasgala2/schemas`' own internal
264
- `templateStylingContract` profile uses (that function is not exported through
265
- the package's public API, so this module reimplements the same
266
- domain-separated formula rather than importing it).
267
- `assertTemplateStylingContractShape` is this repository's own structural
294
+ the same domain-separated digest construction and the digest profile
295
+ `@rathnasgala2/schemas`' own internal `templateStylingContract` profile uses
296
+ (that function is not exported through the package's public API, so this
297
+ module reimplements the same domain-separated formula rather than importing
298
+ it). `assertTemplateStylingContractShape` is this repository's own structural
268
299
  self-validator (root/palette attribute rows, fixed layer/selector/composition
269
300
  constants, hook count/uniqueness bound, `catalogDigest` recomputation); no
270
301
  `urn:gala:schema:template-styling-contract` schema ID is registered in
@@ -286,50 +317,67 @@ generated page:
286
317
  `@rathnasgala2/schemas`' own exported `validateGalaDocument`.
287
318
  - **`src/core/internal/theme-assets.js`** is the optional theme-asset
288
319
  integration layer: given `options.themeDirectory` (an extracted theme package
289
- directory, shaped exactly like the S2-T13 package file set — `theme.json`,
290
- `tokens.css`, `components.css`, an optional `utilities.css`, `print.css`,
291
- `package.json`, `LICENSE`, `README.md`, `assets/*`), it copies the declared
292
- stylesheets and any declared non-CSS passive assets into `assets/theme/`,
293
- enters each as an ordinary `{path, byteLength, sha256}` `manifestAsset` row,
294
- and renders the ordered `<link>` markup every generated page's `<head>`
295
- inserts (`print.css` alone carries `media="print"`). It validates the theme's
296
- own declared `stylesheets` list against the two admitted shapes (with or
297
- without `utilities.css`) and that `cssLayers` byte-equals the corresponding
298
- layer projection, but does **not** re-implement DEC-097's full CSS Syntax
299
- Module admission grammar — that is the shared `theme-release.yml` conformance
300
- runner's job (S2-T11), run once per theme release, never per publication
320
+ directory, shaped exactly like the theme package's own file set —
321
+ `theme.json`, `tokens.css`, `components.css`, an optional `utilities.css`,
322
+ `print.css`, `package.json`, `LICENSE`, `README.md`, `assets/*`), it copies
323
+ the declared stylesheets and any declared non-CSS passive assets into
324
+ `assets/theme/`, enters each as an ordinary `{path, byteLength, sha256}`
325
+ `manifestAsset` row, and renders the ordered `<link>` markup every generated
326
+ page's `<head>` inserts (`print.css` alone carries `media="print"`). It
327
+ validates the theme's own declared `stylesheets` list against the two admitted
328
+ shapes (with or without `utilities.css`) and that `cssLayers` byte-equals the
329
+ corresponding layer projection, but does **not** re-implement the full CSS
330
+ Syntax Module admission grammar — that is the shared `theme-release.yml`
331
+ conformance runner's job, run once per theme release, never per publication
301
332
  build. `renderPublication` falls back to the pre-existing fixed
302
333
  `assets/theme/print.css` link when no `options.themeDirectory` is supplied
303
- (every caller before S2-T12, and this repository's own tests that do not
304
- exercise theme integration). Minimal in-repo fixture theme packages for both
334
+ (every caller that predates theme integration, and this repository's own tests
335
+ that do not exercise it). Minimal in-repo fixture theme packages for both
305
336
  stylesheet-list shapes live under `test/fixtures/theme-fixture-full/` and
306
337
  `test/fixtures/theme-fixture-minimal/` — deliberately not real, conformant
307
- theme CSS (authoring one is S2-T13's task); they exist only to exercise the
308
- copy/link/manifest mechanism.
338
+ theme CSS (authoring one is a theme package's own task); they exist only to
339
+ exercise the copy/link/manifest mechanism.
340
+
341
+ Hardening at consume time (a theme package is lower-trust supply-chain input
342
+ than the repository owner's own authored content, so it is held to at least the
343
+ same bar): `theme.json` is schema-validated against
344
+ `urn:gala:schema:theme-contract:2.0.0` before anything else, and its
345
+ `contractVersion`/`stylingContractDigest`/ `templateRange` are checked against
346
+ this renderer's own published styling contract and version
347
+ (`internal/semver-range.js`: a deliberately closed exact-version/caret-range
348
+ subset). Every declared file's `byteLength`/ `sha256` is verified against the
349
+ bytes actually read, and `theme.json.budgets` is enforced. Every passive asset's
350
+ bytes are sniffed (never trusted from the declared `mediaType`) against a closed
351
+ allowlist: raster formats are admitted directly, and SVG is admitted only after
352
+ `internal/media/theme-svg-sanitizer.js`'s closed grammar rejects any
353
+ `<script>`/event-handler/external-reference construct outright. **Iconography is
354
+ in scope and expected** (TPL-H6 decision): this sanitizer is what makes a
355
+ theme-declared SVG icon mark safe to publish.
356
+
309
357
  - **basePath fix (LOCAL-7 follow-up; independent-review finding B2).** Before
310
- S2-T12, the appearance bootstrap script's `<script src>` and the social-image
311
- `og:image`/`twitter:image` URL were both root-absolute, unprefixed by the
312
- publication's own `basePath` — a non-root `basePath` publication would 404
313
- them. `internal/appearance/contract.js`'s new
358
+ theme integration existed, the appearance bootstrap script's `<script src>`
359
+ and the social-image `og:image`/`twitter:image` URL were both root-absolute,
360
+ unprefixed by the publication's own `basePath` — a non-root `basePath`
361
+ publication would 404 them. `internal/appearance/contract.js`'s new
314
362
  `appearanceBootstrapScriptHref(basePath)` and `internal/seo.js`'s
315
363
  `resolveSocialImageUrl` (now taking `basePath`) both join through
316
364
  `internal/route.js`'s shared `joinBasePathAndRoute`/`projectFixedAssetPath`,
317
365
  exactly like the pre-existing print-stylesheet convention (now folded into
318
366
  `internal/theme-assets.js`) already did for _hrefs_. Independent review
319
367
  (finding B2) found that this was only half the fix: the _physical files_ this
320
- renderer writes for every asset class — theme CSS/passive assets, every S2-T05
321
- media derivative, and the S2-T07 appearance bootstrap script — were still
322
- written, and entered into the manifest, at their bare **unprefixed** paths.
323
- Since the local-directory adapter serves a `manifestAsset.path` verbatim, a
324
- non-root `basePath` publication's `<link>`/`<script src>`/`og:image` href
325
- would point at `<basePath>/assets/...` while the actual file sat at the
326
- unprefixed `assets/...` — a 404 in practice, not just a theoretical href
327
- mismatch. `src/core/index.js` now joins `basePath` into the physical
328
- destination path and the `manifestAsset.path` for media derivatives
329
- (`joinedMediaAssets`, computed with the same `projectFixedAssetPath` every
330
- other fixed-name generated asset already uses) and the appearance bootstrap
331
- script (`joinedAppearanceScriptPath`); `internal/theme-assets.js` computes its
332
- joined output path once per file and derives both the `<link href>` and the
368
+ renderer writes for every asset class — theme CSS/passive assets, every media
369
+ derivative, and the appearance bootstrap script — were still written, and
370
+ entered into the manifest, at their bare **unprefixed** paths. Since the
371
+ local-directory adapter serves a `manifestAsset.path` verbatim, a non-root
372
+ `basePath` publication's `<link>`/`<script src>`/`og:image` href would point
373
+ at `<basePath>/assets/...` while the actual file sat at the unprefixed
374
+ `assets/...` — a 404 in practice, not just a theoretical href mismatch.
375
+ `src/core/index.js` now joins `basePath` into the physical destination path
376
+ and the `manifestAsset.path` for media derivatives (`joinedMediaAssets`,
377
+ computed with the same `projectFixedAssetPath` every other fixed-name
378
+ generated asset already uses) and the appearance bootstrap script
379
+ (`joinedAppearanceScriptPath`); `internal/theme-assets.js` computes its joined
380
+ output path once per file and derives both the `<link href>` and the
333
381
  file/manifest path from that exact same value, so they cannot drift apart.
334
382
  `test/theme-styling-contract.test.js` carries a dedicated non-root,
335
383
  multi-segment `basePath` (`/blog/2024`) fixture test asserting every one of
@@ -356,14 +404,15 @@ generated page:
356
404
 
357
405
  ### Media pipeline scope decisions (documented, not silently assumed)
358
406
 
359
- Neither the S2 brief nor DEC-097 fixes exact derivative widths, output
360
- formats/quality, or a per-publication media byte/count ceiling for _author_
361
- media (DEC-097's own numeric bounds are stated for the closed
362
- `gala-theme-binary-assets-v2` _theme passive-asset_ profile). This renderer
363
- therefore documents its own choices in `src/core/internal/media/limits.js`
364
- rather than inventing undocumented magic numbers:
407
+ Neither this renderer's own governing brief nor its internal styling decision
408
+ fixes exact derivative widths, output formats/quality, or a per-publication
409
+ media byte/count ceiling for _author_ media (those numeric bounds are stated
410
+ only for the closed `gala-theme-binary-assets-v2` _theme passive-asset_
411
+ profile). This renderer therefore documents its own choices in
412
+ `src/core/internal/media/limits.js` rather than inventing undocumented magic
413
+ numbers:
365
414
 
366
- - decode/resource ceilings reuse DEC-097's theme binary-asset numbers
415
+ - decode/resource ceilings reuse the theme binary-asset profile's numbers
367
416
  (dimension, pixel-count, decoded-buffer, source-byte caps), since the
368
417
  decode-bomb risk they defend against is identical regardless of asset class;
369
418
  - responsive derivative widths (`320/640/960/1280/1920`), JPEG derivative
@@ -375,8 +424,8 @@ rather than inventing undocumented magic numbers:
375
424
  proportion to this task, and is flagged as a follow-up rather than attempted
376
425
  partially.
377
426
 
378
- S2-T08 adds every remaining "Required generated output" from brief S2 section 3
379
- on top of S2-T06's page kinds:
427
+ Every remaining "Required generated output" from this renderer's own governing
428
+ brief sits on top of the page kinds above:
380
429
 
381
430
  - **Feeds** (`src/core/internal/feeds.js`): deterministic Atom 1.0
382
431
  (`/feed/atom.xml`) and RSS 2.0 (`/feed/rss.xml`) documents over the exact
@@ -384,7 +433,8 @@ on top of S2-T06's page kinds:
384
433
  `internal/page-kinds.js`'s `index` page kind paginates (the shared
385
434
  `selectPublishedArticles` export), every item URL absolute against
386
435
  `buildInput.baseUrl`, bounded to the most recent 50 entries (documented scoped
387
- decision — neither the brief nor DEC-097 fixes a feed item count).
436
+ decision — neither the brief nor this renderer's own styling decision fixes a
437
+ feed item count).
388
438
  - **Sitemap** (`src/core/internal/sitemap.js`): `/sitemap.xml` over every page
389
439
  `internal/page-kinds.js` generated that this renderer did not itself mark
390
440
  `noindex` (unlisted content), deterministically sorted by each entry's own
@@ -394,9 +444,10 @@ on top of S2-T06's page kinds:
394
444
  `/search-index.json`, a deterministic JSON document indexing every published
395
445
  `article`/`page` record's title, description, a bounded plain-text excerpt,
396
446
  tags and timestamps. Its shape is a documented scoped decision (neither the
397
- brief nor DEC-097 fixes one). **No runtime search JavaScript accompanies it**
398
- — the Light/Dark/System appearance controller (S2-T07) remains the only
399
- browser bootstrap this renderer ever emits in all of S2 (acceptance test C).
447
+ brief nor this renderer's own styling decision fixes one). **No runtime search
448
+ JavaScript accompanies it** — the Light/Dark/System appearance controller
449
+ remains the only browser bootstrap this renderer ever emits (acceptance test
450
+ C).
400
451
  - **SEO/Open-Graph/Twitter/localization metadata**: every generated page's
401
452
  `<head>` now carries a viewport meta, an optional description, an optional
402
453
  `robots` directive (`noindex, follow` for `status: "unlisted"` content and for
@@ -406,16 +457,15 @@ on top of S2-T06's page kinds:
406
457
  `<link>`s. **hreflang** is a documented scope decision, not a silent omission:
407
458
  `build-input:2.0.0` has no translation/locale-variant linkage between distinct
408
459
  `content[]` records (see `internal/seo.js`'s module documentation), so no
409
- `<link rel="alternate" hreflang="...">` is ever emitted in S2.
410
- - **`404.html`**: the S2-T06 `error` page kind's pure `renderErrorPageBody`, now
411
- actually wired into `renderPublication` at DEC-097's exact `errorDocumentKey`
460
+ `<link rel="alternate" hreflang="...">` is ever emitted by this renderer.
461
+ - **`404.html`**: the `error` page kind's pure `renderErrorPageBody`, now
462
+ actually wired into `renderPublication` at the fixed `errorDocumentKey`
412
463
  (`internal/route.js#errorDocumentPath`) — `404.html` at the root, or
413
464
  `<basePath>/404.html` otherwise.
414
- - **Redirect documents**: unchanged from S2-T03
465
+ - **Redirect documents**: unchanged from the renderer adapter
415
466
  (`internal/route.js#renderRedirectDocument`) — every authored redirect was
416
467
  already an ordinary generated artifact with manifest status fixed at `200`;
417
- this task's own scope note is documenting that fact here, not re-implementing
418
- it.
468
+ this is documenting that fact here, not re-implementing it.
419
469
  - **Print CSS hookup** (`src/core/internal/print-stylesheet.js`): every page's
420
470
  `<head>` carries a `<link rel="stylesheet" media="print">` at the documented
421
471
  conventional path `<basePath>/assets/theme/print.css`. A selected theme's own
@@ -430,11 +480,16 @@ by the route's own fixed output path.
430
480
 
431
481
  ## Prerequisites
432
482
 
433
- - Node.js 24.18.0
434
- - npm 11.16.0
483
+ - Node.js >= 24.0.0 (this repository's own development/CI toolchain is Node.js
484
+ 24.18.0)
485
+ - npm >= 11.0.0 (this repository's own development/CI toolchain is npm 11.16.0)
435
486
 
436
- Both are enforced by `package.json` `engines`/`devEngines`; `.nvmrc` and
437
- `.node-version` carry the same pin.
487
+ `package.json`'s published `engines` field declares the supported _range_
488
+ (TPL-H5 fix: an exact pin published in the tarball rejects every consumer not on
489
+ that precise patch version, including under a consumer's own `engine-strict`
490
+ setting). This repository's own exact toolchain is pinned separately in
491
+ `devEngines` (evaluated only for this project root, never for a consumer) and in
492
+ `.nvmrc`/`.node-version`.
438
493
 
439
494
  ## Commands
440
495
 
@@ -443,11 +498,15 @@ npm install
443
498
  npm run verify
444
499
  ```
445
500
 
446
- `npm run verify` runs, in order: Prettier format check, ESLint,
501
+ `npm run verify` (this description is kept in sync with `package.json`'s own
502
+ `verify` script string by `test/readme-verify-description.test.js`, not
503
+ hand-maintained) runs, in order: Prettier format check, ESLint,
447
504
  `tsc --checkJs --noEmit`, the dependency-cruiser
448
505
  architecture/module-tree-absence gate, `jscpd` duplication scan, the
449
- `contracts/*.jcs` canonical-form check, `node --test`, the license inventory
450
- check, and `cyclonedx-npm` SBOM generation plus presence check.
506
+ `contracts/*.jcs` canonical-form check, the no-local-schema-pin check,
507
+ `node --test`, the license inventory check, the SBOM currency check
508
+ (`sbom:check` compares `sbom.cdx.json` against the current lockfile — it is not
509
+ a presence check), the workflow-pin check, and `npm audit --audit-level=high`.
451
510
 
452
511
  Individual gates:
453
512
 
@@ -458,21 +517,25 @@ npm run lint # ESLint flat config
458
517
  npm run typecheck # tsc --checkJs --noEmit against types/index.d.ts
459
518
  npm run architecture # dependency-cruiser: no src/modules/, no circular deps
460
519
  npm run duplication # jscpd, 3% / 50 tokens
461
- npm run contracts:generate # (re)write contracts/*.jcs placeholders
520
+ npm run contracts:generate # (re)write contracts/*.jcs from their reviewed source modules
462
521
  npm run contracts:check # fail if committed contracts drifted
522
+ npm run schema-pin:check # fail on a file: @rathnasgala2/schemas specifier
463
523
  npm test # node --test
464
524
  npm run licenses:generate # (re)write THIRD_PARTY_LICENSES.json
465
525
  npm run licenses:check # fail if it drifted from package-lock.json
466
526
  npm run sbom:generate # cyclonedx-npm -> sbom.cdx.json
467
- npm run sbom:check # fail if sbom.cdx.json is missing
527
+ npm run sbom:check # fail if sbom.cdx.json is stale against package-lock.json
528
+ npm run workflows:check # fail if a GitHub Actions ref is not SHA-pinned
529
+ npm run workflows:drift # fail if workflow files diverge from their own baseline
530
+ npm run audit # npm audit --audit-level=high
468
531
  ```
469
532
 
470
533
  ## Package layout
471
534
 
472
535
  ```text
473
536
  src/core/ # the only source root
474
- contracts/render-policy.jcs # normalized-body policy identity (S2-T04, real content)
475
- contracts/theme-styling-contract.jcs # published styling catalog (S2-T12, real and closed)
537
+ contracts/render-policy.jcs # normalized-body policy identity (real content)
538
+ contracts/theme-styling-contract.jcs # published styling catalog (real and closed)
476
539
  ```
477
540
 
478
541
  No `src/modules/` tree, module import edge, registration stub, module
@@ -481,7 +544,7 @@ configuration, module output or module runtime is ever admitted beside
481
544
  `only-src-core-is-a-source-root` rule both enforce this; `interactions`,
482
545
  `whitelabel`, `newsletter` and `prism` have no module directory, package,
483
546
  configuration schema, stub, output, runtime code, network target or
484
- compatibility promise in this MVP (DEC-097 section 2).
547
+ compatibility promise in this MVP.
485
548
 
486
549
  ## Consuming `@rathnasgala2/schemas`
487
550
 
@@ -508,8 +571,12 @@ and `artifact-manifest` are byte-identical from 2.6.1 to 2.8.0.
508
571
 
509
572
  ## Governing documents
510
573
 
511
- - [Slice brief S2: author-owned publication](../../orchestration/slice-briefs/S2-author-owned-publication.md),
512
- section 3 (`@rathnasgala2/template`)
513
- - [DEC-094](../../orchestration/decisions/DEC-094-javascript-esm-public-packages.md):
514
- JavaScript ESM for public packages
515
- - [WORKSPACE.md](../../orchestration/WORKSPACE.md), sections 4-8 and 13
574
+ The internal specifications this package is built against live outside this
575
+ repository and have no public URL, so they are named here as plain text rather
576
+ than as dead links:
577
+
578
+ - Slice brief S2: author-owned publication, section 3 (`@rathnasgala2/template`)
579
+ (internal)
580
+ - The internal decision record establishing JavaScript ESM for public packages
581
+ (internal)
582
+ - WORKSPACE.md, sections 4-8 and 13 (internal)
@@ -1 +1 @@
1
- {"contentSecurityPolicy":"default-src 'none'; base-uri 'none'; object-src 'none'; frame-ancestors 'none'; form-action 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'none'; media-src 'self'; manifest-src 'self'; worker-src 'none'","highlighter":{"grammars":["bash","css","diff","java","javascript","json","jsx","markdown","markup","python","sql","tsx","typescript","yaml"],"package":"@11ty/eleventy-plugin-syntaxhighlight","tokenClasses":["atrule","attr-equals","attr-name","attr-value","bold","boolean","builtin","cdata","char","class-name","comment","constant","content","coord","deleted","deleted-sign","doctype","entity","for-or-select","function","function-variable","important","inserted","inserted-sign","interpolation","interpolation-punctuation","italic","key","keyword","namespace","null","number","operator","prefix","prolog","property","punctuation","regex","rule","selector","shebang","string","symbol","tag","template-punctuation","template-string","title","token","url","variable"],"version":"5.0.2"},"markdownIt":{"options":{"html":false,"linkify":false,"typographer":false},"package":"markdown-it","version":"14.3.1"},"name":"gala-render-policy","sanitizer":{"allowedAttributes":{"a":["href","rel"],"code":["class"],"h1":["id"],"h2":["id"],"h3":["id"],"h4":["id"],"h5":["id"],"h6":["id"],"img":["src","alt"],"ol":["start"],"pre":["class"],"span":["class"]},"allowedTags":["p","br","hr","h1","h2","h3","h4","h5","h6","ul","ol","li","blockquote","pre","code","strong","em","b","i","u","s","del","ins","sub","sup","mark","small","wbr","a","img","table","thead","tbody","tr","th","td","dl","dt","dd","figure","figcaption","span"],"allowedUrlSchemes":["https","http","mailto"],"package":"sanitize-html","version":"2.17.7"},"version":"2.0.0"}
1
+ {"contentSecurityPolicy":"default-src 'none'; base-uri 'none'; object-src 'none'; frame-ancestors 'none'; form-action 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'none'; media-src 'self'; manifest-src 'self'; worker-src 'none'","contentSecurityPolicyHeaderOnlyDirectives":["frame-ancestors 'none'"],"contentSecurityPolicyMeta":"default-src 'none'; base-uri 'none'; object-src 'none'; form-action 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'none'; media-src 'self'; manifest-src 'self'; worker-src 'none'","highlighter":{"grammars":["bash","css","diff","java","javascript","json","jsx","markdown","markup","python","sql","tsx","typescript","yaml"],"package":"@11ty/eleventy-plugin-syntaxhighlight","tokenClasses":["atrule","attr-equals","attr-name","attr-value","bold","boolean","builtin","cdata","char","class-name","comment","constant","content","coord","deleted","deleted-sign","doctype","entity","for-or-select","function","function-variable","important","inserted","inserted-sign","interpolation","interpolation-punctuation","italic","key","keyword","namespace","null","number","operator","prefix","prolog","property","punctuation","regex","rule","selector","shebang","string","symbol","tag","template-punctuation","template-string","title","token","url","variable"],"version":"5.0.2"},"markdownIt":{"options":{"html":false,"linkify":false,"typographer":false},"package":"markdown-it","version":"14.3.1"},"name":"gala-render-policy","sanitizer":{"allowedAttributes":{"a":["href","rel"],"code":["class"],"h1":["id"],"h2":["id"],"h3":["id"],"h4":["id"],"h5":["id"],"h6":["id"],"img":["src","alt"],"ol":["start"],"pre":["class"],"span":["class"]},"allowedTags":["p","br","hr","h1","h2","h3","h4","h5","h6","ul","ol","li","blockquote","pre","code","strong","em","b","i","u","s","del","ins","sub","sup","mark","small","wbr","a","img","table","thead","tbody","tr","th","td","dl","dt","dd","figure","figcaption","span"],"allowedUrlSchemes":["https","http","mailto"],"package":"sanitize-html","version":"2.17.7"},"version":"2.0.0"}
@@ -1 +1 @@
1
- {"attributes":[{"match":"exact-value","name":"data-gala-page-kind","role":"semantic","values":["archive","article","author","error","index","page","profile","series","tag"]},{"match":"exact-value","name":"data-gala-resolved-color-mode","role":"state","values":["dark","light"]},{"match":"exact-value","name":"data-gala-slot","role":"semantic","values":["account-intent","article-end","article-footer-ad","article-preamble","conversation","edition-selector","footer-auxiliary","footer-profile","header-actions","newsletter"]},{"match":"presence","name":"data-gala-publication-root","role":"root","values":[]}],"catalogDigest":"sha256:46b43a6d1775f88bf2ded331512bd0cf27d232e256e40a89bda773e50897877f","classSelectors":["language-bash","language-css","language-diff","language-java","language-javascript","language-json","language-jsx","language-markdown","language-markup","language-python","language-sql","language-tsx","language-typescript","language-yaml","token"],"combinators":[" "," > "," + "," ~ "],"composition":{"compoundOrder":["type","id","class","attribute","pseudo-class","pseudo-element"],"functionalSelectorArguments":"compound-only","maximumFunctionalDepth":1,"nthExpressionProfile":"gala-positive-an-plus-b-v2","rootScope":"first-compound-required"},"contractVersion":"2.0.0","functionalPseudos":["is","where","not","nth-child","nth-last-child"],"idSelectors":["gala-appearance-color-mode","main-content"],"orderedLayers":["gala-tokens","gala-components","gala-utilities","gala-print"],"profile":"gala-template-styling-contract-v2","pseudoClasses":[],"pseudoElements":["after","before","marker","selection"],"publicThemeSlotHooks":[{"hookId":"code-block","selectorAtom":"pre"},{"hookId":"code-inline","selectorAtom":"code"},{"hookId":"code-language-bash","selectorAtom":".language-bash"},{"hookId":"code-language-css","selectorAtom":".language-css"},{"hookId":"code-language-diff","selectorAtom":".language-diff"},{"hookId":"code-language-java","selectorAtom":".language-java"},{"hookId":"code-language-javascript","selectorAtom":".language-javascript"},{"hookId":"code-language-json","selectorAtom":".language-json"},{"hookId":"code-language-jsx","selectorAtom":".language-jsx"},{"hookId":"code-language-markdown","selectorAtom":".language-markdown"},{"hookId":"code-language-markup","selectorAtom":".language-markup"},{"hookId":"code-language-python","selectorAtom":".language-python"},{"hookId":"code-language-sql","selectorAtom":".language-sql"},{"hookId":"code-language-tsx","selectorAtom":".language-tsx"},{"hookId":"code-language-typescript","selectorAtom":".language-typescript"},{"hookId":"code-language-yaml","selectorAtom":".language-yaml"},{"hookId":"code-token","selectorAtom":".token"},{"hookId":"content-article","selectorAtom":"article"},{"hookId":"control-appearance-select","selectorAtom":"#gala-appearance-color-mode"},{"hookId":"control-label","selectorAtom":"label"},{"hookId":"control-option","selectorAtom":"option"},{"hookId":"control-select","selectorAtom":"select"},{"hookId":"heading-1","selectorAtom":"h1"},{"hookId":"heading-2","selectorAtom":"h2"},{"hookId":"heading-3","selectorAtom":"h3"},{"hookId":"heading-4","selectorAtom":"h4"},{"hookId":"heading-5","selectorAtom":"h5"},{"hookId":"heading-6","selectorAtom":"h6"},{"hookId":"landmark-footer","selectorAtom":"footer"},{"hookId":"landmark-header","selectorAtom":"header"},{"hookId":"landmark-main","selectorAtom":"main"},{"hookId":"landmark-main-content","selectorAtom":"#main-content"},{"hookId":"landmark-navigation","selectorAtom":"nav"},{"hookId":"media-image","selectorAtom":"img"},{"hookId":"page-archive","selectorAtom":"[data-gala-page-kind=\"archive\"]"},{"hookId":"page-article","selectorAtom":"[data-gala-page-kind=\"article\"]"},{"hookId":"page-author","selectorAtom":"[data-gala-page-kind=\"author\"]"},{"hookId":"page-error","selectorAtom":"[data-gala-page-kind=\"error\"]"},{"hookId":"page-index","selectorAtom":"[data-gala-page-kind=\"index\"]"},{"hookId":"page-page","selectorAtom":"[data-gala-page-kind=\"page\"]"},{"hookId":"page-profile","selectorAtom":"[data-gala-page-kind=\"profile\"]"},{"hookId":"page-series","selectorAtom":"[data-gala-page-kind=\"series\"]"},{"hookId":"page-tag","selectorAtom":"[data-gala-page-kind=\"tag\"]"},{"hookId":"prose-blockquote","selectorAtom":"blockquote"},{"hookId":"prose-divider","selectorAtom":"hr"},{"hookId":"prose-emphasis","selectorAtom":"em"},{"hookId":"prose-link","selectorAtom":"a"},{"hookId":"prose-list-item","selectorAtom":"li"},{"hookId":"prose-list-ordered","selectorAtom":"ol"},{"hookId":"prose-list-unordered","selectorAtom":"ul"},{"hookId":"prose-paragraph","selectorAtom":"p"},{"hookId":"prose-span","selectorAtom":"span"},{"hookId":"prose-strong","selectorAtom":"strong"},{"hookId":"prose-time","selectorAtom":"time"},{"hookId":"slot-account-intent","selectorAtom":"[data-gala-slot=\"account-intent\"]"},{"hookId":"slot-article-end","selectorAtom":"[data-gala-slot=\"article-end\"]"},{"hookId":"slot-article-footer-ad","selectorAtom":"[data-gala-slot=\"article-footer-ad\"]"},{"hookId":"slot-article-preamble","selectorAtom":"[data-gala-slot=\"article-preamble\"]"},{"hookId":"slot-conversation","selectorAtom":"[data-gala-slot=\"conversation\"]"},{"hookId":"slot-edition-selector","selectorAtom":"[data-gala-slot=\"edition-selector\"]"},{"hookId":"slot-footer-auxiliary","selectorAtom":"[data-gala-slot=\"footer-auxiliary\"]"},{"hookId":"slot-footer-profile","selectorAtom":"[data-gala-slot=\"footer-profile\"]"},{"hookId":"slot-header-actions","selectorAtom":"[data-gala-slot=\"header-actions\"]"},{"hookId":"slot-newsletter","selectorAtom":"[data-gala-slot=\"newsletter\"]"}],"publicationRootSelector":"[data-gala-publication-root]","resolvedPaletteSelectors":{"dark":"[data-gala-publication-root][data-gala-resolved-color-mode=\"dark\"]","light":"[data-gala-publication-root][data-gala-resolved-color-mode=\"light\"]"},"stylesheetLayers":{"components":"gala-components","print":"gala-print","tokens":"gala-tokens","utilities":"gala-utilities"},"templatePackage":"@rathnasgala2/template","templateVersion":"2.0.0","typeSelectors":["a","article","blockquote","code","em","footer","h1","h2","h3","h4","h5","h6","header","hr","img","label","li","main","nav","ol","option","p","pre","select","span","strong","time","ul"]}
1
+ {"attributes":[{"match":"exact-value","name":"data-gala-page-kind","role":"semantic","values":["archive","article","author","error","index","page","profile","series","tag"]},{"match":"exact-value","name":"data-gala-resolved-color-mode","role":"state","values":["dark","light"]},{"match":"exact-value","name":"data-gala-slot","role":"semantic","values":["account-intent","article-end","article-footer-ad","article-preamble","conversation","edition-selector","footer-auxiliary","footer-profile","header-actions","newsletter"]},{"match":"presence","name":"data-gala-publication-root","role":"root","values":[]}],"catalogDigest":"sha256:4aca7e9794bddfb2c3afd22f4efd078e7dcf69dde9d62ce52d28cec19aef8b91","classSelectors":["language-bash","language-css","language-diff","language-java","language-javascript","language-json","language-jsx","language-markdown","language-markup","language-python","language-sql","language-tsx","language-typescript","language-yaml","token"],"combinators":[" "," > "," + "," ~ "],"composition":{"compoundOrder":["type","id","class","attribute","pseudo-class","pseudo-element"],"functionalPseudoKeywordArguments":["even","odd"],"functionalSelectorArguments":"compound-only","maximumFunctionalDepth":1,"nthExpressionProfile":"gala-positive-an-plus-b-v2","rootScope":"first-compound-required"},"contractVersion":"2.1.0","functionalPseudos":["is","where","not","nth-child","nth-last-child"],"idSelectors":["gala-appearance-color-mode","main-content"],"orderedLayers":["gala-base","gala-tokens","gala-components","gala-utilities","gala-print"],"profile":"gala-template-styling-contract-v2","pseudoClasses":["active","disabled","focus-visible","hover","visited"],"pseudoElements":["after","before","marker","selection"],"publicThemeSlotHooks":[{"hookId":"code-block","selectorAtom":"pre"},{"hookId":"code-inline","selectorAtom":"code"},{"hookId":"code-language-bash","selectorAtom":".language-bash"},{"hookId":"code-language-css","selectorAtom":".language-css"},{"hookId":"code-language-diff","selectorAtom":".language-diff"},{"hookId":"code-language-java","selectorAtom":".language-java"},{"hookId":"code-language-javascript","selectorAtom":".language-javascript"},{"hookId":"code-language-json","selectorAtom":".language-json"},{"hookId":"code-language-jsx","selectorAtom":".language-jsx"},{"hookId":"code-language-markdown","selectorAtom":".language-markdown"},{"hookId":"code-language-markup","selectorAtom":".language-markup"},{"hookId":"code-language-python","selectorAtom":".language-python"},{"hookId":"code-language-sql","selectorAtom":".language-sql"},{"hookId":"code-language-tsx","selectorAtom":".language-tsx"},{"hookId":"code-language-typescript","selectorAtom":".language-typescript"},{"hookId":"code-language-yaml","selectorAtom":".language-yaml"},{"hookId":"code-token","selectorAtom":".token"},{"hookId":"content-article","selectorAtom":"article"},{"hookId":"control-appearance-select","selectorAtom":"#gala-appearance-color-mode"},{"hookId":"control-label","selectorAtom":"label"},{"hookId":"control-option","selectorAtom":"option"},{"hookId":"control-select","selectorAtom":"select"},{"hookId":"heading-1","selectorAtom":"h1"},{"hookId":"heading-2","selectorAtom":"h2"},{"hookId":"heading-3","selectorAtom":"h3"},{"hookId":"heading-4","selectorAtom":"h4"},{"hookId":"heading-5","selectorAtom":"h5"},{"hookId":"heading-6","selectorAtom":"h6"},{"hookId":"landmark-footer","selectorAtom":"footer"},{"hookId":"landmark-header","selectorAtom":"header"},{"hookId":"landmark-main","selectorAtom":"main"},{"hookId":"landmark-main-content","selectorAtom":"#main-content"},{"hookId":"landmark-navigation","selectorAtom":"nav"},{"hookId":"media-image","selectorAtom":"img"},{"hookId":"page-archive","selectorAtom":"[data-gala-page-kind=\"archive\"]"},{"hookId":"page-article","selectorAtom":"[data-gala-page-kind=\"article\"]"},{"hookId":"page-author","selectorAtom":"[data-gala-page-kind=\"author\"]"},{"hookId":"page-error","selectorAtom":"[data-gala-page-kind=\"error\"]"},{"hookId":"page-index","selectorAtom":"[data-gala-page-kind=\"index\"]"},{"hookId":"page-page","selectorAtom":"[data-gala-page-kind=\"page\"]"},{"hookId":"page-profile","selectorAtom":"[data-gala-page-kind=\"profile\"]"},{"hookId":"page-series","selectorAtom":"[data-gala-page-kind=\"series\"]"},{"hookId":"page-tag","selectorAtom":"[data-gala-page-kind=\"tag\"]"},{"hookId":"prose-blockquote","selectorAtom":"blockquote"},{"hookId":"prose-divider","selectorAtom":"hr"},{"hookId":"prose-emphasis","selectorAtom":"em"},{"hookId":"prose-link","selectorAtom":"a"},{"hookId":"prose-list-item","selectorAtom":"li"},{"hookId":"prose-list-ordered","selectorAtom":"ol"},{"hookId":"prose-list-unordered","selectorAtom":"ul"},{"hookId":"prose-paragraph","selectorAtom":"p"},{"hookId":"prose-span","selectorAtom":"span"},{"hookId":"prose-strong","selectorAtom":"strong"},{"hookId":"prose-time","selectorAtom":"time"},{"hookId":"slot-account-intent","selectorAtom":"[data-gala-slot=\"account-intent\"]"},{"hookId":"slot-article-end","selectorAtom":"[data-gala-slot=\"article-end\"]"},{"hookId":"slot-article-footer-ad","selectorAtom":"[data-gala-slot=\"article-footer-ad\"]"},{"hookId":"slot-article-preamble","selectorAtom":"[data-gala-slot=\"article-preamble\"]"},{"hookId":"slot-conversation","selectorAtom":"[data-gala-slot=\"conversation\"]"},{"hookId":"slot-edition-selector","selectorAtom":"[data-gala-slot=\"edition-selector\"]"},{"hookId":"slot-footer-auxiliary","selectorAtom":"[data-gala-slot=\"footer-auxiliary\"]"},{"hookId":"slot-footer-profile","selectorAtom":"[data-gala-slot=\"footer-profile\"]"},{"hookId":"slot-header-actions","selectorAtom":"[data-gala-slot=\"header-actions\"]"},{"hookId":"slot-newsletter","selectorAtom":"[data-gala-slot=\"newsletter\"]"}],"publicationRootSelector":"[data-gala-publication-root]","resolvedPaletteSelectors":{"dark":"[data-gala-publication-root][data-gala-resolved-color-mode=\"dark\"]","light":"[data-gala-publication-root][data-gala-resolved-color-mode=\"light\"]"},"stylesheetLayers":{"components":"gala-components","print":"gala-print","tokens":"gala-tokens","utilities":"gala-utilities"},"templatePackage":"@rathnasgala2/template","templateVersion":"2.1.0","typeSelectors":["a","article","blockquote","code","em","footer","h1","h2","h3","h4","h5","h6","header","hr","img","label","li","main","nav","ol","option","p","pre","select","span","strong","time","ul"]}