@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,19 +1,18 @@
1
1
  /**
2
- * Page-kind assembly (task packet S2-T06): builds every generated page's
2
+ * Page-kind assembly: builds every generated page's
3
3
  * `<main>` content, breadcrumb trail and route from a validated
4
4
  * `build-input:2.0.0` instance, and the shared site chrome (header, primary
5
5
  * navigation, footer) every page kind is composed with through
6
6
  * `internal/skeleton.js`'s {@link import('./skeleton.js').renderPageBody}.
7
7
  *
8
- * Page kinds this module produces, matching brief S2 section 3's "Required
9
- * generated outputs" and task packet S2-T06's page-kind list:
8
+ * Page kinds this module produces:
10
9
  *
11
10
  * - `profile` — the publication's own optional profile page
12
11
  * (`publication.profile`);
13
- * - `author` — one page per entry in `build-input.authors` (DEC-097 section 5:
14
- * this array is already exactly the set of authors referenced by the
15
- * selected content, publication contact or footer card — never an
16
- * unreferenced author), at `/authors/<id>`;
12
+ * - `author` — one page per entry in `build-input.authors` (this array is
13
+ * already exactly the set of authors referenced by the selected content,
14
+ * publication contact or footer card — never an unreferenced author), at
15
+ * `/authors/<id>`;
17
16
  * - `article` / `page` — one page per `content[]` record, at its own
18
17
  * `contentFrontmatterNormalized` route;
19
18
  * - `index` — a paginated reverse-chronological listing of every
@@ -27,10 +26,10 @@
27
26
  * plus one `/archive` root page linking to each year.
28
27
  *
29
28
  * The `error` page kind ({@link renderErrorPageBody}) is exported as a pure
30
- * function, not wired into any generated route here: brief S2 section 3
31
- * assigns the one generated `404.html` artifact to S2-T08, which this task
32
- * packet's own scope notes forbid touching. S2-T08 composes its `404.html`
33
- * from this same function.
29
+ * function, not wired into any generated route here: the one generated
30
+ * `404.html` artifact is assembled elsewhere, which this module's own scope
31
+ * notes forbid touching. That module composes its `404.html` from this same
32
+ * function.
34
33
  *
35
34
  * Every generated page's own `route` field (see {@link GeneratedPage}) is an
36
35
  * un-joined `canonicalRoute`, in the same convention `internal/index.js`
@@ -39,16 +38,18 @@
39
38
  * *link href* this module writes into a page's own body (breadcrumbs, tag and
40
39
  * series listings, pagination controls) is, by contrast, already joined with
41
40
  * `basePath` here, because it is read back only as literal HTML text, never
42
- * re-joined by a caller. This mirrors DEC-097 section 5's navigation
43
- * normalization rule: an *authored* navigation item's route is used exactly
44
- * as authored, never synthesized or rewritten (so `internal/skeleton.js`'s
45
- * navigation renderer never joins it), while every route this renderer itself
46
- * derives (every page kind here) is basePath-joined at the point it becomes
47
- * link text, exactly like S2-T03's own redirect/content route projection.
41
+ * re-joined by a caller. This mirrors the navigation normalization rule: an
42
+ * *authored* navigation item's route is used exactly as authored, never
43
+ * synthesized or rewritten (so `internal/skeleton.js`'s navigation renderer
44
+ * never joins it), while every route this renderer itself derives (every
45
+ * page kind here) is basePath-joined at the point it becomes link text,
46
+ * exactly like the redirect/content route projection elsewhere in this
47
+ * renderer.
48
48
  *
49
49
  * Every listing here is a deliberately scoped decision this module documents
50
50
  * rather than assumes: page size, sort tie-break and the tag/series/archive
51
- * root pages are not specified verbatim by the brief text this task read.
51
+ * root pages have no externally fixed specification this renderer conforms
52
+ * to verbatim.
52
53
  */
53
54
 
