@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
@@ -1,32 +1,32 @@
1
1
  /**
2
- * The single module `contracts/theme-styling-contract.jcs` (S2-T12) targets
3
- * for every Light/Dark/System appearance name this renderer owns (task
4
- * packet S2-T07: "the `data-` attribute/class names the controller sets must
5
- * be fixed constants exported from one module"; DEC-097's closed
6
- * `templateStylingContract` catalog, section "Closed theme-package and
7
- * stylesheet admission": the required root attribute row
2
+ * The single module `contracts/theme-styling-contract.jcs` targets for
3
+ * every Light/Dark/System appearance name this renderer owns: the
4
+ * `data-` attribute/class names the controller sets must be fixed
5
+ * constants exported from one module. The published
6
+ * `templateStylingContract` catalog's closed theme-package and stylesheet
7
+ * admission requires the root attribute row
8
8
  * `{name:data-gala-publication-root,match:presence,values:[],role:root}` and
9
- * the required palette attribute row
10
- * `{name:data-gala-resolved-color-mode,match:exact-value,values:[dark,light],role:state}`;
11
- * "The template renderer emits the exact root attribute and maintains
9
+ * the palette attribute row
10
+ * `{name:data-gala-resolved-color-mode,match:exact-value,values:[dark,light],role:state}`:
11
+ * the template renderer emits the exact root attribute and maintains
12
12
  * exactly one `data-gala-resolved-color-mode="light"|"dark"` value on that
13
- * same element before theme CSS is observed").
13
+ * same element before theme CSS is observed.
14
14
  *
15
- * Two attributes are exposed, deliberately kept separate (brief S2 section 3:
16
- * "selection and resolved state are exposed separately through
17
- * template-owned root attributes; theme CSS consumes only resolved state"):
15
+ * Two attributes are exposed, deliberately kept separate: selection and
16
+ * resolved state are exposed separately through template-owned root
17
+ * attributes; theme CSS consumes only resolved state.
18
18
  *
19
19
  * - {@link APPEARANCE_ROOT_ATTRIBUTE} and {@link APPEARANCE_RESOLVED_MODE_ATTRIBUTE}
20
- * are the two DEC-097-fixed theme-facing attributes above — part of the
21
- * published styling contract S2-T12 emits, never renamed here independent
22
- * of that contract.
20
+ * are the two fixed theme-facing attributes above — part of the published
21
+ * styling contract, never renamed here independent of that contract.
23
22
  * - {@link APPEARANCE_SELECTION_ATTRIBUTE} is this renderer's own
24
23
  * template-internal bookkeeping attribute (the reader's raw `light` /
25
24
  * `dark` / `system` choice, as opposed to `system`'s *resolved* value). It
26
- * is not a theme-styling-contract hook — DEC-097's attribute catalog does
27
- * not name it, and no theme selector may target it — but its name is still
28
- * fixed here, in this one module, exactly like the two contract-owned
29
- * attributes, so no other module ever invents its own literal for it.
25
+ * is not a theme-styling-contract hook — the published attribute catalog
26
+ * does not name it, and no theme selector may target it — but its name is
27
+ * still fixed here, in this one module, exactly like the two
28
+ * contract-owned attributes, so no other module ever invents its own
29
+ * literal for it.
30
30
  *
31
31
  * Both the pre-paint bootstrap script ({@link ../appearance/bootstrap-script.js})
32
32
  * and the server-rendered control markup ({@link ../appearance/controller-markup.js})
@@ -37,7 +37,7 @@
37
37
  import { joinBasePathAndRoute } from '../route.js';
38
38
 
39
39
  /**
40
- * The DEC-097 `templateStylingContract.publicationRootSelector` attribute:
40
+ * The `templateStylingContract.publicationRootSelector` attribute:
41
41
  * a bare-presence attribute the template always renders on the document's
42
42
  * root element (`<html>`), independent of JavaScript (`match:presence`).
43
43
  *
@@ -46,7 +46,7 @@ import { joinBasePathAndRoute } from '../route.js';
46
46
  export const APPEARANCE_ROOT_ATTRIBUTE = 'data-gala-publication-root';
47
47
 
48
48
  /**
49
- * The DEC-097 `templateStylingContract.resolvedPaletteSelectors` attribute:
49
+ * The `templateStylingContract.resolvedPaletteSelectors` attribute:
50
50
  * an exact-value attribute (`"light"` or `"dark"`, never `"system"`) the
51
51
  * pre-paint bootstrap script sets on the root element before first themed
52
52
  * paint. Theme CSS consumes only this attribute, never
@@ -75,9 +75,8 @@ export const APPEARANCE_MODE_DARK = 'dark';
75
75
  export const APPEARANCE_MODE_SYSTEM = 'system';
76
76
 
77
77
  /**
78
- * Every canonical machine-readable selection value (brief S2 section 3:
79
- * "canonical machine values are `light`, `dark`, `system`; `lite` is not a
80
- * value").
78
+ * Every canonical machine-readable selection value: `light`, `dark`,
79
+ * `system`; `lite` is not a value.
81
80
  *
82
81
  * @type {readonly ['light', 'dark', 'system']}
83
82
  */
