@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.
- package/README.md +204 -137
- package/contracts/render-policy.jcs +1 -1
- package/contracts/theme-styling-contract.jcs +1 -1
- package/package.json +4 -5
- package/src/core/errors.js +13 -13
- package/src/core/index.js +102 -66
- 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 +10 -13
- 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 +46 -48
- 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 +16 -20
- package/src/core/internal/search-index.js +12 -14
- package/src/core/internal/semver-range.js +84 -0
- package/src/core/internal/seo.js +21 -23
- package/src/core/internal/sitemap.js +6 -8
- package/src/core/internal/skeleton.js +42 -39
- 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 +294 -67
- package/src/core/internal/uuid.js +4 -5
- package/src/core/manifest.js +0 -0
- package/types/index.d.ts +22 -8
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Page-kind assembly
|
|
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
|
|
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` (
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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:
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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
|
|
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
|
-
*
|
|
67
|
-
* default with no author-facing configuration surface
|
|
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
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* private/incidental selector
|
|
78
|
-
*
|
|
79
|
-
*
|
|
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 (
|
|
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
|
|
139
|
-
*
|
|
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]
|
|
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]
|
|
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
|
-
*
|
|
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
|
|
174
|
+
* only it has run the media pipeline and knows the derivative
|
|
176
175
|
* output path a given `sourceDigest` produced)
|
|
177
|
-
* @property {string} [robotsContent]
|
|
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]
|
|
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
|
|
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
|
|
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" (
|
|
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>`"
|
|
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
|
|
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
|
-
*
|
|
688
|
-
*
|
|
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
|
-
*
|
|
4
|
-
*
|
|
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
|
|
21
|
-
*
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
171
|
-
*
|
|
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
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
16
|
-
* deliberately
|
|
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
|
|
20
|
-
* one new case
|
|
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
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
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
|
-
*
|
|
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
|
|
3
|
-
* "Required generated outputs": "static search index").
|
|
2
|
+
* Static search index generation.
|
|
4
3
|
*
|
|
5
|
-
* Scope note (documented, not silently assumed):
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
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
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
+
}
|