@rathnasgala2/template 2.0.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 (43) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +515 -0
  3. package/contracts/render-policy.jcs +1 -0
  4. package/contracts/theme-styling-contract.jcs +1 -0
  5. package/package.json +96 -0
  6. package/src/core/errors.js +136 -0
  7. package/src/core/index.js +758 -0
  8. package/src/core/internal/appearance/bootstrap-script.js +159 -0
  9. package/src/core/internal/appearance/contract.js +193 -0
  10. package/src/core/internal/appearance/controller-markup.js +80 -0
  11. package/src/core/internal/appearance/styling-contract.js +509 -0
  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 +220 -0
  15. package/src/core/internal/feeds.js +161 -0
  16. package/src/core/internal/fs-walk.js +38 -0
  17. package/src/core/internal/media/avif-probe.js +169 -0
  18. package/src/core/internal/media/font-policy.js +56 -0
  19. package/src/core/internal/media/jpeg-codec.js +0 -0
  20. package/src/core/internal/media/limits.js +129 -0
  21. package/src/core/internal/media/pipeline.js +353 -0
  22. package/src/core/internal/media/png-codec.js +406 -0
  23. package/src/core/internal/media/resize.js +147 -0
  24. package/src/core/internal/media/sniff.js +109 -0
  25. package/src/core/internal/media/webp-probe.js +139 -0
  26. package/src/core/internal/messages.js +78 -0
  27. package/src/core/internal/page-kinds.js +709 -0
  28. package/src/core/internal/provenance.js +49 -0
  29. package/src/core/internal/render-policy-content.js +295 -0
  30. package/src/core/internal/route-labels.js +68 -0
  31. package/src/core/internal/route.js +167 -0
  32. package/src/core/internal/search-index.js +119 -0
  33. package/src/core/internal/seo.js +114 -0
  34. package/src/core/internal/sitemap.js +77 -0
  35. package/src/core/internal/skeleton.js +413 -0
  36. package/src/core/internal/source-inventory.js +145 -0
  37. package/src/core/internal/text-direction.js +59 -0
  38. package/src/core/internal/theme-assets.js +387 -0
  39. package/src/core/internal/uuid.js +51 -0
  40. package/src/core/manifest.js +0 -0
  41. package/types/eleventy-plugin-syntaxhighlight.d.ts +26 -0
  42. package/types/eleventy.d.ts +38 -0
  43. package/types/index.d.ts +347 -0