@@ -89,9 +88,9 @@ export const APPEARANCE_MODE_VALUES = Object.freeze([
89
88
 
90
89
  /**
91
90
  * Every value {@link APPEARANCE_RESOLVED_MODE_ATTRIBUTE} may carry. `system`
92
- * always resolves to one of these two; it is never a third selector value
93
- * (DEC-097: "`system` is resolved to one of those values rather than
94
- * becoming a third selector").
91
+ * always resolves to one of these two; it is never a third selector value —
92
+ * `system` is resolved to one of those values rather than becoming a third
93
+ * selector.
95
94
  *
96
95
  * @type {readonly ['light', 'dark']}
97
96
  */
@@ -101,8 +100,25 @@ export const APPEARANCE_RESOLVED_MODE_VALUES = Object.freeze([
101
100
  ]);
102
101
 
103
102
  /**
104
- * The initial reader default (brief S2 section 3: "initial reader default is
105
- * `system`").
103
+ * TPL-C1 fix: the server-rendered default for
104
+ * {@link APPEARANCE_RESOLVED_MODE_ATTRIBUTE} on `<html>`, present on every
105
+ * page independent of JavaScript (`internal/eleventy-render.js`'s
106
+ * `SKELETON_LAYOUT_SOURCE`). `light` is chosen because it is the value the
107
+ * bootstrap script's own `resolveMode` already falls back to when
108
+ * `prefers-color-scheme` cannot be read (`internal/appearance/
109
+ * bootstrap-script.js`'s `systemPrefersDark` catch branch), so the
110
+ * server-rendered default and the script's own no-signal fallback agree.
111
+ * The bootstrap script's phase 1 always overwrites this attribute
112
+ * synchronously before first paint for a reader whose browser runs it, so
113
+ * this default is observed only by a reader (or crawler) that never runs
114
+ * script at all.
115
+ *
116
+ * @type {'light'}
117
+ */
118
+ export const APPEARANCE_SERVER_DEFAULT_RESOLVED_MODE = APPEARANCE_MODE_LIGHT;
119
+
120
+ /**
121
+ * The initial reader default is `system`.
106
122
  *
107
123
  * @type {'system'}
108
124
  */
@@ -110,9 +126,9 @@ export const APPEARANCE_DEFAULT_MODE = APPEARANCE_MODE_SYSTEM;
110
126
 
111
127
  /**
112
128
  * The one versioned local-storage key the pre-paint bootstrap script and the
113
- * control's change handler both read/write (brief S2 section 3: "selection
114
- * is stored under one versioned local key"). The trailing `v1` is this
115
- * key's own version; a future incompatible storage format change mints a new
129
+ * control's change handler both read/write. Selection is stored under one
130
+ * versioned local key. The trailing `v1` is this key's own version; a
131
+ * future incompatible storage format change mints a new
116
132
  * key rather than reinterpreting old values, so an old browser's already
117
133
  * stored `v1` value is simply orphaned (ignored, never migrated or deleted)
118
134
  * rather than misread as a new format.
@@ -133,10 +149,10 @@ export const APPEARANCE_SELECT_ID = 'gala-appearance-color-mode';
133
149
 
134
150
  /**
135
151
  * The candidate-output-directory-relative path the deterministic pre-paint
136
- * bootstrap script is written to (task packet S2-T07: "entered into the
137
- * manifest through the S2-T03 path with `{path,byteLength,sha256}`"). Not
138
- * content-addressed (unlike the S2-T05 media pipeline's derivative paths):
139
- * this file's bytes are a pure function of this module's own constants, not
152
+ * bootstrap script is written to, entered into the manifest as an ordinary
153
+ * `{path,byteLength,sha256}` asset row. Not content-addressed (unlike the
154
+ * media pipeline's derivative paths): this file's bytes are a pure
155
+ * function of this module's own constants, not
140
156
  * of any per-publication input, so a stable path carries no collision risk.
141
157
  * The `v1` component versions the bootstrap script's own behaviour,
142
158
  * independent of {@link APPEARANCE_STORAGE_KEY}'s own versioning.
@@ -150,9 +166,9 @@ export const APPEARANCE_BOOTSTRAP_SCRIPT_PATH =
150
166
  * The root-absolute `<script src>` reference for a root (`/`) `basePath`
151
167
  * publication. Every actual render joins this with the publication's own
152
168
  * `basePath` through {@link appearanceBootstrapScriptHref} instead of using
153
- * this literal directly (S2-T12 basePath fix, LOCAL-7 follow-up: an earlier
154
- * version of this module used this root-absolute literal unconditionally,
155
- * which 404s under a non-root `basePath` publication). This constant is
169
+ * this literal directly (basePath fix: an earlier version of this module
170
+ * used this root-absolute literal unconditionally, which 404s under a
171
+ * non-root `basePath` publication). This constant is
156
172
  * retained as the one-place root-`basePath` literal every test and the
157
173
  * `joinBasePathAndRoute` call below both agree on.
158
174
  *
@@ -162,7 +178,7 @@ export const APPEARANCE_BOOTSTRAP_SCRIPT_HREF = `/${APPEARANCE_BOOTSTRAP_SCRIPT_
162
178
 
163
179
  /**
164
180
  * The `basePath`-joined `<script src>` reference every generated page's
165
- * skeleton layout actually uses (S2-T12 basePath fix).
181
+ * skeleton layout actually uses.
166
182
  *
167
183
  * @param {string} basePath the build input's `basePath`
168
184
  * @returns {string} the joined `href`
@@ -184,9 +200,8 @@ export const APPEARANCE_BOOTSTRAP_SCRIPT_MEDIA_TYPE =
184
200
  * The `<meta name="color-scheme">` content value: both palettes are
185
201
  * admitted, so the browser's own user-agent styling (form controls,
186
202
  * scrollbars) and initial paint follow `prefers-color-scheme` immediately,
187
- * independent of this renderer's own resolved-mode attribute (brief S2
188
- * section 3: "`color-scheme` meta/CSS handling"; "with JavaScript disabled,
189
- * CSS media queries preserve `system`").
203
+ * independent of this renderer's own resolved-mode attribute — with
204
+ * JavaScript disabled, CSS media queries preserve `system`.
190
205
  *
191
206
  * @type {string}
192
207
  */
@@ -1,11 +1,10 @@
1
1
  /**
2
- * The server-rendered Light/Dark/System appearance control (task packet
3
- * S2-T07: "the three-mode appearance control in the header-actions slot";
4
- * brief S2 section 3: "the control is a native `select`, a radio group, or a
5
- * conforming menu/radio pattern. A three-state unlabeled icon cycle is
6
- * rejected").
2
+ * The server-rendered Light/Dark/System appearance control, placed in the
3
+ * header-actions slot. The control must be a native `select`, a radio
4
+ * group, or a conforming menu/radio pattern; a three-state unlabeled icon
5
+ * cycle is rejected.
7
6
  *
8
- * Scoped decision (documented, not silently assumed): of the brief's three
7
+ * Scoped decision (documented, not silently assumed): of those three
9
8
  * admitted patterns, this module implements a native `<select>` bound to a
10
9
  * visible, programmatically associated `<label>`. A native select needs no
11
10
  * `aria-pressed`/`aria-checked` bookkeeping of its own — its accessible
@@ -19,8 +18,8 @@
19
18
  * `DOMContentLoaded` phase once it runs (see `bootstrap-script.js`), so this
20
19
  * server-rendered default is only ever visible for the brief instant before
21
20
  * that phase completes, or permanently with JavaScript disabled — exactly
22
- * the no-JS fallback the brief requires ("with JavaScript disabled, ...
23
- * `system` [is preserved]").
21
+ * the required no-JS fallback: with JavaScript disabled, `system` is
22
+ * preserved.
24
23
  *
25
24
  * This markup carries no inline `on*` handler and no `javascript:` URL, so
26
25
  * it needs no CSP allowance beyond the renderer's existing
@@ -1,26 +1,26 @@
1
1
  /**
2
- * `contracts/theme-styling-contract.jcs` builder and structural validator
3
- * (task packet S2-T12; DEC-097 section 4 "Closed theme-package and
4
- * stylesheet admission": the `templateStylingContract` closed object).
2
+ * `contracts/theme-styling-contract.jcs` builder and structural validator:
3
+ * the `templateStylingContract` closed object governing closed
4
+ * theme-package and stylesheet admission.
5
5
  *
6
6
  * This is the one reviewed source module `scripts/generate-contracts.mjs`
7
7
  * reads from to emit the published contract file, and the one module
8
8
  * `test/theme-styling-contract.test.js` reads from to drift-check the
9
9
  * contract against what this renderer actually renders. Every catalog leaf
10
- * below is drawn from (or, for the two new S2-T12 hooks documented inline,
10
+ * below is drawn from (or, for the two hooks documented inline,
11
11
  * wired into) the actual rendering modules — `internal/skeleton.js`,
12
12
  * `internal/page-kinds.js`, `internal/render-policy-content.js` and
13
13
  * `internal/appearance/contract.js` — never a hand-typed literal
14
14
  * independent of what those modules emit.
15
15
  *
16
- * Public-hook budget (DEC-097: `publicThemeSlotHooks:[1..64] set`): every
17
- * catalog leaf below (28 type + 15 class + 2 id + 19 attribute-value) has
18
- * exactly one corresponding public hook, for exactly 64 entries — this
19
- * renderer's complete public theming surface for S2. A leaf with no
20
- * matching hook would be a catalog member no theme could ever validly
21
- * select (DEC-097: "a used-but-undeclared public slot hook rejects"), so
22
- * catalogs and hooks are generated from one shared list rather than two
23
- * independently maintained ones.
16
+ * Public-hook budget (`publicThemeSlotHooks` is a `[1..64]`-entry set):
17
+ * every catalog leaf below (28 type + 15 class + 2 id + 19 attribute-value)
18
+ * has exactly one corresponding public hook, for exactly 64 entries — this
19
+ * renderer's complete public theming surface. A leaf with no matching hook
20
+ * would be a catalog member no theme could ever validly select (a
21
+ * used-but-undeclared public slot hook rejects), so catalogs and hooks are
22
+ * generated from one shared list rather than two independently maintained
23
+ * ones.
24
24
  *
25
25
  * Deliberately out of the S2 public hook surface (documented scope
26
26
  * decisions, not omissions):
@@ -29,19 +29,41 @@
29
29
  * `.language-<grammar>` class are public, so a theme can style a
30
30
  * highlighted code block's overall presentation but not recolor
31
31
  * individual token kinds in S2;
32
- * - `html`/`body` type selectors and interactive pseudo-classes
33
- * (`:hover`, `:focus`, `:visited`, ...): themes reach the document root
34
- * only through {@link PUBLICATION_ROOT_SELECTOR}/
35
- * {@link RESOLVED_PALETTE_SELECTORS}, and no pseudo-class hook is
36
- * published in S2 (an empty `pseudoClasses` catalog is schema-valid: `[0..64]`).
32
+ * - `html`/`body` type selectors: themes reach the document root only
33
+ * through {@link PUBLICATION_ROOT_SELECTOR}/{@link RESOLVED_PALETTE_SELECTORS}.
34
+ *
35
+ * Iconography (TPL-H6 decision, recorded here rather than left as an
36
+ * accident of theme authorship): **in scope, and expected.** Nothing in this
37
+ * contract restricts it — `::before`/`::after` are public pseudo-elements, a
38
+ * theme package may declare non-CSS passive assets (`theme.json.assets[]`),
39
+ * and a package-relative `url()` is permitted in theme CSS (only an
40
+ * external-origin `url()` is a theme-tooling lint concern, not a template
41
+ * restriction). The only reason no theme shipped an icon before this
42
+ * decision was recorded is that the prerequisite — safe admission of a
43
+ * theme-declared SVG — did not exist yet. It does now:
44
+ * `internal/media/theme-svg-sanitizer.js`, wired into
45
+ * `internal/theme-assets.js`'s passive-asset pipeline (TPL-C2), is the
46
+ * mechanism that makes shipping an SVG icon mark safe. A theme is free to
47
+ * declare one and reference it from `::before`/`::after` `content: url(...)`
48
+ * or a `background-image`.
49
+ *
50
+ * Contract 2.1.0 (TPL-H2/TPL-H3/TPL-M7 fix): S2.0's empty `pseudoClasses`
51
+ * catalog made every interaction state unstylable while the closed 35-token
52
+ * catalog already required `color-focus` and `color-link-visited`, tokens
53
+ * only a pseudo-class can ever apply. {@link PSEUDO_CLASSES} publishes a
54
+ * small closed catalog instead of widening indefinitely; the module
55
+ * documentation on that constant records why each member is admitted and
56
+ * why the set stops there. This is a template-owned, backward-compatible
57
+ * *contract* addition (a theme still validates if it uses none of these), so
58
+ * it ships as a minor bump: 2.0.0 -> 2.1.0.
37
59
  *
38
60
  * Every tag in `internal/render-policy-content.js`'s `ALLOWED_TAGS` that
39
61
  * markdown-it's own CommonMark grammar (with `html:false`) never actually
40
62
  * produces — `b`, `i`, `u`, `s`, `del`, `ins`, `sub`, `sup`, `mark`, `small`,
41
63
  * `wbr`, `table`, `thead`, `tbody`, `tr`, `th`, `td`, `dl`, `dt`, `dd`,
42
64
  * `figure`, `figcaption` — is likewise excluded from `typeSelectors`: this
43
- * catalog is "every hook the template actually renders" (task packet
44
- * S2-T12), not the sanitizer's own defence-in-depth superset.
65
+ * catalog is every hook the template actually renders, not the sanitizer's
66
+ * own defence-in-depth superset.
45
67
  */
46
68
 
47
69
  import { HIGHLIGHT_GRAMMARS } from '../render-policy-content.js';
@@ -57,15 +79,85 @@ import { canonicalizeJcs, domainDigest } from '../canonical-jcs.js';
57
79
  /** @type {string} */
58
80
  export const TEMPLATE_STYLING_CONTRACT_PACKAGE = '@rathnasgala2/template';
59
81
  /** @type {string} */
60
- export const TEMPLATE_STYLING_CONTRACT_VERSION = '2.0.0';
82
+ export const TEMPLATE_STYLING_CONTRACT_VERSION = '2.1.0';
61
83
  /** @type {string} */
62
- export const TEMPLATE_STYLING_CONTRACT_TEMPLATE_VERSION = '2.0.0';
84
+ export const TEMPLATE_STYLING_CONTRACT_TEMPLATE_VERSION = '2.1.0';
63
85
 
64
86
  /** @type {string} the `main-content` landmark's fixed `id`. */
65
87
  const MAIN_CONTENT_ID = 'main-content';
66
88
 
67
- /** @type {readonly string[]} the ordered `@layer` names DEC-097 fixes. */
89
+ /**
90
+ * The closed pseudo-class catalog (contract 2.1.0, TPL-H2 fix). Each member
91
+ * is admitted for a specific, already-published reason — this is a closed
92
+ * list, not a starting point:
93
+ *
94
+ * - `hover`: pointer affordance on links and the appearance control; the
95
+ * only way any theme can differentiate a hovered interactive element.
96
+ * - `focus-visible`: the keyboard/assistive-technology focus indicator
97
+ * (WCAG 2.2 SC 2.4.11/2.4.13 are about this indicator specifically).
98
+ * `:focus` is deliberately *not* admitted instead: `:focus-visible` is
99
+ * what lets a theme paint a ring for keyboard focus without also painting
100
+ * one on every mouse click, which is why `internal/appearance/
101
+ * base-layer.js`'s own template-owned default ring (unconstrained by this
102
+ * catalog) uses it too.
103
+ * - `active`: pressed-state affordance, the pointer-down counterpart to
104
+ * `hover`.
105
+ * - `visited`: the only way to consume the closed token catalog's own
106
+ * `color-link-visited`, which had no reachable application before this.
107
+ * - `disabled`: the appearance `<select>` is a native form control, which
108
+ * may legitimately be disabled by a later module; themes need a way to
109
+ * style that state today so it is not a breaking addition later.
110
+ *
111
+ * Deliberately still excluded: `:target`, `:checked`, `:required`,
112
+ * `:invalid` and every other state this renderer's own markup can never
113
+ * produce (no form beyond the one native `<select>`, no fragment-target
114
+ * styling contract) — the same "every leaf has a matching hook" discipline
115
+ * {@link ALL_HOOKS} documents above applies to pseudo-classes too: a
116
+ * catalog member with nothing that can ever be in that state is a
117
+ * specification defect, not a convenience.
118
+ *
119
+ * @type {readonly string[]}
120
+ */
121
+ export const PSEUDO_CLASSES = Object.freeze(
122
+ ['active', 'disabled', 'focus-visible', 'hover', 'visited'].sort(),
123
+ );
124
+
125
+ /**
126
+ * Contract 2.1.0 (TPL-H2 fix, alongside {@link PSEUDO_CLASSES}): the
127
+ * `nth-child`/`nth-last-child` functional pseudo-classes were already
128
+ * admitted (see `functionalPseudos` below) but only under
129
+ * `nthExpressionProfile: 'gala-positive-an-plus-b-v2'`, an arithmetic
130
+ * `an+b` grammar with no keyword form — so a theme could not express the
131
+ * conventional "every other row" zebra-striping pattern without a
132
+ * (legal but obscure) `2n`/`2n+1` expression. This publishes the two
133
+ * keyword arguments as an explicit, closed extension of that same
134
+ * profile, admitted only as the sole argument to `nth-child`/
135
+ * `nth-last-child`.
136
+ *
137
+ * @type {readonly string[]}
138
+ */
139
+ export const FUNCTIONAL_PSEUDO_KEYWORD_ARGUMENTS = Object.freeze([
140
+ 'even',
141
+ 'odd',
142
+ ]);
143
+
144
+ /**
145
+ * TPL-H3/TPL-M7: the ordered `@layer` names this renderer fixes, including
146
+ * the template-owned `gala-base` layer as the first (lowest-precedence)
147
+ * entry. `gala-base` is emitted by this renderer itself, never by a theme
148
+ * package (no `theme.json.cssLayers` entry ever names it —
149
+ * `urn:gala:schema:theme-contract:2.0.0` only ever admits a theme's own
150
+ * four-or-five-stylesheet subsequence of the layers *after* it), so it
151
+ * stays first in this list unconditionally: cascade-layer
152
+ * precedence is later-wins, and a theme's own `gala-tokens`/`gala-components`/
153
+ * `gala-utilities` declarations must always be able to override the
154
+ * template's own base defaults (its focus ring, its type scale, ...),
155
+ * never the reverse.
156
+ *
157
+ * @type {readonly string[]}
158
+ */
68
159
  export const ORDERED_LAYERS = Object.freeze([
160
+ 'gala-base',
69
161
  'gala-tokens',
70
162
  'gala-components',
71
163
  'gala-utilities',
@@ -222,9 +314,9 @@ function sortedUniqueBytes(values) {
222
314
 
223
315
  /**
224
316
  * Build the complete `templateStylingContract` object, including
225
- * `catalogDigest` (DEC-097 section 8:
317
+ * `catalogDigest`:
226
318
  * `SHA256(UTF8("GALA-TEMPLATE-STYLING-CONTRACT-V2\0") || JCS(the catalog
227
- * with catalogDigest omitted))`).
319
+ * with catalogDigest omitted))`.
228
320
  *
229
321
  * @returns {Record<string, unknown>} the complete, digested contract object
230
322
  */
@@ -294,7 +386,7 @@ export function buildTemplateStylingContract() {
294
386
  ID_HOOKS.map((hook) => hook.selectorAtom.replace(/^#/, '')),
295
387
  ),
296
388
  attributes: attributesRows,
297
- pseudoClasses: [],
389
+ pseudoClasses: [...PSEUDO_CLASSES],
298
390
  pseudoElements: ['after', 'before', 'marker', 'selection'],
299
391
  functionalPseudos: ['is', 'where', 'not', 'nth-child', 'nth-last-child'],
300
392
  combinators: [' ', ' > ', ' + ', ' ~ '],
@@ -312,6 +404,9 @@ export function buildTemplateStylingContract() {
312
404
  functionalSelectorArguments: 'compound-only',
313
405
  maximumFunctionalDepth: 1,
314
406
  nthExpressionProfile: 'gala-positive-an-plus-b-v2',
407
+ functionalPseudoKeywordArguments: [
408
+ ...FUNCTIONAL_PSEUDO_KEYWORD_ARGUMENTS,
409
+ ],
315
410
  },
316
411
  };
317
412
 
@@ -353,9 +448,9 @@ export const TEMPLATE_STYLING_CONTRACT_INVALID =
353
448
 
354
449
  /**
355
450
  * Structural self-validation of a `templateStylingContract` object against
356
- * DEC-097 section 4's closed shape (the checks this repository can run
451
+ * its closed shape (the checks this repository can run
357
452
  * without a published `urn:gala:schema:template-styling-contract` schema
358
- * entry — no such schema ID is registered in `@rathnasgala2/schemas@2.8.0`;
453
+ * entry — no such schema ID is registered in `@rathnasgala2/schemas@2.11.0`;
359
454
  * only `urn:gala:schema:theme-contract:2.0.0` is, which this module's sibling
360
455
  * theme fixtures are validated against with the schema package's own
361
456
  * exported `validateGalaDocument`, see `test/theme-styling-contract.test.js`).
@@ -375,7 +470,7 @@ export function assertTemplateStylingContractShape(contract) {
375
470
  if (contract.profile !== 'gala-template-styling-contract-v2') {
376
471
  fail('profile');
377
472
  }
378
- if (contract.contractVersion !== '2.0.0') fail('contractVersion');
473
+ if (contract.contractVersion !== '2.1.0') fail('contractVersion');
379
474
  if (contract.templatePackage !== '@rathnasgala2/template') {
380
475
  fail('templatePackage');
381
476
  }
@@ -438,6 +533,12 @@ export function assertTemplateStylingContractShape(contract) {
438
533
  if (rootRows.length !== 1) fail('root attribute row');
439
534
  if (paletteRows.length !== 1) fail('palette attribute row');
440
535
 
536
+ if (
537
+ JSON.stringify(contract.pseudoClasses) !==
538
+ JSON.stringify([...PSEUDO_CLASSES])
539
+ ) {
540
+ fail('pseudoClasses');
541
+ }
441
542
  if (
442
543
  JSON.stringify(contract.pseudoElements) !==
443
544
  JSON.stringify(['after', 'before', 'marker', 'selection'])
@@ -474,7 +575,9 @@ export function assertTemplateStylingContractShape(contract) {
474
575
  ]) ||
475
576
  composition.functionalSelectorArguments !== 'compound-only' ||
476
577
  composition.maximumFunctionalDepth !== 1 ||
477
- composition.nthExpressionProfile !== 'gala-positive-an-plus-b-v2'
578
+ composition.nthExpressionProfile !== 'gala-positive-an-plus-b-v2' ||
579
+ JSON.stringify(composition.functionalPseudoKeywordArguments) !==
580
+ JSON.stringify([...FUNCTIONAL_PSEUDO_KEYWORD_ARGUMENTS])
478
581
  ) {
479
582
  fail('composition');
480
583
  }
Binary file
@@ -3,9 +3,8 @@
3
3
  * Every other module reaches rendered output only through
4
4
  * {@link renderPagesWithEleventy}'s plain-data return value: no Eleventy
5
5
  * instance, plug-in API, config object or Nunjucks environment crosses back
6
- * out of this module (brief S2 section 3: "No Eleventy object, plug-in API,
7
- * config object or Nunjucks environment may cross the adapter boundary in
8
- * either direction").
6
+ * out of this module: no Eleventy object, plug-in API, config object or
7
+ * Nunjucks environment may cross the adapter boundary in either direction.
9
8
  *
10
9
  * Confinement enforced here:
11
10
  * - `configPath: false` on the `Eleventy` constructor disables discovery of
@@ -24,8 +23,8 @@
24
23
  * interpreted as Nunjucks/Liquid template syntax.
25
24
  * - `elev.write()` runs exactly one full build; this module never calls
26
25
  * `.watch()` or constructs an incremental build, and never sets
27
- * `runMode: "watch"` (brief S2 section 3: "Incremental and watch mode
28
- * ... cannot produce a releasable artifact").
26
+ * `runMode: "watch"`: incremental and watch mode cannot produce a
27
+ * releasable artifact.
29
28
  * - No plugin that performs network access is registered, and Eleventy's
30
29
  * own default plugins (`HtmlBasePlugin`, `BundlePlugin`) perform no
31
30
  * network access.
@@ -34,7 +33,9 @@
34
33
  import Eleventy from '@11ty/eleventy';
35
34
 
36
35
  import {
36
+ APPEARANCE_RESOLVED_MODE_ATTRIBUTE,
37
37
  APPEARANCE_ROOT_ATTRIBUTE,
38
+ APPEARANCE_SERVER_DEFAULT_RESOLVED_MODE,
38
39
  COLOR_SCHEME_META_CONTENT,
39
40
  } from './appearance/contract.js';
40
41
  import { contentSecurityPolicyMetaTag } from './content-security.js';
@@ -57,32 +58,41 @@ import { PAGE_KIND_ATTRIBUTE } from './page-kinds.js';
57
58
 
58
59
  /**
59
60
  * The one skeleton layout this repository ships. It is core-owned static
60
- * markup, not discovered from or influenced by the author repository. As of
61
- * S2-T06, `{{ content | safe }}` is the page's *complete* body content
62
- * (skip link, header, primary navigation, main, footer — see
63
- * `internal/skeleton.js`'s `renderPageBody`), so this layout only supplies
64
- * the outer document shell, the per-page `lang`/`dir` attributes, the CSP
65
- * baseline, the S2-T07 appearance-controller wiring and the S2-T08
66
- * SEO/localization `<head>` metadata below; it does not itself wrap
67
- * anything in `<main>`.
61
+ * markup, not discovered from or influenced by the author repository.
62
+ * `{{ content | safe }}` is the page's *complete* body content (skip link,
63
+ * header, primary navigation, main, footer — see `internal/skeleton.js`'s
64
+ * `renderPageBody`), so this layout only supplies the outer document shell,
65
+ * the per-page `lang`/`dir` attributes, the CSP baseline, the
66
+ * appearance-controller wiring and the SEO/localization `<head>` metadata
67
+ * below; it does not itself wrap anything in `<main>`.
68
68
  *
69
- * The one S2-T04 addition on top of S2-T03's minimal layout is the
70
- * byte-exact per-artifact CSP `<meta>` baseline (brief S2 section 3): S2
71
- * materializes no module package, configuration, output or runtime, so the
72
- * same constant tag is correct on every route.
69
+ * On top of that minimal layout is the byte-exact per-artifact CSP
70
+ * `<meta>` baseline: this renderer materializes no module package,
71
+ * configuration, output or runtime, so the same constant tag is correct on
72
+ * every route.
73
73
  *
74
- * S2-T07 additions, all fixed literals from `internal/appearance/contract.js`
75
- * (never re-typed here), each byte-identical on every route exactly like the
76
- * CSP tag above:
74
+ * Appearance-controller additions, all fixed literals from
75
+ * `internal/appearance/contract.js` (never re-typed here), each
76
+ * byte-identical on every route exactly like the CSP tag above:
77
77
  *
78
78
  * - the `data-gala-publication-root` presence attribute on `<html>`, always
79
79
  * rendered independent of JavaScript, so a theme's own no-JS
80
80
  * `prefers-color-scheme` fallback CSS has a stable root scope to key off;
81
+ * - (TPL-C1 fix) `data-gala-resolved-color-mode`, server-rendered as
82
+ * {@link APPEARANCE_SERVER_DEFAULT_RESOLVED_MODE} (`"light"`) on the same
83
+ * `<html>` element, so `RESOLVED_PALETTE_SELECTORS.light` already matches
84
+ * before any script runs and a page is never unstyled UA-default HTML for
85
+ * a no-JS reader, a text-mode/archival crawler, or a load where the
86
+ * blocking bootstrap `<script src>` fails. The bootstrap script's phase 1
87
+ * ({@link ../appearance/bootstrap-script.js}) unconditionally overwrites
88
+ * this attribute with the resolved value for the reader's actual stored
89
+ * selection/system preference the instant it runs, so a scripted reader's
90
+ * experience is unchanged;
81
91
  * - `<meta name="color-scheme" content="light dark">`, so user-agent styling
82
92
  * (form controls, scrollbars) and the initial paint already follow
83
93
  * `prefers-color-scheme` before any script runs;
84
- * - the one `<script src>` this renderer ever emits (brief S2 section 3:
85
- * "the appearance controller is the only browser bootstrap"): a plain,
94
+ * - the one `<script src>` this renderer ever emits (the appearance
95
+ * controller is the only browser bootstrap it ever ships): a plain,
86
96
  * same-origin, non-`defer`/non-`async`/non-`module` classic script, placed
87
97
  * after the CSP `<meta>` so it is governed by the policy it declares.
88
98
  * Being a blocking `<script src>` in `<head>`, it always finishes running
@@ -90,36 +100,36 @@ import { PAGE_KIND_ATTRIBUTE } from './page-kinds.js';
90
100
  * `<body>` (and so any themed paint) is even parsed, satisfying the
91
101
  * pre-paint requirement with no visibility-hiding trick of any kind (see
92
102
  * `internal/appearance/bootstrap-script.js`'s own module documentation).
93
- * Its `href` (S2-T12 basePath fix) is supplied per-page as
94
- * `data.appearanceScriptHref`, already `basePath`-joined by the caller
103
+ * Its `href` is supplied per-page as `data.appearanceScriptHref`, already
104
+ * `basePath`-joined by the caller
95
105
  * (`internal/appearance/contract.js`'s `appearanceBootstrapScriptHref`),
96
106
  * never a literal embedded in this fixed layout source.
97
107
  *
98
- * S2-T08 adds every other `<head>` element: a viewport meta, an optional
99
- * plain-text description, an optional `robots` directive
108
+ * Every other `<head>` element is added on top of that: a viewport meta, an
109
+ * optional plain-text description, an optional `robots` directive
100
110
  * (`internal/page-kinds.js`'s per-page-kind `robotsContent`), an optional
101
111
  * canonical link, Open Graph and Twitter Card meta tags and the Atom/RSS
102
112
  * feed discovery links (present on every page, unconditionally — the feeds
103
- * themselves are site-wide, not per-page). S2-T12 adds the theme stylesheet
104
- * `<link>` elements (`internal/theme-assets.js`, in `cssLayers` order,
105
- * including the print stylesheet hookup that used to be a fixed literal
106
- * line here) as one already-rendered `data.themeStylesheetLinksHtml`
107
- * fragment, and the `data-gala-page-kind` attribute on `<body>`
113
+ * themselves are site-wide, not per-page). The theme stylesheet `<link>`
114
+ * elements (`internal/theme-assets.js`, in `cssLayers` order, including the
115
+ * print stylesheet hookup) are added as one already-rendered
116
+ * `data.themeStylesheetLinksHtml` fragment, along with the
117
+ * `data-gala-page-kind` attribute on `<body>`
108
118
  * (`internal/page-kinds.js`'s `PAGE_KIND_ATTRIBUTE`). Every interpolated
109
119
  * `{{ variable }}`
110
120
  * below relies on Nunjucks' own default `autoescape: true` (verified: no
111
121
  * `nunjucksEnvironmentOptions.autoescape` override exists anywhere in this
112
122
  * repository or in `@11ty/eleventy`'s own source, so Nunjucks' own
113
123
  * documented default stands) for HTML-entity escaping — the same implicit
114
- * contract `{{ title }}` already relied on before S2-T08; only
115
- * `{{ content | safe }}` is deliberately exempted, because that value is
116
- * already sanitized HTML markup, not plain text.
124
+ * contract `{{ title }}` already relied on; only `{{ content | safe }}` is
125
+ * deliberately exempted, because that value is already sanitized HTML
126
+ * markup, not plain text.
117
127
  *
118
128
  * @type {string}
119
129
  */
120
130
  const SKELETON_LAYOUT_SOURCE = [
121
131
  '<!doctype html>',
122
- `<html lang="{{ lang }}" dir="{{ dir }}" ${APPEARANCE_ROOT_ATTRIBUTE}>`,
132
+ `<html lang="{{ lang }}" dir="{{ dir }}" ${APPEARANCE_ROOT_ATTRIBUTE} ${APPEARANCE_RESOLVED_MODE_ATTRIBUTE}="${APPEARANCE_SERVER_DEFAULT_RESOLVED_MODE}">`,
123
133
  '<head>',
124
134
  '<meta charset="utf-8">',
125
135
  '<meta name="viewport" content="width=device-width, initial-scale=1">',
@@ -142,6 +152,13 @@ const SKELETON_LAYOUT_SOURCE = [
142
152
  '{% if ogImage %}<meta name="twitter:image" content="{{ ogImage }}">{% endif %}',
143
153
  '<link rel="alternate" type="application/atom+xml" href="{{ atomFeedUrl }}" title="{{ siteName }}">',
144
154
  '<link rel="alternate" type="application/rss+xml" href="{{ rssFeedUrl }}" title="{{ siteName }}">',
155
+ // TPL-H3/TPL-M7: the template-owned gala-base layer's own `<link>`,
156
+ // always first among the stylesheets — before any theme `<link>` — so its
157
+ // own first-line `@layer` order statement (`internal/appearance/
158
+ // base-layer.js`) is the one every browser sees first, and so a theme's
159
+ // later-loaded gala-tokens/gala-components/gala-utilities layers can
160
+ // always override its defaults.
161
+ '<link rel="stylesheet" href="{{ baseStylesheetHref }}">',
145
162
  '{{ themeStylesheetLinksHtml | safe }}',
146
163
  '</head>',
147
164
  `<body ${PAGE_KIND_ATTRIBUTE}="{{ pageKind }}">`,