@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.
- package/README.md +204 -137
- package/contracts/render-policy.jcs +1 -1
- package/contracts/theme-styling-contract.jcs +1 -1
- package/package.json +4 -5
- package/src/core/errors.js +13 -13
- package/src/core/index.js +102 -66
- 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 +10 -13
- 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 +46 -48
- 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 +16 -20
- package/src/core/internal/search-index.js +12 -14
- package/src/core/internal/semver-range.js +84 -0
- package/src/core/internal/seo.js +21 -23
- package/src/core/internal/sitemap.js +6 -8
- package/src/core/internal/skeleton.js +42 -39
- 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 +294 -67
- package/src/core/internal/uuid.js +4 -5
- package/src/core/manifest.js +0 -0
- package/types/index.d.ts +22 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rathnasgala2/template",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Deterministic Eleventy-backed static publication renderer for Galascribe portable repositories.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -25,12 +25,11 @@
|
|
|
25
25
|
"contracts/",
|
|
26
26
|
"types/",
|
|
27
27
|
"LICENSE",
|
|
28
|
-
"NOTICE",
|
|
29
28
|
"README.md"
|
|
30
29
|
],
|
|
31
30
|
"engines": {
|
|
32
|
-
"node": "24.
|
|
33
|
-
"npm": "11.
|
|
31
|
+
"node": ">=24.0.0",
|
|
32
|
+
"npm": ">=11.0.0"
|
|
34
33
|
},
|
|
35
34
|
"devEngines": {
|
|
36
35
|
"runtime": {
|
|
@@ -61,7 +60,7 @@
|
|
|
61
60
|
"test": "node --test",
|
|
62
61
|
"licenses:generate": "node scripts/generate-license-inventory.mjs",
|
|
63
62
|
"licenses:check": "node scripts/generate-license-inventory.mjs --check",
|
|
64
|
-
"sbom:generate": "cyclonedx-npm --output-file sbom.cdx.json --output-format JSON --spec-version 1.6 && node scripts/apply-sbom-normalization.mjs",
|
|
63
|
+
"sbom:generate": "cyclonedx-npm --package-lock-only --output-file sbom.cdx.json --output-format JSON --spec-version 1.6 && node scripts/apply-sbom-normalization.mjs",
|
|
65
64
|
"sbom:check": "node scripts/check-sbom-current.mjs",
|
|
66
65
|
"workflows:check": "node scripts/check-workflow-pins.mjs",
|
|
67
66
|
"workflows:drift": "node scripts/check-workflow-drift.mjs",
|
package/src/core/errors.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* bare `Error`, so a caller (eventually `publish-kernel`) can distinguish
|
|
5
5
|
* "the supplied build input is not schema-valid" from "the caller's own
|
|
6
6
|
* adapter options are incomplete or unsafe" without string-matching a
|
|
7
|
-
* message
|
|
7
|
+
* message: this renderer fails closed on invalid input.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -43,14 +43,14 @@ export class RenderOptionsError extends Error {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
* The media pipeline
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
46
|
+
* The media pipeline rejected an image or font reference: an unrecognized or
|
|
47
|
+
* malformed format, a decompression or resource ceiling crossing, an
|
|
48
|
+
* unconditionally-rejected SVG image input, a font that is not bounded
|
|
49
|
+
* WOFF2, or a source file whose bytes do not match its declared
|
|
50
50
|
* `build-input` digest. Every one of these fails closed — before any
|
|
51
51
|
* derivative is written and before rendering completes — never as a warning
|
|
52
|
-
* or silent fallback
|
|
53
|
-
*
|
|
52
|
+
* or silent fallback: decompression or resource excess is always rejected,
|
|
53
|
+
* and SVG is never processed as author media.
|
|
54
54
|
*/
|
|
55
55
|
export class MediaPipelineError extends Error {
|
|
56
56
|
/**
|
|
@@ -78,8 +78,8 @@ export class MediaPipelineError extends Error {
|
|
|
78
78
|
* byte-equal the published render-policy identity, a non-empty module or
|
|
79
79
|
* placement value, or sanitized output that still matched a forbidden
|
|
80
80
|
* pattern (an adapter defect, never expected of admitted content). Rendering
|
|
81
|
-
* never completes for the affected build
|
|
82
|
-
* behaviour on any content the policy rejects
|
|
81
|
+
* never completes for the affected build: this renderer has fail-closed
|
|
82
|
+
* behaviour on any content the policy rejects.
|
|
83
83
|
*/
|
|
84
84
|
export class RenderPolicyViolationError extends Error {
|
|
85
85
|
/**
|
|
@@ -95,14 +95,14 @@ export class RenderPolicyViolationError extends Error {
|
|
|
95
95
|
* The selected theme package's `theme.json` (or its declared file set) did
|
|
96
96
|
* not conform to the closed shape `internal/theme-assets.js` needs to copy
|
|
97
97
|
* its stylesheets/passive assets and render its `<link>` elements: an
|
|
98
|
-
* invalid `stylesheets` list shape, a
|
|
99
|
-
*
|
|
100
|
-
*
|
|
98
|
+
* invalid `stylesheets` list shape, a schema/contract-integrity violation
|
|
99
|
+
* (which also covers an inadmissible `cssLayers` projection — see
|
|
100
|
+
* `urn:gala:schema:theme-contract:2.0.0`), or a missing declared file.
|
|
101
101
|
*/
|
|
102
102
|
export class ThemeAssetError extends Error {
|
|
103
103
|
/**
|
|
104
104
|
* @param {string} reasonCode a stable, machine-checkable reason code (for
|
|
105
|
-
* example `THEME_STYLESHEETS_INVALID`, `
|
|
105
|
+
* example `THEME_STYLESHEETS_INVALID`, `THEME_CONTRACT_SCHEMA_INVALID`)
|
|
106
106
|
* @param {string} message a human-readable description
|
|
107
107
|
*/
|
|
108
108
|
constructor(reasonCode, message) {
|
package/src/core/index.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@rathnasgala2/template` public entry point.
|
|
3
3
|
*
|
|
4
|
-
* This is
|
|
5
|
-
* media pipeline: it consumes one validated
|
|
4
|
+
* This is the Eleventy-confined renderer adapter: it consumes one validated
|
|
6
5
|
* `urn:gala:schema:build-input:2.0.0` instance and emits a candidate output
|
|
7
|
-
* directory plus one `urn:gala:schema:artifact-manifest:2.0.0` instance
|
|
8
|
-
* (brief S2 section 3).
|
|
6
|
+
* directory plus one `urn:gala:schema:artifact-manifest:2.0.0` instance.
|
|
9
7
|
*
|
|
10
8
|
* `renderPublication` is the only documented *rendering* entry point. It
|
|
11
9
|
* fails closed: an invalid `build-input` is rejected with
|
|
@@ -14,7 +12,7 @@
|
|
|
14
12
|
* Eleventy ever runs, and a rejected image or font is rejected with
|
|
15
13
|
* {@link MediaPipelineError} before the manifest is assembled.
|
|
16
14
|
*
|
|
17
|
-
* `normalizeAuthoredMarkdown` (re-exported here from
|
|
15
|
+
* `normalizeAuthoredMarkdown` (re-exported here from
|
|
18
16
|
* `internal/content-security.js`) is this package's second public entry
|
|
19
17
|
* point: the upstream normalization step a `build-input:2.0.0` producer
|
|
20
18
|
* (`publish-kernel`/`publish-action`) calls once per authored Markdown
|
|
@@ -25,38 +23,36 @@
|
|
|
25
23
|
* reaches this renderer, and how `renderPublication` verifies that instead
|
|
26
24
|
* of re-deriving it.
|
|
27
25
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
26
|
+
* `options.sourceDirectory` is the caller-mounted, read-only repository
|
|
27
|
+
* source tree `build-input`'s `resolvedFile` references point into.
|
|
28
|
+
* `processMedia` runs after the Eleventy route listing is already computed,
|
|
29
|
+
* so generated derivative files under `assets/media/` are never mistaken for
|
|
30
|
+
* an HTML route.
|
|
33
31
|
*
|
|
34
|
-
* Output-security (markdown/sanitizer/highlighter/CSP
|
|
35
|
-
* pipeline
|
|
36
|
-
*
|
|
37
|
-
*
|
|
32
|
+
* Output-security (markdown/sanitizer/highlighter/CSP) and the media
|
|
33
|
+
* pipeline are layered on the same adapter, alongside the core semantic
|
|
34
|
+
* skeleton and navigation renderer (`internal/skeleton.js`) and every
|
|
35
|
+
* generated page kind (`internal/page-kinds.js`): publication profile,
|
|
38
36
|
* author, article/page, index, tag, series and archive pages, each composed
|
|
39
|
-
* with one shared header/primary-navigation/footer chrome and, where
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
37
|
+
* with one shared header/primary-navigation/footer chrome and, where
|
|
38
|
+
* required, a breadcrumb trail. This adapter also emits every authored
|
|
39
|
+
* static redirect and the feeds/search/sitemap outputs, shaped so that later
|
|
40
|
+
* work can extend the page-body pipeline without touching the confinement,
|
|
41
|
+
* route-projection or manifest-assembly mechanics here.
|
|
44
42
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* `
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* (`contract.js`) — the contract S2-T12's published theme styling catalog
|
|
43
|
+
* The Light/Dark/System appearance controller (`internal/appearance/`) adds:
|
|
44
|
+
* the server-rendered control in the header's `header-actions` slot
|
|
45
|
+
* (`controller-markup.js`), the one deterministic pre-paint bootstrap script
|
|
46
|
+
* this renderer ever emits, written into the candidate output directory and
|
|
47
|
+
* entered into the manifest as an ordinary `manifestAsset` row exactly like a
|
|
48
|
+
* media derivative (`bootstrap-script.js`), and the fixed `data-` attribute
|
|
49
|
+
* names, mode values and storage key every one of those pieces shares
|
|
50
|
+
* (`contract.js`) — the same contract the published theme styling catalog
|
|
54
51
|
* targets.
|
|
55
52
|
*
|
|
56
53
|
* `src/core/` is the only admitted source root in this repository. No
|
|
57
54
|
* `src/modules/` tree, module import edge, registration stub, module
|
|
58
55
|
* configuration, module output or module runtime is ever added beside it
|
|
59
|
-
* (brief S2 section 3; DEC-097 sections 2 and 5).
|
|
60
56
|
*/
|
|
61
57
|
|
|
62
58
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
@@ -70,6 +66,11 @@ import {
|
|
|
70
66
|
appearanceBootstrapScriptHref,
|
|
71
67
|
} from './internal/appearance/contract.js';
|
|
72
68
|
import { APPEARANCE_BOOTSTRAP_SCRIPT_SOURCE } from './internal/appearance/bootstrap-script.js';
|
|
69
|
+
import {
|
|
70
|
+
GALA_BASE_STYLESHEET_MEDIA_TYPE,
|
|
71
|
+
GALA_BASE_STYLESHEET_PATH,
|
|
72
|
+
GALA_BASE_STYLESHEET_SOURCE,
|
|
73
|
+
} from './internal/appearance/base-layer.js';
|
|
73
74
|
import { renderAppearanceControl } from './internal/appearance/controller-markup.js';
|
|
74
75
|
import {
|
|
75
76
|
ArtifactManifestValidationError,
|
|
@@ -151,7 +152,7 @@ export const TEMPLATE_PACKAGE_NAME = '@rathnasgala2/template';
|
|
|
151
152
|
*
|
|
152
153
|
* @type {string}
|
|
153
154
|
*/
|
|
154
|
-
export const TEMPLATE_PACKAGE_VERSION = '2.
|
|
155
|
+
export const TEMPLATE_PACKAGE_VERSION = '2.1.0';
|
|
155
156
|
|
|
156
157
|
/**
|
|
157
158
|
* Validate the caller-supplied render options, independent of build-input
|
|
@@ -180,10 +181,9 @@ function assertOptions(options) {
|
|
|
180
181
|
throw new RenderOptionsError(`options.${key} must be an absolute path`);
|
|
181
182
|
}
|
|
182
183
|
}
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
// so this addition is purely additive.
|
|
184
|
+
// An optional caller-mounted, read-only extracted theme package directory
|
|
185
|
+
// (`internal/theme-assets.js`). Absent entirely by default, so this
|
|
186
|
+
// addition is purely additive.
|
|
187
187
|
if (options.themeDirectory !== undefined) {
|
|
188
188
|
if (
|
|
189
189
|
typeof options.themeDirectory !== 'string' ||
|
|
@@ -228,9 +228,9 @@ function assertOptions(options) {
|
|
|
228
228
|
* render-policy identity, and that no module or placement value leaked
|
|
229
229
|
* through (belt-and-suspenders on top of the `build-input:2.0.0` schema's
|
|
230
230
|
* own `modules: {}` / `placements: []` closure). Fails closed before any
|
|
231
|
-
* directory is created or Eleventy ever runs
|
|
232
|
-
*
|
|
233
|
-
*
|
|
231
|
+
* directory is created or Eleventy ever runs: an absent policy file, wrong
|
|
232
|
+
* path/name/version/digest, mixed identity between records, or body
|
|
233
|
+
* produced under a different policy rejects before build.
|
|
234
234
|
*
|
|
235
235
|
* @param {import('../../types/index.d.ts').NormalizedBuildInput} validatedInput
|
|
236
236
|
* the schema-validated build input
|
|
@@ -320,7 +320,7 @@ export async function renderPublication(buildInput, options) {
|
|
|
320
320
|
// `page.language` below); only the *chrome strings surrounding it* are
|
|
321
321
|
// rendered in one consistent language, matching a reader's expectation
|
|
322
322
|
// that navigation/footer text does not flip language from page to page
|
|
323
|
-
// (
|
|
323
|
+
// (see `internal/messages.js`).
|
|
324
324
|
const siteMessages = getMessages(validatedInput.publication.defaultLanguage);
|
|
325
325
|
const homeRoute = joinBasePathAndRoute(validatedInput.basePath, '/');
|
|
326
326
|
|
|
@@ -350,31 +350,38 @@ export async function renderPublication(buildInput, options) {
|
|
|
350
350
|
messages: siteMessages,
|
|
351
351
|
});
|
|
352
352
|
|
|
353
|
-
// Run before page assembly, not after
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
// Eleventy-rendered HTML route.
|
|
353
|
+
// Run before page assembly, not after: resolving each page's own
|
|
354
|
+
// `og:image`/`twitter:image` needs the media pipeline's own finished
|
|
355
|
+
// `assets` list (only it knows which derivative file extension a given
|
|
356
|
+
// source image's `sourceDigest` produced — see `internal/seo.js`). This
|
|
357
|
+
// only moves the in-memory computation earlier; `mediaFiles`' actual
|
|
358
|
+
// bytes are still written to `outputDirectory` only after the route
|
|
359
|
+
// listing below, so `outputDirectory/assets/media/` still can never be
|
|
360
|
+
// mistaken for an Eleventy-rendered HTML route.
|
|
362
361
|
const { assets: mediaAssets, files: mediaFiles } = await processMedia(
|
|
363
362
|
validatedInput,
|
|
364
363
|
{ sourceDirectory: path.resolve(options.sourceDirectory) },
|
|
365
364
|
);
|
|
366
365
|
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
//
|
|
366
|
+
// The site-wide feeds, resolved once so every page's feed-discovery
|
|
367
|
+
// `<link>` tags and the feed documents themselves agree on the exact same
|
|
368
|
+
// self URLs.
|
|
370
369
|
const feeds = buildFeeds(validatedInput);
|
|
371
370
|
const siteName = validatedInput.publication.title;
|
|
372
371
|
const appearanceScriptHrefValue = appearanceBootstrapScriptHref(
|
|
373
372
|
validatedInput.basePath,
|
|
374
373
|
);
|
|
374
|
+
// TPL-H3/TPL-M7: the template-owned gala-base stylesheet's own basePath-
|
|
375
|
+
// joined href, computed the same way as the appearance script href above
|
|
376
|
+
// so it can never drift from the physical file/manifest path written
|
|
377
|
+
// below.
|
|
378
|
+
const baseStylesheetHrefValue = `/${projectFixedAssetPath(
|
|
379
|
+
validatedInput.basePath,
|
|
380
|
+
`/${GALA_BASE_STYLESHEET_PATH}`,
|
|
381
|
+
)}`;
|
|
375
382
|
|
|
376
|
-
//
|
|
377
|
-
//
|
|
383
|
+
// The selected theme package's copied stylesheets/passive assets and the
|
|
384
|
+
// ordered `<link>` markup every generated page's `<head>` inserts.
|
|
378
385
|
// Run before page assembly (same reasoning as the media pipeline above):
|
|
379
386
|
// every page's `data.themeStylesheetLinksHtml` needs the same one
|
|
380
387
|
// already-rendered fragment.
|
|
@@ -438,6 +445,7 @@ export async function renderPublication(buildInput, options) {
|
|
|
438
445
|
atomFeedUrl: feeds.atomSelfUrl,
|
|
439
446
|
rssFeedUrl: feeds.rssSelfUrl,
|
|
440
447
|
appearanceScriptHref: appearanceScriptHrefValue,
|
|
448
|
+
baseStylesheetHref: baseStylesheetHrefValue,
|
|
441
449
|
themeStylesheetLinksHtml,
|
|
442
450
|
},
|
|
443
451
|
});
|
|
@@ -473,10 +481,10 @@ export async function renderPublication(buildInput, options) {
|
|
|
473
481
|
);
|
|
474
482
|
}
|
|
475
483
|
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
// guard
|
|
479
|
-
//
|
|
484
|
+
// The one ordinary generated `404.html` error page, using the `error`
|
|
485
|
+
// page kind's own pure body function. Pushed after the guard above so the
|
|
486
|
+
// guard's own "is there anything authored to render" check is unaffected
|
|
487
|
+
// by this always-present synthetic page.
|
|
480
488
|
const errorNavHtml = renderPrimaryNavigation({
|
|
481
489
|
items: validatedInput.navigation.items,
|
|
482
490
|
currentRoute: undefined,
|
|
@@ -509,11 +517,12 @@ export async function renderPublication(buildInput, options) {
|
|
|
509
517
|
atomFeedUrl: feeds.atomSelfUrl,
|
|
510
518
|
rssFeedUrl: feeds.rssSelfUrl,
|
|
511
519
|
appearanceScriptHref: appearanceScriptHrefValue,
|
|
520
|
+
baseStylesheetHref: baseStylesheetHrefValue,
|
|
512
521
|
themeStylesheetLinksHtml,
|
|
513
522
|
},
|
|
514
523
|
});
|
|
515
524
|
|
|
516
|
-
//
|
|
525
|
+
// Feeds, sitemap and the static search index. Each is a fixed,
|
|
517
526
|
// literal-extension file — never route-normalized like an ordinary
|
|
518
527
|
// content route (`internal/route.js#projectFixedAssetPath`) — and each
|
|
519
528
|
// bypasses the shared HTML skeleton layout entirely (no `layout` field,
|
|
@@ -570,10 +579,9 @@ export async function renderPublication(buildInput, options) {
|
|
|
570
579
|
outputDirectory,
|
|
571
580
|
});
|
|
572
581
|
|
|
573
|
-
//
|
|
582
|
+
// The exact `routeClass`/`mediaType` this renderer's own generated
|
|
574
583
|
// non-HTML routes carry, keyed by their fixed output path. Every other
|
|
575
|
-
// route defaults to `'html'`/`text/html; charset=utf-8
|
|
576
|
-
// S2-T03.
|
|
584
|
+
// route defaults to `'html'`/`text/html; charset=utf-8`.
|
|
577
585
|
/** @type {Map<string, {routeClass: import('../../types/index.d.ts').ManifestRouteEntry['routeClass'], mediaType: string}>} */
|
|
578
586
|
const specialRoutes = new Map([
|
|
579
587
|
[
|
|
@@ -663,8 +671,8 @@ export async function renderPublication(buildInput, options) {
|
|
|
663
671
|
await writeFile(destination, file.bytes);
|
|
664
672
|
}
|
|
665
673
|
|
|
666
|
-
//
|
|
667
|
-
//
|
|
674
|
+
// The selected theme package's stylesheet/passive-asset bytes, written
|
|
675
|
+
// for the same reason and at the same point as the media pipeline
|
|
668
676
|
// output immediately above (`themeAssetsResult` is `undefined` when no
|
|
669
677
|
// `options.themeDirectory` was supplied, in which case there is nothing to
|
|
670
678
|
// write — the fallback `defaultThemeStylesheetLinksHtml` link above points
|
|
@@ -677,10 +685,10 @@ export async function renderPublication(buildInput, options) {
|
|
|
677
685
|
}
|
|
678
686
|
}
|
|
679
687
|
|
|
680
|
-
//
|
|
681
|
-
//
|
|
682
|
-
//
|
|
683
|
-
//
|
|
688
|
+
// The one deterministic pre-paint appearance-controller bootstrap script:
|
|
689
|
+
// the appearance controller is the only browser bootstrap this renderer
|
|
690
|
+
// ever emits. Written after the routes listing above for the same reason
|
|
691
|
+
// as the media pipeline output immediately above: it must never be
|
|
684
692
|
// mistaken for an Eleventy-rendered HTML route. Its bytes are a pure
|
|
685
693
|
// function of `internal/appearance/contract.js`'s own fixed constants, so
|
|
686
694
|
// this asset row is identical on every build regardless of build input.
|
|
@@ -714,9 +722,36 @@ export async function renderPublication(buildInput, options) {
|
|
|
714
722
|
immutable: false,
|
|
715
723
|
};
|
|
716
724
|
|
|
725
|
+
// TPL-H3/TPL-M7: the template-owned gala-base stylesheet, written and
|
|
726
|
+
// manifested for the same reason and at the same point as the appearance
|
|
727
|
+
// bootstrap script immediately above. Always emitted, independent of
|
|
728
|
+
// whether a theme was selected (`baseStylesheetHrefValue` above is
|
|
729
|
+
// likewise unconditional), because it is what makes contract-mandated
|
|
730
|
+
// chrome presentable even with no theme at all.
|
|
731
|
+
const baseStylesheetBytes = Buffer.from(GALA_BASE_STYLESHEET_SOURCE, 'utf8');
|
|
732
|
+
const joinedBaseStylesheetPath = projectFixedAssetPath(
|
|
733
|
+
validatedInput.basePath,
|
|
734
|
+
`/${GALA_BASE_STYLESHEET_PATH}`,
|
|
735
|
+
);
|
|
736
|
+
const baseStylesheetDestination = path.join(
|
|
737
|
+
outputDirectory,
|
|
738
|
+
joinedBaseStylesheetPath,
|
|
739
|
+
);
|
|
740
|
+
await mkdir(path.dirname(baseStylesheetDestination), { recursive: true });
|
|
741
|
+
await writeFile(baseStylesheetDestination, baseStylesheetBytes);
|
|
742
|
+
/** @type {import('../../types/index.d.ts').ManifestAssetEntry} */
|
|
743
|
+
const baseStylesheetAsset = {
|
|
744
|
+
path: joinedBaseStylesheetPath,
|
|
745
|
+
mediaType: GALA_BASE_STYLESHEET_MEDIA_TYPE,
|
|
746
|
+
byteLength: String(baseStylesheetBytes.byteLength),
|
|
747
|
+
sha256: digestBytes(baseStylesheetBytes),
|
|
748
|
+
immutable: false,
|
|
749
|
+
};
|
|
750
|
+
|
|
717
751
|
const assets = [
|
|
718
752
|
...joinedMediaAssets,
|
|
719
753
|
appearanceScriptAsset,
|
|
754
|
+
baseStylesheetAsset,
|
|
720
755
|
...(themeAssetsResult ? themeAssetsResult.assets : []),
|
|
721
756
|
].sort((a, b) => compareUtf8Bytes(a.path, b.path));
|
|
722
757
|
|
|
@@ -754,5 +789,6 @@ export {
|
|
|
754
789
|
|
|
755
790
|
export {
|
|
756
791
|
computeBodyDigest,
|
|
792
|
+
computeRenderPolicyIdentity,
|
|
757
793
|
normalizeAuthoredMarkdown,
|
|
758
794
|
} from './internal/content-security.js';
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The template-owned `gala-base` cascade layer (TPL-H3/TPL-M7): a
|
|
3
|
+
* stylesheet this renderer itself writes on every build, independent of
|
|
4
|
+
* whether a theme is selected, carrying sane defaults for every
|
|
5
|
+
* contract-mandated chrome element (`internal/skeleton.js`'s skip link,
|
|
6
|
+
* headings, code/table overflow, image sizing, responsive spacing) plus the
|
|
7
|
+
* one rule that makes the closed token catalog's
|
|
8
|
+
* `color-focus`/`focus-width` tokens actually paint a ring (THD-H1): a real
|
|
9
|
+
* `outline-style: solid` under `:focus-visible`.
|
|
10
|
+
*
|
|
11
|
+
* This file is not admitted through, or generated from, the theme package
|
|
12
|
+
* pipeline in `internal/theme-assets.js` — it is not subject to
|
|
13
|
+
* `check-css-hooks.mjs`'s closed selector-admission grammar and may use any
|
|
14
|
+
* selector, property or pseudo-class, including `:focus-visible`,
|
|
15
|
+
* `html`/`body` and functional colors, because it is authored by the
|
|
16
|
+
* template, not by a theme. Its bytes are a pure function of this module's
|
|
17
|
+
* own literal source, not of any per-publication or per-theme input, so a
|
|
18
|
+
* fixed, non-content-addressed output path carries no collision risk
|
|
19
|
+
* (mirrors `internal/appearance/bootstrap-script.js`'s own reasoning for its
|
|
20
|
+
* fixed path).
|
|
21
|
+
*
|
|
22
|
+
* Every custom property this file reads (`--gala-*`) is read through a
|
|
23
|
+
* fallback value, since a theme is optional (`internal/theme-assets.js`'s
|
|
24
|
+
* `defaultThemeStylesheetLinksHtml` fallback path never defines any
|
|
25
|
+
* `--gala-*` property at all) — this layer must still produce a sane,
|
|
26
|
+
* legible default with no theme selected whatsoever.
|
|
27
|
+
*
|
|
28
|
+
* Cascade-layer precedence (TPL-M7): this layer's own declarations always
|
|
29
|
+
* lose to a theme's `gala-tokens`/`gala-components`/`gala-utilities` layer
|
|
30
|
+
* declarations for the same property on the same element, because this
|
|
31
|
+
* stylesheet's own first line is the fixed `@layer gala-base, gala-tokens,
|
|
32
|
+
* gala-components, gala-utilities, gala-print;` order statement — layer
|
|
33
|
+
* order is declared once, explicitly, independent of `<link>` emission
|
|
34
|
+
* order, and independent of which optional stylesheets a theme ships (a
|
|
35
|
+
* theme that omits `utilities.css` never populates `gala-utilities`, which
|
|
36
|
+
* is harmless: an empty layer in a declared order does nothing). A bare
|
|
37
|
+
* `@layer name, name, ...;` statement is not itself a style rule, so it
|
|
38
|
+
* needs no `'unsafe-inline'`/nonce/hash exemption from the
|
|
39
|
+
* `style-src 'self'` CSP baseline the way an inline `<style>` element would
|
|
40
|
+
* (TPL-H4): it ships as the first bytes of this external, same-origin
|
|
41
|
+
* stylesheet, and `internal/eleventy-render.js` always emits this
|
|
42
|
+
* stylesheet's own `<link>` before every theme stylesheet `<link>`.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import { ORDERED_LAYERS } from './styling-contract.js';
|
|
46
|
+
|
|
47
|
+
/** @type {string} the candidate-output-directory-relative path this
|
|
48
|
+
* stylesheet is always written to (never content-addressed; see module
|
|
49
|
+
* documentation). */
|
|
50
|
+
export const GALA_BASE_STYLESHEET_PATH = 'assets/gala-base-v1.css';
|
|
51
|
+
|
|
52
|
+
/** @type {string} the `manifestAsset`/`manifestRoute` `mediaType` for this
|
|
53
|
+
* stylesheet. */
|
|
54
|
+
export const GALA_BASE_STYLESHEET_MEDIA_TYPE = 'text/css; charset=utf-8';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The exact `gala-base` layer source, byte-for-byte deterministic across
|
|
58
|
+
* builds (no interpolated per-build value of any kind). Its first line is
|
|
59
|
+
* the fixed layer-order statement every published page relies on (see
|
|
60
|
+
* module documentation).
|
|
61
|
+
*
|
|
62
|
+
* @type {string}
|
|
63
|
+
*/
|
|
64
|
+
export const GALA_BASE_STYLESHEET_SOURCE = `@layer ${ORDERED_LAYERS.join(', ')};
|
|
65
|
+
|
|
66
|
+
@layer gala-base {
|
|
67
|
+
[data-gala-publication-root] {
|
|
68
|
+
box-sizing: border-box;
|
|
69
|
+
}
|
|
70
|
+
[data-gala-publication-root] *,
|
|
71
|
+
[data-gala-publication-root] *::before,
|
|
72
|
+
[data-gala-publication-root] *::after {
|
|
73
|
+
box-sizing: inherit;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Skip link: the first focusable element on every page, visually hidden
|
|
77
|
+
until it receives keyboard focus, so it is announced and reachable by
|
|
78
|
+
assistive technology and keyboard users without being the first visible
|
|
79
|
+
line of every published page. */
|
|
80
|
+
[data-gala-publication-root] a[href="#main-content"] {
|
|
81
|
+
position: absolute;
|
|
82
|
+
top: -9999px;
|
|
83
|
+
left: 0;
|
|
84
|
+
z-index: 9999;
|
|
85
|
+
padding: var(--gala-space-2, 0.5rem) var(--gala-space-4, 1rem);
|
|
86
|
+
background-color: var(--gala-color-surface, Canvas);
|
|
87
|
+
color: var(--gala-color-text, CanvasText);
|
|
88
|
+
}
|
|
89
|
+
[data-gala-publication-root] a[href="#main-content"]:focus-visible {
|
|
90
|
+
top: 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* The one rule that makes the closed token catalog's color-focus/
|
|
94
|
+
focus-width tokens actually paint a ring: outline-style is never set by
|
|
95
|
+
the token catalog alone (outline-color/outline-width longhands paint
|
|
96
|
+
nothing without it), and this layer's own low cascade precedence means
|
|
97
|
+
any theme-declared outline-color/outline-width in gala-components still
|
|
98
|
+
wins for the same element. */
|
|
99
|
+
[data-gala-publication-root] :focus-visible {
|
|
100
|
+
outline-style: solid;
|
|
101
|
+
outline-width: var(--gala-focus-width, 2px);
|
|
102
|
+
outline-color: var(--gala-color-focus, Highlight);
|
|
103
|
+
outline-offset: 2px;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* Type scale, expressed with clamp() so it scales between a phone and a
|
|
107
|
+
desktop viewport without a discrete breakpoint; no theme sets this. */
|
|
108
|
+
[data-gala-publication-root] h1 {
|
|
109
|
+
font-size: clamp(1.75rem, 1.4rem + 1.5vw, 2.5rem);
|
|
110
|
+
line-height: 1.2;
|
|
111
|
+
}
|
|
112
|
+
[data-gala-publication-root] h2 {
|
|
113
|
+
font-size: clamp(1.375rem, 1.2rem + 0.8vw, 1.875rem);
|
|
114
|
+
line-height: 1.25;
|
|
115
|
+
}
|
|
116
|
+
[data-gala-publication-root] h3 {
|
|
117
|
+
font-size: clamp(1.125rem, 1.05rem + 0.4vw, 1.375rem);
|
|
118
|
+
line-height: 1.3;
|
|
119
|
+
}
|
|
120
|
+
[data-gala-publication-root] h4,
|
|
121
|
+
[data-gala-publication-root] h5,
|
|
122
|
+
[data-gala-publication-root] h6 {
|
|
123
|
+
font-size: clamp(1rem, 0.96rem + 0.2vw, 1.125rem);
|
|
124
|
+
line-height: 1.35;
|
|
125
|
+
}
|
|
126
|
+
[data-gala-publication-root] p,
|
|
127
|
+
[data-gala-publication-root] li {
|
|
128
|
+
font-size: 1rem;
|
|
129
|
+
line-height: 1.6;
|
|
130
|
+
}
|
|
131
|
+
[data-gala-publication-root] code,
|
|
132
|
+
[data-gala-publication-root] pre {
|
|
133
|
+
/* Corrects the monospace optical-size mismatch against surrounding
|
|
134
|
+
proportional text at the same nominal font-size. */
|
|
135
|
+
font-size: 0.9em;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* Overflow/wrap containment: constrains a fenced code block or an
|
|
139
|
+
unbroken long token in prose to the viewport, never the whole page. */
|
|
140
|
+
[data-gala-publication-root] pre {
|
|
141
|
+
overflow-x: auto;
|
|
142
|
+
}
|
|
143
|
+
[data-gala-publication-root] article {
|
|
144
|
+
overflow-wrap: anywhere;
|
|
145
|
+
}
|
|
146
|
+
[data-gala-publication-root] img {
|
|
147
|
+
max-width: 100%;
|
|
148
|
+
height: auto;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* Responsive spacing: a small step at a tablet-and-up viewport width, so
|
|
152
|
+
header/main padding is not identical at 320px and 2560px. */
|
|
153
|
+
[data-gala-publication-root] header,
|
|
154
|
+
[data-gala-publication-root] footer {
|
|
155
|
+
padding: var(--gala-space-4, 1rem) var(--gala-space-4, 1rem);
|
|
156
|
+
}
|
|
157
|
+
[data-gala-publication-root] main {
|
|
158
|
+
padding: var(--gala-space-4, 1rem);
|
|
159
|
+
}
|
|
160
|
+
@media (min-width: 48rem) {
|
|
161
|
+
[data-gala-publication-root] header,
|
|
162
|
+
[data-gala-publication-root] footer {
|
|
163
|
+
padding: var(--gala-space-4, 1rem) var(--gala-space-6, 1.5rem);
|
|
164
|
+
}
|
|
165
|
+
[data-gala-publication-root] main {
|
|
166
|
+
padding: var(--gala-space-6, 1.5rem);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@media (prefers-reduced-motion: reduce) {
|
|
171
|
+
[data-gala-publication-root] *,
|
|
172
|
+
[data-gala-publication-root] *::before,
|
|
173
|
+
[data-gala-publication-root] *::after {
|
|
174
|
+
animation-duration: 0.001ms !important;
|
|
175
|
+
animation-iteration-count: 1 !important;
|
|
176
|
+
transition-duration: 0.001ms !important;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
`;
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The exact deterministic pre-paint appearance bootstrap script
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The exact deterministic pre-paint appearance bootstrap script: a versioned
|
|
3
|
+
* deterministic pre-paint bootstrap owned by `template`/`core` that resolves
|
|
4
|
+
* local selection plus system preference and sets
|
|
5
5
|
* the root state before first themed paint. It is covered by the artifact
|
|
6
6
|
* digest, is CSP-compatible under `script-src 'self'` (therefore an external
|
|
7
7
|
* file, not inline), touches no network, account, content or unrelated
|
|
8
|
-
* storage key, and cannot hide the document indefinitely
|
|
8
|
+
* storage key, and cannot hide the document indefinitely.
|
|
9
9
|
*
|
|
10
|
-
* This is the one and only browser bootstrap
|
|
11
|
-
*
|
|
12
|
-
* bootstrap
|
|
10
|
+
* This is the one and only browser bootstrap this renderer ever emits: the
|
|
11
|
+
* appearance controller is the only browser
|
|
12
|
+
* bootstrap it ships. Every name and value it embeds is read from
|
|
13
13
|
* `internal/appearance/contract.js`, the one module that owns them, so the
|
|
14
14
|
* server-rendered control markup and this script can never drift apart on a
|
|
15
15
|
* literal.
|
|
@@ -29,12 +29,12 @@
|
|
|
29
29
|
* any earlier: the control has not been parsed yet at phase 1's own
|
|
30
30
|
* execution point) and subscribes to live `prefers-color-scheme` changes,
|
|
31
31
|
* updating the resolved attribute only while the current selection is
|
|
32
|
-
* exactly `system
|
|
33
|
-
* overridden by later system changes
|
|
32
|
+
* exactly `system`: an explicit `light`/`dark` choice is not
|
|
33
|
+
* overridden by later system changes.
|
|
34
34
|
*
|
|
35
35
|
* Deliberately absent: no `document.documentElement.style` mutation of any
|
|
36
|
-
* kind (in particular, no hide-until-ready visibility toggle —
|
|
37
|
-
*
|
|
36
|
+
* kind (in particular, no hide-until-ready visibility toggle — this script
|
|
37
|
+
* must never hide the document indefinitely, and never needs to,
|
|
38
38
|
* since it only ever sets attributes, never inline style), no network
|
|
39
39
|
* request, no cookie, no account/tracking identifier, no storage key other
|
|
40
40
|
* than {@link APPEARANCE_STORAGE_KEY}.
|