@rathnasgala2/template 2.0.0 → 2.2.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 +211 -137
  2. package/contracts/render-policy.jcs +1 -1
  3. package/contracts/theme-styling-contract.jcs +1 -1
  4. package/package.json +5 -6
  5. package/src/core/errors.js +13 -13
  6. package/src/core/index.js +130 -75
  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 +15 -15
  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 +50 -51
  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 +58 -20
  32. package/src/core/internal/search-index.js +17 -16
  33. package/src/core/internal/semver-range.js +84 -0
  34. package/src/core/internal/seo.js +24 -24
  35. package/src/core/internal/sitemap.js +11 -12
  36. package/src/core/internal/skeleton.js +65 -46
  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 +314 -68
  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,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 }}">`,
@@ -1,28 +1,25 @@
1
1
  /**
2
- * RSS 2.0 and Atom 1.0 feed generation (task packet S2-T08; brief S2
3
- * section 3 "Required generated outputs": "RSS and Atom feeds").
2
+ * RSS 2.0 and Atom 1.0 feed generation.
4
3
  *
5
4
  * Both feeds are built from the exact same {@link selectPublishedArticles}
6
5
  * selection and ordering `internal/page-kinds.js`'s `index` page kind
7
6
  * paginates (most recently published first, `publishedAt` descending, tied
8
7
  * broken by ascending `id`), so a feed's item order can never drift from the
9
8
  * site's own index listing. Every URL is an absolute `https://` URL resolved
10
- * against `buildInput.baseUrl` (brief S2 section 3: "absolute URLs from the
11
- * build input's public origin"), never a relative path — a feed reader has
12
- * no notion of the publication's own base path to resolve a relative URL
13
- * against.
9
+ * against `buildInput.baseUrl` — always the build input's public origin,
10
+ * never a relative path — since a feed reader has no notion of the
11
+ * publication's own base path to resolve a relative URL against.
14
12
  *
15
- * Item count (documented scoped decision, not fixed by the brief or
16
- * DEC-097): neither source specifies a maximum feed item count. This module
17
- * bounds it at {@link FEED_ITEM_LIMIT} — a conservative, deterministic
18
- * default with no author-facing configuration surface in S2, matching this
19
- * renderer's own established style for an unspecified numeric bound (see
20
- * `internal/page-kinds.js`'s `LISTING_PAGE_SIZE` and the media pipeline
21
- * scope decisions in the package README).
13
+ * Item count: this renderer's build input carries no maximum feed item
14
+ * count. This module bounds it at {@link FEED_ITEM_LIMIT} — a conservative,
15
+ * deterministic default with no author-facing configuration surface,
16
+ * matching this renderer's own established style for an unspecified
17
+ * numeric bound (see `internal/page-kinds.js`'s `LISTING_PAGE_SIZE` and the
18
+ * media pipeline scope decisions in the package README).
22
19
  */
23
20
 
24
21
  import { escapeHtml } from './skeleton.js';