54
55
  import {
@@ -63,22 +64,20 @@ import { routeSegmentForLabel } from './route-labels.js';
63
64
  import { joinBasePathAndRoute } from './route.js';
64
65
 
65
66
  /** Listing page size (index and per-year archive pages). Scoped decision:
66
- * the brief does not fix a page size; 10 is a deterministic, documented
67
- * default with no author-facing configuration surface in S2.
67
+ * this renderer has no externally fixed page size; 10 is a deterministic,
68
+ * documented default with no author-facing configuration surface.
68
69
  * @type {number} */
69
70
  export const LISTING_PAGE_SIZE = 10;
70
71
 
71
72
  /**
72
- * The DEC-097-style semantic template-owned attribute this renderer sets on
73
- * every generated page's `<body>` (S2-T12: `contracts/theme-styling-contract.jcs`
74
- * exposes this as a public theme hook, one per {@link PAGE_KIND_VALUES}
75
- * entry, so a theme can vary presentation by page kind — e.g. a different
76
- * article vs. listing background — without the template ever promoting a
77
- * private/incidental selector). Scoped decision: this attribute did not
78
- * exist before S2-T12; it is this task packet's own addition, introduced
79
- * specifically because the published styling contract needs a
80
- * page-kind-targeting hook the brief's "every page-kind ... class/attribute"
81
- * language calls for.
73
+ * The semantic template-owned attribute this renderer sets on every
74
+ * generated page's `<body>`: `contracts/theme-styling-contract.jcs` exposes
75
+ * this as a public theme hook, one per {@link PAGE_KIND_VALUES} entry, so a
76
+ * theme can vary presentation by page kind — e.g. a different article vs.
77
+ * listing background — without the template ever promoting a
78
+ * private/incidental selector. This attribute is a deliberate addition,
79
+ * introduced specifically because the published styling contract needs a
80
+ * page-kind-targeting hook.
82
81
  *
83
82
  * @type {string}
84
83
  */
@@ -87,8 +86,8 @@ export const PAGE_KIND_ATTRIBUTE = 'data-gala-page-kind';
87
86
  /**
88
87
  * Every value {@link PAGE_KIND_ATTRIBUTE} may carry: every {@link GeneratedPage}
89
88
  * `kind` plus the synthetic `error` kind (`internal/index.js`'s always-present
90
- * generated `404.html`), sorted by UTF-8 bytes (DEC-097's own attribute-value
91
- * sort convention).
89
+ * generated `404.html`), sorted by UTF-8 bytes (this renderer's own
90
+ * attribute-value sort convention throughout).
92
91
  *
93
92
  * @type {readonly string[]}
94
93
  */
@@ -135,8 +134,8 @@ function paginate(items, pageSize) {
135
134
 
136
135
  /**
137
136
  * Resolve an author's display name by ID, failing closed on a dangling
138
- * reference (an adapter defect: `resolvedAuthorIds` is schema/DEC-097
139
- * guaranteed to resolve).
137
+ * reference (an adapter defect: `resolvedAuthorIds` is schema-guaranteed to
138
+ * resolve).
140
139
  *
141
140
  * @param {ReadonlyMap<string, import('../../../types/index.d.ts').AuthorNormalized>} authorsById
142
141
  * @param {string} authorId
@@ -161,30 +160,29 @@ function requireAuthor(authorsById, authorId) {
161
160
  * @property {string} bodyHtml the page's complete `<main>` inner HTML,
162
161
  * starting with exactly one `<h1>`
163
162
  * @property {string} [breadcrumbHtml] an optional already-rendered breadcrumb
164
- * @property {string} [description] task packet S2-T08: an optional plain-text
163
+ * @property {string} [description] an optional plain-text
165
164
  * description this page's SEO/Open-Graph/Twitter `<meta>` tags are built
166
165
  * from (never HTML, never the sanitized body — a distinct authored or
167
166
  * publication-level plain-text field)
168
- * @property {'website' | 'article' | 'profile'} [ogType] task packet S2-T08:
167
+ * @property {'website' | 'article' | 'profile'} [ogType]
169
168
  * the Open Graph `og:type` this page kind maps to; defaults to `'website'`
170
169
  * when omitted
171
170
  * @property {{path: string, sourceDigest: string} | undefined} [socialImageRef]
172
- * task packet S2-T08: an optional `resolvedFile` reference this page's
171
+ * an optional `resolvedFile` reference this page's
173
172
  * social image is resolved from (the caller, `src/core/index.js`, is the
174
173
  * one place that can turn this into an absolute derivative URL, because
175
- * only it has run the S2-T05 media pipeline and knows the derivative
174
+ * only it has run the media pipeline and knows the derivative
176
175
  * output path a given `sourceDigest` produced)
177
- * @property {string} [robotsContent] task packet S2-T08: an optional
176
+ * @property {string} [robotsContent] an optional
178
177
  * `<meta name="robots">` content value (e.g. `'noindex, follow'` for
179
178
  * `status: "unlisted"` content); omitted entirely for ordinarily indexable
180
179
  * pages, matching this renderer's "documented, not silently assumed" style
181
180
  * elsewhere — every page kind's robots decision is made once, here, rather
182
181
  * than re-derived by every later consumer (sitemap, search index)
183
- * @property {string} [lastModified] task packet S2-T08: an optional
182
+ * @property {string} [lastModified] an optional
184
183
  * `rfc3339` timestamp the generated sitemap's `<lastmod>` for this page is
185
184
  * drawn from — always derived from authored content timestamps
186
- * (`publishedAt`/`updatedAt`), never build time (brief S2 section 3:
187
- * "sitemap ... `lastmod` from content timestamps not build time"). Omitted
185
+ * (`publishedAt`/`updatedAt`), never build time. Omitted
188
186
  * for a page kind with no natural underlying timestamp (`profile`,
189
187
  * `author`), which the generated sitemap then emits with no `<lastmod>`
190
188
  * element at all (a sitemap's own `<lastmod>` is optional per the Sitemaps
@@ -194,7 +192,7 @@ function requireAuthor(authorsById, authorId) {
194
192
  /**
195
193
  * The reverse-chronological, deterministically tie-broken set of every
196
194
  * `status: "published"`, `kind: "article"` record — the exact selection and
197
- * ordering the `index` page kind paginates, and (task packet S2-T08) the
195
+ * ordering the `index` page kind paginates, and the
198
196
  * same selection the generated feeds draw from, so a feed's item order can
199
197
  * never drift from the index listing's own order. Extracted to its own
200
198
  * export rather than duplicated, since `internal/fs-walk.js`'s duplication
@@ -224,7 +222,7 @@ export function selectPublishedArticles(content) {
224
222
  /**
225
223
  * The most recent of a content record's own `updatedAt` (when authored) or
226
224
  * `publishedAt` — this renderer's one definition of "a content record's own
227
- * last-modified instant" (task packet S2-T08's sitemap `<lastmod>` and the
225
+ * last-modified instant" (the sitemap's `<lastmod>` and the
228
226
  * static search index both read this, rather than each re-deriving it).
229
227
  *
230
228
  * @param {import('../../../types/index.d.ts').ContentFrontmatterNormalized} frontmatter
@@ -237,7 +235,7 @@ export function contentLastModified(frontmatter) {
237
235
  /**
238
236
  * The latest {@link contentLastModified} across a non-empty set of records —
239
237
  * this renderer's definition of "a generated listing page's own
240
- * `<lastmod>`" (task packet S2-T08): the most recent authored timestamp
238
+ * `<lastmod>`": the most recent authored timestamp
241
239
  * among the content actually shown on that page, never build time.
242
240
  *
243
241
  * @param {readonly import('../../../types/index.d.ts').ContentBuildRecord[]} records
@@ -682,10 +680,10 @@ export function buildGeneratedPages(validatedInput) {
682
680
  }
683
681
 
684
682
  /**
685
- * Render the `error` page kind's `<main>` body (task packet S2-T06). This is
683
+ * Render the `error` page kind's `<main>` body. This is
686
684
  * a pure function with no route/build-input dependency, exported for a
687
- * future caller (S2-T08's generated `404.html`, out of this task packet's
688
- * scope) and for this repository's own structural/golden tests.
685
+ * caller elsewhere (the generated `404.html`, assembled outside this module)
686
+ * and for this repository's own structural/golden tests.
689
687
  *
690
688
  * @param {object} options rendering options
691
689
  * @param {Readonly<Record<string, string | ((...args: string[]) => string)>>} options.messages
@@ -1,7 +1,7 @@
1
1
  /**
2
- * The single source of truth for `contracts/render-policy.jcs`'s content
3
- * (brief S2 section 3 "Markdown, sanitizer, highlighter" and "Per-artifact
4
- * CSP baseline"; DEC-097 section 5 `renderPolicyIdentity`).
2
+ * The single source of truth for `contracts/render-policy.jcs`'s content:
3
+ * the markdown, sanitizer, highlighter and per-artifact CSP baseline, and
4
+ * the `renderPolicyIdentity` derived from them.
5
5
  *
6
6
  * `scripts/generate-contracts.mjs` emits this exact document as compact JCS
7
7
  * to `contracts/render-policy.jcs`; `src/core/internal/content-security.js`
@@ -17,9 +17,9 @@
17
17
  export const RENDER_POLICY_NAME = 'gala-render-policy';
18
18
 
19
19
  /**
20
- * Byte-equal to the template lock's `contractVersion` (DEC-097 section 5),
21
- * which every S2 `build-input:2.0.0` fixture carries as the literal string
22
- * `"2.0.0"` (matches `manifest.js`'s `buildInputContractVersion`).
20
+ * Byte-equal to the template lock's `contractVersion`, which every
21
+ * `build-input:2.0.0` fixture in this repository carries as the literal
22
+ * string `"2.0.0"` (matches `manifest.js`'s `buildInputContractVersion`).
23
23
  *
24
24
  * @type {string}
25
25
  */
@@ -31,7 +31,7 @@ export const MARKDOWN_IT_PACKAGE = 'markdown-it';
31
31
  export const MARKDOWN_IT_VERSION = '14.3.1';
32
32
 
33
33
  /**
34
- * The exact markdown-it construction options brief S2 section 3 requires.
34
+ * The exact markdown-it construction options this renderer requires.
35
35
  *
36
36
  * @type {{html: false, linkify: false, typographer: false}}
37
37
  */
@@ -56,7 +56,7 @@ export const ALLOWED_URL_SCHEMES = Object.freeze(['https', 'http', 'mailto']);
56
56
 
57
57
  /**
58
58
  * The closed allowlist of tags this renderer ever emits from author
59
- * markdown: "semantic text and content tags" only (brief S2 section 3).
59
+ * markdown: semantic text and content tags only.
60
60
  * No form, frame, SVG, MathML, script, style, object or template element is
61
61
  * ever admitted.
62
62
  *
@@ -110,8 +110,7 @@ export const ALLOWED_TAGS = Object.freeze([
110
110
  /**
111
111
  * The closed per-tag attribute allowlist. `id` is admitted only on headings
112
112
  * (this renderer's own generated heading IDs); `style`, every `on*` handler,
113
- * `srcdoc` and every other undeclared attribute is always stripped (brief S2
114
- * section 3).
113
+ * `srcdoc` and every other undeclared attribute is always stripped.
115
114
  *
116
115
  * @type {Readonly<Record<string, readonly string[]>>}
117
116
  */
@@ -138,7 +137,7 @@ export const HIGHLIGHTER_VERSION = '5.0.2';
138
137
  /**
139
138
  * The closed grammar-name catalog the owned highlighter accepts. A fence
140
139
  * language outside this list always falls back to escaped, unhighlighted
141
- * code (brief S2 section 3).
140
+ * code.
142
141
  *
143
142
  * @type {readonly string[]}
144
143
  */
@@ -167,8 +166,8 @@ export const HIGHLIGHT_GRAMMARS = Object.freeze(
167
166
  * can produce (Prism's documented generic token set plus this
168
167
  * repository's own empirical sample across every admitted grammar). `token`
169
168
  * itself is always the base class; every other admitted value is a
170
- * secondary token-class (brief S2 section 3: "`span` elements carrying
171
- * `token` and admitted token-class values").
169
+ * secondary token-class: `span` elements carry `token` plus admitted
170
+ * token-class values.
172
171
  *
173
172
  * @type {readonly string[]}
174
173
  */
@@ -241,15 +240,51 @@ export const ADMITTED_LANGUAGE_CLASSES = Object.freeze(
241
240
  );
242
241
 
243
242
  /**
244
- * The exact per-artifact CSP baseline (brief S2 section 3). Directive order
245
- * is byte-exact as written in the brief; every directive's value list is
246
- * already sorted bytewise and duplicate-free because S2 materializes no
247
- * module package, configuration, output or runtime, so this string is
243
+ * The CSP specification explicitly ignores `frame-ancestors`
244
+ * (and `report-uri`/`sandbox`, neither used here) when a policy is
245
+ * delivered via `<meta http-equiv>` — browsers drop the directive and log a
246
+ * console warning on every page load. `frame-ancestors` is therefore
247
+ * split out as the one header-only directive: clickjacking protection for a
248
+ * published artifact is a hosting-layer obligation (a response header, most
249
+ * commonly `Content-Security-Policy` or `X-Frame-Options`), recorded here so
250
+ * every consuming adapter/deployment knows to add it, rather than a directive
251
+ * that silently does nothing inside the `<meta>` tag every page already
252
+ * carries.
253
+ *
254
+ * @type {readonly string[]}
255
+ */
256
+ export const CSP_HEADER_ONLY_DIRECTIVES = Object.freeze([
257
+ "frame-ancestors 'none'",
258
+ ]);
259
+
260
+ /**
261
+ * The exact per-artifact CSP baseline actually deliverable inside a
262
+ * `<meta http-equiv="Content-Security-Policy">` tag: {@link CSP_HEADER_ONLY_DIRECTIVES}
263
+ * is excluded. Directive order is byte-exact, minus
264
+ * the excluded directive; every directive's value list is already sorted
265
+ * bytewise and duplicate-free because this renderer materializes no module
266
+ * package, configuration, output or runtime, so this string is
248
267
  * byte-identical on every route.
249
268
  *
250
269
  * @type {string}
251
270
  */
252
- export const CONTENT_SECURITY_POLICY_BASELINE =
271
+ export const CONTENT_SECURITY_POLICY_META_BASELINE =
272
+ "default-src 'none'; base-uri 'none'; object-src 'none'; " +
273
+ "form-action 'none'; script-src 'self'; " +
274
+ "style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'none'; " +
275
+ "media-src 'self'; manifest-src 'self'; worker-src 'none'";
276
+
277
+ /**
278
+ * The complete CSP baseline this renderer originally specified as one
279
+ * string — the meta-safe directives plus the header-only directive, in
280
+ * this fixed byte-exact order — kept only as the render-policy identity's own
281
+ * digest input (`renderPolicyDocument`'s `contentSecurityPolicy` field): a
282
+ * hosting adapter that delivers this policy as a response header, rather
283
+ * than a `<meta>` tag, may deliver this complete string unmodified.
284
+ *
285
+ * @type {string}
286
+ */
287
+ export const CONTENT_SECURITY_POLICY_COMPLETE =
253
288
  "default-src 'none'; base-uri 'none'; object-src 'none'; " +
254
289
  "frame-ancestors 'none'; form-action 'none'; script-src 'self'; " +
255
290
  "style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'none'; " +
@@ -258,8 +293,7 @@ export const CONTENT_SECURITY_POLICY_BASELINE =
258
293
  /**
259
294
  * Build the exact plain-object document `contracts/render-policy.jcs`
260
295
  * publishes: `name`, `version` byte-equal to the template lock's
261
- * `contractVersion`, a closed sanitizer/parser catalog, and no self-digest
262
- * (DEC-097 section 5).
296
+ * `contractVersion`, a closed sanitizer/parser catalog, and no self-digest.
263
297
  *
264
298
  * @returns {Record<string, unknown>} the render-policy document
265
299
  */
@@ -267,7 +301,9 @@ export function renderPolicyDocument() {
267
301
  return {
268
302
  name: RENDER_POLICY_NAME,
269
303
  version: RENDER_POLICY_VERSION,
270
- contentSecurityPolicy: CONTENT_SECURITY_POLICY_BASELINE,
304
+ contentSecurityPolicy: CONTENT_SECURITY_POLICY_COMPLETE,
305
+ contentSecurityPolicyMeta: CONTENT_SECURITY_POLICY_META_BASELINE,
306
+ contentSecurityPolicyHeaderOnlyDirectives: [...CSP_HEADER_ONLY_DIRECTIVES],
271
307
  markdownIt: {
272
308
  package: MARKDOWN_IT_PACKAGE,
273
309
  version: MARKDOWN_IT_VERSION,
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * Deterministic route-segment derivation for generated listing pages (tag,
3
- * series, archive) from an authored `plainLabel` (task packet S2-T06:
4
- * navigation renderer/page kinds).
3
+ * series, archive) from an authored `plainLabel`.
5
4
  *
6
5
  * `contentFrontmatterNormalized.tags`/`.series` are `plainLabel` values:
7
6
  * normalized Unicode text, 1..80 graphemes, with no further route-safety
@@ -12,12 +11,12 @@
12
11
  * or series label can therefore contain spaces, uppercase letters, or any
13
12
  * non-Latin script.
14
13
  *
15
- * `route.js` already documents that this renderer's route projection is
16
- * deliberately not DEC-097 section 8's full Unicode-17 percent-encoding and
14
+ * `route.js` already documents that this renderer's route projection
15
+ * deliberately excludes a full Unicode percent-encoding and
17
16
  * collision-resolution engine (out of scope; it only ever projects
18
17
  * already-schema-validated `canonicalRoute` values, never derives a route
19
- * from an arbitrary label). This module makes the same scoped choice for the
20
- * one new case S2-T06 introduces — turning an arbitrary label into a route
18
+ * from an arbitrary label). This module makes the same scoped choice for
19
+ * the one new case it introduces — turning an arbitrary label into a route
21
20
  * segment — and closes the collision risk a lossy ASCII-only slug would
22
21
  * otherwise introduce (two distinct Unicode labels both losing all their
23
22
  * characters to filtering) by always appending a short, deterministic content
@@ -1,16 +1,14 @@
1
1
  /**
2
2
  * Route-to-output-path projection and the byte-exact generated redirect
3
- * document (brief S2 section 3 "Required generated outputs"; DEC-097
4
- * section 6 artifact-manifest redirect subsection).
3
+ * document.
5
4
  *
6
5
  * This module implements the `directory-index` and `explicit-file` cases of
7
- * DEC-097 section 7's `gala-portable-v2` route projection only for routes
8
- * that are already valid `canonicalRoute` strings (schema-enforced before
9
- * this module ever runs). The full Unicode-17 percent-encoding/collision
10
- * engine DEC-097 section 8 describes is out of S2-T03's scope: it governs
11
- * arbitrary repository-relative source paths, which this renderer never
12
- * derives routes from (every route it projects comes from an
13
- * already-schema-validated `canonicalRoute` field in `build-input`).
6
+ * route projection only for routes that are already valid `canonicalRoute`
7
+ * strings (schema-enforced before this module ever runs). A full Unicode
8
+ * percent-encoding/collision engine for arbitrary repository-relative
9
+ * source paths is out of scope here: this renderer never derives routes
10
+ * from source paths — every route it projects comes from an
11
+ * already-schema-validated `canonicalRoute` field in `build-input`.
14
12
  */
15
13
 
16
14
  /**
@@ -75,16 +73,14 @@ export function projectFilePathToRoute(filePath, profile) {
75
73
  }
76
74
 
77
75
  /**
78
- * Project a `basePath` to the one fixed generated error-document output path
79
- * DEC-097's Spaces website-configuration subsection defines: `errorDocumentKey`
80
- * is exactly `404.html` when `basePath` is `/`; otherwise the base path with
81
- * its one leading and terminal slash removed, followed by `/404.html`. This
82
- * is deliberately independent of the selected `RouteNormalizationProfile`
83
- * (`directory-index` vs `explicit-file`): the generated error document is
84
- * always this one literal filename, never route-normalized like an ordinary
85
- * content route (brief S2 section 3: "one ordinary generated `404.html` at
86
- * the selected base path"; DEC-097's Spaces/Pages/local-directory adapters
87
- * all bind their provider error-document configuration to this exact key).
76
+ * Project a `basePath` to the one fixed generated error-document output
77
+ * path: exactly `404.html` when `basePath` is `/`; otherwise the base path
78
+ * with its one leading and terminal slash removed, followed by
79
+ * `/404.html`. This is deliberately independent of the selected
80
+ * `RouteNormalizationProfile` (`directory-index` vs `explicit-file`): the
81
+ * generated error document is always this one literal filename, never
82
+ * route-normalized like an ordinary content route — this fixed key is what
83
+ * each hosting adapter's provider error-document configuration binds to.
88
84
  *
89
85
  * @param {string} basePath the build input's `basePath` (`canonicalRoute`,
90
86
  * always starts with `/`)
@@ -143,7 +139,7 @@ export function escapeRedirectTarget(targetRoute) {
143
139
 
144
140
  /**
145
141
  * Render the exact UTF-8, no-BOM, LF-terminated generated redirect document
146
- * DEC-097 section 6 and brief S2 section 3 specify byte-for-byte.
142
+ * this renderer must produce byte-for-byte.
147
143
  *
148
144
  * @param {string} targetRoute the route this redirect document points to
149
145
  * @returns {string} the complete document text
@@ -1,12 +1,10 @@
1
1
  /**
2
- * Static search index generation (task packet S2-T08; brief S2 section 3
3
- * "Required generated outputs": "static search index").
2
+ * Static search index generation.
4
3
  *
5
- * Scope note (documented, not silently assumed): neither the brief text this
6
- * task read nor DEC-097 fixes an exact search-index document shape (checked
7
- * with a full-text grep of both for "search" — no hit beyond this brief
8
- * phrase). This module therefore documents its own deterministic shape here,
9
- * matching this renderer's established style for an unfixed structural
4
+ * Scope note (documented, not silently assumed): this renderer has no
5
+ * externally fixed search-index document shape to conform to. This module
6
+ * therefore documents its own deterministic shape here, matching this
7
+ * renderer's established style for an unfixed structural
10
8
  * decision (see `internal/route-labels.js`'s route-segment grammar and the
11
9
  * media pipeline scope decisions in the package README): one JSON document,
12
10
  * `{schemaVersion, generatedAt, publication, documents[]}`, sorted by each
@@ -15,17 +13,17 @@
15
13
  * `internal/source-inventory.js#compareUtf8Bytes`).
16
14
  *
17
15
  * `generatedAt` is `buildInput.buildEpoch` — a build-input *field*, sourced
18
- * deterministically from the selected Git commit's own committer timestamp
19
- * (DEC-097 section 5), not read from the wall clock at build time — matching
16
+ * deterministically from the selected Git commit's own committer timestamp,
17
+ * not read from the wall clock at build time — matching
20
18
  * `src/core/manifest.js`'s own `manifest.generatedAt` convention, so two
21
19
  * clean builds of the same `build-input` still produce byte-identical index
22
20
  * bytes.
23
21
  *
24
- * No runtime search JavaScript accompanies this document (brief S2 section
25
- * 1: "reads completely with JavaScript disabled"; acceptance test C: "the
26
- * appearance controller is the only browser bootstrap" — this renderer's one
27
- * admitted client-side script in all of S2). This module's only output is
28
- * the index document's bytes; nothing wires a `<script>` to it.
22
+ * No runtime search JavaScript accompanies this document: this renderer
23
+ * must read completely with JavaScript disabled, and the appearance
24
+ * controller is the only client-side script it ever admits. This module's
25
+ * only output is the index document's bytes; nothing wires a `<script>` to
26
+ * it.
29
27
  *
30
28
  * Only actual authored content (`article`/`page` records, `status:
31
29
  * "published"`) is indexed — never a synthetic listing page
@@ -0,0 +1,84 @@
1
+ /**
2
+ * A closed subset of npm-compatible SemVer range satisfaction (TPL-H1), used
3
+ * only to check a theme package's own declared `theme.json.templateRange`
4
+ * against this renderer's published version. This is deliberately not a
5
+ * general-purpose SemVer range engine: `@rathnasgala2/schemas`'
6
+ * `theme-contract.schema.json` `templateRange` field (`$defs/semverRange`)
7
+ * admits an open-ended npm range grammar, but every legitimate value this
8
+ * closed-contract ecosystem actually emits is either an exact version or a
9
+ * caret range — the same two forms `package.json` dependency pins in this
10
+ * very repository use — so this module supports exactly those two forms and
11
+ * fails closed (rejects) on anything else, exactly like every other closed
12
+ * catalog in this repository (`nthExpressionProfile`, the pseudo-class
13
+ * catalog, `functionalSelectorArguments`, ...).
14
+ */
15
+
16
+ const EXACT_VERSION_PATTERN = /^(\d+)\.(\d+)\.(\d+)$/;
17
+ const CARET_RANGE_PATTERN = /^\^(\d+)\.(\d+)\.(\d+)$/;
18
+
19
+ /**
20
+ * @param {string} version a canonical `major.minor.patch` SemVer string
21
+ * @returns {[number, number, number]} the parsed components
22
+ */
23
+ function parseExactVersion(version) {
24
+ const match = EXACT_VERSION_PATTERN.exec(version);
25
+ if (!match) {
26
+ throw new Error(`not a canonical major.minor.patch version: ${version}`);
27
+ }
28
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
29
+ }
30
+
31
+ /**
32
+ * @param {[number, number, number]} a
33
+ * @param {[number, number, number]} b
34
+ * @returns {number} negative, zero or positive per standard comparator
35
+ * contract
36
+ */
37
+ function compareVersions(a, b) {
38
+ for (let index = 0; index < 3; index += 1) {
39
+ if (a[index] !== b[index]) return a[index] - b[index];
40
+ }
41
+ return 0;
42
+ }
43
+
44
+ /**
45
+ * Whether `version` satisfies `range`, supporting only an exact
46
+ * `major.minor.patch` range (equality) or a caret range `^major.minor.patch`
47
+ * (matches `>=major.minor.patch <(major+1).0.0`, or, for a `0.x.y` caret
48
+ * range, npm's own tightened `0.x` semantics: `>=0.minor.patch <0.(minor+1).0`).
49
+ *
50
+ * @param {string} version this renderer's own exact published version
51
+ * @param {string} range the theme's declared `templateRange`
52
+ * @returns {boolean} whether `version` satisfies `range`
53
+ * @throws {Error} when `range` is not one of the two closed forms this
54
+ * module admits
55
+ */
56
+ export function satisfiesTemplateRange(version, range) {
57
+ const parsedVersion = parseExactVersion(version);
58
+ const exactMatch = EXACT_VERSION_PATTERN.exec(range);
59
+ if (exactMatch) {
60
+ return compareVersions(parsedVersion, parseExactVersion(range)) === 0;
61
+ }
62
+ const caretMatch = CARET_RANGE_PATTERN.exec(range);
63
+ if (caretMatch) {
64
+ const lower = /** @type {[number, number, number]} */ ([
65
+ Number(caretMatch[1]),
66
+ Number(caretMatch[2]),
67
+ Number(caretMatch[3]),
68
+ ]);
69
+ if (compareVersions(parsedVersion, lower) < 0) return false;
70
+ const [major, minor] = lower;
71
+ if (major > 0) {
72
+ return parsedVersion[0] === major;
73
+ }
74
+ if (minor > 0) {
75
+ return parsedVersion[0] === 0 && parsedVersion[1] === minor;
76
+ }
77
+ // ^0.0.z: npm pins this to the exact patch version.
78
+ return compareVersions(parsedVersion, lower) === 0;
79
+ }
80
+ throw new Error(
81
+ `templateRange must be an exact "major.minor.patch" version or a ` +
82
+ `"^major.minor.patch" caret range, got ${JSON.stringify(range)}`,
83
+ );
84
+ }