@cosmicdrift/kumiko-cli 0.404.1 → 0.406.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/package.json +12 -12
- package/src/changes.json +6 -0
- package/src/handbuch/render.ts +32 -39
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.406.0",
|
|
4
4
|
"description": "CLI for Kumiko apps. `bunx @cosmicdrift/kumiko-cli new app <name>` scaffolds a runnable workspace; `add feature <name>` mounts a feature; `check` runs boot validation plus the guard suites with the step list derived from kumiko.json; `agent lint`, `project` and `consumer` operate on an existing app; `handbuch build` renders the HTML handbuch.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -36,19 +36,19 @@
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@cosmicdrift/kumiko-dev-server": "0.
|
|
40
|
-
"@cosmicdrift/kumiko-feature-ast": "0.
|
|
41
|
-
"@cosmicdrift/kumiko-guards": "0.
|
|
42
|
-
"@cosmicdrift/kumiko-server-runtime": "0.
|
|
43
|
-
"@cosmicdrift/kumiko-testing": "0.
|
|
39
|
+
"@cosmicdrift/kumiko-dev-server": "0.406.0",
|
|
40
|
+
"@cosmicdrift/kumiko-feature-ast": "0.406.0",
|
|
41
|
+
"@cosmicdrift/kumiko-guards": "0.406.0",
|
|
42
|
+
"@cosmicdrift/kumiko-server-runtime": "0.406.0",
|
|
43
|
+
"@cosmicdrift/kumiko-testing": "0.406.0",
|
|
44
44
|
"marked": "^18.1.2",
|
|
45
45
|
"zod": "^4.6.7"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"sharp": "^0.35.5",
|
|
49
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
50
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
51
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
49
|
+
"@cosmicdrift/kumiko-bundled-features": "0.406.0",
|
|
50
|
+
"@cosmicdrift/kumiko-framework": "0.406.0",
|
|
51
|
+
"@cosmicdrift/kumiko-renderer": "0.406.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependenciesMeta": {
|
|
54
54
|
"sharp": {
|
|
@@ -65,9 +65,9 @@
|
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
69
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
70
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
68
|
+
"@cosmicdrift/kumiko-bundled-features": "0.406.0",
|
|
69
|
+
"@cosmicdrift/kumiko-framework": "0.406.0",
|
|
70
|
+
"@cosmicdrift/kumiko-renderer": "0.406.0",
|
|
71
71
|
"sharp": "^0.35.5"
|
|
72
72
|
},
|
|
73
73
|
"publishConfig": {
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.405.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "handbuch escapes HTML through the renderer helper",
|
|
6
|
+
"detail": "The handbuch renderer uses escapeHtmlAttr from @cosmicdrift/kumiko-renderer/headless instead of its own copy. The escaped characters are the same as before, including the single quote."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.389.0",
|
|
4
10
|
"type": "fix",
|
package/src/handbuch/render.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { raw } from "@cosmicdrift/kumiko-renderer/headless";
|
|
2
|
+
import { escapeHtmlAttr, raw } from "@cosmicdrift/kumiko-renderer/headless";
|
|
3
3
|
import { Marked } from "marked";
|
|
4
4
|
import { type BuildInfo, buildInfoLines } from "./buildinfo";
|
|
5
5
|
import { chromeFor } from "./chrome";
|
|
@@ -46,17 +46,8 @@ export function createImagePlan(withLarge: boolean): ImagePlan {
|
|
|
46
46
|
return { jobs: new Map(), withLarge };
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export function escapeHtml(value: string): string {
|
|
50
|
-
return value
|
|
51
|
-
.replace(/&/g, "&")
|
|
52
|
-
.replace(/</g, "<")
|
|
53
|
-
.replace(/>/g, ">")
|
|
54
|
-
.replace(/"/g, """)
|
|
55
|
-
.replace(/'/g, "'");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
49
|
function missingImageBox(href: string, locale: string): string {
|
|
59
|
-
return `<p class="gap-box gap-image"><span class="tag warn">${
|
|
50
|
+
return `<p class="gap-box gap-image"><span class="tag warn">${escapeHtmlAttr(chromeFor(locale).imageMissing)}</span> <code>${escapeHtmlAttr(href)}</code></p>`;
|
|
60
51
|
}
|
|
61
52
|
|
|
62
53
|
export function renderMarkdown(
|
|
@@ -72,14 +63,14 @@ export function renderMarkdown(
|
|
|
72
63
|
gfm: true,
|
|
73
64
|
renderer: {
|
|
74
65
|
image({ href, text }) {
|
|
75
|
-
const alt =
|
|
66
|
+
const alt = escapeHtmlAttr(text ?? "");
|
|
76
67
|
if (!isLocalImageRef(href))
|
|
77
|
-
return `<img src="${
|
|
68
|
+
return `<img src="${escapeHtmlAttr(href)}" alt="${alt}" loading="lazy">`;
|
|
78
69
|
const source = resolveImagePath(context.textsRoot, href);
|
|
79
70
|
if (!existsSync(source)) return missingImageBox(href, context.locale);
|
|
80
71
|
const dest = inlineImageDest(context.locale, context.textsRoot, source);
|
|
81
72
|
const large = registerImage(context.images, dest, source, 1600);
|
|
82
|
-
return `<figure class="shot"><a href="${
|
|
73
|
+
return `<figure class="shot"><a href="${escapeHtmlAttr(large)}" data-large="${escapeHtmlAttr(large)}" target="_blank" rel="noopener"><img src="${escapeHtmlAttr(dest)}" alt="${alt}" loading="lazy"></a>${text ? `<figcaption>${alt}</figcaption>` : ""}</figure>`;
|
|
83
74
|
},
|
|
84
75
|
paragraph({ tokens }) {
|
|
85
76
|
const innerHtml = this.parser.parseInline(tokens);
|
|
@@ -95,8 +86,8 @@ export function renderMarkdown(
|
|
|
95
86
|
const innerHtml = this.parser.parseInline(tokens);
|
|
96
87
|
const target = href.replace(/\.md$/, "");
|
|
97
88
|
if (href.endsWith(".md") && context.chapterIds.has(target))
|
|
98
|
-
return `<a href="#${
|
|
99
|
-
return `<a href="${
|
|
89
|
+
return `<a href="#${escapeHtmlAttr(target)}">${innerHtml}</a>`;
|
|
90
|
+
return `<a href="${escapeHtmlAttr(href)}" target="_blank" rel="noopener noreferrer">${innerHtml}</a>`;
|
|
100
91
|
},
|
|
101
92
|
},
|
|
102
93
|
});
|
|
@@ -129,18 +120,18 @@ function framesHtml(
|
|
|
129
120
|
const notice =
|
|
130
121
|
picked.shotsLocale === locale
|
|
131
122
|
? ""
|
|
132
|
-
: `<p class="notice">${
|
|
123
|
+
: `<p class="notice">${escapeHtmlAttr(chromeFor(model.defaultLocale).shotsFallbackNotice(picked.shotsLocale.toUpperCase()))}</p>`;
|
|
133
124
|
const frames = VIEWPORTS.flatMap((viewport) => {
|
|
134
125
|
const source = picked.shots[viewport];
|
|
135
126
|
if (!source) return [];
|
|
136
127
|
const dest = `shots/${destBase}/${picked.shotsLocale}/${viewport}.webp`;
|
|
137
128
|
const large = registerImage(images, dest, source, VIEWPORT_WIDTH[viewport]);
|
|
138
|
-
const labelHtml =
|
|
129
|
+
const labelHtml = escapeHtmlAttr(chromeFor(model.defaultLocale).viewports[viewport]);
|
|
139
130
|
return [
|
|
140
|
-
`<figure class="frame" data-vp="${viewport}"><figcaption>${labelHtml}</figcaption><div class="dev ${viewport}"><a href="${
|
|
131
|
+
`<figure class="frame" data-vp="${viewport}"><figcaption>${labelHtml}</figcaption><div class="dev ${viewport}"><a href="${escapeHtmlAttr(large)}" data-large="${escapeHtmlAttr(large)}" target="_blank" rel="noopener"><img src="${escapeHtmlAttr(dest)}" alt="${escapeHtmlAttr(altBase)} (${labelHtml})" loading="lazy"></a></div></figure>`,
|
|
141
132
|
];
|
|
142
133
|
});
|
|
143
|
-
return `<div class="stage-loc" data-loc="${
|
|
134
|
+
return `<div class="stage-loc" data-loc="${escapeHtmlAttr(locale)}">${notice}<div class="frames">${raw(frames.join(""))}</div></div>`;
|
|
144
135
|
})
|
|
145
136
|
.join("\n");
|
|
146
137
|
}
|
|
@@ -174,10 +165,10 @@ function stepsHtml(chapter: Chapter, model: Model, images: ImagePlan): string {
|
|
|
174
165
|
chapterIds,
|
|
175
166
|
})
|
|
176
167
|
: "";
|
|
177
|
-
return `<div data-loc="${
|
|
168
|
+
return `<div data-loc="${escapeHtmlAttr(locale)}">${body ? `<div class="prose step-text">${raw(body)}</div>` : ""}</div>`;
|
|
178
169
|
})
|
|
179
170
|
.join("");
|
|
180
|
-
return `<article class="step"><h3>${
|
|
171
|
+
return `<article class="step"><h3>${escapeHtmlAttr(step.label)}</h3><div class="stage-col">${framesHtml(`${chapter.id}/${step.id}`, `${step.label}`, step.shots, model, images)}</div>${raw(texts)}</article>`;
|
|
181
172
|
})
|
|
182
173
|
.join("\n");
|
|
183
174
|
}
|
|
@@ -192,8 +183,10 @@ function localeTextHtml(chapter: Chapter, locale: string, model: Model, images:
|
|
|
192
183
|
const textLocale = own ? locale : model.defaultLocale;
|
|
193
184
|
const notice = own
|
|
194
185
|
? ""
|
|
195
|
-
: `<p class="notice">${
|
|
196
|
-
const badges = text.prozesse
|
|
186
|
+
: `<p class="notice">${escapeHtmlAttr(c.textFallbackNotice(model.defaultLocale.toUpperCase()))}</p>`;
|
|
187
|
+
const badges = text.prozesse
|
|
188
|
+
.map((p) => `<span class="badge">${escapeHtmlAttr(p)}</span>`)
|
|
189
|
+
.join("");
|
|
197
190
|
const chapterIds = new Set(model.groups.flatMap((g) => g.chapters.map((c) => c.id)));
|
|
198
191
|
const bodyMd = chapter.steps
|
|
199
192
|
? splitStepSections(text.bodyMd, new Set(chapter.steps.map((step) => step.slug))).intro
|
|
@@ -206,7 +199,7 @@ function localeTextHtml(chapter: Chapter, locale: string, model: Model, images:
|
|
|
206
199
|
});
|
|
207
200
|
const stale =
|
|
208
201
|
text.staleTerms.length > 0
|
|
209
|
-
? `<div class="gap-box"><span class="tag warn">${raw(c.textStale)}</span> ${raw(c.staleBody)} ${text.staleTerms.map((term) => `<code>${
|
|
202
|
+
? `<div class="gap-box"><span class="tag warn">${raw(c.textStale)}</span> ${raw(c.staleBody)} ${text.staleTerms.map((term) => `<code>${escapeHtmlAttr(term)}</code>`).join(", ")}</div>`
|
|
210
203
|
: "";
|
|
211
204
|
return `${notice}${stale}${badges ? `<p class="badges">${badges}</p>` : ""}<div class="prose">${raw(body)}</div>`;
|
|
212
205
|
}
|
|
@@ -225,17 +218,17 @@ function chapterHtml(chapter: Chapter, model: Model, images: ImagePlan): string
|
|
|
225
218
|
const head = model.locales
|
|
226
219
|
.map(
|
|
227
220
|
(locale) =>
|
|
228
|
-
`<div data-loc="${
|
|
221
|
+
`<div data-loc="${escapeHtmlAttr(locale)}"><h2>${escapeHtmlAttr(chapterTitle(chapter, locale, model.defaultLocale))}</h2></div>`,
|
|
229
222
|
)
|
|
230
223
|
.join("");
|
|
231
224
|
const text = model.locales
|
|
232
225
|
.map(
|
|
233
226
|
(locale) =>
|
|
234
|
-
`<div data-loc="${
|
|
227
|
+
`<div data-loc="${escapeHtmlAttr(locale)}">${localeTextHtml(chapter, locale, model, images)}</div>`,
|
|
235
228
|
)
|
|
236
229
|
.join("\n");
|
|
237
|
-
return `<section class="chapter${matrix && !chapter.steps ? " has-matrix" : ""}${isTextMissing(chapter) ? " no-text" : ""}" id="${
|
|
238
|
-
<header class="chapter-head">${head}<p class="meta"><code>${
|
|
230
|
+
return `<section class="chapter${matrix && !chapter.steps ? " has-matrix" : ""}${isTextMissing(chapter) ? " no-text" : ""}" id="${escapeHtmlAttr(chapter.id)}" data-nav="${escapeHtmlAttr(chapter.id)}"${raw(matrix ? ` data-matrix="1" data-vps="${availableViewports(chapter).join(" ")}"` : "")}>
|
|
231
|
+
<header class="chapter-head">${head}<p class="meta"><code>${escapeHtmlAttr(chapter.id)}</code>${raw(flags)}</p></header>
|
|
239
232
|
${matrix && !chapter.steps ? `<div class="stage-col">${framesHtml(chapter.id, chapter.id, chapter.matrix, model, images)}</div>` : ""}
|
|
240
233
|
<div class="text-col">${text}</div>
|
|
241
234
|
${chapter.steps ? `<div class="steps">${stepsHtml(chapter, model, images)}</div>` : ""}
|
|
@@ -251,7 +244,7 @@ function overviewHtml(model: Model): string {
|
|
|
251
244
|
: `<h3>${raw(title)}</h3><ul class="gap-list">${chapters
|
|
252
245
|
.map(
|
|
253
246
|
(c) =>
|
|
254
|
-
`<li><a href="#${
|
|
247
|
+
`<li><a href="#${escapeHtmlAttr(c.id)}">${escapeHtmlAttr(chapterTitle(c, model.defaultLocale, model.defaultLocale))}</a>${detail ? ` <code>${escapeHtmlAttr(detail(c))}</code>` : ""}</li>`,
|
|
255
248
|
)
|
|
256
249
|
.join("")}</ul>`;
|
|
257
250
|
const documented = model.groups.flatMap((group) => group.chapters);
|
|
@@ -285,7 +278,7 @@ function plannedHtml(model: Model): string {
|
|
|
285
278
|
const rows = model.planned
|
|
286
279
|
.map(
|
|
287
280
|
({ chapter, groupLabel }) =>
|
|
288
|
-
`<tr><td>${
|
|
281
|
+
`<tr><td>${escapeHtmlAttr(chapter.screenTitle ?? chapter.id)}</td><td><code>${escapeHtmlAttr(chapter.id)}</code></td><td>${escapeHtmlAttr(groupLabel)}</td></tr>`,
|
|
289
282
|
)
|
|
290
283
|
.join("");
|
|
291
284
|
const table =
|
|
@@ -300,14 +293,14 @@ function plannedHtml(model: Model): string {
|
|
|
300
293
|
function navHtml(model: Model): { nav: string; select: string } {
|
|
301
294
|
const c = chromeFor(model.defaultLocale);
|
|
302
295
|
const link = (id: string, label: string, tags = "") =>
|
|
303
|
-
`<a href="#${
|
|
296
|
+
`<a href="#${escapeHtmlAttr(id)}" data-nav-link="${escapeHtmlAttr(id)}"><span>${escapeHtmlAttr(label)}</span>${raw(tags)}</a>`;
|
|
304
297
|
const option = (id: string, label: string) =>
|
|
305
|
-
`<option value="${
|
|
298
|
+
`<option value="${escapeHtmlAttr(id)}">${escapeHtmlAttr(label)}</option>`;
|
|
306
299
|
const navParts = [link("uebersicht", c.overview)];
|
|
307
300
|
const selectParts = [option("uebersicht", c.overview)];
|
|
308
301
|
for (const group of model.groups) {
|
|
309
|
-
navParts.push(`<h3>${
|
|
310
|
-
selectParts.push(`<optgroup label="${
|
|
302
|
+
navParts.push(`<h3>${escapeHtmlAttr(group.label)}</h3>`);
|
|
303
|
+
selectParts.push(`<optgroup label="${escapeHtmlAttr(group.label)}">`);
|
|
311
304
|
for (const chapter of group.chapters) {
|
|
312
305
|
const title = chapterTitle(chapter, model.defaultLocale, model.defaultLocale);
|
|
313
306
|
const tags =
|
|
@@ -350,11 +343,11 @@ export function renderPage(
|
|
|
350
343
|
)
|
|
351
344
|
.join("\n");
|
|
352
345
|
return `<!doctype html>
|
|
353
|
-
<html lang="${
|
|
346
|
+
<html lang="${escapeHtmlAttr(model.defaultLocale)}" data-locale="${escapeHtmlAttr(model.defaultLocale)}" data-vp="desktop">
|
|
354
347
|
<head>
|
|
355
348
|
<meta charset="utf-8">
|
|
356
349
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
357
|
-
<title>${
|
|
350
|
+
<title>${escapeHtmlAttr(title)}</title>
|
|
358
351
|
<style>
|
|
359
352
|
${PAGE_STYLES}
|
|
360
353
|
${raw(localeRules)}
|
|
@@ -363,8 +356,8 @@ ${raw(localeRules)}
|
|
|
363
356
|
<body>
|
|
364
357
|
<div class="app">
|
|
365
358
|
<aside>
|
|
366
|
-
<div class="brand"><h1>${
|
|
367
|
-
<p class="build-info">${raw(buildInfoLines(info, c.builtAt).map(
|
|
359
|
+
<div class="brand"><h1>${escapeHtmlAttr(title)}</h1>
|
|
360
|
+
<p class="build-info">${raw(buildInfoLines(info, c.builtAt).map(escapeHtmlAttr).join(" · "))}</p>
|
|
368
361
|
<div class="stats"><span><b>${stats.chapters}</b> ${raw(c.chapters)}</span><span><b>${stats.textMissing}</b> ${raw(c.textMissing)}</span><span><b>${stats.imageMissing}</b> ${raw(c.imageMissing)}</span><span><b>${stats.staleText}</b> ${raw(c.staleShort)}</span><span><b>${stats.planned}</b> ${raw(c.plannedShort)}</span></div></div>
|
|
369
362
|
<div class="mobile-pick"><label for="pick">${raw(c.chapters)}</label><select id="pick">${raw(select)}</select></div>
|
|
370
363
|
<nav id="nav" aria-label="${raw(c.chapters)}">${raw(nav)}</nav>
|