@rathnasgala2/template 2.0.0 → 2.1.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 (42) hide show
  1. package/README.md +204 -137
  2. package/contracts/render-policy.jcs +1 -1
  3. package/contracts/theme-styling-contract.jcs +1 -1
  4. package/package.json +4 -5
  5. package/src/core/errors.js +13 -13
  6. package/src/core/index.js +102 -66
  7. package/src/core/internal/appearance/base-layer.js +180 -0
  8. package/src/core/internal/appearance/bootstrap-script.js +11 -11
  9. package/src/core/internal/appearance/contract.js +59 -44
  10. package/src/core/internal/appearance/controller-markup.js +7 -8
  11. package/src/core/internal/appearance/styling-contract.js +132 -29
  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 +52 -35
  15. package/src/core/internal/feeds.js +10 -13
  16. package/src/core/internal/fs-walk.js +2 -2
  17. package/src/core/internal/media/avif-probe.js +4 -4
  18. package/src/core/internal/media/font-policy.js +2 -2
  19. package/src/core/internal/media/jpeg-codec.js +0 -0
  20. package/src/core/internal/media/limits.js +32 -34
  21. package/src/core/internal/media/pipeline.js +4 -4
  22. package/src/core/internal/media/png-codec.js +7 -7
  23. package/src/core/internal/media/resize.js +2 -2
  24. package/src/core/internal/media/sniff.js +4 -4
  25. package/src/core/internal/media/theme-svg-sanitizer.js +278 -0
  26. package/src/core/internal/media/webp-probe.js +3 -3
  27. package/src/core/internal/messages.js +8 -8
  28. package/src/core/internal/page-kinds.js +46 -48
  29. package/src/core/internal/render-policy-content.js +57 -21
  30. package/src/core/internal/route-labels.js +5 -6
  31. package/src/core/internal/route.js +16 -20
  32. package/src/core/internal/search-index.js +12 -14
  33. package/src/core/internal/semver-range.js +84 -0
  34. package/src/core/internal/seo.js +21 -23
  35. package/src/core/internal/sitemap.js +6 -8
  36. package/src/core/internal/skeleton.js +42 -39
  37. package/src/core/internal/source-inventory.js +4 -5
  38. package/src/core/internal/text-direction.js +1 -2
  39. package/src/core/internal/theme-assets.js +294 -67
  40. package/src/core/internal/uuid.js +4 -5
  41. package/src/core/manifest.js +0 -0
  42. package/types/index.d.ts +22 -8
@@ -1,28 +1,26 @@
1
1
  /**
2
- * SEO/localization metadata assembly (task packet S2-T08; brief S2 section 3
3
- * "Required generated outputs": "canonical share and copy affordances,
4
- * localization metadata, SEO metadata").
2
+ * SEO/localization metadata assembly: canonical share and copy affordances,
3
+ * localization metadata, SEO metadata.
5
4
  *
6
5
  * This module resolves the per-page facts `internal/page-kinds.js` already
7
6
  * decided (`description`, `ogType`, `socialImageRef`, `robotsContent`) into
8
7
  * the concrete absolute-URL strings a page's `<head>` needs. It is kept
9
8
  * separate from `page-kinds.js` because resolving a `socialImageRef` into an
10
9
  * absolute derivative URL needs two facts only `src/core/index.js` has at
11
- * that point: the S2-T05 media pipeline's own finished `assets` list (so the
12
- * exact derivative file extension a given source image produced is known —
10
+ * that point: the media pipeline's own finished `assets` list (so the exact
11
+ * derivative file extension a given source image produced is known —
13
12
  * `page-kinds.js` never decodes an image) and `buildInput.baseUrl` (the
14
13
  * origin every absolute URL this renderer emits is resolved against).
15
14
  *
16
- * S2-T12 basePath fix (LOCAL-7 follow-up): every derivative-asset URL this
17
- * module resolves is now `basePath`-joined through `internal/route.js`'s
18
- * shared {@link import('./route.js').joinBasePathAndRoute} before being
19
- * resolved against `baseUrl`, exactly like every other renderer-derived
20
- * route (`internal/page-kinds.js`, `internal/print-stylesheet.js`). Before
21
- * this fix, {@link resolveSocialImageUrl} joined a bare root-absolute
15
+ * basePath fix: every derivative-asset URL this module resolves is
16
+ * `basePath`-joined through `internal/route.js`'s shared
17
+ * {@link import('./route.js').joinBasePathAndRoute} before being resolved
18
+ * against `baseUrl`, exactly like every other renderer-derived route
19
+ * (`internal/page-kinds.js`, `internal/print-stylesheet.js`). Without this,
20
+ * {@link resolveSocialImageUrl} would join a bare root-absolute
22
21
  * `/${derivativePath}` against `baseUrl`, which 404s under a non-root
23
- * `basePath` publication (`internal/theme-assets.js` is the S2-T12 module
24
- * that fixes the equivalent theme/print stylesheet and appearance-script
25
- * hrefs).
22
+ * `basePath` publication (`internal/theme-assets.js` applies the equivalent
23
+ * fix for the theme/print stylesheet and appearance-script hrefs).
26
24
  *
27
25
  * hreflang note (documented scope decision, not a silent omission):
28
26
  * `build-input:2.0.0` has no translation/locale-variant linkage between
@@ -31,11 +29,11 @@
31
29
  * `$defs` entry named anything like `translation`/`localizedContent`/
32
30
  * `alternateLanguage` exists anywhere in the schema; `authorNormalized`'s own
33
31
  * `localized` array carries only a translated author bio, never a route). A
34
- * page's own `<html lang>` already carries its resolved language (S2-T06),
35
- * which is the entire localization signal this build-input shape declares.
36
- * `<link rel="alternate" hreflang="...">` is therefore correctly *empty* for
37
- * S2 — brief S2 section 3's "hreflang/localization metadata... where the
38
- * input declares translations" is satisfied vacuously, since the input never
32
+ * page's own `<html lang>` already carries its resolved language, which is
33
+ * the entire localization signal this build-input shape declares.
34
+ * `<link rel="alternate" hreflang="...">` is therefore correctly *empty*
35
+ * here — the requirement to emit hreflang/localization metadata where the
36
+ * input declares translations is satisfied vacuously, since the input never
39
37
  * declares one. This module has no `hreflang` export for that reason; a
40
38
  * later build-input major version that adds translation linkage is the
41
39
  * natural place to add one, at which point every page-kind call site below
@@ -52,14 +50,14 @@ import { joinBasePathAndRoute } from './route.js';
52
50
  * know this path shape, so a change to either stays a one-place fix).
53
51
  *
54
52
  * @param {readonly {path: string, mediaType: string}[]} mediaAssets the
55
- * S2-T05 media pipeline's own finished `assets` list
53
+ * media pipeline's own finished `assets` list
56
54
  * @param {{path: string, sourceDigest: string} | undefined} reference a
57
55
  * `resolvedFile`-shaped reference (`publication.defaultImage`,
58
56
  * `author.avatar`, `frontmatter.socialImage`, `frontmatter.hero.file`)
59
57
  * @returns {string | undefined} the derivative asset's own output-relative
60
58
  * path (never the raw source path), or `undefined` when no reference was
61
59
  * supplied or the media pipeline produced no matching derivative (an image
62
- * this renderer's own S2-T05 pipeline was never asked to process, e.g. a
60
+ * this renderer's own media pipeline was never asked to process, e.g. a
63
61
  * test fixture exercising SEO metadata without the media pipeline)
64
62
  */