@@ -0,0 +1,159 @@
1
+ /**
2
+ * The exact deterministic pre-paint appearance bootstrap script (brief S2
3
+ * section 3: "a versioned deterministic pre-paint bootstrap owned by
4
+ * `template`/`core` resolves local selection plus system preference and sets
5
+ * the root state before first themed paint. It is covered by the artifact
6
+ * digest, is CSP-compatible under `script-src 'self'` (therefore an external
7
+ * file, not inline), touches no network, account, content or unrelated
8
+ * storage key, and cannot hide the document indefinitely").
9
+ *
10
+ * This is the one and only browser bootstrap S2 ever emits ("Module absence
11
+ * and CSP equality": "the appearance controller is the only browser
12
+ * bootstrap"). Every name and value it embeds is read from
13
+ * `internal/appearance/contract.js`, the one module that owns them, so the
14
+ * server-rendered control markup and this script can never drift apart on a
15
+ * literal.
16
+ *
17
+ * Two independent phases, both inside the same one external file so the
18
+ * template never ships a second script:
19
+ *
20
+ * 1. A synchronous, top-level phase that runs immediately when this
21
+ * blocking, non-`defer`/non-`async`/non-`module` `<script src>` executes
22
+ * in `<head>` — before `<body>` is parsed, and so before any themed
23
+ * paint. It resolves the stored selection (or `system`, gracefully
24
+ * substituted for a missing/unreadable/invalid stored value — storage
25
+ * failure never leaves the session unusable) against
26
+ * `prefers-color-scheme` and sets both root attributes.
27
+ * 2. A `DOMContentLoaded`-deferred phase that wires the server-rendered
28
+ * `<select>` control's `change` event once `<body>` exists (it cannot run
29
+ * any earlier: the control has not been parsed yet at phase 1's own
30
+ * execution point) and subscribes to live `prefers-color-scheme` changes,
31
+ * updating the resolved attribute only while the current selection is
32
+ * exactly `system` (brief: "an explicit `light`/`dark` choice is not
33
+ * overridden by later system changes").
34
+ *
35
+ * Deliberately absent: no `document.documentElement.style` mutation of any
36
+ * kind (in particular, no hide-until-ready visibility toggle — brief:
37
+ * "cannot hide the document indefinitely" — this script never needs one,
38
+ * since it only ever sets attributes, never inline style), no network
39
+ * request, no cookie, no account/tracking identifier, no storage key other
40
+ * than {@link APPEARANCE_STORAGE_KEY}.
41
+ */
42
+
43
+ import {
44
+ APPEARANCE_MODE_DARK,
45
+ APPEARANCE_MODE_LIGHT,
46
+ APPEARANCE_MODE_SYSTEM,
47
+ APPEARANCE_RESOLVED_MODE_ATTRIBUTE,
48
+ APPEARANCE_SELECTION_ATTRIBUTE,
49
+ APPEARANCE_SELECT_ID,
50
+ APPEARANCE_STORAGE_KEY,
51
+ } from './contract.js';
52
+
53
+ /**
54
+ * The exact bootstrap script source, byte-for-byte deterministic across
55
+ * builds: every interpolated value below is a fixed literal from
56
+ * `contract.js`, never a per-publication or per-build input.
57
+ *
58
+ * @type {string}
59
+ */
60
+ export const APPEARANCE_BOOTSTRAP_SCRIPT_SOURCE = `(function () {
61
+ 'use strict';
62
+ var STORAGE_KEY = ${JSON.stringify(APPEARANCE_STORAGE_KEY)};
63
+ var RESOLVED_ATTRIBUTE = ${JSON.stringify(APPEARANCE_RESOLVED_MODE_ATTRIBUTE)};
64
+ var SELECTION_ATTRIBUTE = ${JSON.stringify(APPEARANCE_SELECTION_ATTRIBUTE)};
65
+ var SELECT_ID = ${JSON.stringify(APPEARANCE_SELECT_ID)};
66
+ var MODE_LIGHT = ${JSON.stringify(APPEARANCE_MODE_LIGHT)};
67
+ var MODE_DARK = ${JSON.stringify(APPEARANCE_MODE_DARK)};
68
+ var MODE_SYSTEM = ${JSON.stringify(APPEARANCE_MODE_SYSTEM)};
69
+
70
+ function isKnownMode(value) {
71
+ return value === MODE_LIGHT || value === MODE_DARK || value === MODE_SYSTEM;
72
+ }
73
+
74
+ function readStoredSelection() {
75
+ try {
76
+ var value = window.localStorage.getItem(STORAGE_KEY);
77
+ if (isKnownMode(value)) {
78
+ return value;
79
+ }
80
+ } catch (error) {
81
+ // Storage blocked (private mode, disabled site data, quota): the
82
+ // session stays usable and this load falls back to MODE_SYSTEM below.
83
+ }
84
+ return MODE_SYSTEM;
85
+ }
86
+
87
+ function writeStoredSelection(mode) {
88
+ try {
89
+ window.localStorage.setItem(STORAGE_KEY, mode);
90
+ } catch (error) {
91
+ // Storage blocked: the selection simply does not persist past this
92
+ // load; the visible page state this call site also applies still
93
+ // updates normally.
94
+ }
95
+ }
96
+
97
+ function systemPrefersDark() {
98
+ try {
99
+ return !!(
100
+ window.matchMedia &&
101
+ window.matchMedia('(prefers-color-scheme: dark)').matches
102
+ );
103
+ } catch (error) {
104
+ return false;
105
+ }
106
+ }
107
+
108
+ function resolveMode(selection) {
109
+ if (selection === MODE_LIGHT || selection === MODE_DARK) {
110
+ return selection;
111
+ }
112
+ return systemPrefersDark() ? MODE_DARK : MODE_LIGHT;
113
+ }
114
+
115
+ function applySelection(selection) {
116
+ var root = document.documentElement;
117
+ root.setAttribute(SELECTION_ATTRIBUTE, selection);
118
+ root.setAttribute(RESOLVED_ATTRIBUTE, resolveMode(selection));
119
+ }
120
+
121
+ // Phase 1 (synchronous, pre-paint): resolve and apply immediately.
122
+ applySelection(readStoredSelection());
123
+
124
+ // Live system-preference changes, only while the reader's own selection is
125
+ // still exactly "system" (an explicit light/dark choice is never
126
+ // overridden by a later system change).
127
+ if (window.matchMedia) {
128
+ try {
129
+ var media = window.matchMedia('(prefers-color-scheme: dark)');
130
+ var onSystemChange = function () {
131
+ if (readStoredSelection() === MODE_SYSTEM) {
132
+ applySelection(MODE_SYSTEM);
133
+ }
134
+ };
135
+ if (typeof media.addEventListener === 'function') {
136
+ media.addEventListener('change', onSystemChange);
137
+ } else if (typeof media.addListener === 'function') {
138
+ media.addListener(onSystemChange);
139
+ }
140
+ } catch (error) {
141
+ // No live system-change subscription available: the resolved mode
142
+ // this load already applied simply stays static until next load.
143
+ }
144
+ }
145
+
146
+ // Phase 2 (deferred): wire the server-rendered control once it exists.
147
+ document.addEventListener('DOMContentLoaded', function () {
148
+ var select = document.getElementById(SELECT_ID);
149
+ if (!select) return;
150
+ select.value = readStoredSelection();
151
+ select.addEventListener('change', function () {
152
+ var next = select.value;
153
+ if (!isKnownMode(next)) return;
154
+ writeStoredSelection(next);
155
+ applySelection(next);
156
+ });
157
+ });
158
+ })();
159
+ `;
@@ -0,0 +1,193 @@
1
+ /**
2
+ * The single module `contracts/theme-styling-contract.jcs` (S2-T12) targets
3
+ * for every Light/Dark/System appearance name this renderer owns (task
4
+ * packet S2-T07: "the `data-` attribute/class names the controller sets must
5
+ * be fixed constants exported from one module"; DEC-097's closed
6
+ * `templateStylingContract` catalog, section "Closed theme-package and
7
+ * stylesheet admission": the required root attribute row
8
+ * `{name:data-gala-publication-root,match:presence,values:[],role:root}` and
9
+ * the required palette attribute row
10
+ * `{name:data-gala-resolved-color-mode,match:exact-value,values:[dark,light],role:state}`;
11
+ * "The template renderer emits the exact root attribute and maintains
12
+ * exactly one `data-gala-resolved-color-mode="light"|"dark"` value on that
13
+ * same element before theme CSS is observed").
14
+ *
15
+ * Two attributes are exposed, deliberately kept separate (brief S2 section 3:
16
+ * "selection and resolved state are exposed separately through
17
+ * template-owned root attributes; theme CSS consumes only resolved state"):
18
+ *
19
+ * - {@link APPEARANCE_ROOT_ATTRIBUTE} and {@link APPEARANCE_RESOLVED_MODE_ATTRIBUTE}
20
+ * are the two DEC-097-fixed theme-facing attributes above — part of the
21
+ * published styling contract S2-T12 emits, never renamed here independent
22
+ * of that contract.
23
+ * - {@link APPEARANCE_SELECTION_ATTRIBUTE} is this renderer's own
24
+ * template-internal bookkeeping attribute (the reader's raw `light` /
25
+ * `dark` / `system` choice, as opposed to `system`'s *resolved* value). It
26
+ * is not a theme-styling-contract hook — DEC-097's attribute catalog does
27
+ * not name it, and no theme selector may target it — but its name is still
28
+ * fixed here, in this one module, exactly like the two contract-owned
29
+ * attributes, so no other module ever invents its own literal for it.
30
+ *
31
+ * Both the pre-paint bootstrap script ({@link ../appearance/bootstrap-script.js})
32
+ * and the server-rendered control markup ({@link ../appearance/controller-markup.js})
33
+ * import every name/value from here; neither ever writes an attribute name,
34
+ * a mode value or the storage key as its own literal.
35
+ */
36
+
37
+ import { joinBasePathAndRoute } from '../route.js';
38
+
39
+ /**
40
+ * The DEC-097 `templateStylingContract.publicationRootSelector` attribute:
41
+ * a bare-presence attribute the template always renders on the document's
42
+ * root element (`<html>`), independent of JavaScript (`match:presence`).
43
+ *
44
+ * @type {string}
45
+ */
46
+ export const APPEARANCE_ROOT_ATTRIBUTE = 'data-gala-publication-root';
47
+
48
+ /**
49
+ * The DEC-097 `templateStylingContract.resolvedPaletteSelectors` attribute:
50
+ * an exact-value attribute (`"light"` or `"dark"`, never `"system"`) the
51
+ * pre-paint bootstrap script sets on the root element before first themed
52
+ * paint. Theme CSS consumes only this attribute, never
53
+ * {@link APPEARANCE_SELECTION_ATTRIBUTE}.
54
+ *
55
+ * @type {string}
56
+ */
57
+ export const APPEARANCE_RESOLVED_MODE_ATTRIBUTE =
58
+ 'data-gala-resolved-color-mode';
59
+
60
+ /**
61
+ * Template-internal bookkeeping attribute carrying the reader's raw
62
+ * selection (`light` / `dark` / `system`), as opposed to
63
+ * {@link APPEARANCE_RESOLVED_MODE_ATTRIBUTE}'s resolved value. Not part of
64
+ * the published theme styling contract; no theme selector may target it.
65
+ *
66
+ * @type {string}
67
+ */
68
+ export const APPEARANCE_SELECTION_ATTRIBUTE = 'data-gala-color-mode-selection';
69
+
70
+ /** @type {'light'} */
71
+ export const APPEARANCE_MODE_LIGHT = 'light';
72
+ /** @type {'dark'} */
73
+ export const APPEARANCE_MODE_DARK = 'dark';
74
+ /** @type {'system'} */
75
+ export const APPEARANCE_MODE_SYSTEM = 'system';
76
+
77
+ /**
78
+ * Every canonical machine-readable selection value (brief S2 section 3:
79
+ * "canonical machine values are `light`, `dark`, `system`; `lite` is not a
80
+ * value").
81
+ *
82
+ * @type {readonly ['light', 'dark', 'system']}
83
+ */
84
+ export const APPEARANCE_MODE_VALUES = Object.freeze([
85
+ APPEARANCE_MODE_LIGHT,
86
+ APPEARANCE_MODE_DARK,
87
+ APPEARANCE_MODE_SYSTEM,
88
+ ]);
89
+
90
+ /**
91
+ * Every value {@link APPEARANCE_RESOLVED_MODE_ATTRIBUTE} may carry. `system`
92
+ * always resolves to one of these two; it is never a third selector value
93
+ * (DEC-097: "`system` is resolved to one of those values rather than
94
+ * becoming a third selector").
95
+ *
96
+ * @type {readonly ['light', 'dark']}
97
+ */
98
+ export const APPEARANCE_RESOLVED_MODE_VALUES = Object.freeze([
99
+ APPEARANCE_MODE_LIGHT,
100
+ APPEARANCE_MODE_DARK,
101
+ ]);
102
+
103
+ /**
104
+ * The initial reader default (brief S2 section 3: "initial reader default is
105
+ * `system`").
106
+ *
107
+ * @type {'system'}
108
+ */
109
+ export const APPEARANCE_DEFAULT_MODE = APPEARANCE_MODE_SYSTEM;
110
+
111
+ /**
112
+ * The one versioned local-storage key the pre-paint bootstrap script and the
113
+ * control's change handler both read/write (brief S2 section 3: "selection
114
+ * is stored under one versioned local key"). The trailing `v1` is this
115
+ * key's own version; a future incompatible storage format change mints a new
116
+ * key rather than reinterpreting old values, so an old browser's already
117
+ * stored `v1` value is simply orphaned (ignored, never migrated or deleted)
118
+ * rather than misread as a new format.
119
+ *
120
+ * @type {string}
121
+ */
122
+ export const APPEARANCE_STORAGE_KEY = 'gala:appearance:color-mode:v1';
123
+
124
+ /**
125
+ * The `id` the server-rendered `<select>` control carries, so the bootstrap
126
+ * script's post-`DOMContentLoaded` wiring can find it with a single
127
+ * `document.getElementById` call. Also used as the paired `<label for>`
128
+ * target.
129
+ *
130
+ * @type {string}
131
+ */
132
+ export const APPEARANCE_SELECT_ID = 'gala-appearance-color-mode';
133
+
134
+ /**
135
+ * The candidate-output-directory-relative path the deterministic pre-paint
136
+ * bootstrap script is written to (task packet S2-T07: "entered into the
137
+ * manifest through the S2-T03 path with `{path,byteLength,sha256}`"). Not
138
+ * content-addressed (unlike the S2-T05 media pipeline's derivative paths):
139
+ * this file's bytes are a pure function of this module's own constants, not
140
+ * of any per-publication input, so a stable path carries no collision risk.
141
+ * The `v1` component versions the bootstrap script's own behaviour,
142
+ * independent of {@link APPEARANCE_STORAGE_KEY}'s own versioning.
143
+ *
144
+ * @type {string}
145
+ */
146
+ export const APPEARANCE_BOOTSTRAP_SCRIPT_PATH =
147
+ 'assets/gala-appearance-bootstrap-v1.js';
148
+
149
+ /**
150
+ * The root-absolute `<script src>` reference for a root (`/`) `basePath`
151
+ * publication. Every actual render joins this with the publication's own
152
+ * `basePath` through {@link appearanceBootstrapScriptHref} instead of using
153
+ * this literal directly (S2-T12 basePath fix, LOCAL-7 follow-up: an earlier
154
+ * version of this module used this root-absolute literal unconditionally,
155
+ * which 404s under a non-root `basePath` publication). This constant is
156
+ * retained as the one-place root-`basePath` literal every test and the
157
+ * `joinBasePathAndRoute` call below both agree on.
158
+ *
159
+ * @type {string}
160
+ */
161
+ export const APPEARANCE_BOOTSTRAP_SCRIPT_HREF = `/${APPEARANCE_BOOTSTRAP_SCRIPT_PATH}`;
162
+
163
+ /**
164
+ * The `basePath`-joined `<script src>` reference every generated page's
165
+ * skeleton layout actually uses (S2-T12 basePath fix).
166
+ *
167
+ * @param {string} basePath the build input's `basePath`
168
+ * @returns {string} the joined `href`
169
+ */
170
+ export function appearanceBootstrapScriptHref(basePath) {
171
+ return joinBasePathAndRoute(basePath, APPEARANCE_BOOTSTRAP_SCRIPT_HREF);
172
+ }
173
+
174
+ /**
175
+ * The `manifestAsset`/`manifestRoute` `mediaType` enum member for a
176
+ * JavaScript file (`@rathnasgala2/schemas`' `artifact-manifest.schema.json`).
177
+ *
178
+ * @type {string}
179
+ */
180
+ export const APPEARANCE_BOOTSTRAP_SCRIPT_MEDIA_TYPE =
181
+ 'application/javascript; charset=utf-8';
182
+
183
+ /**
184
+ * The `<meta name="color-scheme">` content value: both palettes are
185
+ * admitted, so the browser's own user-agent styling (form controls,
186
+ * scrollbars) and initial paint follow `prefers-color-scheme` immediately,
187
+ * independent of this renderer's own resolved-mode attribute (brief S2
188
+ * section 3: "`color-scheme` meta/CSS handling"; "with JavaScript disabled,
189
+ * CSS media queries preserve `system`").
190
+ *
191
+ * @type {string}
192
+ */
193
+ export const COLOR_SCHEME_META_CONTENT = 'light dark';
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The server-rendered Light/Dark/System appearance control (task packet
3
+ * S2-T07: "the three-mode appearance control in the header-actions slot";
4
+ * brief S2 section 3: "the control is a native `select`, a radio group, or a
5
+ * conforming menu/radio pattern. A three-state unlabeled icon cycle is
6
+ * rejected").
7
+ *
8
+ * Scoped decision (documented, not silently assumed): of the brief's three
9
+ * admitted patterns, this module implements a native `<select>` bound to a
10
+ * visible, programmatically associated `<label>`. A native select needs no
11
+ * `aria-pressed`/`aria-checked` bookkeeping of its own — its accessible
12
+ * state is the browser-native selected-`<option>` relationship, exposed to
13
+ * every platform accessibility API without any ARIA authored here — and it
14
+ * is keyboard-operable (arrow keys, type-ahead, Enter/Space to open) by
15
+ * native semantics alone, with no custom widget code to keep synchronized.
16
+ * The select's initial selected option is always
17
+ * {@link APPEARANCE_DEFAULT_MODE} (`system`): the actual stored/resolved
18
+ * value is applied to `select.value` by the pre-paint bootstrap script's own
19
+ * `DOMContentLoaded` phase once it runs (see `bootstrap-script.js`), so this
20
+ * server-rendered default is only ever visible for the brief instant before
21
+ * that phase completes, or permanently with JavaScript disabled — exactly
22
+ * the no-JS fallback the brief requires ("with JavaScript disabled, ...
23
+ * `system` [is preserved]").
24
+ *
25
+ * This markup carries no inline `on*` handler and no `javascript:` URL, so
26
+ * it needs no CSP allowance beyond the renderer's existing
27
+ * `script-src 'self'` baseline; the bootstrap script (an external, same-
28
+ * origin file) is what makes it interactive.
29
+ */
30
+
31
+ import { escapeHtml } from '../skeleton.js';
32
+ import {
33
+ APPEARANCE_DEFAULT_MODE,
34
+ APPEARANCE_MODE_DARK,
35
+ APPEARANCE_MODE_LIGHT,
36
+ APPEARANCE_MODE_SYSTEM,
37
+ APPEARANCE_SELECT_ID,
38
+ } from './contract.js';
39
+
40
+ /**
41
+ * Render the appearance control's inner markup (a `<label>` plus its bound
42
+ * `<select>`), for insertion into the header-actions slot.
43
+ *
44
+ * @param {object} options rendering options
45
+ * @param {Readonly<Record<string, string | ((...args: string[]) => string)>>} options.messages
46
+ * the resolved message catalog
47
+ * @returns {string} the control markup
48
+ */
49
+ export function renderAppearanceControl({ messages }) {
50
+ const label = escapeHtml(
51
+ /** @type {string} */ (messages.appearanceControlLabel),
52
+ );
53
+ const selectId = escapeHtml(APPEARANCE_SELECT_ID);
54
+ /** @type {readonly {value: string, text: string}[]} */
55
+ const modes = [
56
+ {
57
+ value: APPEARANCE_MODE_LIGHT,
58
+ text: /** @type {string} */ (messages.appearanceModeLightLabel),
59
+ },
60
+ {
61
+ value: APPEARANCE_MODE_DARK,
62
+ text: /** @type {string} */ (messages.appearanceModeDarkLabel),
63
+ },
64
+ {
65
+ value: APPEARANCE_MODE_SYSTEM,
66
+ text: /** @type {string} */ (messages.appearanceModeSystemLabel),
67
+ },
68
+ ];
69
+ const options = modes
70
+ .map(({ value, text }) => {
71
+ const selectedAttribute =
72
+ value === APPEARANCE_DEFAULT_MODE ? ' selected' : '';
73
+ return `<option value="${escapeHtml(value)}"${selectedAttribute}>${escapeHtml(text)}</option>`;
74
+ })
75
+ .join('');
76
+ return (
77
+ `<label for="${selectId}">${label}</label>` +
78
+ `<select id="${selectId}" name="${selectId}">${options}</select>`
79
+ );
80
+ }