@rathnasgala2/template 2.0.0 → 2.2.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 +211 -137
- package/contracts/render-policy.jcs +1 -1
- package/contracts/theme-styling-contract.jcs +1 -1
- package/package.json +5 -6
- package/src/core/errors.js +13 -13
- package/src/core/index.js +130 -75
- 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 +15 -15
- 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 +50 -51
- 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 +58 -20
- package/src/core/internal/search-index.js +17 -16
- package/src/core/internal/semver-range.js +84 -0
- package/src/core/internal/seo.js +24 -24
- package/src/core/internal/sitemap.js +11 -12
- package/src/core/internal/skeleton.js +65 -46
- 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 +314 -68
- package/src/core/internal/uuid.js +4 -5
- package/src/core/manifest.js +0 -0
- package/types/index.d.ts +22 -8
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Core semantic HTML skeleton and navigation renderer
|
|
3
|
-
* brief S2 section 3 "Semantic skeleton and appearance controller").
|
|
2
|
+
* Core semantic HTML skeleton and navigation renderer.
|
|
4
3
|
*
|
|
5
4
|
* This module renders only inert, static, CSP-`script-src 'self'`-compatible
|
|
6
5
|
* markup: no inline `on*` attribute, no inline `<script>`, no `javascript:`
|
|
7
6
|
* URL. Every user-visible chrome string it emits comes from
|
|
8
7
|
* `internal/messages.js`'s catalog, never an inline literal, so localization
|
|
9
|
-
* can find every string in one place
|
|
8
|
+
* can find every string in one place.
|
|
10
9
|
*
|
|
11
10
|
* Landmark contract every page kind in `internal/page-kinds.js` composes
|
|
12
11
|
* through {@link renderPageBody}:
|
|
@@ -25,43 +24,43 @@
|
|
|
25
24
|
* `conversation`, `newsletter`, `edition-selector`).
|
|
26
25
|
*
|
|
27
26
|
* Core owns nine versioned semantic slots plus the collapsed
|
|
28
|
-
* `article-footer-ad` slot
|
|
27
|
+
* `article-footer-ad` slot: `header-actions`, `article-preamble`,
|
|
29
28
|
* `article-end`, `footer-profile`, `footer-auxiliary`, `account-intent`,
|
|
30
|
-
* `conversation`, `newsletter` and `edition-selector`.
|
|
31
|
-
* system (`build-input.modules` is always `{}`), so every slot
|
|
32
|
-
* except `footer-profile`, which always wraps the
|
|
33
|
-
* always-rendered footer card/author-profile content
|
|
34
|
-
* module content to distinguish it from
|
|
35
|
-
* `data-gala-slot="<name>"` attribute on an otherwise plain,
|
|
36
|
-
* `<div>` — an empty element with no accessible name
|
|
37
|
-
* the accessibility tree, so an empty slot is
|
|
38
|
-
* does not exist. The collapsed
|
|
39
|
-
* carries the `hidden` attribute, so
|
|
40
|
-
* layout, network or telemetry
|
|
41
|
-
*
|
|
42
|
-
* exist
|
|
29
|
+
* `conversation`, `newsletter` and `edition-selector`. This renderer has no
|
|
30
|
+
* module system (`build-input.modules` is always `{}`), so every slot
|
|
31
|
+
* renders empty except `footer-profile`, which always wraps the
|
|
32
|
+
* publication's own always-rendered footer card/author-profile content
|
|
33
|
+
* (there is no separate module content to distinguish it from here). Each
|
|
34
|
+
* slot is one `data-gala-slot="<name>"` attribute on an otherwise plain,
|
|
35
|
+
* non-landmark `<div>` — an empty element with no accessible name
|
|
36
|
+
* contributes nothing to the accessibility tree, so an empty slot is
|
|
37
|
+
* exactly as inert as one that does not exist. The collapsed
|
|
38
|
+
* `article-footer-ad` slot additionally carries the `hidden` attribute, so
|
|
39
|
+
* it produces no visible, accessibility, layout, network or telemetry
|
|
40
|
+
* effect at all until a later module (not built here) removes `hidden` for
|
|
41
|
+
* a selected placement — none exist here (`build-input.placements` is
|
|
42
|
+
* always `[]`).
|
|
43
43
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
44
|
+
* `header-actions` is not always-empty: it is core's own home for the
|
|
45
|
+
* Light/Dark/System appearance control (the three-mode appearance control
|
|
46
|
+
* in the header-actions slot) — core content, not a module, so this slot's
|
|
47
|
+
* earlier "every slot ... renders empty" description does not cover it.
|
|
48
|
+
* Every other slot is unaffected.
|
|
49
49
|
*
|
|
50
50
|
* The exact `selectorAtom`/hook-naming decisions here (`data-gala-slot`
|
|
51
|
-
* attribute, one slot name per value) are
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* decision here rather than pre-authoring that contract file itself.
|
|
51
|
+
* attribute, one slot name per value) are what
|
|
52
|
+
* `contracts/theme-styling-contract.jcs` later locks into its published
|
|
53
|
+
* catalog, so this module documents the decision here rather than
|
|
54
|
+
* pre-authoring that contract file itself.
|
|
56
55
|
*/
|
|
57
56
|
|
|
58
57
|
/**
|
|
59
58
|
* Every `data-gala-slot` value this renderer ever writes (the nine
|
|
60
|
-
* versioned core slots plus the collapsed `article-footer-ad` slot,
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
59
|
+
* versioned core slots plus the collapsed `article-footer-ad` slot), sorted
|
|
60
|
+
* by UTF-8 bytes. `contracts/theme-styling-contract.jcs` exposes one public
|
|
61
|
+
* theme hook per entry here, so this is the single source of truth both
|
|
62
|
+
* `renderSlot` below and that contract builder read from — never a second
|
|
63
|
+
* hand-typed literal list.
|
|
65
64
|
*
|
|
66
65
|
* @type {readonly string[]}
|
|
67
66
|
*/
|
|
@@ -89,6 +88,8 @@ const HTML_ESCAPES = Object.freeze({
|
|
|
89
88
|
"'": ''',
|
|
90
89
|
});
|
|
91
90
|
|
|
91
|
+
import { joinPublicRoute } from './route.js';
|
|
92
|
+
|
|
92
93
|
/**
|
|
93
94
|
* Escape a string for safe insertion into HTML text or a quoted attribute
|
|
94
95
|
* value.
|
|
@@ -111,8 +112,8 @@ export function escapeHtml(value) {
|
|
|
111
112
|
* collapsed `article-footer-ad` slot
|
|
112
113
|
* @param {object} [options] rendering options
|
|
113
114
|
* @param {boolean} [options.collapsed] when `true`, additionally sets
|
|
114
|
-
* `hidden` (
|
|
115
|
-
* @param {string} [options.html] already-rendered inner markup (
|
|
115
|
+
* `hidden` (the collapsed `article-footer-ad` slot)
|
|
116
|
+
* @param {string} [options.html] already-rendered inner markup (the
|
|
116
117
|
* `header-actions` slot's appearance control); every other slot omits this
|
|
117
118
|
* and stays empty
|
|
118
119
|
* @returns {string} the slot element
|
|
@@ -143,19 +144,25 @@ export function renderSkipLink(messages) {
|
|
|
143
144
|
* `navigationItem` (or `navigationLeaf`, which never has children)
|
|
144
145
|
* @param {string | undefined} currentRoute the current page's own joined
|
|
145
146
|
* route, so the matching link can carry `aria-current="page"`
|
|
147
|
+
* @param {string} urlPrefix the `baseUrl` path prefix prepended to an
|
|
148
|
+
* internal route's href (`/` when `baseUrl` has no path)
|
|
146
149
|
* @returns {string} the rendered `<li>`
|
|
147
150
|
*/
|
|
148
|
-
function renderNavItem(item, currentRoute) {
|
|
151
|
+
function renderNavItem(item, currentRoute, urlPrefix) {
|
|
149
152
|
const label = escapeHtml(item.label);
|
|
150
153
|
const href = escapeHtml(
|
|
151
|
-
/** @type {string} */ (
|
|
154
|
+
/** @type {string} */ (
|
|
155
|
+
item.type === 'internal'
|
|
156
|
+
? joinPublicRoute(urlPrefix, /** @type {string} */ (item.route))
|
|
157
|
+
: item.url
|
|
158
|
+
),
|
|
152
159
|
);
|
|
153
160
|
const isCurrent = item.type === 'internal' && item.route === currentRoute;
|
|
154
161
|
const currentAttribute = isCurrent ? ' aria-current="page"' : '';
|
|
155
162
|
const children = item.children ?? [];
|
|
156
163
|
const childList =
|
|
157
164
|
children.length > 0
|
|
158
|
-
? `<ul>${children.map((child) => renderNavItem(child, currentRoute)).join('')}</ul>`
|
|
165
|
+
? `<ul>${children.map((child) => renderNavItem(child, currentRoute, urlPrefix)).join('')}</ul>`
|
|
159
166
|
: '';
|
|
160
167
|
return `<li><a href="${href}"${currentAttribute}>${label}</a>${childList}</li>`;
|
|
161
168
|
}
|
|
@@ -171,16 +178,27 @@ function renderNavItem(item, currentRoute) {
|
|
|
171
178
|
* route
|
|
172
179
|
* @param {Readonly<Record<string, string | ((...args: string[]) => string)>>} options.messages
|
|
173
180
|
* the resolved message catalog
|
|
174
|
-
* @
|
|
175
|
-
*
|
|
176
|
-
*
|
|
181
|
+
* @param {string} [options.urlPrefix] the `baseUrl` path prefix for internal
|
|
182
|
+
* hrefs (default `/`)
|
|
183
|
+
* @returns {string} the rendered primary navigation `<nav>`, always present
|
|
184
|
+
* even when `items` is empty (TPL-M3): every page kind renders exactly
|
|
185
|
+
* one primary-navigation landmark unconditionally
|
|
186
|
+
* (`internal/page-kinds.js`'s own module documentation), so a reader
|
|
187
|
+
* navigating by landmark list sees the same, predictable chrome structure
|
|
188
|
+
* on every page regardless of whether that publication has authored any
|
|
189
|
+
* navigation items yet.
|
|
177
190
|
*/
|
|
178
|
-
export function renderPrimaryNavigation({
|
|
191
|
+
export function renderPrimaryNavigation({
|
|
192
|
+
items,
|
|
193
|
+
currentRoute,
|
|
194
|
+
messages,
|
|
195
|
+
urlPrefix = '/',
|
|
196
|
+
}) {
|
|
179
197
|
const label = escapeHtml(
|
|
180
198
|
/** @type {string} */ (messages.primaryNavigationLabel),
|
|
181
199
|
);
|
|
182
200
|
const itemsHtml = items
|
|
183
|
-
.map((item) => renderNavItem(item, currentRoute))
|
|
201
|
+
.map((item) => renderNavItem(item, currentRoute, urlPrefix))
|
|
184
202
|
.join('');
|
|
185
203
|
return `<nav aria-label="${label}"><ul>${itemsHtml}</ul></nav>`;
|
|
186
204
|
}
|
|
@@ -193,14 +211,15 @@ export function renderPrimaryNavigation({ items, currentRoute, messages }) {
|
|
|
193
211
|
* `navigationNormalized.footerItems` (0..50 root items)
|
|
194
212
|
* @param {Readonly<Record<string, string | ((...args: string[]) => string)>>} options.messages
|
|
195
213
|
* the resolved message catalog
|
|
214
|
+
* @param {string} [options.urlPrefix] the `baseUrl` path prefix (default `/`)
|
|
196
215
|
* @returns {string} the rendered footer navigation
|
|
197
216
|
*/
|
|
198
|
-
export function renderFooterNavigation({ items, messages }) {
|
|
217
|
+
export function renderFooterNavigation({ items, messages, urlPrefix = '/' }) {
|
|
199
218
|
const label = escapeHtml(
|
|
200
219
|
/** @type {string} */ (messages.footerNavigationLabel),
|
|
201
220
|
);
|
|
202
221
|
const itemsHtml = items
|
|
203
|
-
.map((item) => renderNavItem(item, undefined))
|
|
222
|
+
.map((item) => renderNavItem(item, undefined, urlPrefix))
|
|
204
223
|
.join('');
|
|
205
224
|
return `<nav aria-label="${label}"><ul>${itemsHtml}</ul></nav>`;
|
|
206
225
|
}
|
|
@@ -306,8 +325,8 @@ export function renderPagination({
|
|
|
306
325
|
* @param {{route: string, alt: string} | undefined} options.logo an optional
|
|
307
326
|
* core logo image, always carrying alternative text
|
|
308
327
|
* @param {string} [options.appearanceControlHtml] the already-rendered
|
|
309
|
-
* Light/Dark/System appearance control
|
|
310
|
-
* `internal/appearance/controller-markup.js`'s `renderAppearanceControl`),
|
|
328
|
+
* Light/Dark/System appearance control
|
|
329
|
+
* (`internal/appearance/controller-markup.js`'s `renderAppearanceControl`),
|
|
311
330
|
* inserted into the `header-actions` slot
|
|
312
331
|
* @returns {string} the rendered header
|
|
313
332
|
*/
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Collect the distinct source records this renderer actually consumed from
|
|
3
|
-
* a validated `build-input:2.0.0` instance, as
|
|
4
|
-
* `
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* and the role is the earliest value in the enum order").
|
|
3
|
+
* a validated `build-input:2.0.0` instance, as `manifestIncludedSource[]`
|
|
4
|
+
* rows: `includedSources` has set equality with the distinct source records
|
|
5
|
+
* placed in the verified-input carrier, a file used by multiple records
|
|
6
|
+
* appears once, and its role is the earliest value in the enum order.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
/** @type {readonly string[]} */
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
* against `node_modules/@rathnasgala2/schemas/schemas/build-input.schema.json`:
|
|
8
8
|
* no `$defs` entry or property named `dir`/`direction` exists anywhere in the
|
|
9
9
|
* schema). The semantic skeleton must still set an accurate `dir` attribute
|
|
10
|
-
*
|
|
11
|
-
* acceptance test A's golden fixture), so this module derives it from the
|
|
10
|
+
* even for localized content with mixed direction, so this module derives it from the
|
|
12
11
|
* tag's primary language subtag against the closed set of right-to-left
|
|
13
12
|
* script languages in Unicode CLDR's exemplar set — the same approach HTML's
|
|
14
13
|
* own `Intl.Locale` `textInfo.direction` uses, reimplemented here as a small
|