@rathnasgala2/template 2.2.0 → 3.0.1
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 +90 -61
- package/contracts/render-policy.jcs +1 -1
- package/contracts/theme-styling-contract.jcs +1 -1
- package/package.json +2 -2
- package/src/core/index.js +180 -50
- package/src/core/internal/appearance/base-layer.js +711 -137
- package/src/core/internal/appearance/bootstrap-script.js +570 -83
- package/src/core/internal/appearance/contract.js +6 -5
- package/src/core/internal/appearance/controller-markup.js +46 -55
- package/src/core/internal/appearance/minify-script.js +69 -0
- package/src/core/internal/appearance/styling-contract.js +201 -19
- package/src/core/internal/chrome.js +199 -0
- package/src/core/internal/components.js +371 -0
- package/src/core/internal/eleventy-render.js +13 -4
- package/src/core/internal/feeds.js +74 -6
- package/src/core/internal/icons.js +52 -0
- package/src/core/internal/llms.js +252 -0
- package/src/core/internal/media/pipeline.js +32 -17
- package/src/core/internal/messages.js +104 -0
- package/src/core/internal/page-kinds.js +918 -290
- package/src/core/internal/render-policy-content.js +2 -2
- package/src/core/internal/robots.js +70 -0
- package/src/core/internal/route-labels.js +31 -0
- package/src/core/internal/seo.js +53 -0
- package/src/core/internal/skeleton.js +54 -129
- package/src/core/internal/structured-data.js +247 -0
- package/types/index.d.ts +2 -0
|
@@ -138,14 +138,15 @@ export const APPEARANCE_DEFAULT_MODE = APPEARANCE_MODE_SYSTEM;
|
|
|
138
138
|
export const APPEARANCE_STORAGE_KEY = 'gala:appearance:color-mode:v1';
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
|
-
* The `id` the server-rendered `<
|
|
142
|
-
* script's
|
|
143
|
-
* `document.getElementById` call.
|
|
144
|
-
*
|
|
141
|
+
* The `id` the server-rendered colour-mode toggle `<button>` carries, so
|
|
142
|
+
* the site script's wiring can find it with a single
|
|
143
|
+
* `document.getElementById` call. The button is rendered `hidden`; the
|
|
144
|
+
* script reveals it (with JavaScript off the site simply follows the
|
|
145
|
+
* server-rendered default palette).
|
|
145
146
|
*
|
|
146
147
|
* @type {string}
|
|
147
148
|
*/
|
|
148
|
-
export const
|
|
149
|
+
export const APPEARANCE_TOGGLE_ID = 'gala-appearance-color-mode';
|
|
149
150
|
|
|
150
151
|
/**
|
|
151
152
|
* The candidate-output-directory-relative path the deterministic pre-paint
|
|
@@ -1,44 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The server-rendered
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* The server-rendered colour-mode toggle, placed in the header-actions slot:
|
|
3
|
+
* one icon button that the site script reveals and wires to cycle
|
|
4
|
+
* system, light, dark (with a circular-reveal view transition where the
|
|
5
|
+
* browser supports it). It is rendered `hidden` because it does nothing
|
|
6
|
+
* without the script; with JavaScript off the site simply follows the
|
|
7
|
+
* server-rendered default palette and no dead control is shown.
|
|
6
8
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* is keyboard-operable (arrow keys, type-ahead, Enter/Space to open) by
|
|
14
|
-
* native semantics alone, with no custom widget code to keep synchronized.
|
|
15
|
-
* The select's initial selected option is always
|
|
16
|
-
* {@link APPEARANCE_DEFAULT_MODE} (`system`): the actual stored/resolved
|
|
17
|
-
* value is applied to `select.value` by the pre-paint bootstrap script's own
|
|
18
|
-
* `DOMContentLoaded` phase once it runs (see `bootstrap-script.js`), so this
|
|
19
|
-
* server-rendered default is only ever visible for the brief instant before
|
|
20
|
-
* that phase completes, or permanently with JavaScript disabled — exactly
|
|
21
|
-
* the required no-JS fallback: with JavaScript disabled, `system` is
|
|
22
|
-
* preserved.
|
|
23
|
-
*
|
|
24
|
-
* This markup carries no inline `on*` handler and no `javascript:` URL, so
|
|
25
|
-
* it needs no CSP allowance beyond the renderer's existing
|
|
26
|
-
* `script-src 'self'` baseline; the bootstrap script (an external, same-
|
|
27
|
-
* origin file) is what makes it interactive.
|
|
9
|
+
* The button's accessible name carries the current mode (for example
|
|
10
|
+
* "Appearance: System"); the script keeps it current from the three
|
|
11
|
+
* `data-label-*` attributes, which are the only localized text it needs.
|
|
12
|
+
* Which of the three mode icons shows is pure CSS, keyed off the root's
|
|
13
|
+
* selection attribute. The markup carries no inline `on*` handler and no
|
|
14
|
+
* `javascript:` URL, so it needs no CSP allowance beyond `script-src 'self'`.
|
|
28
15
|
*/
|
|
29
16
|
|
|
30
17
|
import { escapeHtml } from '../skeleton.js';
|
|
18
|
+
import { icon } from '../icons.js';
|
|
31
19
|
import {
|
|
32
20
|
APPEARANCE_DEFAULT_MODE,
|
|
33
21
|
APPEARANCE_MODE_DARK,
|
|
34
22
|
APPEARANCE_MODE_LIGHT,
|
|
35
23
|
APPEARANCE_MODE_SYSTEM,
|
|
36
|
-
|
|
24
|
+
APPEARANCE_TOGGLE_ID,
|
|
37
25
|
} from './contract.js';
|
|
38
26
|
|
|
39
27
|
/**
|
|
40
|
-
* Render the appearance
|
|
41
|
-
* `<select>`), for insertion into the header-actions slot.
|
|
28
|
+
* Render the appearance toggle button.
|
|
42
29
|
*
|
|
43
30
|
* @param {object} options rendering options
|
|
44
31
|
* @param {Readonly<Record<string, string | ((...args: string[]) => string)>>} options.messages
|
|
@@ -46,34 +33,38 @@ import {
|
|
|
46
33
|
* @returns {string} the control markup
|
|
47
34
|
*/
|
|
48
35
|
export function renderAppearanceControl({ messages }) {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
{
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
{
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
.map((
|
|
70
|
-
const selectedAttribute =
|
|
71
|
-
value === APPEARANCE_DEFAULT_MODE ? ' selected' : '';
|
|
72
|
-
return `<option value="${escapeHtml(value)}"${selectedAttribute}>${escapeHtml(text)}</option>`;
|
|
73
|
-
})
|
|
36
|
+
const control = /** @type {string} */ (messages.appearanceControlLabel);
|
|
37
|
+
/** @type {Record<string, string>} */
|
|
38
|
+
const modeText = {
|
|
39
|
+
[APPEARANCE_MODE_LIGHT]: /** @type {string} */ (
|
|
40
|
+
messages.appearanceModeLightLabel
|
|
41
|
+
),
|
|
42
|
+
[APPEARANCE_MODE_DARK]: /** @type {string} */ (
|
|
43
|
+
messages.appearanceModeDarkLabel
|
|
44
|
+
),
|
|
45
|
+
[APPEARANCE_MODE_SYSTEM]: /** @type {string} */ (
|
|
46
|
+
messages.appearanceModeSystemLabel
|
|
47
|
+
),
|
|
48
|
+
};
|
|
49
|
+
const labelOf = (/** @type {string} */ mode) =>
|
|
50
|
+
escapeHtml(`${control}: ${modeText[mode]}`);
|
|
51
|
+
const labelAttributes = [
|
|
52
|
+
APPEARANCE_MODE_SYSTEM,
|
|
53
|
+
APPEARANCE_MODE_LIGHT,
|
|
54
|
+
APPEARANCE_MODE_DARK,
|
|
55
|
+
]
|
|
56
|
+
.map((mode) => ` data-label-${mode}="${labelOf(mode)}"`)
|
|
74
57
|
.join('');
|
|
58
|
+
const modeIcon = (
|
|
59
|
+
/** @type {string} */ mode,
|
|
60
|
+
/** @type {string} */ iconName,
|
|
61
|
+
) => `<span class="g-mode-icon g-mode-${mode}">${icon(iconName)}</span>`;
|
|
75
62
|
return (
|
|
76
|
-
`<
|
|
77
|
-
|
|
63
|
+
`<button class="g-icon-btn" id="${escapeHtml(APPEARANCE_TOGGLE_ID)}" type="button" hidden` +
|
|
64
|
+
` data-action="mode"${labelAttributes} aria-label="${labelOf(APPEARANCE_DEFAULT_MODE)}" title="${labelOf(APPEARANCE_DEFAULT_MODE)}">` +
|
|
65
|
+
modeIcon(APPEARANCE_MODE_SYSTEM, 'monitor') +
|
|
66
|
+
modeIcon(APPEARANCE_MODE_LIGHT, 'sun') +
|
|
67
|
+
modeIcon(APPEARANCE_MODE_DARK, 'moon') +
|
|
68
|
+
`</button>`
|
|
78
69
|
);
|
|
79
70
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A deliberately small, dependency-free, safe minifier for the one site
|
|
3
|
+
* script: it removes comments and indentation and drops blank lines. It
|
|
4
|
+
* never renames, reorders or joins statements, and it keeps every newline
|
|
5
|
+
* that remains, so automatic semicolon insertion behaves exactly as in the
|
|
6
|
+
* readable source. String literals and regular-expression literals are
|
|
7
|
+
* copied byte for byte.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} previous the last non-whitespace character already
|
|
12
|
+
* emitted ('' at the start of the source)
|
|
13
|
+
* @returns {boolean} whether a `/` here starts a regular-expression literal
|
|
14
|
+
* (rather than a division)
|
|
15
|
+
*/
|
|
16
|
+
function regexAllowed(previous) {
|
|
17
|
+
return previous === '' || '(,=:[!&|?{};+-*%<>~^'.includes(previous);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} source readable JavaScript source (no template literals)
|
|
22
|
+
* @returns {string} the same program without comments, indentation or blank
|
|
23
|
+
* lines
|
|
24
|
+
*/
|
|
25
|
+
export function minifyScript(source) {
|
|
26
|
+
let out = '';
|
|
27
|
+
let previous = '';
|
|
28
|
+
let index = 0;
|
|
29
|
+
const length = source.length;
|
|
30
|
+
while (index < length) {
|
|
31
|
+
const character = source[index];
|
|
32
|
+
const next = source[index + 1];
|
|
33
|
+
if (character === "'" || character === '"') {
|
|
34
|
+
let end = index + 1;
|
|
35
|
+
while (end < length && source[end] !== character) {
|
|
36
|
+
end += source[end] === '\\' ? 2 : 1;
|
|
37
|
+
}
|
|
38
|
+
out += source.slice(index, end + 1);
|
|
39
|
+
previous = character;
|
|
40
|
+
index = end + 1;
|
|
41
|
+
} else if (character === '/' && next === '/') {
|
|
42
|
+
while (index < length && source[index] !== '\n') index += 1;
|
|
43
|
+
} else if (character === '/' && next === '*') {
|
|
44
|
+
const end = source.indexOf('*/', index + 2);
|
|
45
|
+
index = end === -1 ? length : end + 2;
|
|
46
|
+
} else if (character === '/' && regexAllowed(previous)) {
|
|
47
|
+
let end = index + 1;
|
|
48
|
+
let inClass = false;
|
|
49
|
+
while (end < length && (inClass || source[end] !== '/')) {
|
|
50
|
+
if (source[end] === '\\') end += 1;
|
|
51
|
+
else if (source[end] === '[') inClass = true;
|
|
52
|
+
else if (source[end] === ']') inClass = false;
|
|
53
|
+
end += 1;
|
|
54
|
+
}
|
|
55
|
+
out += source.slice(index, end + 1);
|
|
56
|
+
previous = '/';
|
|
57
|
+
index = end + 1;
|
|
58
|
+
} else {
|
|
59
|
+
out += character;
|
|
60
|
+
if (!/\s/.test(character)) previous = character;
|
|
61
|
+
index += 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return `${out
|
|
65
|
+
.split('\n')
|
|
66
|
+
.map((line) => line.trim())
|
|
67
|
+
.filter((line) => line !== '')
|
|
68
|
+
.join('\n')}\n`;
|
|
69
|
+
}
|
|
@@ -13,9 +13,10 @@
|
|
|
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 (`publicThemeSlotHooks` is a `[1..
|
|
17
|
-
* every catalog leaf below (
|
|
18
|
-
* has exactly one corresponding public hook, for exactly
|
|
16
|
+
* Public-hook budget (`publicThemeSlotHooks` is a `[1..256]`-entry set since
|
|
17
|
+
* contract 3.0.0): every catalog leaf below (30 type + 127 class + 2 id + 19
|
|
18
|
+
* attribute-value) has exactly one corresponding public hook, for exactly
|
|
19
|
+
* 178 entries — this
|
|
19
20
|
* renderer's complete public theming surface. A leaf with no matching hook
|
|
20
21
|
* would be a catalog member no theme could ever validly select (a
|
|
21
22
|
* used-but-undeclared public slot hook rejects), so catalogs and hooks are
|
|
@@ -48,7 +49,7 @@
|
|
|
48
49
|
* or a `background-image`.
|
|
49
50
|
*
|
|
50
51
|
* 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
|
|
52
|
+
* catalog made every interaction state unstylable while the closed 116-token
|
|
52
53
|
* catalog already required `color-focus` and `color-link-visited`, tokens
|
|
53
54
|
* only a pseudo-class can ever apply. {@link PSEUDO_CLASSES} publishes a
|
|
54
55
|
* small closed catalog instead of widening indefinitely; the module
|
|
@@ -61,7 +62,7 @@
|
|
|
61
62
|
* markdown-it's own CommonMark grammar (with `html:false`) never actually
|
|
62
63
|
* produces — `b`, `i`, `u`, `s`, `del`, `ins`, `sub`, `sup`, `mark`, `small`,
|
|
63
64
|
* `wbr`, `table`, `thead`, `tbody`, `tr`, `th`, `td`, `dl`, `dt`, `dd`,
|
|
64
|
-
* `
|
|
65
|
+
* `figcaption` — is likewise excluded from `typeSelectors`: this
|
|
65
66
|
* catalog is every hook the template actually renders, not the sanitizer's
|
|
66
67
|
* own defence-in-depth superset.
|
|
67
68
|
*/
|
|
@@ -70,7 +71,7 @@ import { HIGHLIGHT_GRAMMARS } from '../render-policy-content.js';
|
|
|
70
71
|
import {
|
|
71
72
|
APPEARANCE_RESOLVED_MODE_ATTRIBUTE,
|
|
72
73
|
APPEARANCE_ROOT_ATTRIBUTE,
|
|
73
|
-
|
|
74
|
+
APPEARANCE_TOGGLE_ID,
|
|
74
75
|
} from './contract.js';
|
|
75
76
|
import { PAGE_KIND_ATTRIBUTE, PAGE_KIND_VALUES } from '../page-kinds.js';
|
|
76
77
|
import { SLOT_NAMES } from '../skeleton.js';
|
|
@@ -79,9 +80,9 @@ import { canonicalizeJcs, domainDigest } from '../canonical-jcs.js';
|
|
|
79
80
|
/** @type {string} */
|
|
80
81
|
export const TEMPLATE_STYLING_CONTRACT_PACKAGE = '@rathnasgala2/template';
|
|
81
82
|
/** @type {string} */
|
|
82
|
-
export const TEMPLATE_STYLING_CONTRACT_VERSION = '
|
|
83
|
+
export const TEMPLATE_STYLING_CONTRACT_VERSION = '3.0.0';
|
|
83
84
|
/** @type {string} */
|
|
84
|
-
export const TEMPLATE_STYLING_CONTRACT_TEMPLATE_VERSION = '
|
|
85
|
+
export const TEMPLATE_STYLING_CONTRACT_TEMPLATE_VERSION = '3.0.0';
|
|
85
86
|
|
|
86
87
|
/** @type {string} the `main-content` landmark's fixed `id`. */
|
|
87
88
|
const MAIN_CONTENT_ID = 'main-content';
|
|
@@ -104,7 +105,7 @@ const MAIN_CONTENT_ID = 'main-content';
|
|
|
104
105
|
* `hover`.
|
|
105
106
|
* - `visited`: the only way to consume the closed token catalog's own
|
|
106
107
|
* `color-link-visited`, which had no reachable application before this.
|
|
107
|
-
* - `disabled`: the appearance
|
|
108
|
+
* - `disabled`: the appearance toggle is a native button control, which
|
|
108
109
|
* may legitimately be disabled by a later module; themes need a way to
|
|
109
110
|
* style that state today so it is not a breaking addition later.
|
|
110
111
|
*
|
|
@@ -213,11 +214,13 @@ const TYPE_HOOKS = Object.freeze(
|
|
|
213
214
|
['prose-link', 'a'],
|
|
214
215
|
['media-image', 'img'],
|
|
215
216
|
['prose-divider', 'hr'],
|
|
216
|
-
['control-label', 'label'],
|
|
217
|
-
['control-select', 'select'],
|
|
218
|
-
['control-option', 'option'],
|
|
219
217
|
['prose-span', 'span'],
|
|
220
218
|
['prose-time', 'time'],
|
|
219
|
+
['control-button', 'button'],
|
|
220
|
+
['disclosure-details', 'details'],
|
|
221
|
+
['disclosure-summary', 'summary'],
|
|
222
|
+
['landmark-section', 'section'],
|
|
223
|
+
['media-figure', 'figure'],
|
|
221
224
|
].map(([hookId, tag]) =>
|
|
222
225
|
Object.freeze({
|
|
223
226
|
hookId,
|
|
@@ -228,13 +231,188 @@ const TYPE_HOOKS = Object.freeze(
|
|
|
228
231
|
);
|
|
229
232
|
|
|
230
233
|
/**
|
|
231
|
-
*
|
|
232
|
-
*
|
|
234
|
+
* The most public hooks a theme contract may declare
|
|
235
|
+
* (`theme-contract.slotHooks.maxItems`, raised from 64 to 256 in contract
|
|
236
|
+
* 3.0.0).
|
|
237
|
+
*
|
|
238
|
+
* @type {number}
|
|
239
|
+
*/
|
|
240
|
+
export const MAX_PUBLIC_HOOKS = 256;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Contract 3.0.0: every `g-*` component class the template's own markup
|
|
244
|
+
* (`internal/chrome.js`, `internal/components.js`, `internal/page-kinds.js`
|
|
245
|
+
* and `internal/skeleton.js`) emits, grouped by component. The naming
|
|
246
|
+
* convention is the approved design reference's: `g-<component>` for the
|
|
247
|
+
* block and `g-<component>-<part>` for its parts; a variant adds a second
|
|
248
|
+
* class (`g-card g-card-row`). One hook per class, published as
|
|
249
|
+
* `ui-<name>`; `test/theme-styling-contract.test.js` fails if a rendered
|
|
250
|
+
* `g-*` class is missing here, or a class listed here is never rendered.
|
|
251
|
+
* Pure-layout needs a component can meet with an element/descendant
|
|
252
|
+
* selector (`.g-card h3`) deliberately get no class of their own, to stay
|
|
253
|
+
* under the 256-hook cap.
|
|
254
|
+
*
|
|
255
|
+
* @type {Readonly<Record<string, readonly string[]>>}
|
|
256
|
+
*/
|
|
257
|
+
export const COMPONENT_CLASS_GROUPS = Object.freeze({
|
|
258
|
+
shared: [
|
|
259
|
+
'g-wrap',
|
|
260
|
+
'g-main',
|
|
261
|
+
'g-skip',
|
|
262
|
+
'g-sr',
|
|
263
|
+
'g-icon',
|
|
264
|
+
'g-dot',
|
|
265
|
+
'g-label',
|
|
266
|
+
'g-dek',
|
|
267
|
+
'g-btn',
|
|
268
|
+
'g-icon-btn',
|
|
269
|
+
'g-more',
|
|
270
|
+
'g-chip',
|
|
271
|
+
'g-badge',
|
|
272
|
+
'g-eyebrow',
|
|
273
|
+
'g-meta',
|
|
274
|
+
'g-avatar',
|
|
275
|
+
'g-byline',
|
|
276
|
+
'g-byline-text',
|
|
277
|
+
'g-section',
|
|
278
|
+
'g-section-head',
|
|
279
|
+
'g-grid',
|
|
280
|
+
'g-list',
|
|
281
|
+
'g-pills',
|
|
282
|
+
'g-pill',
|
|
283
|
+
'g-pill-count',
|
|
284
|
+
'g-crumbs',
|
|
285
|
+
'g-pagination',
|
|
286
|
+
'g-toast',
|
|
287
|
+
],
|
|
288
|
+
header: [
|
|
289
|
+
'g-header',
|
|
290
|
+
'g-header-row',
|
|
291
|
+
'g-brand',
|
|
292
|
+
'g-mark',
|
|
293
|
+
'g-brand-text',
|
|
294
|
+
'g-brand-name',
|
|
295
|
+
'g-brand-tag',
|
|
296
|
+
'g-nav',
|
|
297
|
+
'g-actions',
|
|
298
|
+
'g-search-btn',
|
|
299
|
+
'g-menu',
|
|
300
|
+
'g-mode-icon',
|
|
301
|
+
'g-mode-system',
|
|
302
|
+
'g-mode-light',
|
|
303
|
+
'g-mode-dark',
|
|
304
|
+
],
|
|
305
|
+
footer: [
|
|
306
|
+
'g-footer',
|
|
307
|
+
'g-footer-grid',
|
|
308
|
+
'g-footer-brand',
|
|
309
|
+
'g-footer-about',
|
|
310
|
+
'g-footer-col',
|
|
311
|
+
'g-footer-base',
|
|
312
|
+
'g-social',
|
|
313
|
+
],
|
|
314
|
+
card: [
|
|
315
|
+
'g-card',
|
|
316
|
+
'g-card-row',
|
|
317
|
+
'g-card-link',
|
|
318
|
+
'g-card-media',
|
|
319
|
+
'g-card-placeholder',
|
|
320
|
+
'g-card-body',
|
|
321
|
+
'g-card-top',
|
|
322
|
+
'g-card-excerpt',
|
|
323
|
+
'g-card-foot',
|
|
324
|
+
'g-series-tag',
|
|
325
|
+
],
|
|
326
|
+
home: [
|
|
327
|
+
'g-hero',
|
|
328
|
+
'g-hero-media',
|
|
329
|
+
'g-hero-body',
|
|
330
|
+
'g-topic-strip',
|
|
331
|
+
'g-series-card',
|
|
332
|
+
'g-series-list',
|
|
333
|
+
'g-series-n',
|
|
334
|
+
'g-newsletter',
|
|
335
|
+
'g-panel',
|
|
336
|
+
'g-panel-copy',
|
|
337
|
+
],
|
|
338
|
+
article: [
|
|
339
|
+
'g-article',
|
|
340
|
+
'g-progress',
|
|
341
|
+
'g-article-head',
|
|
342
|
+
'g-article-labels',
|
|
343
|
+
'g-article-meta',
|
|
344
|
+
'g-share',
|
|
345
|
+
'g-article-cover',
|
|
346
|
+
'g-article-grid',
|
|
347
|
+
'g-toc',
|
|
348
|
+
'g-toc-list',
|
|
349
|
+
'g-toc-mobile',
|
|
350
|
+
'g-prose',
|
|
351
|
+
'g-tags',
|
|
352
|
+
'g-tag',
|
|
353
|
+
'g-article-foot',
|
|
354
|
+
'g-author-card',
|
|
355
|
+
'g-author-name',
|
|
356
|
+
'g-author-bio',
|
|
357
|
+
'g-series-box',
|
|
358
|
+
'g-series-here',
|
|
359
|
+
'g-pager',
|
|
360
|
+
'g-pager-link',
|
|
361
|
+
'g-pager-next',
|
|
362
|
+
],
|
|
363
|
+
listing: ['g-topic-hero', 'g-topic-head', 'g-topic-icon'],
|
|
364
|
+
// Created at runtime by the optional site script (never server-rendered):
|
|
365
|
+
// the search dialog and its results, and the code block chrome. Published
|
|
366
|
+
// all the same so a theme can restyle them.
|
|
367
|
+
script: [
|
|
368
|
+
'g-kbd',
|
|
369
|
+
'g-search',
|
|
370
|
+
'g-search-box',
|
|
371
|
+
'g-search-input',
|
|
372
|
+
'g-search-results',
|
|
373
|
+
'g-search-hit',
|
|
374
|
+
'g-search-hit-text',
|
|
375
|
+
'g-search-hit-title',
|
|
376
|
+
'g-search-hit-meta',
|
|
377
|
+
'g-search-empty',
|
|
378
|
+
'g-codeblock',
|
|
379
|
+
'g-codebar',
|
|
380
|
+
'g-codebar-lang',
|
|
381
|
+
'g-copy',
|
|
382
|
+
'g-copy-idle',
|
|
383
|
+
'g-copy-done',
|
|
384
|
+
],
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* The classes only the site script creates (the `script` group of
|
|
389
|
+
* {@link COMPONENT_CLASS_GROUPS}); the drift test checks these against the
|
|
390
|
+
* script source instead of the server-rendered HTML.
|
|
391
|
+
*
|
|
392
|
+
* @type {readonly string[]}
|
|
393
|
+
*/
|
|
394
|
+
export const SCRIPT_CREATED_CLASSES = Object.freeze([
|
|
395
|
+
...COMPONENT_CLASS_GROUPS.script,
|
|
396
|
+
]);
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Every class-selector hook: the `g-*` component classes
|
|
400
|
+
* ({@link COMPONENT_CLASS_GROUPS}), the base Prism `.token` class and one
|
|
401
|
+
* `.language-<grammar>` hook per admitted highlight grammar (no
|
|
233
402
|
* per-token-kind class is public — see module documentation).
|
|
234
403
|
*
|
|
235
404
|
* @type {readonly StyleHookDefinition[]}
|
|
236
405
|
*/
|
|
237
406
|
const CLASS_HOOKS = Object.freeze([
|
|
407
|
+
...Object.values(COMPONENT_CLASS_GROUPS)
|
|
408
|
+
.flat()
|
|
409
|
+
.map((className) =>
|
|
410
|
+
Object.freeze({
|
|
411
|
+
hookId: `ui-${className.slice(2)}`,
|
|
412
|
+
selectorAtom: `.${className}`,
|
|
413
|
+
kind: /** @type {const} */ ('class'),
|
|
414
|
+
}),
|
|
415
|
+
),
|
|
238
416
|
Object.freeze({
|
|
239
417
|
hookId: 'code-token',
|
|
240
418
|
selectorAtom: '.token',
|
|
@@ -262,8 +440,8 @@ const ID_HOOKS = Object.freeze([
|
|
|
262
440
|
kind: /** @type {const} */ ('id'),
|
|
263
441
|
}),
|
|
264
442
|
Object.freeze({
|
|
265
|
-
hookId: 'control-appearance-
|
|
266
|
-
selectorAtom: `#${
|
|
443
|
+
hookId: 'control-appearance-toggle',
|
|
444
|
+
selectorAtom: `#${APPEARANCE_TOGGLE_ID}`,
|
|
267
445
|
kind: /** @type {const} */ ('id'),
|
|
268
446
|
}),
|
|
269
447
|
]);
|
|
@@ -450,7 +628,7 @@ export const TEMPLATE_STYLING_CONTRACT_INVALID =
|
|
|
450
628
|
* Structural self-validation of a `templateStylingContract` object against
|
|
451
629
|
* its closed shape (the checks this repository can run
|
|
452
630
|
* without a published `urn:gala:schema:template-styling-contract` schema
|
|
453
|
-
* entry — no such schema ID is registered in `@rathnasgala2/schemas@
|
|
631
|
+
* entry — no such schema ID is registered in `@rathnasgala2/schemas@3.0.0`;
|
|
454
632
|
* only `urn:gala:schema:theme-contract:2.0.0` is, which this module's sibling
|
|
455
633
|
* theme fixtures are validated against with the schema package's own
|
|
456
634
|
* exported `validateGalaDocument`, see `test/theme-styling-contract.test.js`).
|
|
@@ -470,7 +648,7 @@ export function assertTemplateStylingContractShape(contract) {
|
|
|
470
648
|
if (contract.profile !== 'gala-template-styling-contract-v2') {
|
|
471
649
|
fail('profile');
|
|
472
650
|
}
|
|
473
|
-
if (contract.contractVersion !== '
|
|
651
|
+
if (contract.contractVersion !== '3.0.0') fail('contractVersion');
|
|
474
652
|
if (contract.templatePackage !== '@rathnasgala2/template') {
|
|
475
653
|
fail('templatePackage');
|
|
476
654
|
}
|
|
@@ -585,7 +763,11 @@ export function assertTemplateStylingContractShape(contract) {
|
|
|
585
763
|
const hooks = /** @type {{hookId: string, selectorAtom: string}[]} */ (
|
|
586
764
|
contract.publicThemeSlotHooks
|
|
587
765
|
);
|
|
588
|
-
if (
|
|
766
|
+
if (
|
|
767
|
+
!Array.isArray(hooks) ||
|
|
768
|
+
hooks.length < 1 ||
|
|
769
|
+
hooks.length > MAX_PUBLIC_HOOKS
|
|
770
|
+
) {
|
|
589
771
|
fail('publicThemeSlotHooks length');
|
|
590
772
|
}
|
|
591
773
|
const seenAtoms = new Set();
|