@rathnasgala2/template 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +515 -0
  3. package/contracts/render-policy.jcs +1 -0
  4. package/contracts/theme-styling-contract.jcs +1 -0
  5. package/package.json +96 -0
  6. package/src/core/errors.js +136 -0
  7. package/src/core/index.js +758 -0
  8. package/src/core/internal/appearance/bootstrap-script.js +159 -0
  9. package/src/core/internal/appearance/contract.js +193 -0
  10. package/src/core/internal/appearance/controller-markup.js +80 -0
  11. package/src/core/internal/appearance/styling-contract.js +509 -0
  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 +220 -0
  15. package/src/core/internal/feeds.js +161 -0
  16. package/src/core/internal/fs-walk.js +38 -0
  17. package/src/core/internal/media/avif-probe.js +169 -0
  18. package/src/core/internal/media/font-policy.js +56 -0
  19. package/src/core/internal/media/jpeg-codec.js +0 -0
  20. package/src/core/internal/media/limits.js +129 -0
  21. package/src/core/internal/media/pipeline.js +353 -0
  22. package/src/core/internal/media/png-codec.js +406 -0
  23. package/src/core/internal/media/resize.js +147 -0
  24. package/src/core/internal/media/sniff.js +109 -0
  25. package/src/core/internal/media/webp-probe.js +139 -0
  26. package/src/core/internal/messages.js +78 -0
  27. package/src/core/internal/page-kinds.js +709 -0
  28. package/src/core/internal/provenance.js +49 -0
  29. package/src/core/internal/render-policy-content.js +295 -0
  30. package/src/core/internal/route-labels.js +68 -0
  31. package/src/core/internal/route.js +167 -0
  32. package/src/core/internal/search-index.js +119 -0
  33. package/src/core/internal/seo.js +114 -0
  34. package/src/core/internal/sitemap.js +77 -0
  35. package/src/core/internal/skeleton.js +413 -0
  36. package/src/core/internal/source-inventory.js +145 -0
  37. package/src/core/internal/text-direction.js +59 -0
  38. package/src/core/internal/theme-assets.js +387 -0
  39. package/src/core/internal/uuid.js +51 -0
  40. package/src/core/manifest.js +0 -0
  41. package/types/eleventy-plugin-syntaxhighlight.d.ts +26 -0
  42. package/types/eleventy.d.ts +38 -0
  43. package/types/index.d.ts +347 -0
