@cosmicdrift/kumiko-cli 0.404.1 → 0.405.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-cli",
3
- "version": "0.404.1",
3
+ "version": "0.405.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.404.1",
40
- "@cosmicdrift/kumiko-feature-ast": "0.404.1",
41
- "@cosmicdrift/kumiko-guards": "0.404.1",
42
- "@cosmicdrift/kumiko-server-runtime": "0.404.1",
43
- "@cosmicdrift/kumiko-testing": "0.404.1",
39
+ "@cosmicdrift/kumiko-dev-server": "0.405.0",
40
+ "@cosmicdrift/kumiko-feature-ast": "0.405.0",
41
+ "@cosmicdrift/kumiko-guards": "0.405.0",
42
+ "@cosmicdrift/kumiko-server-runtime": "0.405.0",
43
+ "@cosmicdrift/kumiko-testing": "0.405.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.404.1",
50
- "@cosmicdrift/kumiko-framework": "0.404.1",
51
- "@cosmicdrift/kumiko-renderer": "0.404.1"
49
+ "@cosmicdrift/kumiko-bundled-features": "0.405.0",
50
+ "@cosmicdrift/kumiko-framework": "0.405.0",
51
+ "@cosmicdrift/kumiko-renderer": "0.405.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.404.1",
69
- "@cosmicdrift/kumiko-framework": "0.404.1",
70
- "@cosmicdrift/kumiko-renderer": "0.404.1",
68
+ "@cosmicdrift/kumiko-bundled-features": "0.405.0",
69
+ "@cosmicdrift/kumiko-framework": "0.405.0",
70
+ "@cosmicdrift/kumiko-renderer": "0.405.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",
@@ -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, "&amp;")
52
- .replace(/</g, "&lt;")
53
- .replace(/>/g, "&gt;")
54
- .replace(/"/g, "&quot;")
55
- .replace(/'/g, "&#39;");
56
- }
57
-
58
49
  function missingImageBox(href: string, locale: string): string {
59
- return `<p class="gap-box gap-image"><span class="tag warn">${escapeHtml(chromeFor(locale).imageMissing)}</span> <code>${escapeHtml(href)}</code></p>`;
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 = escapeHtml(text ?? "");
66
+ const alt = escapeHtmlAttr(text ?? "");
76
67
  if (!isLocalImageRef(href))
77
- return `<img src="${escapeHtml(href)}" alt="${alt}" loading="lazy">`;
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="${escapeHtml(large)}" data-large="${escapeHtml(large)}" target="_blank" rel="noopener"><img src="${escapeHtml(dest)}" alt="${alt}" loading="lazy"></a>${text ? `<figcaption>${alt}</figcaption>` : ""}</figure>`;
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="#${escapeHtml(target)}">${innerHtml}</a>`;
99
- return `<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${innerHtml}</a>`;
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">${escapeHtml(chromeFor(model.defaultLocale).shotsFallbackNotice(picked.shotsLocale.toUpperCase()))}</p>`;
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 = escapeHtml(chromeFor(model.defaultLocale).viewports[viewport]);
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="${escapeHtml(large)}" data-large="${escapeHtml(large)}" target="_blank" rel="noopener"><img src="${escapeHtml(dest)}" alt="${escapeHtml(altBase)} (${labelHtml})" loading="lazy"></a></div></figure>`,
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="${escapeHtml(locale)}">${notice}<div class="frames">${raw(frames.join(""))}</div></div>`;
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="${escapeHtml(locale)}">${body ? `<div class="prose step-text">${raw(body)}</div>` : ""}</div>`;
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>${escapeHtml(step.label)}</h3><div class="stage-col">${framesHtml(`${chapter.id}/${step.id}`, `${step.label}`, step.shots, model, images)}</div>${raw(texts)}</article>`;
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">${escapeHtml(c.textFallbackNotice(model.defaultLocale.toUpperCase()))}</p>`;
196
- const badges = text.prozesse.map((p) => `<span class="badge">${escapeHtml(p)}</span>`).join("");
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>${escapeHtml(term)}</code>`).join(", ")}</div>`
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="${escapeHtml(locale)}"><h2>${escapeHtml(chapterTitle(chapter, locale, model.defaultLocale))}</h2></div>`,
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="${escapeHtml(locale)}">${localeTextHtml(chapter, locale, model, images)}</div>`,
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="${escapeHtml(chapter.id)}" data-nav="${escapeHtml(chapter.id)}"${raw(matrix ? ` data-matrix="1" data-vps="${availableViewports(chapter).join(" ")}"` : "")}>
238
- <header class="chapter-head">${head}<p class="meta"><code>${escapeHtml(chapter.id)}</code>${raw(flags)}</p></header>
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="#${escapeHtml(c.id)}">${escapeHtml(chapterTitle(c, model.defaultLocale, model.defaultLocale))}</a>${detail ? ` <code>${escapeHtml(detail(c))}</code>` : ""}</li>`,
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>${escapeHtml(chapter.screenTitle ?? chapter.id)}</td><td><code>${escapeHtml(chapter.id)}</code></td><td>${escapeHtml(groupLabel)}</td></tr>`,
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="#${escapeHtml(id)}" data-nav-link="${escapeHtml(id)}"><span>${escapeHtml(label)}</span>${raw(tags)}</a>`;
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="${escapeHtml(id)}">${escapeHtml(label)}</option>`;
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>${escapeHtml(group.label)}</h3>`);
310
- selectParts.push(`<optgroup label="${escapeHtml(group.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="${escapeHtml(model.defaultLocale)}" data-locale="${escapeHtml(model.defaultLocale)}" data-vp="desktop">
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>${escapeHtml(title)}</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>${escapeHtml(title)}</h1>
367
- <p class="build-info">${raw(buildInfoLines(info, c.builtAt).map(escapeHtml).join(" · "))}</p>
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>