65
63
  export function resolveOriginalDerivativePath(mediaAssets, reference) {
@@ -77,11 +75,11 @@ export function resolveOriginalDerivativePath(mediaAssets, reference) {
77
75
  *
78
76
  * @param {object} options resolution options
79
77
  * @param {readonly {path: string, mediaType: string}[]} options.mediaAssets
80
- * the S2-T05 media pipeline's own finished `assets` list
78
+ * the media pipeline's own finished `assets` list
81
79
  * @param {{path: string, sourceDigest: string} | undefined} options.reference
82
80
  * the candidate `resolvedFile` reference
83
81
  * @param {string} options.baseUrl the build input's origin-only `baseUrl`
84
- * @param {string} options.basePath the build input's `basePath` (S2-T12: the
82
+ * @param {string} options.basePath the build input's `basePath` (the
85
83
  * derivative path is joined with this before being resolved against
86
84
  * `baseUrl`, so a non-root publication's social-image URL is correct)
87
85
  * @returns {string | undefined} the absolute derivative image URL, or
@@ -1,22 +1,20 @@
1
1
  /**
2
- * Sitemap generation (task packet S2-T08; brief S2 section 3 "Required
3
- * generated outputs": "sitemap"; task packet instructions: "published routes
4
- * only, deterministic order, `lastmod` from content timestamps not build
5
- * time").
2
+ * Sitemap generation: published routes only, deterministic order, with
3
+ * `lastmod` derived from content timestamps rather than build time.
6
4
  *
7
5
  * Every entry comes from `internal/page-kinds.js`'s own
8
6
  * {@link import('./page-kinds.js').GeneratedPage} list — the single source
9
7
  * of truth for which page kinds exist and what each one's own
10
8
  * `robotsContent`/`lastModified` is — filtered to exclude any page this
11
- * renderer itself marked `noindex` (task packet: "robots handling for
12
- * unlisted content"; `status: "unlisted"` content is exactly the case
9
+ * renderer itself marked `noindex` (robots handling for unlisted content:
10
+ * `status: "unlisted"` content is exactly the case
13
11
  * `internal/page-kinds.js` sets `robotsContent` for). The generated `404`
14
12
  * error page, the feeds, the sitemap document itself and the static search
15
13
  * index are never `GeneratedPage` entries, so they are structurally excluded
16
14
  * without a second exclusion list. A `manifestRedirect` route is likewise
17
15
  * never in this list; a redirect's fixed byte-for-byte document already
18
- * carries its own `<link rel="canonical">` to the real content page (brief
19
- * S2 section 3), which is what a crawler should index instead.
16
+ * carries its own `<link rel="canonical">` to the real content page, which
17
+ * is what a crawler should index instead.
20
18
  */
21
19
 
22
20
  import { escapeHtml } from './skeleton.js';
@@ -1,12 +1,11 @@
1
1
  /**
2
- * Core semantic HTML skeleton and navigation renderer (task packet S2-T06;
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 (task packet S2-T06).
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 (DEC-060): `header-actions`, `article-preamble`,
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`. S2 has no module
31
- * system (`build-input.modules` is always `{}`), so every slot renders empty
32
- * except `footer-profile`, which always wraps the publication's own
33
- * always-rendered footer card/author-profile content (there is no separate
34
- * module content to distinguish it from in S2). Each slot is one
35
- * `data-gala-slot="<name>"` attribute on an otherwise plain, non-landmark
36
- * `<div>` — an empty element with no accessible name contributes nothing to
37
- * the accessibility tree, so an empty slot is exactly as inert as one that
38
- * does not exist. The collapsed `article-footer-ad` slot additionally
39
- * carries the `hidden` attribute, so it produces no visible, accessibility,
40
- * layout, network or telemetry effect at all (DEC-060) until a later module
41
- * task (out of S2 scope) removes `hidden` for a selected placement — none
42
- * exist in S2 (`build-input.placements` is always `[]`).
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
- * S2-T07 amendment: `header-actions` is no longer always-empty. It is core's
45
- * own home for the Light/Dark/System appearance control (brief S2 section 3:
46
- * "the three-mode appearance control in the header-actions slot") — core
47
- * content, not a module, so this slot's earlier "every slot ... renders
48
- * empty" description no longer covers it. Every other slot is unaffected.
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 S2-T06's own scoped proposal for
52
- * what S2-T12 later locks into the published
53
- * `contracts/theme-styling-contract.jcs` catalog; T12 has not run yet
54
- * (S2-T06 precedes it — S2-T07 then S2-T12), so this module documents the
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
- * DEC-060), sorted by UTF-8 bytes (S2-T12: `contracts/theme-styling-contract.jcs`
62
- * exposes one public theme hook per entry here, so this is the single
63
- * source of truth both `renderSlot` below and that contract builder read
64
- * from — never a second hand-typed literal list).
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
  */
@@ -111,8 +110,8 @@ export function escapeHtml(value) {
111
110
  * collapsed `article-footer-ad` slot
112
111
  * @param {object} [options] rendering options
113
112
  * @param {boolean} [options.collapsed] when `true`, additionally sets
114
- * `hidden` (DEC-060's collapsed `article-footer-ad` slot)
115
- * @param {string} [options.html] already-rendered inner markup (S2-T07: the
113
+ * `hidden` (the collapsed `article-footer-ad` slot)
114
+ * @param {string} [options.html] already-rendered inner markup (the
116
115
  * `header-actions` slot's appearance control); every other slot omits this
117
116
  * and stays empty
118
117
  * @returns {string} the slot element
@@ -171,9 +170,13 @@ function renderNavItem(item, currentRoute) {
171
170
  * route
172
171
  * @param {Readonly<Record<string, string | ((...args: string[]) => string)>>} options.messages
173
172
  * the resolved message catalog
174
- * @returns {string} the rendered primary navigation, or an empty string when
175
- * there are no authored items (the landmark is still meaningful with no
176
- * items for a publication with no authored navigation)
173
+ * @returns {string} the rendered primary navigation `<nav>`, always present
174
+ * even when `items` is empty (TPL-M3): every page kind renders exactly
175
+ * one primary-navigation landmark unconditionally
176
+ * (`internal/page-kinds.js`'s own module documentation), so a reader
177
+ * navigating by landmark list sees the same, predictable chrome structure
178
+ * on every page regardless of whether that publication has authored any
179
+ * navigation items yet.
177
180
  */
178
181
  export function renderPrimaryNavigation({ items, currentRoute, messages }) {
179
182
  const label = escapeHtml(
@@ -306,8 +309,8 @@ export function renderPagination({
306
309
  * @param {{route: string, alt: string} | undefined} options.logo an optional
307
310
  * core logo image, always carrying alternative text
308
311
  * @param {string} [options.appearanceControlHtml] the already-rendered
309
- * Light/Dark/System appearance control (S2-T07:
310
- * `internal/appearance/controller-markup.js`'s `renderAppearanceControl`),
312
+ * Light/Dark/System appearance control
313
+ * (`internal/appearance/controller-markup.js`'s `renderAppearanceControl`),
311
314
  * inserted into the `header-actions` slot
312
315
  * @returns {string} the rendered header
313
316
  */
@@ -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
- * `manifestIncludedSource[]` rows (DEC-097 section 6: "`includedSources` has
5
- * set equality with the distinct source records placed in the
6
- * verified-input carrier ... a file used by multiple records appears once
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
- * per brief S2 section 3 ("localized content with mixed direction" is part of
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