package/README.md ADDED
@@ -0,0 +1,515 @@
1
+ # `@rathnasgala2/template`
2
+
3
+ The deterministic Eleventy-backed static publication renderer for Galascribe's
4
+ portable author repository format. It is one of the three author-facing S2
5
+ artifacts, alongside one `@rathnasgala2/theme-*` package and
6
+ `@rathnasgala2/publish-action`.
7
+
8
+ This package is JavaScript ESM on Node 24, authored without TypeScript sources
9
+ (DEC-094): every export carries JSDoc types, checked by `tsc --checkJs --noEmit`
10
+ against a hand-maintained `.d.ts` surface.
11
+
12
+ ## Status
13
+
14
+ S2-T02's repository scaffold (exact pins, `src/core/`-only layout, module-tree
15
+ absence gate, quality-gate tooling) plus S2-T03's renderer adapter:
16
+ `renderPublication(buildInput, options)`, exported from `src/core/index.js`, is
17
+ the package's one documented entry point. It:
18
+
19
+ 1. validates a candidate `urn:gala:schema:build-input:2.0.0` document with
20
+ `@rathnasgala2/schemas`' `validateGalaDocument` and fails closed
21
+ (`BuildInputValidationError`, before anything is written) on an invalid one;
22
+ 2. renders the publication profile (when selected) and every selected content
23
+ record, plus every authored static redirect document, through a single
24
+ confined, single-pass programmatic Eleventy 3.1.6 build behind
25
+ `src/core/internal/eleventy-render.js` — the only module in this repository
26
+ permitted to import `@11ty/eleventy`, so no Eleventy object, plug-in API,
27
+ config object or Nunjucks environment ever crosses back out of it; and
28
+ 3. assembles, digests and schema-validates a closed
29
+ `urn:gala:schema:artifact-manifest:2.0.0` instance describing the candidate
30
+ output directory.
31
+
32
+ S2-T05 adds the media pipeline under `src/core/internal/media/`, run from
33
+ `renderPublication` after the Eleventy route listing so its output is never
34
+ mistaken for an HTML route:
35
+
36
+ - **Bounded decode.** JPEG (via the pinned pure-JS `jpeg-js` decoder, no native
37
+ addon or WebAssembly) and PNG (hand-written, using Node's built-in `zlib` with
38
+ a decompression-bomb-bounded inflate) are fully decoded to RGBA8; WebP and
39
+ AVIF get bounded container/declared-dimension validation without a full
40
+ entropy decode (a documented scope decision — see
41
+ `webp-probe.js`/`avif-probe.js`). Every format shares one set of resource
42
+ ceilings in `limits.js` (raster dimension, pixel count, decoded-buffer and
43
+ source-byte caps reused from DEC-097's own closed binary-asset decoder bounds,
44
+ plus this renderer's own documented per-publication ceilings).
45
+ - **Metadata strip, orientation.** PNG/JPEG derivatives are always re-encoded
46
+ from decoded pixels, never byte-copied, so no ancillary/EXIF metadata survives
47
+ into a derivative; JPEG's EXIF `Orientation` tag is read and applied
48
+ (rotate/flip) before any derivative is produced.
49
+ - **Deterministic responsive derivatives.** Content-addressed
50
+ `assets/media/<sourceDigest>/{original,<width>w}.<ext>` files at fixed widths
51
+ `320/640/960/1280/1920` (below the source width), re-encoded with fixed,
52
+ pinned encoder settings so two clean builds of the same source produce
53
+ byte-identical derivative files.
54
+ - **SVG rejection.** Any byte sequence recognized as SVG/XML is rejected
55
+ unconditionally as an image reference (`MEDIA_SVG_REJECTED`) — SVG is never
56
+ processed as author media.
57
+ - **Font policy.** `appearance.fontAssets` entries must be bounded WOFF2 (exact
58
+ signature, DEC-097's 4,194,304-byte font cap); a remote font reference cannot
59
+ enter `build-input` in the first place (`resolvedFile` is always a
60
+ repository-relative path), so "no remote font is ever retrieved" holds
61
+ structurally.
62
+ - **Fail-closed errors.** Every rejection — oversize, decompression bomb, digest
63
+ mismatch, SVG, unrecognized format, invalid font — is a `MediaPipelineError`
64
+ with a stable `reasonCode`, thrown before the manifest is assembled.
65
+
66
+ `options.sourceDirectory` (new in S2-T05) is the caller-mounted, read-only
67
+ repository source tree `build-input`'s `resolvedFile` references point into;
68
+ every referenced file's bytes are read from there and verified against
69
+ `build-input`'s declared `sourceDigest` before being decoded.
70
+
71
+ S2-T04 adds the complete output-security pipeline
72
+ (`src/core/internal/content-security.js`) on top of that adapter. **Division of
73
+ labour** (DEC-097 section 5: normalization replaces an authored body path with a
74
+ `renderableBody` carrying "the deterministic policy output/digest";
75
+ `build-input.schema.json` fixes `renderableBody.bodyMediaType` as
76
+ `const: "text/html"`): a `renderableBody.body` inside a validated
77
+ `build-input:2.0.0` instance is **already** render-policy-conformant HTML,
78
+ produced once upstream of this renderer. This package therefore exposes two
79
+ distinct public entry points:
80
+
81
+ - **`normalizeAuthoredMarkdown(markdownSource) -> { html, bodyDigest }`**
82
+ (exported from `src/core/index.js`) is the pipeline an upstream
83
+ `build-input:2.0.0` producer (`publish-kernel`/`publish-action`) runs exactly
84
+ once per authored Markdown source: `markdown-it` 14.3.1 configured exactly
85
+ `{ html: false, linkify: false, typographer: false }`, with a closed
86
+ URL-scheme allowlist (`https`, `http`, `mailto`, or a schemeless relative
87
+ path/fragment — rejecting `javascript:`, `data:`, `blob:`, `vbscript:` and
88
+ protocol-relative URLs, including WHATWG-style tab/newline scheme obfuscation)
89
+ and deterministic, collision-free heading `id`s; fenced code through an owned
90
+ highlighter over `@11ty/eleventy-plugin-syntaxhighlight` 5.0.2 (only a closed
91
+ grammar catalog — `bash`, `css`, `diff`, `java`, `javascript`, `json`, `jsx`,
92
+ `markdown`, `markup`, `python`, `sql`, `tsx`, `typescript`, `yaml` — is ever
93
+ highlighted; every other fence language falls back to escaped, unhighlighted
94
+ code); then `sanitize-html` 2.17.7 against a closed tag/attribute/class/scheme
95
+ allowlist (no `style`, no `on*`, no `srcdoc`, no `id` outside generated
96
+ heading IDs, no script/style/iframe/object/embed/ svg/math/template content)
97
+ and a fail-closed residual-active-content scan.
98
+ - **`renderPublication`** (the renderer itself) never runs Markdown parsing on a
99
+ `renderableBody.body` it receives — re-parsing already-rendered HTML under
100
+ `html:false` would double-escape every tag (`<p>` becoming `&lt;p&gt;`).
101
+ Instead it verifies, before any directory is created: every
102
+ `renderableBody.renderPolicy` (publication profile, footer card, every content
103
+ record) byte-equals
104
+ `SHA256(UTF8("GALA-RENDER-POLICY-V2\0") || <contract file bytes>)`; every
105
+ `bodyDigest` byte-equals a plain SHA-256 of its `body` bytes; and
106
+ re-sanitizing `body` reproduces it byte-for-byte (an idempotence check —
107
+ genuinely policy-conformant output cannot change under its own sanitizer). Any
108
+ mismatch, or a non-empty `modules`/`placements` value, fails closed with
109
+ `RenderPolicyViolationError` before anything is written. A verified body is
110
+ then inserted into the page verbatim.
111
+
112
+ `contracts/render-policy.jcs` is the real, closed sanitizer/parser catalog
113
+ (name, version, CSP baseline, markdown-it options, sanitizer allowlist,
114
+ highlighter grammar/token-class catalog) both surfaces above read from,
115
+ generated deterministically by `scripts/generate-contracts.mjs` from
116
+ `src/core/internal/render-policy-content.js` — the single source of truth, so
117
+ the published contract and the enforced policy cannot drift apart. Every
118
+ rendered page also carries the exact per-artifact CSP baseline as a
119
+ `<meta http-equiv="Content-Security-Policy">` tag (byte-identical on every
120
+ route, since S2 materializes no module package/configuration/output/runtime).
121
+
122
+ `img[src]` is restricted to repository-relative paths only in this pipeline;
123
+ full manifest-coverage verification of media references is the S2-T05 media
124
+ pipeline's job (immediately above). A handful of manifest fields (`builder`,
125
+ `sourceIdentity.repository`, `workflowIdentity`, `buildToolVersions`,
126
+ `excludedInputs`) describe facts `build-input` does not itself carry;
127
+ `renderPublication` accepts them as an explicit `options.provenance` bundle
128
+ rather than fabricating them — see `src/core/manifest.js`'s module
129
+ documentation. `contracts/theme-styling-contract.jcs` is S2-T12's real, closed
130
+ `templateStylingContract` catalog (see the S2-T12 section below).
131
+
132
+ S2-T06 adds the core semantic skeleton, page kinds and navigation renderer on
133
+ top of the S2-T04 output-security pipeline every generated page still passes
134
+ through:
135
+
136
+ - **`src/core/internal/skeleton.js`** renders the one landmark contract every
137
+ page kind shares, through its one `renderPageBody` composition: a skip link
138
+ (the first focusable element, targeting `#main-content`), one `<header>` (home
139
+ link with the validated publication name, plus the always-present
140
+ `header-actions` slot), one primary `<nav>` from `navigationNormalized.items`,
141
+ one `<main id="main-content">` (an optional breadcrumb `<nav>` before the
142
+ page's own single `<h1>`), and one `<footer>` (publication name, footer
143
+ navigation, the `footer-profile` slot, the remaining core slots, and the
144
+ template attribution line). Core owns nine versioned semantic slots
145
+ (`header-actions`, `article-preamble`, `article-end`, `footer-profile`,
146
+ `footer-auxiliary`, `account-intent`, `conversation`, `newsletter`,
147
+ `edition-selector`) plus the collapsed `article-footer-ad` slot (DEC-060):
148
+ every slot is one `data-gala-slot="<name>"` attribute on an otherwise empty,
149
+ non-landmark `<div>`, and the collapsed slot additionally carries `hidden`. S2
150
+ has no module system, so every slot except `footer-profile` (which always
151
+ wraps the publication's own footer card) and `header-actions` (S2-T07's
152
+ appearance control, core content rather than a module) renders empty. This
153
+ module also renders breadcrumbs and deterministic pagination controls
154
+ (`rel="prev"`/`rel="next"`, an `aria-current="page"` status).
155
+ - **`src/core/internal/page-kinds.js`** builds every generated page kind from a
156
+ validated `build-input:2.0.0`: `profile` (the optional publication profile),
157
+ `author` (one page per `build-input.authors` entry, at `/authors/<id>`),
158
+ `article`/`page` (one page per `content[]` record, at its own route), `index`
159
+ (a paginated reverse-chronological listing of every `status: "published"`,
160
+ `kind: "article"` record — an `unlisted` record is reachable only by its own
161
+ direct route, never through a generated listing), `tag`/`series` (one listing
162
+ page per distinct authored value, plus a `/tags`/`/series` root page linking
163
+ to each), and `archive` (one listing page per UTC calendar year, plus a
164
+ `/archive` root page). The `error` page kind (`renderErrorPageBody`) is
165
+ exported as a pure function for a future `404.html` caller (S2-T08, out of
166
+ this task's scope), not wired into any generated route here. Listing page size
167
+ (10), sort tie-break, and the tag/series/archive root pages are documented
168
+ scoped decisions the brief text this task read does not fix verbatim.
169
+ - **`src/core/internal/messages.js`** is the message catalog every user-visible
170
+ chrome string above is looked up from (never an inline literal), keyed by
171
+ BCP-47 tag with a fixed `en` fallback — the only catalog S2 authors;
172
+ additional locales are future work that changes no call site.
173
+ - **`src/core/internal/text-direction.js`** derives a page's `dir` attribute
174
+ from its own BCP-47 language tag (`build-input:2.0.0` carries no separate
175
+ direction field) against a closed right-to-left primary-language-subtag table
176
+ — the same approach `Intl.Locale`'s `textInfo.direction` uses, reimplemented
177
+ as a small static table to add no runtime dependency.
178
+ - **`src/core/internal/route-labels.js`** derives a deterministic,
179
+ collision-free route segment for an arbitrary tag/series label (a
180
+ `plainLabel`, not a route-safe `slug`): an ASCII-readable prefix plus a
181
+ content-hash suffix, so two distinct Unicode labels can never collide on the
182
+ same generated route.
183
+
184
+ Chrome text is resolved once, in the publication's own `defaultLanguage`, for
185
+ every page (a scoped decision: navigation/footer text does not flip language
186
+ page to page); each page's own `<html lang>`/`dir` still reflects that
187
+ individual page's own content language.
188
+
189
+ S2-T07 adds the Light/Dark/System appearance controller
190
+ (`src/core/internal/appearance/`), the only browser bootstrap S2 emits (brief S2
191
+ section 3, "Module absence and CSP equality"):
192
+
193
+ - **`contract.js`** is the one module every other appearance-related file
194
+ imports every name/value from: the two DEC-097-fixed root attributes
195
+ (`data-gala-publication-root` presence, `data-gala-resolved-color-mode`
196
+ exact-value `"light"`/`"dark"`), the template-internal
197
+ `data-gala-color-mode-selection` bookkeeping attribute, the three canonical
198
+ mode values (`light`/`dark`/`system`), the versioned local storage key
199
+ (`gala:appearance:color-mode:v1`), the control's `id`, the bootstrap script's
200
+ fixed output path/media type and the `color-scheme` meta content — the
201
+ contract S2-T12's published theme styling catalog targets.
202
+ - **`controller-markup.js`** renders a native `<select>` (a brief-admitted
203
+ pattern; needs no `aria-pressed`/`aria-checked` bookkeeping of its own, unlike
204
+ a custom button-group or radio-group would) bound to a visible `<label>`, with
205
+ message-catalog labels, inserted into the header's `header-actions` slot. Its
206
+ default selected option is always `system`; the bootstrap script's own
207
+ `DOMContentLoaded` phase corrects `select.value` to the actual stored/resolved
208
+ value once `<body>` exists.
209
+ - **`bootstrap-script.js`** is the exact deterministic pre-paint bootstrap
210
+ (every literal from `contract.js`, so it and the server-rendered control can
211
+ never drift): a synchronous phase, run the instant this blocking,
212
+ non-`defer`/non-`async`/non-`module` `<script src>` executes in `<head>` —
213
+ before `<body>` is even parsed — that reads the stored selection (falling back
214
+ to `system` on a missing/invalid/unreadable value; a `localStorage` read/write
215
+ failure is always caught, so a blocked or private-mode session stays fully
216
+ usable) and resolves it against `prefers-color-scheme` (`system` resolves to
217
+ `light` when unavailable), then applies both root attributes. It never touches
218
+ `document.documentElement.style` (no hide-until-ready trick of any kind), no
219
+ network, no cookie, no account/tracking identifier, and no storage key other
220
+ than its own. A `DOMContentLoaded`-deferred phase wires the control's `change`
221
+ event and a live `prefers-color-scheme` subscription that only updates the
222
+ resolved attribute while the current selection is still exactly `system` (an
223
+ explicit `light`/`dark` choice is never overridden by a later system change).
224
+ - `src/core/index.js` writes the bootstrap script into the candidate output
225
+ directory and enters it into the manifest as an ordinary `manifestAsset` row
226
+ (`{path, byteLength, sha256}` plus its fixed
227
+ `application/javascript; charset=utf-8` media type), after the Eleventy route
228
+ listing — exactly like an S2-T05 media derivative — so it is never mistaken
229
+ for an HTML route. `src/core/internal/eleventy-render.js`'s skeleton layout
230
+ carries the `data-gala-publication-root` attribute, the
231
+ `<meta name="color-scheme" content="light dark">` tag and the one
232
+ `<script src>` reference, all fixed literals from `contract.js`, so every
233
+ route's `<head>` is still byte-identical (S2 materializes no module
234
+ package/configuration/output/runtime).
235
+ - With JavaScript disabled, the control still renders (a native `<select>`,
236
+ fully keyboard-operable and usable with no script at all) and the resolved
237
+ root attribute is simply never set; `<meta name="color-scheme">` alone already
238
+ carries `prefers-color-scheme` through to user-agent styling and the initial
239
+ paint independent of any script.
240
+
241
+ S2-T12 replaces `contracts/theme-styling-contract.jcs`'s S2-T02 scaffold
242
+ placeholder with the real, closed `templateStylingContract` catalog DEC-097
243
+ section 4 defines, and wires the selected theme package's stylesheets into every
244
+ generated page:
245
+
246
+ - **`src/core/internal/appearance/styling-contract.js`** is the one reviewed
247
+ source module `scripts/generate-contracts.mjs` emits the contract from: the
248
+ ordered four-layer catalog (`gala-tokens`/`gala-components`/`gala-utilities`/
249
+ `gala-print`), the publication-root/resolved-palette selectors, the closed
250
+ type/class/id/attribute leaf catalogs, and exactly 64 `publicThemeSlotHooks` —
251
+ one per catalog leaf, so every leaf a theme could validly select is a named,
252
+ documented hook and no catalog member is orphaned. Every leaf is drawn from
253
+ what this renderer actually renders: 28 type-selector hooks (every
254
+ landmark/prose/code/control element `internal/skeleton.js`,
255
+ `internal/page-kinds.js` and the markdown-it CommonMark pipeline can produce),
256
+ 15 class-selector hooks (the base Prism `.token` class plus one
257
+ `.language-<grammar>` hook per admitted highlight grammar — fine-grained
258
+ per-token-kind classes are a documented S2 scope exclusion), 2 id-selector
259
+ hooks (`#main-content`, the appearance `<select>`'s fixed id) and 19
260
+ attribute-value hooks (one per `data-gala-slot` value, one per the new
261
+ `data-gala-page-kind` value). `catalogDigest` is
262
+ `SHA256(UTF8("GALA-TEMPLATE-STYLING-CONTRACT-V2\0") || JCS(...))`, matching
263
+ DEC-097 section 8 and the digest profile `@rathnasgala2/schemas`' own internal
264
+ `templateStylingContract` profile uses (that function is not exported through
265
+ the package's public API, so this module reimplements the same
266
+ domain-separated formula rather than importing it).
267
+ `assertTemplateStylingContractShape` is this repository's own structural
268
+ self-validator (root/palette attribute rows, fixed layer/selector/composition
269
+ constants, hook count/uniqueness bound, `catalogDigest` recomputation); no
270
+ `urn:gala:schema:template-styling-contract` schema ID is registered in
271
+ `@rathnasgala2/schemas@2.0.0` for `validateGalaDocument` to check this object
272
+ against directly.
273
+ - **`data-gala-page-kind`** (`internal/page-kinds.js`) is a new template-owned
274
+ semantic attribute on every generated page's `<body>`, one exact value per
275
+ `PAGE_KIND_VALUES` entry (`archive`/`article`/`author`/`error`/`index`/
276
+ `page`/`profile`/`series`/`tag`), so a theme can vary presentation by page
277
+ kind without promoting a private selector.
278
+ - **`test/theme-styling-contract.test.js`** is the drift gate: it renders a rich
279
+ fixture whose one extra article exercises every markdown-derived hook (every
280
+ heading level, every admitted highlight grammar, lists, blockquote, emphasis,
281
+ a link and an image) and asserts, in both directions, that every published
282
+ hook is actually rendered and every rendered hook-like construct
283
+ (`data-gala-*` attribute, `<pre>`/`<code>` class, or element tag inside
284
+ `<body>`) is published. It also validates two `theme-contract:2.0.0` fixtures
285
+ — one light-dominant, one dark-dominant palette — with
286
+ `@rathnasgala2/schemas`' own exported `validateGalaDocument`.
287
+ - **`src/core/internal/theme-assets.js`** is the optional theme-asset
288
+ integration layer: given `options.themeDirectory` (an extracted theme package
289
+ directory, shaped exactly like the S2-T13 package file set — `theme.json`,
290
+ `tokens.css`, `components.css`, an optional `utilities.css`, `print.css`,
291
+ `package.json`, `LICENSE`, `README.md`, `assets/*`), it copies the declared
292
+ stylesheets and any declared non-CSS passive assets into `assets/theme/`,
293
+ enters each as an ordinary `{path, byteLength, sha256}` `manifestAsset` row,
294
+ and renders the ordered `<link>` markup every generated page's `<head>`
295
+ inserts (`print.css` alone carries `media="print"`). It validates the theme's
296
+ own declared `stylesheets` list against the two admitted shapes (with or
297
+ without `utilities.css`) and that `cssLayers` byte-equals the corresponding
298
+ layer projection, but does **not** re-implement DEC-097's full CSS Syntax
299
+ Module admission grammar — that is the shared `theme-release.yml` conformance
300
+ runner's job (S2-T11), run once per theme release, never per publication
301
+ build. `renderPublication` falls back to the pre-existing fixed
302
+ `assets/theme/print.css` link when no `options.themeDirectory` is supplied
303
+ (every caller before S2-T12, and this repository's own tests that do not
304
+ exercise theme integration). Minimal in-repo fixture theme packages for both
305
+ stylesheet-list shapes live under `test/fixtures/theme-fixture-full/` and
306
+ `test/fixtures/theme-fixture-minimal/` — deliberately not real, conformant
307
+ theme CSS (authoring one is S2-T13's task); they exist only to exercise the
308
+ copy/link/manifest mechanism.
309
+ - **basePath fix (LOCAL-7 follow-up; independent-review finding B2).** Before
310
+ S2-T12, the appearance bootstrap script's `<script src>` and the social-image
311
+ `og:image`/`twitter:image` URL were both root-absolute, unprefixed by the
312
+ publication's own `basePath` — a non-root `basePath` publication would 404
313
+ them. `internal/appearance/contract.js`'s new
314
+ `appearanceBootstrapScriptHref(basePath)` and `internal/seo.js`'s
315
+ `resolveSocialImageUrl` (now taking `basePath`) both join through
316
+ `internal/route.js`'s shared `joinBasePathAndRoute`/`projectFixedAssetPath`,
317
+ exactly like the pre-existing print-stylesheet convention (now folded into
318
+ `internal/theme-assets.js`) already did for _hrefs_. Independent review
319
+ (finding B2) found that this was only half the fix: the _physical files_ this
320
+ renderer writes for every asset class — theme CSS/passive assets, every S2-T05
321
+ media derivative, and the S2-T07 appearance bootstrap script — were still
322
+ written, and entered into the manifest, at their bare **unprefixed** paths.
323
+ Since the local-directory adapter serves a `manifestAsset.path` verbatim, a
324
+ non-root `basePath` publication's `<link>`/`<script src>`/`og:image` href
325
+ would point at `<basePath>/assets/...` while the actual file sat at the
326
+ unprefixed `assets/...` — a 404 in practice, not just a theoretical href
327
+ mismatch. `src/core/index.js` now joins `basePath` into the physical
328
+ destination path and the `manifestAsset.path` for media derivatives
329
+ (`joinedMediaAssets`, computed with the same `projectFixedAssetPath` every
330
+ other fixed-name generated asset already uses) and the appearance bootstrap
331
+ script (`joinedAppearanceScriptPath`); `internal/theme-assets.js` computes its
332
+ joined output path once per file and derives both the `<link href>` and the
333
+ file/manifest path from that exact same value, so they cannot drift apart.
334
+ `test/theme-styling-contract.test.js` carries a dedicated non-root,
335
+ multi-segment `basePath` (`/blog/2024`) fixture test asserting every one of
336
+ these hrefs (media, appearance script, theme CSS, print CSS) is consistently
337
+ joined **and** that the physical file for one asset of each class actually
338
+ exists at that exact joined path.
339
+
340
+ - **Theme-asset path containment (independent-review finding B1).** A theme
341
+ package's `theme.json` `stylesheets`/`assets` paths are untrusted input from
342
+ the selected theme package, not fixed literals. `internal/theme-assets.js`'s
343
+ `assertSafeThemeRelativePath` rejects an absolute path, a backslash, a non-NFC
344
+ path, and any empty/`.`/`..` segment, before the file is ever opened;
345
+ `assertContainedThemePath` then walks every path component under
346
+ `themeDirectory` with `lstat`, rejecting a symlink at any component (so a
347
+ symlink planted inside the theme directory cannot be used to escape it), and
348
+ verifies the fully resolved path still starts inside the resolved
349
+ `themeDirectory`. Only paths this module reads directly off `theme.json`'s own
350
+ declared arrays are ever opened or copied, so a file physically present in
351
+ `themeDirectory` but never declared there can never be copied into the
352
+ candidate output directory. `test/theme-styling-contract.test.js` has
353
+ dedicated path-containment tests for `../` traversal, an absolute path, a
354
+ backslash-separated path, a symlink escaping the theme directory, and an
355
+ undeclared file never being copied.
356
+
357
+ ### Media pipeline scope decisions (documented, not silently assumed)
358
+
359
+ Neither the S2 brief nor DEC-097 fixes exact derivative widths, output
360
+ formats/quality, or a per-publication media byte/count ceiling for _author_
361
+ media (DEC-097's own numeric bounds are stated for the closed
362
+ `gala-theme-binary-assets-v2` _theme passive-asset_ profile). This renderer
363
+ therefore documents its own choices in `src/core/internal/media/limits.js`
364
+ rather than inventing undocumented magic numbers:
365
+
366
+ - decode/resource ceilings reuse DEC-097's theme binary-asset numbers
367
+ (dimension, pixel-count, decoded-buffer, source-byte caps), since the
368
+ decode-bomb risk they defend against is identical regardless of asset class;
369
+ - responsive derivative widths (`320/640/960/1280/1920`), JPEG derivative
370
+ quality (`82`), and the per-publication image/font count and aggregate byte
371
+ ceilings are this renderer's own conservative choices;
372
+ - WebP and AVIF get bounded container/dimension validation but no full
373
+ VP8/VP8L/AV1 entropy decode or derivative generation — hand-authoring an
374
+ independently verifiable decoder for either format was judged out of
375
+ proportion to this task, and is flagged as a follow-up rather than attempted
376
+ partially.
377
+
378
+ S2-T08 adds every remaining "Required generated output" from brief S2 section 3
379
+ on top of S2-T06's page kinds:
380
+
381
+ - **Feeds** (`src/core/internal/feeds.js`): deterministic Atom 1.0
382
+ (`/feed/atom.xml`) and RSS 2.0 (`/feed/rss.xml`) documents over the exact
383
+ reverse-chronological `status: "published"`, `kind: "article"` selection
384
+ `internal/page-kinds.js`'s `index` page kind paginates (the shared
385
+ `selectPublishedArticles` export), every item URL absolute against
386
+ `buildInput.baseUrl`, bounded to the most recent 50 entries (documented scoped
387
+ decision — neither the brief nor DEC-097 fixes a feed item count).
388
+ - **Sitemap** (`src/core/internal/sitemap.js`): `/sitemap.xml` over every page
389
+ `internal/page-kinds.js` generated that this renderer did not itself mark
390
+ `noindex` (unlisted content), deterministically sorted by each entry's own
391
+ absolute URL, with `<lastmod>` sourced from each page's own authored
392
+ `publishedAt`/`updatedAt` — never build time.
393
+ - **Static search index** (`src/core/internal/search-index.js`):
394
+ `/search-index.json`, a deterministic JSON document indexing every published
395
+ `article`/`page` record's title, description, a bounded plain-text excerpt,
396
+ tags and timestamps. Its shape is a documented scoped decision (neither the
397
+ brief nor DEC-097 fixes one). **No runtime search JavaScript accompanies it**
398
+ — the Light/Dark/System appearance controller (S2-T07) remains the only
399
+ browser bootstrap this renderer ever emits in all of S2 (acceptance test C).
400
+ - **SEO/Open-Graph/Twitter/localization metadata**: every generated page's
401
+ `<head>` now carries a viewport meta, an optional description, an optional
402
+ `robots` directive (`noindex, follow` for `status: "unlisted"` content and for
403
+ the generated `404.html`), a canonical link, `og:*`/ `twitter:*` meta tags
404
+ (image resolved to an absolute content-addressed media derivative URL via
405
+ `internal/seo.js`, never the raw source path) and the Atom/RSS feed-discovery
406
+ `<link>`s. **hreflang** is a documented scope decision, not a silent omission:
407
+ `build-input:2.0.0` has no translation/locale-variant linkage between distinct
408
+ `content[]` records (see `internal/seo.js`'s module documentation), so no
409
+ `<link rel="alternate" hreflang="...">` is ever emitted in S2.
410
+ - **`404.html`**: the S2-T06 `error` page kind's pure `renderErrorPageBody`, now
411
+ actually wired into `renderPublication` at DEC-097's exact `errorDocumentKey`
412
+ (`internal/route.js#errorDocumentPath`) — `404.html` at the root, or
413
+ `<basePath>/404.html` otherwise.
414
+ - **Redirect documents**: unchanged from S2-T03
415
+ (`internal/route.js#renderRedirectDocument`) — every authored redirect was
416
+ already an ordinary generated artifact with manifest status fixed at `200`;
417
+ this task's own scope note is documenting that fact here, not re-implementing
418
+ it.
419
+ - **Print CSS hookup** (`src/core/internal/print-stylesheet.js`): every page's
420
+ `<head>` carries a `<link rel="stylesheet" media="print">` at the documented
421
+ conventional path `<basePath>/assets/theme/print.css`. A selected theme's own
422
+ compiled `print.css` bytes land there through a later theme-integration layer,
423
+ outside this renderer's own `build-input:2.0.0` contract —
424
+ `appearanceNormalized` carries only the theme's package identity, never a
425
+ stylesheet path.
426
+
427
+ Every route this task adds carries the schema's exact `routeClass`/ `mediaType`
428
+ pair (`error`, `feed` ×2, `sitemap`, `asset`), classified in `src/core/index.js`
429
+ by the route's own fixed output path.
430
+
431
+ ## Prerequisites
432
+
433
+ - Node.js 24.18.0
434
+ - npm 11.16.0
435
+
436
+ Both are enforced by `package.json` `engines`/`devEngines`; `.nvmrc` and
437
+ `.node-version` carry the same pin.
438
+
439
+ ## Commands
440
+
441
+ ```sh
442
+ npm install
443
+ npm run verify
444
+ ```
445
+
446
+ `npm run verify` runs, in order: Prettier format check, ESLint,
447
+ `tsc --checkJs --noEmit`, the dependency-cruiser
448
+ architecture/module-tree-absence gate, `jscpd` duplication scan, the
449
+ `contracts/*.jcs` canonical-form check, `node --test`, the license inventory
450
+ check, and `cyclonedx-npm` SBOM generation plus presence check.
451
+
452
+ Individual gates:
453
+
454
+ ```sh
455
+ npm run format # apply Prettier
456
+ npm run format:check # CI check
457
+ npm run lint # ESLint flat config
458
+ npm run typecheck # tsc --checkJs --noEmit against types/index.d.ts
459
+ npm run architecture # dependency-cruiser: no src/modules/, no circular deps
460
+ npm run duplication # jscpd, 3% / 50 tokens
461
+ npm run contracts:generate # (re)write contracts/*.jcs placeholders
462
+ npm run contracts:check # fail if committed contracts drifted
463
+ npm test # node --test
464
+ npm run licenses:generate # (re)write THIRD_PARTY_LICENSES.json
465
+ npm run licenses:check # fail if it drifted from package-lock.json
466
+ npm run sbom:generate # cyclonedx-npm -> sbom.cdx.json
467
+ npm run sbom:check # fail if sbom.cdx.json is missing
468
+ ```
469
+
470
+ ## Package layout
471
+
472
+ ```text
473
+ src/core/ # the only source root
474
+ contracts/render-policy.jcs # normalized-body policy identity (S2-T04, real content)
475
+ contracts/theme-styling-contract.jcs # published styling catalog (S2-T12, real and closed)
476
+ ```
477
+
478
+ No `src/modules/` tree, module import edge, registration stub, module
479
+ configuration, module output or module runtime is ever admitted beside
480
+ `src/core/`. `test/module-tree-absence.test.js` and `.dependency-cruiser.cjs`'s
481
+ `only-src-core-is-a-source-root` rule both enforce this; `interactions`,
482
+ `whitelabel`, `newsletter` and `prism` have no module directory, package,
483
+ configuration schema, stub, output, runtime code, network target or
484
+ compatibility promise in this MVP (DEC-097 section 2).
485
+
486
+ ## Consuming `@rathnasgala2/schemas`
487
+
488
+ This repository consumes `@rathnasgala2/schemas@2.11.0` from the public npm
489
+ registry, declared as `"@rathnasgala2/schemas": "2.11.0"` (exact pin, no range)
490
+ so `package-lock.json` records the resolved version and integrity hash. The
491
+ LOCAL-1/LOCAL-39 local-tarball convention
492
+ (`file:../../local-packages/rathnasgala2-schemas-*.tgz`) is retired for this
493
+ package now that it publishes to the registry. The 2.8.0->2.11.0 convergence
494
+ changes none of the four roots this renderer validates, for the same reason the
495
+ 2.7.0-2.8.0 delta below did not. Only the `"."` export (`validateGalaDocument`,
496
+ `GALA_SCHEMA_IDS`) is consumed; the 2.6.0 `./runtime-origins` narrow export and
497
+ every other addition through 2.6.1 (new OpenAPI operations, App route/catalog
498
+ copy, the `PrincipalSummary`/`AuthenticatorSummary`/keyset field additions) are
499
+ App/API-surface changes this renderer does not read. The `"."` export's 2.2.2
500
+ browser-safety refactor (Node builtins replaced with browser-safe equivalents
501
+ behind the same public API) is byte-identical in behaviour for a Node consumer
502
+ such as this one. The 2.7.0-2.8.0 delta (the `generationFence` sentinel,
503
+ `callClassBinding` and `providerBinding` in the four deployment roots, the
504
+ receipt-exchange discriminators, the `201` repository-binding response, the
505
+ review/deployment reads and the App route catalog moves) changes none of the
506
+ four roots this renderer validates -- `theme-contract`, `build-input`, `lock`
507
+ and `artifact-manifest` are byte-identical from 2.6.1 to 2.8.0.
508
+
509
+ ## Governing documents
510
+
511
+ - [Slice brief S2: author-owned publication](../../orchestration/slice-briefs/S2-author-owned-publication.md),
512
+ section 3 (`@rathnasgala2/template`)
513
+ - [DEC-094](../../orchestration/decisions/DEC-094-javascript-esm-public-packages.md):
514
+ JavaScript ESM for public packages
515
+ - [WORKSPACE.md](../../orchestration/WORKSPACE.md), sections 4-8 and 13
@@ -0,0 +1 @@
1
+ {"contentSecurityPolicy":"default-src 'none'; base-uri 'none'; object-src 'none'; frame-ancestors 'none'; form-action 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'none'; media-src 'self'; manifest-src 'self'; worker-src 'none'","highlighter":{"grammars":["bash","css","diff","java","javascript","json","jsx","markdown","markup","python","sql","tsx","typescript","yaml"],"package":"@11ty/eleventy-plugin-syntaxhighlight","tokenClasses":["atrule","attr-equals","attr-name","attr-value","bold","boolean","builtin","cdata","char","class-name","comment","constant","content","coord","deleted","deleted-sign","doctype","entity","for-or-select","function","function-variable","important","inserted","inserted-sign","interpolation","interpolation-punctuation","italic","key","keyword","namespace","null","number","operator","prefix","prolog","property","punctuation","regex","rule","selector","shebang","string","symbol","tag","template-punctuation","template-string","title","token","url","variable"],"version":"5.0.2"},"markdownIt":{"options":{"html":false,"linkify":false,"typographer":false},"package":"markdown-it","version":"14.3.1"},"name":"gala-render-policy","sanitizer":{"allowedAttributes":{"a":["href","rel"],"code":["class"],"h1":["id"],"h2":["id"],"h3":["id"],"h4":["id"],"h5":["id"],"h6":["id"],"img":["src","alt"],"ol":["start"],"pre":["class"],"span":["class"]},"allowedTags":["p","br","hr","h1","h2","h3","h4","h5","h6","ul","ol","li","blockquote","pre","code","strong","em","b","i","u","s","del","ins","sub","sup","mark","small","wbr","a","img","table","thead","tbody","tr","th","td","dl","dt","dd","figure","figcaption","span"],"allowedUrlSchemes":["https","http","mailto"],"package":"sanitize-html","version":"2.17.7"},"version":"2.0.0"}
@@ -0,0 +1 @@
1
+ {"attributes":[{"match":"exact-value","name":"data-gala-page-kind","role":"semantic","values":["archive","article","author","error","index","page","profile","series","tag"]},{"match":"exact-value","name":"data-gala-resolved-color-mode","role":"state","values":["dark","light"]},{"match":"exact-value","name":"data-gala-slot","role":"semantic","values":["account-intent","article-end","article-footer-ad","article-preamble","conversation","edition-selector","footer-auxiliary","footer-profile","header-actions","newsletter"]},{"match":"presence","name":"data-gala-publication-root","role":"root","values":[]}],"catalogDigest":"sha256:46b43a6d1775f88bf2ded331512bd0cf27d232e256e40a89bda773e50897877f","classSelectors":["language-bash","language-css","language-diff","language-java","language-javascript","language-json","language-jsx","language-markdown","language-markup","language-python","language-sql","language-tsx","language-typescript","language-yaml","token"],"combinators":[" "," > "," + "," ~ "],"composition":{"compoundOrder":["type","id","class","attribute","pseudo-class","pseudo-element"],"functionalSelectorArguments":"compound-only","maximumFunctionalDepth":1,"nthExpressionProfile":"gala-positive-an-plus-b-v2","rootScope":"first-compound-required"},"contractVersion":"2.0.0","functionalPseudos":["is","where","not","nth-child","nth-last-child"],"idSelectors":["gala-appearance-color-mode","main-content"],"orderedLayers":["gala-tokens","gala-components","gala-utilities","gala-print"],"profile":"gala-template-styling-contract-v2","pseudoClasses":[],"pseudoElements":["after","before","marker","selection"],"publicThemeSlotHooks":[{"hookId":"code-block","selectorAtom":"pre"},{"hookId":"code-inline","selectorAtom":"code"},{"hookId":"code-language-bash","selectorAtom":".language-bash"},{"hookId":"code-language-css","selectorAtom":".language-css"},{"hookId":"code-language-diff","selectorAtom":".language-diff"},{"hookId":"code-language-java","selectorAtom":".language-java"},{"hookId":"code-language-javascript","selectorAtom":".language-javascript"},{"hookId":"code-language-json","selectorAtom":".language-json"},{"hookId":"code-language-jsx","selectorAtom":".language-jsx"},{"hookId":"code-language-markdown","selectorAtom":".language-markdown"},{"hookId":"code-language-markup","selectorAtom":".language-markup"},{"hookId":"code-language-python","selectorAtom":".language-python"},{"hookId":"code-language-sql","selectorAtom":".language-sql"},{"hookId":"code-language-tsx","selectorAtom":".language-tsx"},{"hookId":"code-language-typescript","selectorAtom":".language-typescript"},{"hookId":"code-language-yaml","selectorAtom":".language-yaml"},{"hookId":"code-token","selectorAtom":".token"},{"hookId":"content-article","selectorAtom":"article"},{"hookId":"control-appearance-select","selectorAtom":"#gala-appearance-color-mode"},{"hookId":"control-label","selectorAtom":"label"},{"hookId":"control-option","selectorAtom":"option"},{"hookId":"control-select","selectorAtom":"select"},{"hookId":"heading-1","selectorAtom":"h1"},{"hookId":"heading-2","selectorAtom":"h2"},{"hookId":"heading-3","selectorAtom":"h3"},{"hookId":"heading-4","selectorAtom":"h4"},{"hookId":"heading-5","selectorAtom":"h5"},{"hookId":"heading-6","selectorAtom":"h6"},{"hookId":"landmark-footer","selectorAtom":"footer"},{"hookId":"landmark-header","selectorAtom":"header"},{"hookId":"landmark-main","selectorAtom":"main"},{"hookId":"landmark-main-content","selectorAtom":"#main-content"},{"hookId":"landmark-navigation","selectorAtom":"nav"},{"hookId":"media-image","selectorAtom":"img"},{"hookId":"page-archive","selectorAtom":"[data-gala-page-kind=\"archive\"]"},{"hookId":"page-article","selectorAtom":"[data-gala-page-kind=\"article\"]"},{"hookId":"page-author","selectorAtom":"[data-gala-page-kind=\"author\"]"},{"hookId":"page-error","selectorAtom":"[data-gala-page-kind=\"error\"]"},{"hookId":"page-index","selectorAtom":"[data-gala-page-kind=\"index\"]"},{"hookId":"page-page","selectorAtom":"[data-gala-page-kind=\"page\"]"},{"hookId":"page-profile","selectorAtom":"[data-gala-page-kind=\"profile\"]"},{"hookId":"page-series","selectorAtom":"[data-gala-page-kind=\"series\"]"},{"hookId":"page-tag","selectorAtom":"[data-gala-page-kind=\"tag\"]"},{"hookId":"prose-blockquote","selectorAtom":"blockquote"},{"hookId":"prose-divider","selectorAtom":"hr"},{"hookId":"prose-emphasis","selectorAtom":"em"},{"hookId":"prose-link","selectorAtom":"a"},{"hookId":"prose-list-item","selectorAtom":"li"},{"hookId":"prose-list-ordered","selectorAtom":"ol"},{"hookId":"prose-list-unordered","selectorAtom":"ul"},{"hookId":"prose-paragraph","selectorAtom":"p"},{"hookId":"prose-span","selectorAtom":"span"},{"hookId":"prose-strong","selectorAtom":"strong"},{"hookId":"prose-time","selectorAtom":"time"},{"hookId":"slot-account-intent","selectorAtom":"[data-gala-slot=\"account-intent\"]"},{"hookId":"slot-article-end","selectorAtom":"[data-gala-slot=\"article-end\"]"},{"hookId":"slot-article-footer-ad","selectorAtom":"[data-gala-slot=\"article-footer-ad\"]"},{"hookId":"slot-article-preamble","selectorAtom":"[data-gala-slot=\"article-preamble\"]"},{"hookId":"slot-conversation","selectorAtom":"[data-gala-slot=\"conversation\"]"},{"hookId":"slot-edition-selector","selectorAtom":"[data-gala-slot=\"edition-selector\"]"},{"hookId":"slot-footer-auxiliary","selectorAtom":"[data-gala-slot=\"footer-auxiliary\"]"},{"hookId":"slot-footer-profile","selectorAtom":"[data-gala-slot=\"footer-profile\"]"},{"hookId":"slot-header-actions","selectorAtom":"[data-gala-slot=\"header-actions\"]"},{"hookId":"slot-newsletter","selectorAtom":"[data-gala-slot=\"newsletter\"]"}],"publicationRootSelector":"[data-gala-publication-root]","resolvedPaletteSelectors":{"dark":"[data-gala-publication-root][data-gala-resolved-color-mode=\"dark\"]","light":"[data-gala-publication-root][data-gala-resolved-color-mode=\"light\"]"},"stylesheetLayers":{"components":"gala-components","print":"gala-print","tokens":"gala-tokens","utilities":"gala-utilities"},"templatePackage":"@rathnasgala2/template","templateVersion":"2.0.0","typeSelectors":["a","article","blockquote","code","em","footer","h1","h2","h3","h4","h5","h6","header","hr","img","label","li","main","nav","ol","option","p","pre","select","span","strong","time","ul"]}
package/package.json ADDED
@@ -0,0 +1,96 @@
1
+ {
2
+ "name": "@rathnasgala2/template",
3
+ "version": "2.0.0",
4
+ "description": "Deterministic Eleventy-backed static publication renderer for Galascribe portable repositories.",
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/rathnasgala2/template.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/rathnasgala2/template/issues"
13
+ },
14
+ "homepage": "https://github.com/rathnasgala2/template#readme",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./types/index.d.ts",
18
+ "import": "./src/core/index.js"
19
+ },
20
+ "./package.json": "./package.json"
21
+ },
22
+ "types": "./types/index.d.ts",
23
+ "files": [
24
+ "src/core/",
25
+ "contracts/",
26
+ "types/",
27
+ "LICENSE",
28
+ "NOTICE",
29
+ "README.md"
30
+ ],
31
+ "engines": {
32
+ "node": "24.18.0",
33
+ "npm": "11.16.0"
34
+ },
35
+ "devEngines": {
36
+ "runtime": {
37
+ "name": "node",
38
+ "version": "24.18.0",
39
+ "onFail": "error"
40
+ },
41
+ "packageManager": {
42
+ "name": "npm",
43
+ "version": "11.16.0",
44
+ "onFail": "error"
45
+ }
46
+ },
47
+ "packageManager": "npm@11.16.0",
48
+ "publishConfig": {
49
+ "access": "public",
50
+ "provenance": true
51
+ },
52
+ "scripts": {
53
+ "format": "prettier --write .",
54
+ "format:check": "prettier --check .",
55
+ "lint": "eslint .",
56
+ "typecheck": "tsc --project tsconfig.json",
57
+ "architecture": "depcruise --validate .dependency-cruiser.cjs src contracts eslint.config.js",
58
+ "duplication": "jscpd src scripts test eslint.config.js",
59
+ "contracts:generate": "node scripts/generate-contracts.mjs",
60
+ "contracts:check": "node scripts/generate-contracts.mjs --check",
61
+ "test": "node --test",
62
+ "licenses:generate": "node scripts/generate-license-inventory.mjs",
63
+ "licenses:check": "node scripts/generate-license-inventory.mjs --check",
64
+ "sbom:generate": "cyclonedx-npm --output-file sbom.cdx.json --output-format JSON --spec-version 1.6 && node scripts/apply-sbom-normalization.mjs",
65
+ "sbom:check": "node scripts/check-sbom-current.mjs",
66
+ "workflows:check": "node scripts/check-workflow-pins.mjs",
67
+ "workflows:drift": "node scripts/check-workflow-drift.mjs",
68
+ "schema-pin:check": "node scripts/check-no-local-schema-pin.mjs",
69
+ "audit": "npm audit --audit-level=high",
70
+ "verify": "npm run format:check && npm run lint && npm run typecheck && npm run architecture && npm run duplication && npm run contracts:check && npm run schema-pin:check && npm test && npm run licenses:check && npm run sbom:check && npm run workflows:check && npm run audit"
71
+ },
72
+ "dependencies": {
73
+ "@11ty/eleventy": "3.1.6",
74
+ "@11ty/eleventy-plugin-syntaxhighlight": "5.0.2",
75
+ "@rathnasgala2/schemas": "2.11.0",
76
+ "jpeg-js": "0.4.4",
77
+ "markdown-it": "14.3.1",
78
+ "sanitize-html": "2.17.7"
79
+ },
80
+ "devDependencies": {
81
+ "@cyclonedx/cyclonedx-npm": "6.0.1",
82
+ "@eslint/js": "10.0.1",
83
+ "@types/markdown-it": "14.2.0",
84
+ "@types/node": "24.13.4",
85
+ "@types/sanitize-html": "2.16.1",
86
+ "dependency-cruiser": "18.2.0",
87
+ "eslint": "10.10.0",
88
+ "eslint-plugin-jsdoc": "64.3.9",
89
+ "globals": "17.12.0",
90
+ "jscpd": "5.2.0",
91
+ "jsdom": "30.0.1",
92
+ "parse5": "8.0.1",
93
+ "prettier": "3.9.6",
94
+ "typescript": "5.9.3"
95
+ }
96
+ }