@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.
- package/README.md +211 -137
- package/contracts/render-policy.jcs +1 -1
- package/contracts/theme-styling-contract.jcs +1 -1
- package/package.json +5 -6
- package/src/core/errors.js +13 -13
- package/src/core/index.js +130 -75
- package/src/core/internal/appearance/base-layer.js +180 -0
- package/src/core/internal/appearance/bootstrap-script.js +11 -11
- package/src/core/internal/appearance/contract.js +59 -44
- package/src/core/internal/appearance/controller-markup.js +7 -8
- package/src/core/internal/appearance/styling-contract.js +132 -29
- package/src/core/internal/canonical-jcs.js +0 -0
- package/src/core/internal/content-security.js +0 -0
- package/src/core/internal/eleventy-render.js +52 -35
- package/src/core/internal/feeds.js +15 -15
- package/src/core/internal/fs-walk.js +2 -2
- package/src/core/internal/media/avif-probe.js +4 -4
- package/src/core/internal/media/font-policy.js +2 -2
- package/src/core/internal/media/jpeg-codec.js +0 -0
- package/src/core/internal/media/limits.js +32 -34
- package/src/core/internal/media/pipeline.js +4 -4
- package/src/core/internal/media/png-codec.js +7 -7
- package/src/core/internal/media/resize.js +2 -2
- package/src/core/internal/media/sniff.js +4 -4
- package/src/core/internal/media/theme-svg-sanitizer.js +278 -0
- package/src/core/internal/media/webp-probe.js +3 -3
- package/src/core/internal/messages.js +8 -8
- package/src/core/internal/page-kinds.js +50 -51
- package/src/core/internal/render-policy-content.js +57 -21
- package/src/core/internal/route-labels.js +5 -6
- package/src/core/internal/route.js +58 -20
- package/src/core/internal/search-index.js +17 -16
- package/src/core/internal/semver-range.js +84 -0
- package/src/core/internal/seo.js +24 -24
- package/src/core/internal/sitemap.js +11 -12
- package/src/core/internal/skeleton.js +65 -46
- package/src/core/internal/source-inventory.js +4 -5
- package/src/core/internal/text-direction.js +1 -2
- package/src/core/internal/theme-assets.js +314 -68
- package/src/core/internal/uuid.js +4 -5
- package/src/core/manifest.js +0 -0
- package/types/index.d.ts +22 -8
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `contracts/theme-styling-contract.jcs` builder and structural validator
|
|
3
|
-
*
|
|
4
|
-
* stylesheet admission
|
|
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
|
|
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 (
|
|
17
|
-
* catalog leaf below (28 type + 15 class + 2 id + 19 attribute-value)
|
|
18
|
-
* exactly one corresponding public hook, for exactly 64 entries — this
|
|
19
|
-
* renderer's complete public theming surface
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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
|
|
44
|
-
*
|
|
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.
|
|
82
|
+
export const TEMPLATE_STYLING_CONTRACT_VERSION = '2.1.0';
|
|
61
83
|
/** @type {string} */
|
|
62
|
-
export const TEMPLATE_STYLING_CONTRACT_TEMPLATE_VERSION = '2.
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
*
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
7
|
-
*
|
|
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"
|
|
28
|
-
*
|
|
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.
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* `
|
|
64
|
-
* the
|
|
65
|
-
*
|
|
66
|
-
*
|
|
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
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
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
|
-
*
|
|
75
|
-
* (never re-typed here), each
|
|
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 (
|
|
85
|
-
*
|
|
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`
|
|
94
|
-
* `
|
|
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
|
-
*
|
|
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).
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
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
|
|
115
|
-
*
|
|
116
|
-
*
|
|
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
|
|
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`
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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 {
|
|
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(
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
Binary file
|
|
@@ -1,37 +1,34 @@
|
|
|
1
1
|
import { MediaPipelineError } from '../../errors.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Media pipeline resource ceilings
|
|
5
|
-
*
|
|
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
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* bytes
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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
|
|
48
|
-
* raster-specific cap (16,777,216) and its general
|
|
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 (
|
|
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
|
|
78
|
-
* whole-artifact ceiling, which this constant does not
|
|
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.
|
|
120
|
-
* non-resetting deadline
|
|
121
|
-
*
|
|
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
|
|
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
|
-
*
|
|
7
|
-
*
|
|
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
|