25
- import { joinBasePathAndRoute } from './route.js';
22
+ import { derivePublicBasePath, joinPublicRoute } from './route.js';
26
23
  import {
27
24
  contentLastModified,
28
25
  contentRoute,
@@ -69,7 +66,10 @@ export function buildFeeds(validatedInput) {
69
66
  * @returns {string} the absolute `https://` URL
70
67
  */
71
68
  const absoluteUrl = (route) =>
72
- new URL(joinBasePathAndRoute(basePath, route), baseUrl).toString();
69
+ new URL(
70
+ joinPublicRoute(derivePublicBasePath(baseUrl, basePath), route),
71
+ baseUrl,
72
+ ).toString();
73
73
 
74
74
  const siteUrl = absoluteUrl('/');
75
75
  const atomSelfUrl = absoluteUrl(ATOM_FEED_ROUTE);
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Filesystem readdir order is not portable, so every caller that needs a
5
5
  * reproducible route/asset ordering must sort explicitly; this module never
6
- * returns raw enumeration order (brief S2 section 3: "no
7
- * filesystem-enumeration-order dependence").
6
+ * returns raw enumeration order, so this renderer has no
7
+ * filesystem-enumeration-order dependence.
8
8
  */
9
9
 
10
10
  import { readdir } from 'node:fs/promises';
@@ -7,17 +7,17 @@
7
7
  * enough to confirm an `ftyp` box with major brand `avif`, locate the
8
8
  * `meta` box, and read the primary item's `ispe` (image spatial extents)
9
9
  * property for declared width/height, but it does not run a full AV1
10
- * bitstream decode. DEC-097's closed `gala-theme-binary-assets-v2` profile
10
+ * bitstream decode. This renderer's closed passive-theme-asset decoder
11
11
  * requires a complete single-frame AV1 decode for *passive theme assets*;
12
12
  * an equivalent from-scratch AV1 decoder for *author media* is out of
13
- * proportion to this task's scope, flagged to the orchestrator as a
13
+ * proportion to this module's scope, flagged to the orchestrator as a
14
14
  * follow-up. AVIF is therefore admitted as a *source* reference
15
15
  * (recorded, digest-verified, dimension-bounded) with no generated
16
16
  * responsive derivative; its original bytes pass through as the sole asset
17
17
  * entry, exactly like WebP.
18
18
  *
19
- * The box walk is bounded: at most 4,096 boxes are visited (matching
20
- * DEC-097's own "4,096 container structures" isolation bound for the binary
19
+ * The box walk is bounded: at most 4,096 boxes are visited (matching this
20
+ * renderer's own container-structure isolation bound for the binary
21
21
  * decoder family) and every box's declared size is checked against the
22
22
  * remaining buffer length before it is used to slice, so a malformed or
23
23
  * adversarial box table cannot cause an out-of-bounds read or an unbounded
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Font policy for `appearance.fontAssets` (brief S2 section 3: "Custom fonts
2
+ * Font policy for `appearance.fontAssets`: custom fonts
3
3
  * require licensing, glyph and language coverage plus fallback evidence, and
4
- * no remote font is ever retrieved").
4
+ * no remote font is ever retrieved.
5
5
  *
6
6
  * `build-input:2.0.0`'s `resolvedFile` shape (the only shape
7
7
  * `appearance.fontAssets` entries use) is `{path, sourceDigest}` — a
@@ -1,37 +1,34 @@
1
1
  import { MediaPipelineError } from '../../errors.js';
2
2
 
3
3
  /**
4
- * Media pipeline resource ceilings (brief S2 section 3, S2-T05: "bounded
5
- * decoding worker", "decompression and resource limits").
4
+ * Media pipeline resource ceilings: a bounded decoding worker with fixed
5
+ * decompression and resource limits, so a hostile or malformed image/font
6
+ * can never trigger unbounded allocation or a decode bomb.
6
7
  *
7
- * The raster-specific numbers below are DEC-097's own closed
8
- * `gala-theme-binary-assets-v2` bounds (decision document section titled
9
- * "Binary theme assets"): non-animated raster, width/height each `1..8192`,
10
- * at most 16,777,216 pixels, at most 67,108,864 decoded RGBA8 buffer bytes,
11
- * source raster at most 16,777,216 bytes, source font at most 4,194,304
12
- * bytes. DEC-097 states those bounds for the closed theme *passive-asset*
13
- * decoder profile; the brief's author-media pipeline (S2-T05) gives no
14
- * separate numeric profile of its own, so this renderer reuses the same
15
- * numbers for the author-media decode-bound worker rather than inventing an
16
- * unrelated set — the physical decode-bomb risk (integer overflow, unbounded
17
- * allocation) is identical for both call sites. This is an explicit,
18
- * documented implementer decision, not a value taken from author-media text.
8
+ * The raster-specific numbers below are this renderer's closed binary-asset
9
+ * bounds, shared between the theme passive-asset decoder profile and the
10
+ * author-media pipeline (which has no separate numeric profile of its own):
11
+ * non-animated raster, width/height each `1..8192`, at most 16,777,216
12
+ * pixels, at most 67,108,864 decoded RGBA8 buffer bytes, source raster at
13
+ * most 16,777,216 bytes, source font at most 4,194,304 bytes. Reusing the
14
+ * same numbers for both call sites, rather than inventing an unrelated set,
15
+ * is a deliberate choice: the physical decode-bomb risk (integer overflow,
16
+ * unbounded allocation) is identical for both.
19
17
  *
20
- * DEC-097's separate "document-22 operational release" MVP launch gate also
21
- * fixes a smaller general per-source-file ceiling (10,485,760 bytes) that
22
- * applies to *every* repository source file regardless of kind, with "the
23
- * smaller declared per-file/package budget still wins." Applied here: the
18
+ * A separate, smaller general per-source-file ceiling (10,485,760 bytes)
19
+ * applies to *every* repository source file regardless of kind, and the
20
+ * smaller declared per-file/package budget always wins. Applied here: the
24
21
  * effective per-image source-byte ceiling is `min(16,777,216, 10,485,760)`.
25
22
  * The font ceiling (4,194,304) is already smaller than the general cap, so
26
23
  * it is unaffected.
27
24
  *
28
25
  * `MAX_IMAGES_PER_PUBLICATION` and `MAX_TOTAL_MEDIA_BYTES_PER_PUBLICATION`
29
- * are not fixed anywhere in the brief or DEC-097; there is no cited
30
- * per-publication *media-specific* aggregate. These two values are this
31
- * renderer's own conservative, documented ceilings (well under DEC-097's
32
- * unrelated 50,000-file / 1,073,741,824-byte whole-artifact ceiling), so a
33
- * publication cannot exhaust the media pipeline's bounded decode budget by
34
- * sheer reference count even though each individual file is itself bounded.
26
+ * have no externally fixed per-publication *media-specific* aggregate to
27
+ * conform to. These two values are this renderer's own conservative,
28
+ * documented ceilings (well under this renderer's separate, unrelated
29
+ * 50,000-file / 1,073,741,824-byte whole-artifact ceiling), so a publication
30
+ * cannot exhaust the media pipeline's bounded decode budget by sheer
31
+ * reference count even though each individual file is itself bounded.
35
32
  */
36
33
 
37
34
  /** Maximum admitted raster width or height, in pixels, inclusive. */
@@ -44,15 +41,15 @@ export const MAX_IMAGE_PIXELS = 16_777_216;
44
41
  export const MAX_DECODED_RGBA_BYTES = 67_108_864;
45
42
 
46
43
  /**
47
- * Maximum admitted raster source byte length: the smaller of DEC-097's
48
- * raster-specific cap (16,777,216) and its general per-source-file cap
49
- * (10,485,760).
44
+ * Maximum admitted raster source byte length: the smaller of this
45
+ * renderer's raster-specific cap (16,777,216) and its general
46
+ * per-source-file cap (10,485,760).
50
47
  */
51
48
  export const MAX_IMAGE_SOURCE_BYTES = 10_485_760;
52
49
 
53
50
  /**
54
- * Maximum admitted font source byte length (DEC-097's binary font cap,
55
- * already smaller than the general per-source-file cap).
51
+ * Maximum admitted font source byte length (this renderer's binary font
52
+ * cap, already smaller than the general per-source-file cap).
56
53
  */
57
54
  export const MAX_FONT_SOURCE_BYTES = 4_194_304;
58
55
 
@@ -74,8 +71,9 @@ export const MAX_FONTS_PER_PUBLICATION = 8;
74
71
  /**
75
72
  * Maximum aggregate decoded-plus-derivative byte budget the media pipeline
76
73
  * will spend for one publication (implementer-chosen; see module
77
- * documentation). Comfortably under DEC-097's unrelated 1,073,741,824-byte
78
- * whole-artifact ceiling, which this constant does not replace or relax.
74
+ * documentation). Comfortably under this renderer's separate, unrelated
75
+ * 1,073,741,824-byte whole-artifact ceiling, which this constant does not
76
+ * replace or relax.
79
77
  */
80
78
  export const MAX_TOTAL_MEDIA_BYTES_PER_PUBLICATION = 268_435_456;
81
79
 
@@ -116,9 +114,9 @@ export function assertDimensionsWithinCeiling(
116
114
 
117
115
  /**
118
116
  * Non-resetting wall-clock budget for one file's bounded decode, in
119
- * milliseconds. DEC-097's own closed binary-asset decoder uses a ten-second
120
- * non-resetting deadline; this renderer reuses that figure for the same
121
- * documented reason as the size/pixel ceilings above.
117
+ * milliseconds. This renderer's closed binary-asset decoder uses a
118
+ * ten-second non-resetting deadline, for the same documented reason as the
119
+ * size/pixel ceilings above.
122
120
  */
123
121
  export const DECODE_DEADLINE_MS = 10_000;
124
122
 
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Media pipeline orchestration (brief S2 section 3, S2-T05).
2
+ * Media pipeline orchestration.
3
3
  *
4
4
  * `build-input:2.0.0` never carries raw asset bytes for a `resolvedFile`
5
- * reference — only its repository-relative `path` and a `sourceDigest`
6
- * (DEC-097 section 6: "`build` exposes this decoded set as a read-only
7
- * source mount"). This pipeline therefore reads actual bytes from
5
+ * reference — only its repository-relative `path` and a `sourceDigest`:
6
+ * `build` exposes this decoded set as a read-only source mount. This
7
+ * pipeline therefore reads actual bytes from
8
8
  * `options.sourceDirectory`, the caller-mounted read-only source tree, and
9
9
  * fails closed (`MEDIA_SOURCE_DIGEST_MISMATCH`) if what it reads there does
10
10
  * not hash to the digest `build-input` declared — the same fail-closed