@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.
- package/LICENSE +201 -0
- package/README.md +515 -0
- package/contracts/render-policy.jcs +1 -0
- package/contracts/theme-styling-contract.jcs +1 -0
- package/package.json +96 -0
- package/src/core/errors.js +136 -0
- package/src/core/index.js +758 -0
- package/src/core/internal/appearance/bootstrap-script.js +159 -0
- package/src/core/internal/appearance/contract.js +193 -0
- package/src/core/internal/appearance/controller-markup.js +80 -0
- package/src/core/internal/appearance/styling-contract.js +509 -0
- 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 +220 -0
- package/src/core/internal/feeds.js +161 -0
- package/src/core/internal/fs-walk.js +38 -0
- package/src/core/internal/media/avif-probe.js +169 -0
- package/src/core/internal/media/font-policy.js +56 -0
- package/src/core/internal/media/jpeg-codec.js +0 -0
- package/src/core/internal/media/limits.js +129 -0
- package/src/core/internal/media/pipeline.js +353 -0
- package/src/core/internal/media/png-codec.js +406 -0
- package/src/core/internal/media/resize.js +147 -0
- package/src/core/internal/media/sniff.js +109 -0
- package/src/core/internal/media/webp-probe.js +139 -0
- package/src/core/internal/messages.js +78 -0
- package/src/core/internal/page-kinds.js +709 -0
- package/src/core/internal/provenance.js +49 -0
- package/src/core/internal/render-policy-content.js +295 -0
- package/src/core/internal/route-labels.js +68 -0
- package/src/core/internal/route.js +167 -0
- package/src/core/internal/search-index.js +119 -0
- package/src/core/internal/seo.js +114 -0
- package/src/core/internal/sitemap.js +77 -0
- package/src/core/internal/skeleton.js +413 -0
- package/src/core/internal/source-inventory.js +145 -0
- package/src/core/internal/text-direction.js +59 -0
- package/src/core/internal/theme-assets.js +387 -0
- package/src/core/internal/uuid.js +51 -0
- package/src/core/manifest.js +0 -0
- package/types/eleventy-plugin-syntaxhighlight.d.ts +26 -0
- package/types/eleventy.d.ts +38 -0
- package/types/index.d.ts +347 -0
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `contracts/theme-styling-contract.jcs` builder and structural validator
|
|
3
|
+
* (task packet S2-T12; DEC-097 section 4 "Closed theme-package and
|
|
4
|
+
* stylesheet admission": the `templateStylingContract` closed object).
|
|
5
|
+
*
|
|
6
|
+
* This is the one reviewed source module `scripts/generate-contracts.mjs`
|
|
7
|
+
* reads from to emit the published contract file, and the one module
|
|
8
|
+
* `test/theme-styling-contract.test.js` reads from to drift-check the
|
|
9
|
+
* contract against what this renderer actually renders. Every catalog leaf
|
|
10
|
+
* below is drawn from (or, for the two new S2-T12 hooks documented inline,
|
|
11
|
+
* wired into) the actual rendering modules — `internal/skeleton.js`,
|
|
12
|
+
* `internal/page-kinds.js`, `internal/render-policy-content.js` and
|
|
13
|
+
* `internal/appearance/contract.js` — never a hand-typed literal
|
|
14
|
+
* independent of what those modules emit.
|
|
15
|
+
*
|
|
16
|
+
* Public-hook budget (DEC-097: `publicThemeSlotHooks:[1..64] set`): every
|
|
17
|
+
* catalog leaf below (28 type + 15 class + 2 id + 19 attribute-value) has
|
|
18
|
+
* exactly one corresponding public hook, for exactly 64 entries — this
|
|
19
|
+
* renderer's complete public theming surface for S2. A leaf with no
|
|
20
|
+
* matching hook would be a catalog member no theme could ever validly
|
|
21
|
+
* select (DEC-097: "a used-but-undeclared public slot hook rejects"), so
|
|
22
|
+
* catalogs and hooks are generated from one shared list rather than two
|
|
23
|
+
* independently maintained ones.
|
|
24
|
+
*
|
|
25
|
+
* Deliberately out of the S2 public hook surface (documented scope
|
|
26
|
+
* decisions, not omissions):
|
|
27
|
+
* - fine-grained Prism token-class hooks (`.keyword`, `.string`, ...): only
|
|
28
|
+
* the base `.token` class and one hook per admitted highlight grammar's
|
|
29
|
+
* `.language-<grammar>` class are public, so a theme can style a
|
|
30
|
+
* highlighted code block's overall presentation but not recolor
|
|
31
|
+
* individual token kinds in S2;
|
|
32
|
+
* - `html`/`body` type selectors and interactive pseudo-classes
|
|
33
|
+
* (`:hover`, `:focus`, `:visited`, ...): themes reach the document root
|
|
34
|
+
* only through {@link PUBLICATION_ROOT_SELECTOR}/
|
|
35
|
+
* {@link RESOLVED_PALETTE_SELECTORS}, and no pseudo-class hook is
|
|
36
|
+
* published in S2 (an empty `pseudoClasses` catalog is schema-valid: `[0..64]`).
|
|
37
|
+
*
|
|
38
|
+
* Every tag in `internal/render-policy-content.js`'s `ALLOWED_TAGS` that
|
|
39
|
+
* markdown-it's own CommonMark grammar (with `html:false`) never actually
|
|
40
|
+
* produces — `b`, `i`, `u`, `s`, `del`, `ins`, `sub`, `sup`, `mark`, `small`,
|
|
41
|
+
* `wbr`, `table`, `thead`, `tbody`, `tr`, `th`, `td`, `dl`, `dt`, `dd`,
|
|
42
|
+
* `figure`, `figcaption` — is likewise excluded from `typeSelectors`: this
|
|
43
|
+
* catalog is "every hook the template actually renders" (task packet
|
|
44
|
+
* S2-T12), not the sanitizer's own defence-in-depth superset.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import { HIGHLIGHT_GRAMMARS } from '../render-policy-content.js';
|
|
48
|
+
import {
|
|
49
|
+
APPEARANCE_RESOLVED_MODE_ATTRIBUTE,
|
|
50
|
+
APPEARANCE_ROOT_ATTRIBUTE,
|
|
51
|
+
APPEARANCE_SELECT_ID,
|
|
52
|
+
} from './contract.js';
|
|
53
|
+
import { PAGE_KIND_ATTRIBUTE, PAGE_KIND_VALUES } from '../page-kinds.js';
|
|
54
|
+
import { SLOT_NAMES } from '../skeleton.js';
|
|
55
|
+
import { canonicalizeJcs, domainDigest } from '../canonical-jcs.js';
|
|
56
|
+
|
|
57
|
+
/** @type {string} */
|
|
58
|
+
export const TEMPLATE_STYLING_CONTRACT_PACKAGE = '@rathnasgala2/template';
|
|
59
|
+
/** @type {string} */
|
|
60
|
+
export const TEMPLATE_STYLING_CONTRACT_VERSION = '2.0.0';
|
|
61
|
+
/** @type {string} */
|
|
62
|
+
export const TEMPLATE_STYLING_CONTRACT_TEMPLATE_VERSION = '2.0.0';
|
|
63
|
+
|
|
64
|
+
/** @type {string} the `main-content` landmark's fixed `id`. */
|
|
65
|
+
const MAIN_CONTENT_ID = 'main-content';
|
|
66
|
+
|
|
67
|
+
/** @type {readonly string[]} the ordered `@layer` names DEC-097 fixes. */
|
|
68
|
+
export const ORDERED_LAYERS = Object.freeze([
|
|
69
|
+
'gala-tokens',
|
|
70
|
+
'gala-components',
|
|
71
|
+
'gala-utilities',
|
|
72
|
+
'gala-print',
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
/** @type {string} */
|
|
76
|
+
export const PUBLICATION_ROOT_SELECTOR = '[data-gala-publication-root]';
|
|
77
|
+
|
|
78
|
+
/** @type {{light: string, dark: string}} */
|
|
79
|
+
export const RESOLVED_PALETTE_SELECTORS = Object.freeze({
|
|
80
|
+
light: `[${APPEARANCE_ROOT_ATTRIBUTE}][${APPEARANCE_RESOLVED_MODE_ATTRIBUTE}="light"]`,
|
|
81
|
+
dark: `[${APPEARANCE_ROOT_ATTRIBUTE}][${APPEARANCE_RESOLVED_MODE_ATTRIBUTE}="dark"]`,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @typedef {object} StyleHookDefinition
|
|
86
|
+
* @property {string} hookId
|
|
87
|
+
* @property {string} selectorAtom
|
|
88
|
+
* @property {'type' | 'class' | 'id' | 'attribute'} kind
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Every type-selector hook: one per HTML element this renderer's own
|
|
93
|
+
* `internal/skeleton.js`/`internal/page-kinds.js` chrome, or its
|
|
94
|
+
* `internal/content-security.js` markdown-it pipeline (`html:false`,
|
|
95
|
+
* CommonMark grammar only), ever actually emits.
|
|
96
|
+
*
|
|
97
|
+
* @type {readonly StyleHookDefinition[]}
|
|
98
|
+
*/
|
|
99
|
+
const TYPE_HOOKS = Object.freeze(
|
|
100
|
+
[
|
|
101
|
+
['landmark-header', 'header'],
|
|
102
|
+
['landmark-navigation', 'nav'],
|
|
103
|
+
['landmark-main', 'main'],
|
|
104
|
+
['landmark-footer', 'footer'],
|
|
105
|
+
['content-article', 'article'],
|
|
106
|
+
['heading-1', 'h1'],
|
|
107
|
+
['heading-2', 'h2'],
|
|
108
|
+
['heading-3', 'h3'],
|
|
109
|
+
['heading-4', 'h4'],
|
|
110
|
+
['heading-5', 'h5'],
|
|
111
|
+
['heading-6', 'h6'],
|
|
112
|
+
['prose-paragraph', 'p'],
|
|
113
|
+
['prose-list-unordered', 'ul'],
|
|
114
|
+
['prose-list-ordered', 'ol'],
|
|
115
|
+
['prose-list-item', 'li'],
|
|
116
|
+
['prose-blockquote', 'blockquote'],
|
|
117
|
+
['code-block', 'pre'],
|
|
118
|
+
['code-inline', 'code'],
|
|
119
|
+
['prose-strong', 'strong'],
|
|
120
|
+
['prose-emphasis', 'em'],
|
|
121
|
+
['prose-link', 'a'],
|
|
122
|
+
['media-image', 'img'],
|
|
123
|
+
['prose-divider', 'hr'],
|
|
124
|
+
['control-label', 'label'],
|
|
125
|
+
['control-select', 'select'],
|
|
126
|
+
['control-option', 'option'],
|
|
127
|
+
['prose-span', 'span'],
|
|
128
|
+
['prose-time', 'time'],
|
|
129
|
+
].map(([hookId, tag]) =>
|
|
130
|
+
Object.freeze({
|
|
131
|
+
hookId,
|
|
132
|
+
selectorAtom: tag,
|
|
133
|
+
kind: /** @type {const} */ ('type'),
|
|
134
|
+
}),
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Every class-selector hook: the base Prism `.token` class plus one
|
|
140
|
+
* `.language-<grammar>` hook per admitted highlight grammar (S2 scope: no
|
|
141
|
+
* per-token-kind class is public — see module documentation).
|
|
142
|
+
*
|
|
143
|
+
* @type {readonly StyleHookDefinition[]}
|
|
144
|
+
*/
|
|
145
|
+
const CLASS_HOOKS = Object.freeze([
|
|
146
|
+
Object.freeze({
|
|
147
|
+
hookId: 'code-token',
|
|
148
|
+
selectorAtom: '.token',
|
|
149
|
+
kind: /** @type {const} */ ('class'),
|
|
150
|
+
}),
|
|
151
|
+
...HIGHLIGHT_GRAMMARS.map((grammar) =>
|
|
152
|
+
Object.freeze({
|
|
153
|
+
hookId: `code-language-${grammar}`,
|
|
154
|
+
selectorAtom: `.language-${grammar}`,
|
|
155
|
+
kind: /** @type {const} */ ('class'),
|
|
156
|
+
}),
|
|
157
|
+
),
|
|
158
|
+
]);
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Every id-selector hook: the `<main>` landmark's fixed `id` and the
|
|
162
|
+
* appearance control's fixed `<select id>`.
|
|
163
|
+
*
|
|
164
|
+
* @type {readonly StyleHookDefinition[]}
|
|
165
|
+
*/
|
|
166
|
+
const ID_HOOKS = Object.freeze([
|
|
167
|
+
Object.freeze({
|
|
168
|
+
hookId: 'landmark-main-content',
|
|
169
|
+
selectorAtom: `#${MAIN_CONTENT_ID}`,
|
|
170
|
+
kind: /** @type {const} */ ('id'),
|
|
171
|
+
}),
|
|
172
|
+
Object.freeze({
|
|
173
|
+
hookId: 'control-appearance-select',
|
|
174
|
+
selectorAtom: `#${APPEARANCE_SELECT_ID}`,
|
|
175
|
+
kind: /** @type {const} */ ('id'),
|
|
176
|
+
}),
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Every attribute-value hook: one per `data-gala-slot` value
|
|
181
|
+
* ({@link SLOT_NAMES}) and one per `data-gala-page-kind` value
|
|
182
|
+
* ({@link PAGE_KIND_VALUES}).
|
|
183
|
+
*
|
|
184
|
+
* @type {readonly StyleHookDefinition[]}
|
|
185
|
+
*/
|
|
186
|
+
const ATTRIBUTE_VALUE_HOOKS = Object.freeze([
|
|
187
|
+
...SLOT_NAMES.map((slotName) =>
|
|
188
|
+
Object.freeze({
|
|
189
|
+
hookId: `slot-${slotName}`,
|
|
190
|
+
selectorAtom: `[data-gala-slot="${slotName}"]`,
|
|
191
|
+
kind: /** @type {const} */ ('attribute'),
|
|
192
|
+
}),
|
|
193
|
+
),
|
|
194
|
+
...PAGE_KIND_VALUES.map((pageKind) =>
|
|
195
|
+
Object.freeze({
|
|
196
|
+
hookId: `page-${pageKind}`,
|
|
197
|
+
selectorAtom: `[${PAGE_KIND_ATTRIBUTE}="${pageKind}"]`,
|
|
198
|
+
kind: /** @type {const} */ ('attribute'),
|
|
199
|
+
}),
|
|
200
|
+
),
|
|
201
|
+
]);
|
|
202
|
+
|
|
203
|
+
/** @type {readonly StyleHookDefinition[]} every public hook, unsorted. */
|
|
204
|
+
const ALL_HOOKS = Object.freeze([
|
|
205
|
+
...TYPE_HOOKS,
|
|
206
|
+
...CLASS_HOOKS,
|
|
207
|
+
...ID_HOOKS,
|
|
208
|
+
...ATTRIBUTE_VALUE_HOOKS,
|
|
209
|
+
]);
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* @param {readonly string[]} values
|
|
213
|
+
* @returns {string[]} a new, UTF-8-byte-sorted, duplicate-free copy
|
|
214
|
+
*/
|
|
215
|
+
function sortedUniqueBytes(values) {
|
|
216
|
+
return [...new Set(values)].sort((a, b) => {
|
|
217
|
+
const bufferA = Buffer.from(a, 'utf8');
|
|
218
|
+
const bufferB = Buffer.from(b, 'utf8');
|
|
219
|
+
return Buffer.compare(bufferA, bufferB);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Build the complete `templateStylingContract` object, including
|
|
225
|
+
* `catalogDigest` (DEC-097 section 8:
|
|
226
|
+
* `SHA256(UTF8("GALA-TEMPLATE-STYLING-CONTRACT-V2\0") || JCS(the catalog
|
|
227
|
+
* with catalogDigest omitted))`).
|
|
228
|
+
*
|
|
229
|
+
* @returns {Record<string, unknown>} the complete, digested contract object
|
|
230
|
+
*/
|
|
231
|
+
export function buildTemplateStylingContract() {
|
|
232
|
+
const attributesRows = [
|
|
233
|
+
{
|
|
234
|
+
name: APPEARANCE_ROOT_ATTRIBUTE,
|
|
235
|
+
match: 'presence',
|
|
236
|
+
values: [],
|
|
237
|
+
role: 'root',
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: APPEARANCE_RESOLVED_MODE_ATTRIBUTE,
|
|
241
|
+
match: 'exact-value',
|
|
242
|
+
values: sortedUniqueBytes(['dark', 'light']),
|
|
243
|
+
role: 'state',
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: 'data-gala-slot',
|
|
247
|
+
match: 'exact-value',
|
|
248
|
+
values: sortedUniqueBytes(SLOT_NAMES),
|
|
249
|
+
role: 'semantic',
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
name: PAGE_KIND_ATTRIBUTE,
|
|
253
|
+
match: 'exact-value',
|
|
254
|
+
values: sortedUniqueBytes(PAGE_KIND_VALUES),
|
|
255
|
+
role: 'semantic',
|
|
256
|
+
},
|
|
257
|
+
].sort((a, b) => {
|
|
258
|
+
const bufferA = Buffer.from(canonicalizeJcs(a), 'utf8');
|
|
259
|
+
const bufferB = Buffer.from(canonicalizeJcs(b), 'utf8');
|
|
260
|
+
return Buffer.compare(bufferA, bufferB);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const publicThemeSlotHooks = ALL_HOOKS.map(({ hookId, selectorAtom }) => ({
|
|
264
|
+
hookId,
|
|
265
|
+
selectorAtom,
|
|
266
|
+
})).sort((a, b) => {
|
|
267
|
+
const bufferA = Buffer.from(a.hookId, 'utf8');
|
|
268
|
+
const bufferB = Buffer.from(b.hookId, 'utf8');
|
|
269
|
+
return Buffer.compare(bufferA, bufferB);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
/** @type {Record<string, unknown>} */
|
|
273
|
+
const withoutDigest = {
|
|
274
|
+
profile: 'gala-template-styling-contract-v2',
|
|
275
|
+
contractVersion: TEMPLATE_STYLING_CONTRACT_VERSION,
|
|
276
|
+
templatePackage: TEMPLATE_STYLING_CONTRACT_PACKAGE,
|
|
277
|
+
templateVersion: TEMPLATE_STYLING_CONTRACT_TEMPLATE_VERSION,
|
|
278
|
+
orderedLayers: [...ORDERED_LAYERS],
|
|
279
|
+
stylesheetLayers: {
|
|
280
|
+
tokens: 'gala-tokens',
|
|
281
|
+
components: 'gala-components',
|
|
282
|
+
utilities: 'gala-utilities',
|
|
283
|
+
print: 'gala-print',
|
|
284
|
+
},
|
|
285
|
+
publicationRootSelector: PUBLICATION_ROOT_SELECTOR,
|
|
286
|
+
resolvedPaletteSelectors: { ...RESOLVED_PALETTE_SELECTORS },
|
|
287
|
+
typeSelectors: sortedUniqueBytes(
|
|
288
|
+
TYPE_HOOKS.map((hook) => hook.selectorAtom),
|
|
289
|
+
),
|
|
290
|
+
classSelectors: sortedUniqueBytes(
|
|
291
|
+
CLASS_HOOKS.map((hook) => hook.selectorAtom.replace(/^\./, '')),
|
|
292
|
+
),
|
|
293
|
+
idSelectors: sortedUniqueBytes(
|
|
294
|
+
ID_HOOKS.map((hook) => hook.selectorAtom.replace(/^#/, '')),
|
|
295
|
+
),
|
|
296
|
+
attributes: attributesRows,
|
|
297
|
+
pseudoClasses: [],
|
|
298
|
+
pseudoElements: ['after', 'before', 'marker', 'selection'],
|
|
299
|
+
functionalPseudos: ['is', 'where', 'not', 'nth-child', 'nth-last-child'],
|
|
300
|
+
combinators: [' ', ' > ', ' + ', ' ~ '],
|
|
301
|
+
publicThemeSlotHooks,
|
|
302
|
+
composition: {
|
|
303
|
+
rootScope: 'first-compound-required',
|
|
304
|
+
compoundOrder: [
|
|
305
|
+
'type',
|
|
306
|
+
'id',
|
|
307
|
+
'class',
|
|
308
|
+
'attribute',
|
|
309
|
+
'pseudo-class',
|
|
310
|
+
'pseudo-element',
|
|
311
|
+
],
|
|
312
|
+
functionalSelectorArguments: 'compound-only',
|
|
313
|
+
maximumFunctionalDepth: 1,
|
|
314
|
+
nthExpressionProfile: 'gala-positive-an-plus-b-v2',
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const catalogDigest = domainDigest(
|
|
319
|
+
'GALA-TEMPLATE-STYLING-CONTRACT-V2\0',
|
|
320
|
+
withoutDigest,
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
return { ...withoutDigest, catalogDigest };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Every distinct catalog leaf this contract admits, independent of hooks
|
|
328
|
+
* (used by the drift gate to cross-check rendered HTML).
|
|
329
|
+
*
|
|
330
|
+
* @returns {{types: string[], classes: string[], ids: string[]}}
|
|
331
|
+
*/
|
|
332
|
+
export function templateStylingContractLeaves() {
|
|
333
|
+
return {
|
|
334
|
+
types: TYPE_HOOKS.map((hook) => hook.selectorAtom),
|
|
335
|
+
classes: CLASS_HOOKS.map((hook) => hook.selectorAtom.replace(/^\./, '')),
|
|
336
|
+
ids: ID_HOOKS.map((hook) => hook.selectorAtom.replace(/^#/, '')),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** @returns {readonly StyleHookDefinition[]} every public hook definition. */
|
|
341
|
+
export function templateStylingContractHooks() {
|
|
342
|
+
return ALL_HOOKS;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* A thrown error's message prefix for every structural violation
|
|
347
|
+
* {@link assertTemplateStylingContractShape} detects.
|
|
348
|
+
*
|
|
349
|
+
* @type {string}
|
|
350
|
+
*/
|
|
351
|
+
export const TEMPLATE_STYLING_CONTRACT_INVALID =
|
|
352
|
+
'TEMPLATE_STYLING_CONTRACT_INVALID';
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Structural self-validation of a `templateStylingContract` object against
|
|
356
|
+
* DEC-097 section 4's closed shape (the checks this repository can run
|
|
357
|
+
* without a published `urn:gala:schema:template-styling-contract` schema
|
|
358
|
+
* entry — no such schema ID is registered in `@rathnasgala2/schemas@2.8.0`;
|
|
359
|
+
* only `urn:gala:schema:theme-contract:2.0.0` is, which this module's sibling
|
|
360
|
+
* theme fixtures are validated against with the schema package's own
|
|
361
|
+
* exported `validateGalaDocument`, see `test/theme-styling-contract.test.js`).
|
|
362
|
+
* Fails closed with a descriptive `Error` on the first violation found.
|
|
363
|
+
*
|
|
364
|
+
* @param {Record<string, unknown>} contract a candidate contract object
|
|
365
|
+
* @returns {void}
|
|
366
|
+
*/
|
|
367
|
+
export function assertTemplateStylingContractShape(contract) {
|
|
368
|
+
/**
|
|
369
|
+
* @param {string} reason a short violation description
|
|
370
|
+
* @returns {never}
|
|
371
|
+
*/
|
|
372
|
+
const fail = (reason) => {
|
|
373
|
+
throw new Error(`${TEMPLATE_STYLING_CONTRACT_INVALID}: ${reason}`);
|
|
374
|
+
};
|
|
375
|
+
if (contract.profile !== 'gala-template-styling-contract-v2') {
|
|
376
|
+
fail('profile');
|
|
377
|
+
}
|
|
378
|
+
if (contract.contractVersion !== '2.0.0') fail('contractVersion');
|
|
379
|
+
if (contract.templatePackage !== '@rathnasgala2/template') {
|
|
380
|
+
fail('templatePackage');
|
|
381
|
+
}
|
|
382
|
+
if (typeof contract.templateVersion !== 'string') fail('templateVersion');
|
|
383
|
+
if (
|
|
384
|
+
!Array.isArray(contract.orderedLayers) ||
|
|
385
|
+
JSON.stringify(contract.orderedLayers) !== JSON.stringify(ORDERED_LAYERS)
|
|
386
|
+
) {
|
|
387
|
+
fail('orderedLayers');
|
|
388
|
+
}
|
|
389
|
+
const layers = /** @type {Record<string, unknown>} */ (
|
|
390
|
+
contract.stylesheetLayers
|
|
391
|
+
);
|
|
392
|
+
if (
|
|
393
|
+
!layers ||
|
|
394
|
+
layers.tokens !== 'gala-tokens' ||
|
|
395
|
+
layers.components !== 'gala-components' ||
|
|
396
|
+
layers.utilities !== 'gala-utilities' ||
|
|
397
|
+
layers.print !== 'gala-print'
|
|
398
|
+
) {
|
|
399
|
+
fail('stylesheetLayers');
|
|
400
|
+
}
|
|
401
|
+
if (contract.publicationRootSelector !== PUBLICATION_ROOT_SELECTOR) {
|
|
402
|
+
fail('publicationRootSelector');
|
|
403
|
+
}
|
|
404
|
+
const palettes = /** @type {Record<string, unknown>} */ (
|
|
405
|
+
contract.resolvedPaletteSelectors
|
|
406
|
+
);
|
|
407
|
+
if (
|
|
408
|
+
!palettes ||
|
|
409
|
+
palettes.light !== RESOLVED_PALETTE_SELECTORS.light ||
|
|
410
|
+
palettes.dark !== RESOLVED_PALETTE_SELECTORS.dark
|
|
411
|
+
) {
|
|
412
|
+
fail('resolvedPaletteSelectors');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const attributes = /** @type {Record<string, unknown>[]} */ (
|
|
416
|
+
contract.attributes
|
|
417
|
+
);
|
|
418
|
+
if (!Array.isArray(attributes) || attributes.length < 2) {
|
|
419
|
+
fail('attributes');
|
|
420
|
+
}
|
|
421
|
+
const rootRows = attributes.filter(
|
|
422
|
+
(row) =>
|
|
423
|
+
row.name === APPEARANCE_ROOT_ATTRIBUTE &&
|
|
424
|
+
row.match === 'presence' &&
|
|
425
|
+
row.role === 'root' &&
|
|
426
|
+
Array.isArray(row.values) &&
|
|
427
|
+
row.values.length === 0,
|
|
428
|
+
);
|
|
429
|
+
const paletteRows = attributes.filter(
|
|
430
|
+
(row) =>
|
|
431
|
+
row.name === APPEARANCE_RESOLVED_MODE_ATTRIBUTE &&
|
|
432
|
+
row.match === 'exact-value' &&
|
|
433
|
+
row.role === 'state' &&
|
|
434
|
+
Array.isArray(row.values) &&
|
|
435
|
+
JSON.stringify([...row.values].sort()) ===
|
|
436
|
+
JSON.stringify(['dark', 'light']),
|
|
437
|
+
);
|
|
438
|
+
if (rootRows.length !== 1) fail('root attribute row');
|
|
439
|
+
if (paletteRows.length !== 1) fail('palette attribute row');
|
|
440
|
+
|
|
441
|
+
if (
|
|
442
|
+
JSON.stringify(contract.pseudoElements) !==
|
|
443
|
+
JSON.stringify(['after', 'before', 'marker', 'selection'])
|
|
444
|
+
) {
|
|
445
|
+
fail('pseudoElements');
|
|
446
|
+
}
|
|
447
|
+
if (
|
|
448
|
+
JSON.stringify(contract.functionalPseudos) !==
|
|
449
|
+
JSON.stringify(['is', 'where', 'not', 'nth-child', 'nth-last-child'])
|
|
450
|
+
) {
|
|
451
|
+
fail('functionalPseudos');
|
|
452
|
+
}
|
|
453
|
+
if (
|
|
454
|
+
JSON.stringify(contract.combinators) !==
|
|
455
|
+
JSON.stringify([' ', ' > ', ' + ', ' ~ '])
|
|
456
|
+
) {
|
|
457
|
+
fail('combinators');
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const composition = /** @type {Record<string, unknown>} */ (
|
|
461
|
+
contract.composition
|
|
462
|
+
);
|
|
463
|
+
if (
|
|
464
|
+
!composition ||
|
|
465
|
+
composition.rootScope !== 'first-compound-required' ||
|
|
466
|
+
JSON.stringify(composition.compoundOrder) !==
|
|
467
|
+
JSON.stringify([
|
|
468
|
+
'type',
|
|
469
|
+
'id',
|
|
470
|
+
'class',
|
|
471
|
+
'attribute',
|
|
472
|
+
'pseudo-class',
|
|
473
|
+
'pseudo-element',
|
|
474
|
+
]) ||
|
|
475
|
+
composition.functionalSelectorArguments !== 'compound-only' ||
|
|
476
|
+
composition.maximumFunctionalDepth !== 1 ||
|
|
477
|
+
composition.nthExpressionProfile !== 'gala-positive-an-plus-b-v2'
|
|
478
|
+
) {
|
|
479
|
+
fail('composition');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const hooks = /** @type {{hookId: string, selectorAtom: string}[]} */ (
|
|
483
|
+
contract.publicThemeSlotHooks
|
|
484
|
+
);
|
|
485
|
+
if (!Array.isArray(hooks) || hooks.length < 1 || hooks.length > 64) {
|
|
486
|
+
fail('publicThemeSlotHooks length');
|
|
487
|
+
}
|
|
488
|
+
const seenAtoms = new Set();
|
|
489
|
+
const seenHookIds = new Set();
|
|
490
|
+
let previousHookId = '';
|
|
491
|
+
for (const hook of hooks) {
|
|
492
|
+
if (seenAtoms.has(hook.selectorAtom)) fail('duplicate hook selectorAtom');
|
|
493
|
+
if (seenHookIds.has(hook.hookId)) fail('duplicate hookId');
|
|
494
|
+
if (previousHookId !== '' && hook.hookId <= previousHookId) {
|
|
495
|
+
fail('publicThemeSlotHooks not sorted by hookId');
|
|
496
|
+
}
|
|
497
|
+
previousHookId = hook.hookId;
|
|
498
|
+
seenAtoms.add(hook.selectorAtom);
|
|
499
|
+
seenHookIds.add(hook.hookId);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const withoutDigest = { ...contract };
|
|
503
|
+
delete withoutDigest.catalogDigest;
|
|
504
|
+
const expectedDigest = domainDigest(
|
|
505
|
+
'GALA-TEMPLATE-STYLING-CONTRACT-V2\0',
|
|
506
|
+
withoutDigest,
|
|
507
|
+
);
|
|
508
|
+
if (contract.catalogDigest !== expectedDigest) fail('catalogDigest');
|
|
509
|
+
}
|
|
Binary file
|
|
Binary file
|