@rathnasgala2/template 3.2.0 → 4.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/README.md +17 -13
- package/contracts/theme-styling-contract.jcs +1 -1
- package/package.json +4 -2
- package/src/core/index.js +25 -17
- package/src/core/internal/appearance/base-layer.js +46 -4
- package/src/core/internal/appearance/styling-contract.js +24 -3
- package/src/core/internal/chrome.js +41 -13
- package/src/core/internal/eleventy-render.js +1 -1
- package/src/core/internal/page-kinds.js +131 -25
- package/src/core/internal/template-composition.js +185 -0
- package/src/core/internal/theme-assets.js +18 -3
- package/src/modules/interactions/browser/gala-interactions.js +74 -26
|
@@ -88,6 +88,10 @@ import { getMessages } from './messages.js';
|
|
|
88
88
|
import { resolveTextDirection } from './text-direction.js';
|
|
89
89
|
import { deriveAuthorSlugs, routeSegmentForLabel } from './route-labels.js';
|
|
90
90
|
import { derivePublicBasePath, joinPublicRoute } from './route.js';
|
|
91
|
+
import {
|
|
92
|
+
assertSupportedComposition,
|
|
93
|
+
EDITORIAL_COMPOSITION,
|
|
94
|
+
} from './template-composition.js';
|
|
91
95
|
|
|
92
96
|
/** Listing page size (index and per-year archive pages). Scoped decision:
|
|
93
97
|
* this renderer has no externally fixed page size; 10 is a deterministic,
|
|
@@ -383,11 +387,46 @@ export function homeDocumentTitle(name, tagline) {
|
|
|
383
387
|
* @param {import('../../../types/index.d.ts').InteractionsModule} [options.interactions]
|
|
384
388
|
* the validated interactions module; when set, every article page carries
|
|
385
389
|
* the interactions section and is interaction-bearing
|
|
390
|
+
* @param {unknown} [options.composition] the schema-validated structural
|
|
391
|
+
* composition selected by the theme; defaults to Editorial only for
|
|
392
|
+
* theme-less renderer use
|
|
386
393
|
* @returns {GeneratedPage[]} every generated page, in a stable, deterministic
|
|
387
394
|
* order
|
|
388
395
|
*/
|
|
389
396
|
export function buildGeneratedPages(validatedInput, options) {
|
|
390
397
|
const { publication, authors, content, basePath, baseUrl } = validatedInput;
|
|
398
|
+
const composition = assertSupportedComposition(
|
|
399
|
+
options.composition ?? EDITORIAL_COMPOSITION,
|
|
400
|
+
);
|
|
401
|
+
/**
|
|
402
|
+
* Preserve every content region while giving each article direction its
|
|
403
|
+
* own reading sequence and semantic container.
|
|
404
|
+
*
|
|
405
|
+
* @param {{head: string, cover: string, toc: string, prose: string, foot: string}} parts
|
|
406
|
+
* @returns {string}
|
|
407
|
+
*/
|
|
408
|
+
const articleStructure = ({ head, cover, toc, prose, foot }) => {
|
|
409
|
+
/**
|
|
410
|
+
* @param {string} contents rendered article regions
|
|
411
|
+
* @param {string} [element] semantic wrapper element
|
|
412
|
+
* @returns {string}
|
|
413
|
+
*/
|
|
414
|
+
const grid = (contents, element = 'div') =>
|
|
415
|
+
`<${element} class="g-wrap g-article-grid">${contents}</${element}>`;
|
|
416
|
+
if (composition.profile === 'newsletter') {
|
|
417
|
+
return `<section>${head}${cover}${grid(prose + toc, 'section')}${foot}</section>`;
|
|
418
|
+
}
|
|
419
|
+
if (composition.profile === 'visual') {
|
|
420
|
+
return `<div>${cover}${head}${grid(prose + toc)}</div>${foot}`;
|
|
421
|
+
}
|
|
422
|
+
if (composition.profile === 'personal') {
|
|
423
|
+
return `<section>${head}${cover}${grid(prose)}${toc ? grid(toc, 'section') : ''}${foot}</section>`;
|
|
424
|
+
}
|
|
425
|
+
if (composition.profile === 'developer') {
|
|
426
|
+
return `<div>${head}${cover}${grid(toc + prose, 'section')}${foot}</div>`;
|
|
427
|
+
}
|
|
428
|
+
return head + cover + grid(toc + prose) + foot;
|
|
429
|
+
};
|
|
391
430
|
const interactionsModule = interactionsActive(options.interactions)
|
|
392
431
|
? options.interactions
|
|
393
432
|
: undefined;
|
|
@@ -612,13 +651,32 @@ export function buildGeneratedPages(validatedInput, options) {
|
|
|
612
651
|
* @param {string} [afterHtml] markup after the list (pagination)
|
|
613
652
|
* @returns {string}
|
|
614
653
|
*/
|
|
615
|
-
const rowList = (items, afterHtml = '') =>
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
654
|
+
const rowList = (items, afterHtml = '') => {
|
|
655
|
+
const grid = composition.listingLayout === 'image-grid';
|
|
656
|
+
const issueCards = composition.listingLayout === 'issues';
|
|
657
|
+
const datedRows = composition.listingLayout === 'dated-rows';
|
|
658
|
+
const outer =
|
|
659
|
+
composition.profile === 'personal' || composition.profile === 'developer'
|
|
660
|
+
? 'div'
|
|
661
|
+
: 'section';
|
|
662
|
+
const contents =
|
|
663
|
+
items.length > 0
|
|
664
|
+
? datedRows
|
|
665
|
+
? `<div class="g-list g-dated-list">${items
|
|
666
|
+
.map(
|
|
667
|
+
(record) =>
|
|
668
|
+
`<div class="g-dated-row"><time class="g-date-stamp" datetime="${escapeHtml(record.frontmatter.publishedAt)}">` +
|
|
669
|
+
`${call('formatDate', record.frontmatter.publishedAt)}</time>${cardOf(record, 'row', 2)}</div>`,
|
|
670
|
+
)
|
|
671
|
+
.join('')}</div>`
|
|
672
|
+
: `<div class="${grid ? 'g-grid' : 'g-list'}">${items
|
|
673
|
+
.map((record) =>
|
|
674
|
+
cardOf(record, grid || issueCards ? 'grid' : 'row', 2),
|
|
675
|
+
)
|
|
676
|
+
.join('')}</div>`
|
|
677
|
+
: `<p>${text('emptyListingLabel')}</p>`;
|
|
678
|
+
return `<${outer} class="g-wrap g-section" data-gala-listing-layout="${composition.listingLayout}">${contents}${afterHtml}</${outer}>`;
|
|
679
|
+
};
|
|
622
680
|
|
|
623
681
|
/**
|
|
624
682
|
* One listing page's metadata: bare title, `<title>`, description,
|
|
@@ -710,6 +768,28 @@ export function buildGeneratedPages(validatedInput, options) {
|
|
|
710
768
|
? `<p class="g-dek">${escapeHtml(author.biography)}</p>`
|
|
711
769
|
: '') +
|
|
712
770
|
links;
|
|
771
|
+
const authorHead = listingHead({
|
|
772
|
+
breadcrumbHtml: crumbs(
|
|
773
|
+
[{ label: /** @type {string} */ (messages.authorsSectionLabel) }],
|
|
774
|
+
[{ label: author.displayName }],
|
|
775
|
+
),
|
|
776
|
+
kicker: /** @type {string} */ (messages.authorKickerLabel),
|
|
777
|
+
count: authored.length,
|
|
778
|
+
title: author.displayName,
|
|
779
|
+
descriptionHtml: intro,
|
|
780
|
+
markHtml: avatar(author),
|
|
781
|
+
});
|
|
782
|
+
const authoredList = rowList(authored);
|
|
783
|
+
const authorContents =
|
|
784
|
+
composition.profile === 'newsletter'
|
|
785
|
+
? `<section>${authorHead}${authoredList}</section>`
|
|
786
|
+
: composition.profile === 'visual'
|
|
787
|
+
? `<div>${authorHead}<section>${authoredList}</section></div>`
|
|
788
|
+
: composition.profile === 'personal'
|
|
789
|
+
? `<section>${authorHead}<div>${authoredList}</div></section>`
|
|
790
|
+
: composition.profile === 'developer'
|
|
791
|
+
? `<div>${authorHead}<div>${authoredList}</div></div>`
|
|
792
|
+
: authorHead + authoredList;
|
|
713
793
|
pages.push({
|
|
714
794
|
kind: 'author',
|
|
715
795
|
route: authorRoute(author),
|
|
@@ -737,17 +817,9 @@ export function buildGeneratedPages(validatedInput, options) {
|
|
|
737
817
|
}),
|
|
738
818
|
),
|
|
739
819
|
bodyHtml:
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
[{ label: author.displayName }],
|
|
744
|
-
),
|
|
745
|
-
kicker: /** @type {string} */ (messages.authorKickerLabel),
|
|
746
|
-
count: authored.length,
|
|
747
|
-
title: author.displayName,
|
|
748
|
-
descriptionHtml: intro,
|
|
749
|
-
markHtml: avatar(author),
|
|
750
|
-
}) + rowList(authored),
|
|
820
|
+
`<div data-gala-author-layout="${composition.authorLayout}">` +
|
|
821
|
+
authorContents +
|
|
822
|
+
`</div>`,
|
|
751
823
|
});
|
|
752
824
|
}
|
|
753
825
|
|
|
@@ -822,7 +894,7 @@ export function buildGeneratedPages(validatedInput, options) {
|
|
|
822
894
|
editionSelector +
|
|
823
895
|
`</div>`;
|
|
824
896
|
const cover = hero
|
|
825
|
-
? `<figure class="g-wrap g-article-cover">${renderImage({ image: hero, alt: heroMedia?.role === 'decorative' ? '' : (heroMedia?.alt ?? ''), priority: true })}</figure>`
|
|
897
|
+
? `<figure class="${composition.articleLayout === 'full-bleed' ? 'g-article-cover g-article-cover-full-bleed' : 'g-wrap g-article-cover'}">${renderImage({ image: hero, alt: heroMedia?.role === 'decorative' ? '' : (heroMedia?.alt ?? ''), priority: true })}</figure>`
|
|
826
898
|
: '';
|
|
827
899
|
const tocNav = contents
|
|
828
900
|
? `<nav class="g-toc" aria-label="${text('onThisPageLabel')}"><p class="g-label">${icon('list')}${text('onThisPageLabel')}</p>${contents}</nav>`
|
|
@@ -1001,8 +1073,8 @@ export function buildGeneratedPages(validatedInput, options) {
|
|
|
1001
1073
|
interactionBearing: Boolean(interactions),
|
|
1002
1074
|
lastModified: contentLastModified(frontmatter),
|
|
1003
1075
|
bodyHtml:
|
|
1004
|
-
`<article class="g-article">${isArticle || isEdition ? '<div class="g-progress" aria-hidden="true"></div>' : ''}
|
|
1005
|
-
|
|
1076
|
+
`<article class="g-article" data-gala-article-layout="${composition.articleLayout}">${isArticle || isEdition ? '<div class="g-progress" aria-hidden="true"></div>' : ''}` +
|
|
1077
|
+
`${articleStructure({ head, cover, toc: tocNav, prose, foot })}</article>` +
|
|
1006
1078
|
after +
|
|
1007
1079
|
(isArticle ? newsletterHtml : ''),
|
|
1008
1080
|
});
|
|
@@ -1093,15 +1165,49 @@ export function buildGeneratedPages(validatedInput, options) {
|
|
|
1093
1165
|
? `<section class="g-wrap g-section"><div class="g-section-head"><h2>${text('moreArticlesHeading')}</h2></div>` +
|
|
1094
1166
|
`<div class="g-list">${items.map((r) => cardOf(r, 'row')).join('')}</div>${pagination}</section>`
|
|
1095
1167
|
: '';
|
|
1096
|
-
const
|
|
1097
|
-
|
|
1168
|
+
const homeParts =
|
|
1169
|
+
composition.profile === 'newsletter'
|
|
1098
1170
|
? homeHeading +
|
|
1099
|
-
homeFeatured +
|
|
1100
1171
|
homeTags +
|
|
1172
|
+
homeFeatured +
|
|
1101
1173
|
homeLatest +
|
|
1102
|
-
homeSeries +
|
|
1103
1174
|
moreList +
|
|
1175
|
+
homeSeries +
|
|
1104
1176
|
newsletterHtml
|
|
1177
|
+
: composition.profile === 'visual'
|
|
1178
|
+
? homeHeading +
|
|
1179
|
+
homeFeatured +
|
|
1180
|
+
homeLatest +
|
|
1181
|
+
homeSeries +
|
|
1182
|
+
homeTags +
|
|
1183
|
+
moreList +
|
|
1184
|
+
newsletterHtml
|
|
1185
|
+
: composition.profile === 'personal'
|
|
1186
|
+
? homeHeading +
|
|
1187
|
+
homeLatest +
|
|
1188
|
+
homeFeatured +
|
|
1189
|
+
moreList +
|
|
1190
|
+
homeTags +
|
|
1191
|
+
homeSeries +
|
|
1192
|
+
newsletterHtml
|
|
1193
|
+
: composition.profile === 'developer'
|
|
1194
|
+
? homeHeading +
|
|
1195
|
+
homeTags +
|
|
1196
|
+
homeLatest +
|
|
1197
|
+
homeSeries +
|
|
1198
|
+
homeFeatured +
|
|
1199
|
+
moreList +
|
|
1200
|
+
newsletterHtml
|
|
1201
|
+
: homeHeading +
|
|
1202
|
+
homeFeatured +
|
|
1203
|
+
homeTags +
|
|
1204
|
+
homeLatest +
|
|
1205
|
+
homeSeries +
|
|
1206
|
+
moreList +
|
|
1207
|
+
newsletterHtml;
|
|
1208
|
+
const bodyHtml =
|
|
1209
|
+
pageNumber === 1
|
|
1210
|
+
? `<div data-gala-home-layout="${composition.homeLayout}">${homeParts}</div>`
|
|
1105
1211
|
: listingHead({
|
|
1106
1212
|
breadcrumbHtml: crumbs([
|
|
1107
1213
|
{ label: /** @type {string} */ (messages.indexHeading) },
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generation-4 template composition admission.
|
|
3
|
+
*
|
|
4
|
+
* A composition is one complete, immutable product direction. Admission is
|
|
5
|
+
* deliberately by exact object equality so fields from different directions
|
|
6
|
+
* cannot be combined into renderer states that were never designed or tested.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** The immutable generation-4 composition schema identity. */
|
|
10
|
+
export const TEMPLATE_COMPOSITION_SCHEMA_ID =
|
|
11
|
+
'urn:gala:schema:template-composition:2.0.0';
|
|
12
|
+
|
|
13
|
+
const SCHEMA_FIELDS = Object.freeze({
|
|
14
|
+
schemaId: TEMPLATE_COMPOSITION_SCHEMA_ID,
|
|
15
|
+
schemaVersion: '2.0.0',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/** The complete Editorial composition. */
|
|
19
|
+
export const EDITORIAL_COMPOSITION = Object.freeze({
|
|
20
|
+
...SCHEMA_FIELDS,
|
|
21
|
+
profile: 'editorial',
|
|
22
|
+
homeLayout: 'magazine',
|
|
23
|
+
articleLayout: 'editorial',
|
|
24
|
+
authorLayout: 'portrait',
|
|
25
|
+
listingLayout: 'rows',
|
|
26
|
+
headerLayout: 'masthead',
|
|
27
|
+
footerLayout: 'expanded',
|
|
28
|
+
mediaRatio: 'landscape',
|
|
29
|
+
density: 'comfortable',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/** The complete Newsletter composition. */
|
|
33
|
+
export const NEWSLETTER_COMPOSITION = Object.freeze({
|
|
34
|
+
...SCHEMA_FIELDS,
|
|
35
|
+
profile: 'newsletter',
|
|
36
|
+
homeLayout: 'newspaper',
|
|
37
|
+
articleLayout: 'issue',
|
|
38
|
+
authorLayout: 'profile',
|
|
39
|
+
listingLayout: 'issues',
|
|
40
|
+
headerLayout: 'publication',
|
|
41
|
+
footerLayout: 'expanded',
|
|
42
|
+
mediaRatio: 'landscape',
|
|
43
|
+
density: 'comfortable',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/** The complete Visual composition. */
|
|
47
|
+
export const VISUAL_COMPOSITION = Object.freeze({
|
|
48
|
+
...SCHEMA_FIELDS,
|
|
49
|
+
profile: 'visual',
|
|
50
|
+
homeLayout: 'gallery',
|
|
51
|
+
articleLayout: 'full-bleed',
|
|
52
|
+
authorLayout: 'portfolio',
|
|
53
|
+
listingLayout: 'image-grid',
|
|
54
|
+
headerLayout: 'minimal',
|
|
55
|
+
footerLayout: 'compact',
|
|
56
|
+
mediaRatio: 'mixed',
|
|
57
|
+
density: 'comfortable',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/** The complete Personal composition. */
|
|
61
|
+
export const PERSONAL_COMPOSITION = Object.freeze({
|
|
62
|
+
...SCHEMA_FIELDS,
|
|
63
|
+
profile: 'personal',
|
|
64
|
+
homeLayout: 'chronological',
|
|
65
|
+
articleLayout: 'prose',
|
|
66
|
+
authorLayout: 'biography',
|
|
67
|
+
listingLayout: 'dated-rows',
|
|
68
|
+
headerLayout: 'compact',
|
|
69
|
+
footerLayout: 'compact',
|
|
70
|
+
mediaRatio: 'landscape',
|
|
71
|
+
density: 'spacious',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/** The complete Developer composition. */
|
|
75
|
+
export const DEVELOPER_COMPOSITION = Object.freeze({
|
|
76
|
+
...SCHEMA_FIELDS,
|
|
77
|
+
profile: 'developer',
|
|
78
|
+
homeLayout: 'topic-index',
|
|
79
|
+
articleLayout: 'technical',
|
|
80
|
+
authorLayout: 'biography',
|
|
81
|
+
listingLayout: 'rows',
|
|
82
|
+
headerLayout: 'utility',
|
|
83
|
+
footerLayout: 'compact',
|
|
84
|
+
mediaRatio: 'landscape',
|
|
85
|
+
density: 'compact',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/** Every complete composition implemented by generation 4. */
|
|
89
|
+
export const SUPPORTED_COMPOSITIONS = Object.freeze([
|
|
90
|
+
EDITORIAL_COMPOSITION,
|
|
91
|
+
NEWSLETTER_COMPOSITION,
|
|
92
|
+
VISUAL_COMPOSITION,
|
|
93
|
+
PERSONAL_COMPOSITION,
|
|
94
|
+
DEVELOPER_COMPOSITION,
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
/** @type {Readonly<Record<string, (typeof SUPPORTED_COMPOSITIONS)[number]>>} */
|
|
98
|
+
const COMPOSITION_BY_PROFILE = Object.freeze(
|
|
99
|
+
Object.fromEntries(
|
|
100
|
+
SUPPORTED_COMPOSITIONS.map((composition) => [
|
|
101
|
+
composition.profile,
|
|
102
|
+
composition,
|
|
103
|
+
]),
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const COMPOSITION_ATTRIBUTE_NAMES = Object.freeze({
|
|
108
|
+
profile: 'data-gala-profile',
|
|
109
|
+
homeLayout: 'data-gala-home-layout',
|
|
110
|
+
articleLayout: 'data-gala-article-layout',
|
|
111
|
+
authorLayout: 'data-gala-author-layout',
|
|
112
|
+
listingLayout: 'data-gala-listing-layout',
|
|
113
|
+
headerLayout: 'data-gala-header-layout',
|
|
114
|
+
footerLayout: 'data-gala-footer-layout',
|
|
115
|
+
mediaRatio: 'data-gala-media-ratio',
|
|
116
|
+
density: 'data-gala-density',
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
/** @typedef {(typeof SUPPORTED_COMPOSITIONS)[number]} SupportedComposition */
|
|
120
|
+
/** @typedef {Exclude<keyof SupportedComposition, 'schemaId' | 'schemaVersion'>} CompositionField */
|
|
121
|
+
|
|
122
|
+
/** Public semantic attribute catalogs corresponding to composition fields. */
|
|
123
|
+
export const COMPOSITION_ATTRIBUTE_CATALOG = Object.freeze(
|
|
124
|
+
/** @type {readonly [CompositionField, string][]} */ (
|
|
125
|
+
Object.entries(COMPOSITION_ATTRIBUTE_NAMES)
|
|
126
|
+
).map(([field, name]) =>
|
|
127
|
+
Object.freeze({
|
|
128
|
+
field,
|
|
129
|
+
name,
|
|
130
|
+
values: Object.freeze([
|
|
131
|
+
...new Set(
|
|
132
|
+
SUPPORTED_COMPOSITIONS.map((composition) => composition[field]),
|
|
133
|
+
),
|
|
134
|
+
]),
|
|
135
|
+
}),
|
|
136
|
+
),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
/** Every public semantic attribute/value pair themes may select. */
|
|
140
|
+
export const COMPOSITION_ATTRIBUTES = Object.freeze(
|
|
141
|
+
COMPOSITION_ATTRIBUTE_CATALOG.flatMap(({ field, name, values }) =>
|
|
142
|
+
values.map((value) => Object.freeze({ field, name, value })),
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Admit exactly one complete composition implemented by this renderer.
|
|
148
|
+
*
|
|
149
|
+
* @param {unknown} candidate a complete template-composition document
|
|
150
|
+
* @returns {(typeof SUPPORTED_COMPOSITIONS)[number]} canonical composition
|
|
151
|
+
*/
|
|
152
|
+
export function assertSupportedComposition(candidate) {
|
|
153
|
+
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`composition must be a complete ${TEMPLATE_COMPOSITION_SCHEMA_ID} document`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
const composition = /** @type {Record<string, unknown>} */ (candidate);
|
|
159
|
+
const selected =
|
|
160
|
+
typeof composition.profile === 'string' &&
|
|
161
|
+
Object.hasOwn(COMPOSITION_BY_PROFILE, composition.profile)
|
|
162
|
+
? COMPOSITION_BY_PROFILE[composition.profile]
|
|
163
|
+
: undefined;
|
|
164
|
+
if (!selected) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`composition.profile (${JSON.stringify(composition.profile)}) is not implemented by this renderer`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
const expectedKeys = Object.keys(selected).sort();
|
|
170
|
+
const actualKeys = Object.keys(composition).sort();
|
|
171
|
+
if (JSON.stringify(actualKeys) !== JSON.stringify(expectedKeys)) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`composition must contain exactly ${expectedKeys.join(', ')}`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
for (const [key, expected] of Object.entries(selected)) {
|
|
177
|
+
if (composition[key] !== expected) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`composition.${key} (${JSON.stringify(composition[key])}) is not ` +
|
|
180
|
+
`implemented for ${selected.profile}; expected ${JSON.stringify(expected)}`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return selected;
|
|
185
|
+
}
|
|
@@ -85,8 +85,9 @@ import { sanitizeThemeSvg } from './media/theme-svg-sanitizer.js';
|
|
|
85
85
|
import { projectFixedAssetPath } from './route.js';
|
|
86
86
|
import { satisfiesTemplateRange } from './semver-range.js';
|
|
87
87
|
import { escapeHtml } from './skeleton.js';
|
|
88
|
+
import { assertSupportedComposition } from './template-composition.js';
|
|
88
89
|
|
|
89
|
-
/** @type {string} the `urn:gala:schema:theme-contract:2.0.0` schema id
|
|
90
|
+
/** @type {string} the immutable `urn:gala:schema:theme-contract:2.0.0` schema id
|
|
90
91
|
* every consumed `theme.json` is validated against (TPL-H1). */
|
|
91
92
|
const THEME_CONTRACT_SCHEMA_ID = 'urn:gala:schema:theme-contract:2.0.0';
|
|
92
93
|
|
|
@@ -415,7 +416,7 @@ function assertThemeContractIntegrity(themeJson) {
|
|
|
415
416
|
if (!validation.valid) {
|
|
416
417
|
throw new ThemeAssetError(
|
|
417
418
|
'THEME_CONTRACT_SCHEMA_INVALID',
|
|
418
|
-
`theme.json failed
|
|
419
|
+
`theme.json failed ${THEME_CONTRACT_SCHEMA_ID} validation ` +
|
|
419
420
|
`(${validation.diagnostics.length} diagnostic(s)): ` +
|
|
420
421
|
JSON.stringify(validation.diagnostics),
|
|
421
422
|
);
|
|
@@ -458,6 +459,14 @@ function assertThemeContractIntegrity(themeJson) {
|
|
|
458
459
|
`(${TEMPLATE_STYLING_CONTRACT_TEMPLATE_VERSION})`,
|
|
459
460
|
);
|
|
460
461
|
}
|
|
462
|
+
try {
|
|
463
|
+
assertSupportedComposition(themeJson.composition);
|
|
464
|
+
} catch (error) {
|
|
465
|
+
throw new ThemeAssetError(
|
|
466
|
+
'THEME_COMPOSITION_UNSUPPORTED',
|
|
467
|
+
/** @type {Error} */ (error).message,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
461
470
|
}
|
|
462
471
|
|
|
463
472
|
/**
|
|
@@ -490,6 +499,7 @@ function subresourceIntegrityAttribute(bytes) {
|
|
|
490
499
|
* files: {path: string, bytes: Buffer}[],
|
|
491
500
|
* assets: import('../../../types/index.d.ts').ManifestAssetEntry[],
|
|
492
501
|
* linkTagsHtml: string,
|
|
502
|
+
* composition: (typeof import('./template-composition.js').SUPPORTED_COMPOSITIONS)[number],
|
|
493
503
|
* }>} the files to write (already `basePath`-joined), their manifest rows
|
|
494
504
|
* (same joined paths), and the ready-to-insert `<link>` markup
|
|
495
505
|
*/
|
|
@@ -611,7 +621,12 @@ export async function loadThemeAssets({
|
|
|
611
621
|
});
|
|
612
622
|
}
|
|
613
623
|
|
|
614
|
-
return {
|
|
624
|
+
return {
|
|
625
|
+
files,
|
|
626
|
+
assets,
|
|
627
|
+
linkTagsHtml: linkTags.join(''),
|
|
628
|
+
composition: assertSupportedComposition(themeJson.composition),
|
|
629
|
+
};
|
|
615
630
|
}
|
|
616
631
|
|
|
617
632
|
/**
|