@cosmicdrift/kumiko-cli 0.415.0 → 0.417.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/commands/handbuch.ts +91 -23
- package/src/handbuch/chrome.ts +3 -0
- package/src/handbuch/config.ts +1 -0
- package/src/handbuch/knowledge.ts +82 -0
- package/src/handbuch/model.ts +105 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.417.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.417.0",
|
|
40
|
+
"@cosmicdrift/kumiko-feature-ast": "0.417.0",
|
|
41
|
+
"@cosmicdrift/kumiko-guards": "0.417.0",
|
|
42
|
+
"@cosmicdrift/kumiko-server-runtime": "0.417.0",
|
|
43
|
+
"@cosmicdrift/kumiko-testing": "0.417.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.417.0",
|
|
50
|
+
"@cosmicdrift/kumiko-framework": "0.417.0",
|
|
51
|
+
"@cosmicdrift/kumiko-renderer": "0.417.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.417.0",
|
|
69
|
+
"@cosmicdrift/kumiko-framework": "0.417.0",
|
|
70
|
+
"@cosmicdrift/kumiko-renderer": "0.417.0",
|
|
71
71
|
"sharp": "^0.35.5"
|
|
72
72
|
},
|
|
73
73
|
"publishConfig": {
|
package/src/commands/handbuch.ts
CHANGED
|
@@ -20,10 +20,23 @@ const BUILD_OPTIONS = {
|
|
|
20
20
|
"no-large": { type: "boolean" },
|
|
21
21
|
} as const;
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
const KNOWLEDGE_USAGE =
|
|
24
|
+
"Usage: kumiko handbuch knowledge <config.json> --out <file.json> [--root <dir>] [--texts <dir>] [--web-texts <dir>] [--web-repo <dir>] [--repo <dir>] [--locale <l>]";
|
|
25
|
+
|
|
26
|
+
const KNOWLEDGE_OPTIONS = {
|
|
27
|
+
root: { type: "string" },
|
|
28
|
+
out: { type: "string" },
|
|
29
|
+
texts: { type: "string" },
|
|
30
|
+
"web-texts": { type: "string" },
|
|
31
|
+
"web-repo": { type: "string" },
|
|
32
|
+
repo: { type: "string" },
|
|
33
|
+
locale: { type: "string" },
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
function usageError(ctx: CliCommandContext, message?: string, usage: string = USAGE): number {
|
|
24
37
|
ctx.out.err("");
|
|
25
38
|
if (message) ctx.out.err(` ${message}`);
|
|
26
|
-
ctx.out.err(` ${
|
|
39
|
+
ctx.out.err(` ${usage}`);
|
|
27
40
|
ctx.out.err("");
|
|
28
41
|
return 2;
|
|
29
42
|
}
|
|
@@ -73,31 +86,23 @@ function wipesBuildInputs({
|
|
|
73
86
|
return wipesProtectedDir || lieInsideSource;
|
|
74
87
|
}
|
|
75
88
|
|
|
76
|
-
|
|
77
|
-
let parsed: ReturnType<
|
|
78
|
-
typeof parseArgs<{ options: typeof BUILD_OPTIONS; allowPositionals: true }>
|
|
79
|
-
>;
|
|
80
|
-
try {
|
|
81
|
-
parsed = parseArgs({ args: [...args], options: BUILD_OPTIONS, allowPositionals: true });
|
|
82
|
-
} catch (error) {
|
|
83
|
-
return usageError(ctx, describeError(error));
|
|
84
|
-
}
|
|
85
|
-
const { values, positionals } = parsed;
|
|
86
|
-
const configArg = positionals[0];
|
|
87
|
-
if (!configArg) return usageError(ctx, "Missing <config.json>.");
|
|
89
|
+
type ConfigOverrides = { locale?: string; repo?: string; "web-repo"?: string };
|
|
88
90
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
// Reports a bad config itself; the caller only maps undefined to exit code 2.
|
|
92
|
+
async function loadConfigWithOverrides(
|
|
93
|
+
ctx: CliCommandContext,
|
|
94
|
+
configPath: string,
|
|
95
|
+
values: ConfigOverrides,
|
|
96
|
+
usage: string,
|
|
97
|
+
): Promise<HandbuchConfig | undefined> {
|
|
91
98
|
// Lazy: the builder pulls in optional peers (framework, headless) that other commands must not need.
|
|
92
99
|
const { loadHandbuchConfig, restrictToLocale } = await import("../handbuch/config");
|
|
93
|
-
const { buildHandbuch, SHARP_MISSING_MESSAGE } = await import("../handbuch/build");
|
|
94
|
-
let config: HandbuchConfig;
|
|
95
100
|
try {
|
|
96
101
|
const loaded = loadHandbuchConfig(configPath);
|
|
97
102
|
const narrowed = values.locale ? restrictToLocale(loaded, values.locale) : loaded;
|
|
98
103
|
// Worktree checkouts hold the code the texts describe; the configured
|
|
99
104
|
// sibling checkouts may sit on another commit.
|
|
100
|
-
|
|
105
|
+
return {
|
|
101
106
|
...narrowed,
|
|
102
107
|
repo: values.repo ? resolve(ctx.cwd, values.repo) : narrowed.repo,
|
|
103
108
|
flowSources: narrowed.flowSources?.map((source) => ({
|
|
@@ -109,10 +114,71 @@ async function runBuild(ctx: CliCommandContext, args: ReadonlyArray<string>): Pr
|
|
|
109
114
|
ctx.out.err("");
|
|
110
115
|
ctx.out.err(` Invalid handbuch config ${configPath}:`);
|
|
111
116
|
ctx.out.err(` ${describeError(error)}`);
|
|
112
|
-
ctx.out.err(` ${
|
|
117
|
+
ctx.out.err(` ${usage}`);
|
|
113
118
|
ctx.out.err("");
|
|
114
|
-
return
|
|
119
|
+
return undefined;
|
|
115
120
|
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function runKnowledge(ctx: CliCommandContext, args: ReadonlyArray<string>): Promise<number> {
|
|
124
|
+
let parsed: ReturnType<
|
|
125
|
+
typeof parseArgs<{ options: typeof KNOWLEDGE_OPTIONS; allowPositionals: true }>
|
|
126
|
+
>;
|
|
127
|
+
try {
|
|
128
|
+
parsed = parseArgs({ args: [...args], options: KNOWLEDGE_OPTIONS, allowPositionals: true });
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return usageError(ctx, describeError(error), KNOWLEDGE_USAGE);
|
|
131
|
+
}
|
|
132
|
+
const { values, positionals } = parsed;
|
|
133
|
+
const configArg = positionals[0];
|
|
134
|
+
if (!configArg) return usageError(ctx, "Missing <config.json>.", KNOWLEDGE_USAGE);
|
|
135
|
+
if (!values.out) return usageError(ctx, "Missing --out <file>.", KNOWLEDGE_USAGE);
|
|
136
|
+
|
|
137
|
+
const rootDir = resolve(ctx.cwd, values.root ?? ".");
|
|
138
|
+
const configPath = resolve(rootDir, configArg);
|
|
139
|
+
const config = await loadConfigWithOverrides(ctx, configPath, values, KNOWLEDGE_USAGE);
|
|
140
|
+
if (!config) return 2;
|
|
141
|
+
|
|
142
|
+
const { writeKnowledge } = await import("../handbuch/knowledge");
|
|
143
|
+
try {
|
|
144
|
+
const result = await writeKnowledge(config, {
|
|
145
|
+
rootDir,
|
|
146
|
+
outFile: resolve(ctx.cwd, values.out),
|
|
147
|
+
textsDir: values.texts ? resolve(ctx.cwd, values.texts) : undefined,
|
|
148
|
+
flowTextsDir: values["web-texts"] ? resolve(ctx.cwd, values["web-texts"]) : undefined,
|
|
149
|
+
});
|
|
150
|
+
const counts = Object.entries(result.pageCounts)
|
|
151
|
+
.map(([locale, count]) => `${locale} ${count}`)
|
|
152
|
+
.join(", ");
|
|
153
|
+
ctx.out.log(`${config.title}: ${counts} Seiten`);
|
|
154
|
+
ctx.out.log(result.outFile);
|
|
155
|
+
return 0;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
ctx.out.err("");
|
|
158
|
+
ctx.out.err(` ✗ ${describeError(error)}`);
|
|
159
|
+
ctx.out.err("");
|
|
160
|
+
return 1;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function runBuild(ctx: CliCommandContext, args: ReadonlyArray<string>): Promise<number> {
|
|
165
|
+
let parsed: ReturnType<
|
|
166
|
+
typeof parseArgs<{ options: typeof BUILD_OPTIONS; allowPositionals: true }>
|
|
167
|
+
>;
|
|
168
|
+
try {
|
|
169
|
+
parsed = parseArgs({ args: [...args], options: BUILD_OPTIONS, allowPositionals: true });
|
|
170
|
+
} catch (error) {
|
|
171
|
+
return usageError(ctx, describeError(error));
|
|
172
|
+
}
|
|
173
|
+
const { values, positionals } = parsed;
|
|
174
|
+
const configArg = positionals[0];
|
|
175
|
+
if (!configArg) return usageError(ctx, "Missing <config.json>.");
|
|
176
|
+
|
|
177
|
+
const rootDir = resolve(ctx.cwd, values.root ?? ".");
|
|
178
|
+
const configPath = resolve(rootDir, configArg);
|
|
179
|
+
const { SHARP_MISSING_MESSAGE, buildHandbuch } = await import("../handbuch/build");
|
|
180
|
+
const config = await loadConfigWithOverrides(ctx, configPath, values, USAGE);
|
|
181
|
+
if (!config) return 2;
|
|
116
182
|
|
|
117
183
|
// buildHandbuch wipes outDir recursively, so a mistyped --out must never be
|
|
118
184
|
// (or contain) a directory the build reads from or the user works in.
|
|
@@ -162,10 +228,12 @@ async function runBuild(ctx: CliCommandContext, args: ReadonlyArray<string>): Pr
|
|
|
162
228
|
|
|
163
229
|
export const handbuchCommand: CliCommand = {
|
|
164
230
|
id: "handbuch",
|
|
165
|
-
description:
|
|
166
|
-
|
|
231
|
+
description:
|
|
232
|
+
"Build the HTML handbuch (build <config.json>) or export its texts as JSON (knowledge <config.json>)",
|
|
233
|
+
help: `${USAGE}\n${KNOWLEDGE_USAGE}\n\nknowledge writes the page texts per locale (without images) as deterministic JSON for the assistant.\n\nRenders the handbuch page from the texts and screenshots a config points at.\nRelative paths in the config resolve against --root (default: cwd); the output\ngoes to <root>/.handbuch/<config-name> unless --out is given. Needs sharp\n(bun add -d sharp) for the image conversion.`,
|
|
167
234
|
run: async (ctx) => {
|
|
168
235
|
const [sub, ...rest] = ctx.argv;
|
|
236
|
+
if (sub === "knowledge") return runKnowledge(ctx, rest);
|
|
169
237
|
if (sub !== "build") return usageError(ctx);
|
|
170
238
|
return runBuild(ctx, rest);
|
|
171
239
|
},
|
package/src/handbuch/chrome.ts
CHANGED
|
@@ -21,6 +21,7 @@ export type Chrome = {
|
|
|
21
21
|
lightbox: string;
|
|
22
22
|
close: string;
|
|
23
23
|
fallbackGroup: string;
|
|
24
|
+
processGroup: string;
|
|
24
25
|
builtAt: string;
|
|
25
26
|
viewports: { desktop: string; tablet: string; mobile: string; all: string };
|
|
26
27
|
};
|
|
@@ -48,6 +49,7 @@ const de: Chrome = {
|
|
|
48
49
|
lightbox: "Screenshot vergrößert",
|
|
49
50
|
close: "Schließen",
|
|
50
51
|
fallbackGroup: "Weitere",
|
|
52
|
+
processGroup: "Aufgaben",
|
|
51
53
|
builtAt: "Stand",
|
|
52
54
|
viewports: {
|
|
53
55
|
desktop: "Desktop",
|
|
@@ -80,6 +82,7 @@ const en: Chrome = {
|
|
|
80
82
|
lightbox: "Screenshot enlarged",
|
|
81
83
|
close: "Close",
|
|
82
84
|
fallbackGroup: "Other",
|
|
85
|
+
processGroup: "Tasks",
|
|
83
86
|
builtAt: "Built",
|
|
84
87
|
viewports: {
|
|
85
88
|
desktop: "Desktop",
|
package/src/handbuch/config.ts
CHANGED
|
@@ -50,6 +50,7 @@ export const handbuchConfigSchema = z
|
|
|
50
50
|
groups: z.array(groupSchema).min(1),
|
|
51
51
|
translations: z.record(z.string(), translationsSchema).optional(),
|
|
52
52
|
textsDir: z.string().includes("{locale}"),
|
|
53
|
+
processTextsDir: z.string().includes("{locale}").optional(),
|
|
53
54
|
shotsDir: z.string().optional(),
|
|
54
55
|
uiStrings: z.array(z.string().min(1)).optional(),
|
|
55
56
|
uiStringPackages: z
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import type { HandbuchConfig } from "./config";
|
|
4
|
+
import { type BuildModelOptions, buildModel, type Chapter, type Model } from "./model";
|
|
5
|
+
|
|
6
|
+
export type KnowledgePageKind = "process" | "screen";
|
|
7
|
+
|
|
8
|
+
export type KnowledgePage = {
|
|
9
|
+
id: string;
|
|
10
|
+
kind: KnowledgePageKind;
|
|
11
|
+
title: string;
|
|
12
|
+
questions: string[];
|
|
13
|
+
screens: string[];
|
|
14
|
+
start?: string;
|
|
15
|
+
bodyMd: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type KnowledgeFile = {
|
|
19
|
+
defaultLocale: string;
|
|
20
|
+
locales: Record<string, KnowledgePage[]>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const MARKDOWN_IMAGE = /!\[[^\]]*\]\([^)]*\)/g;
|
|
24
|
+
|
|
25
|
+
export function stripMarkdownImages(bodyMd: string): string {
|
|
26
|
+
return bodyMd
|
|
27
|
+
.replace(MARKDOWN_IMAGE, "")
|
|
28
|
+
.replace(/[ \t]+$/gm, "")
|
|
29
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
30
|
+
.trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function compareText(a: string, b: string): number {
|
|
34
|
+
if (a < b) return -1;
|
|
35
|
+
return a > b ? 1 : 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function pageOf(chapter: Chapter, locale: string): KnowledgePage | undefined {
|
|
39
|
+
const text = chapter.texts.get(locale);
|
|
40
|
+
if (!text) return undefined;
|
|
41
|
+
const kind: KnowledgePageKind = chapter.isProcess ? "process" : "screen";
|
|
42
|
+
return {
|
|
43
|
+
id: chapter.id,
|
|
44
|
+
kind,
|
|
45
|
+
title: text.title,
|
|
46
|
+
questions: text.questions,
|
|
47
|
+
screens: kind === "process" ? text.screens : [chapter.id],
|
|
48
|
+
...(kind === "process" && text.start !== undefined && { start: text.start }),
|
|
49
|
+
bodyMd: stripMarkdownImages(text.bodyMd),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function comparePages(a: KnowledgePage, b: KnowledgePage): number {
|
|
54
|
+
return compareText(a.kind, b.kind) || compareText(a.id, b.id);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function buildKnowledge(model: Model): KnowledgeFile {
|
|
58
|
+
const chapters = model.groups.flatMap((group) => group.chapters);
|
|
59
|
+
const locales: Record<string, KnowledgePage[]> = {};
|
|
60
|
+
for (const locale of [...model.locales].sort(compareText)) {
|
|
61
|
+
locales[locale] = chapters
|
|
62
|
+
.flatMap((chapter) => pageOf(chapter, locale) ?? [])
|
|
63
|
+
.sort(comparePages);
|
|
64
|
+
}
|
|
65
|
+
return { defaultLocale: model.defaultLocale, locales };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function writeKnowledge(
|
|
69
|
+
config: HandbuchConfig,
|
|
70
|
+
options: BuildModelOptions & { outFile: string },
|
|
71
|
+
): Promise<{ outFile: string; pageCounts: Record<string, number> }> {
|
|
72
|
+
const { outFile, ...modelOptions } = options;
|
|
73
|
+
const knowledge = buildKnowledge(await buildModel(config, modelOptions));
|
|
74
|
+
mkdirSync(dirname(outFile), { recursive: true });
|
|
75
|
+
writeFileSync(outFile, `${JSON.stringify(knowledge, null, 2)}\n`);
|
|
76
|
+
return {
|
|
77
|
+
outFile,
|
|
78
|
+
pageCounts: Object.fromEntries(
|
|
79
|
+
Object.entries(knowledge.locales).map(([locale, pages]) => [locale, pages.length]),
|
|
80
|
+
),
|
|
81
|
+
};
|
|
82
|
+
}
|
package/src/handbuch/model.ts
CHANGED
|
@@ -22,6 +22,8 @@ export const VIEWPORT_WIDTH: Record<Viewport, number> = {
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
const FALLBACK_GROUP_ID = "weitere";
|
|
25
|
+
const PROCESS_GROUP_ID = "aufgaben";
|
|
26
|
+
const PROCESS_ID_PREFIX = "prozess-";
|
|
25
27
|
|
|
26
28
|
const LIGHT_THEME_DIR = "default-light";
|
|
27
29
|
const LOCAL_IMAGE_REF = /\.(png|jpe?g|webp|gif|svg)$/i;
|
|
@@ -35,7 +37,10 @@ export type LocaleText = {
|
|
|
35
37
|
bodyMd: string;
|
|
36
38
|
dir: string;
|
|
37
39
|
gruppe?: string;
|
|
40
|
+
start?: string;
|
|
38
41
|
prozesse: string[];
|
|
42
|
+
questions: string[];
|
|
43
|
+
screens: string[];
|
|
39
44
|
missingImages: string[];
|
|
40
45
|
uiIgnore: string[];
|
|
41
46
|
staleTerms: string[];
|
|
@@ -56,6 +61,7 @@ export type Chapter = {
|
|
|
56
61
|
matrix: Map<string, MatrixShots>;
|
|
57
62
|
steps?: Step[];
|
|
58
63
|
inScreenList: boolean;
|
|
64
|
+
isProcess?: boolean;
|
|
59
65
|
};
|
|
60
66
|
|
|
61
67
|
export type Model = {
|
|
@@ -134,6 +140,24 @@ export function parseList(raw: string | undefined): string[] {
|
|
|
134
140
|
.filter(Boolean);
|
|
135
141
|
}
|
|
136
142
|
|
|
143
|
+
// Inline `key: [a, b]` or a YAML block list; parseFrontmatter only keeps single-line values.
|
|
144
|
+
export function parseFrontmatterList(raw: string, key: string): string[] {
|
|
145
|
+
const block = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1];
|
|
146
|
+
if (block === undefined) return [];
|
|
147
|
+
const lines = block.split(/\r?\n/);
|
|
148
|
+
const start = lines.findIndex((line) => line.startsWith(`${key}:`));
|
|
149
|
+
if (start === -1) return [];
|
|
150
|
+
const inline = (lines[start] ?? "").slice(key.length + 1).trim();
|
|
151
|
+
if (inline) return parseList(inline);
|
|
152
|
+
const items: string[] = [];
|
|
153
|
+
for (const line of lines.slice(start + 1)) {
|
|
154
|
+
const item = line.match(/^\s*-\s+(.*)$/)?.[1];
|
|
155
|
+
if (item === undefined) break;
|
|
156
|
+
items.push(item.trim().replace(/^["']|["']$/g, ""));
|
|
157
|
+
}
|
|
158
|
+
return items;
|
|
159
|
+
}
|
|
160
|
+
|
|
137
161
|
export function stripLeadingHeading(body: string): string {
|
|
138
162
|
const lines = body.split(/\r?\n/);
|
|
139
163
|
let index = 0;
|
|
@@ -170,7 +194,8 @@ export function readTexts(textsRoot: string): Map<string, LocaleText & { id: str
|
|
|
170
194
|
for (const file of readdirSync(textsRoot)
|
|
171
195
|
.filter((name) => name.endsWith(".md"))
|
|
172
196
|
.sort()) {
|
|
173
|
-
const
|
|
197
|
+
const raw = readFileSync(join(textsRoot, file), "utf8");
|
|
198
|
+
const { data, body } = parseFrontmatter(raw);
|
|
174
199
|
const id = data["screenId"] || basename(file, ".md");
|
|
175
200
|
const bodyMd = stripLeadingHeading(body);
|
|
176
201
|
texts.set(id, {
|
|
@@ -179,7 +204,10 @@ export function readTexts(textsRoot: string): Map<string, LocaleText & { id: str
|
|
|
179
204
|
bodyMd,
|
|
180
205
|
dir: textsRoot,
|
|
181
206
|
gruppe: data["gruppe"] || undefined,
|
|
207
|
+
start: data["start"] || undefined,
|
|
182
208
|
prozesse: parseList(data["prozesse"]),
|
|
209
|
+
questions: parseFrontmatterList(raw, "fragen"),
|
|
210
|
+
screens: parseFrontmatterList(raw, "masken"),
|
|
183
211
|
uiIgnore: parseList(data["ui-ignore"]),
|
|
184
212
|
staleTerms: [],
|
|
185
213
|
missingImages: localImageRefs(bodyMd).filter(
|
|
@@ -190,6 +218,16 @@ export function readTexts(textsRoot: string): Map<string, LocaleText & { id: str
|
|
|
190
218
|
return texts;
|
|
191
219
|
}
|
|
192
220
|
|
|
221
|
+
// Prefixed so a process page never shares an id with a screen chapter.
|
|
222
|
+
function readProcessTexts(textsRoot: string): Map<string, LocaleText & { id: string }> {
|
|
223
|
+
const processTexts = new Map<string, LocaleText & { id: string }>();
|
|
224
|
+
for (const [rawId, text] of readTexts(textsRoot)) {
|
|
225
|
+
const id = `${PROCESS_ID_PREFIX}${rawId}`;
|
|
226
|
+
processTexts.set(id, { ...text, id, gruppe: PROCESS_GROUP_ID });
|
|
227
|
+
}
|
|
228
|
+
return processTexts;
|
|
229
|
+
}
|
|
230
|
+
|
|
193
231
|
export function discoverMatrix(
|
|
194
232
|
shotsDir: string,
|
|
195
233
|
locales: readonly string[],
|
|
@@ -395,19 +433,12 @@ function registerChapters(
|
|
|
395
433
|
return { chapters, sourceOrder, configGroupOf };
|
|
396
434
|
}
|
|
397
435
|
|
|
398
|
-
|
|
436
|
+
function loadTextsByLocale(
|
|
399
437
|
config: HandbuchConfig,
|
|
400
438
|
options: BuildModelOptions,
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
options.screens ??
|
|
405
|
-
config.screens ??
|
|
406
|
-
(config.screensModule
|
|
407
|
-
? await loadScreensModule(options.rootDir, config.repo, config.screensModule)
|
|
408
|
-
: []);
|
|
409
|
-
|
|
410
|
-
const textsByLocale = new Map(
|
|
439
|
+
repoDir: string,
|
|
440
|
+
): Map<string, Map<string, LocaleText>> {
|
|
441
|
+
return new Map(
|
|
411
442
|
config.locales.map((locale) => [
|
|
412
443
|
locale,
|
|
413
444
|
readTexts(
|
|
@@ -417,19 +448,63 @@ export async function buildModel(
|
|
|
417
448
|
),
|
|
418
449
|
]),
|
|
419
450
|
);
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function applyStaleTerms(
|
|
454
|
+
byLocale: Map<string, Map<string, LocaleText>>,
|
|
455
|
+
uiStrings: Map<string, string[]>,
|
|
456
|
+
): void {
|
|
457
|
+
const uiIndex = buildUiIndex(uiStrings);
|
|
458
|
+
for (const [locale, texts] of byLocale) {
|
|
459
|
+
for (const text of texts.values()) {
|
|
460
|
+
text.staleTerms = findMissingUiTerms(text.bodyMd, locale, uiIndex, text.uiIgnore);
|
|
429
461
|
}
|
|
430
|
-
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function mergeProcessTexts(
|
|
466
|
+
config: HandbuchConfig,
|
|
467
|
+
repoDir: string,
|
|
468
|
+
textsByLocale: Map<string, Map<string, LocaleText>>,
|
|
469
|
+
): Set<string> {
|
|
470
|
+
const processIds = new Set<string>();
|
|
471
|
+
const processTextsDir = config.processTextsDir;
|
|
472
|
+
if (!processTextsDir) return processIds;
|
|
473
|
+
for (const locale of config.locales) {
|
|
474
|
+
const processTexts = readProcessTexts(
|
|
475
|
+
join(repoDir, processTextsDir.replace("{locale}", locale)),
|
|
476
|
+
);
|
|
477
|
+
for (const [id, text] of processTexts) {
|
|
478
|
+
textsByLocale.get(locale)?.set(id, text);
|
|
479
|
+
processIds.add(id);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return processIds;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function markProcessChapters(chapters: Map<string, Chapter>, processIds: Set<string>): void {
|
|
486
|
+
for (const id of processIds) {
|
|
487
|
+
const chapter = chapters.get(id);
|
|
488
|
+
if (chapter) chapter.isProcess = true;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export async function buildModel(
|
|
493
|
+
config: HandbuchConfig,
|
|
494
|
+
options: BuildModelOptions,
|
|
495
|
+
): Promise<Model> {
|
|
496
|
+
const repoDir = resolve(options.rootDir, config.repo);
|
|
497
|
+
const screens =
|
|
498
|
+
options.screens ??
|
|
499
|
+
config.screens ??
|
|
500
|
+
(config.screensModule
|
|
501
|
+
? await loadScreensModule(options.rootDir, config.repo, config.screensModule)
|
|
502
|
+
: []);
|
|
503
|
+
|
|
504
|
+
const textsByLocale = loadTextsByLocale(config, options, repoDir);
|
|
505
|
+
const processIds = mergeProcessTexts(config, repoDir, textsByLocale);
|
|
431
506
|
if (config.uiStrings || config.uiStringPackages) {
|
|
432
|
-
|
|
507
|
+
applyStaleTerms(
|
|
433
508
|
textsByLocale,
|
|
434
509
|
collectUiStrings({
|
|
435
510
|
repoDir,
|
|
@@ -445,6 +520,10 @@ export async function buildModel(
|
|
|
445
520
|
...group,
|
|
446
521
|
label: tr?.groups[group.id] ?? group.label,
|
|
447
522
|
});
|
|
523
|
+
const processGroups: Group[] =
|
|
524
|
+
processIds.size > 0 && !config.groups.some((group) => group.id === PROCESS_GROUP_ID)
|
|
525
|
+
? [labelGroup({ id: PROCESS_GROUP_ID, label: chromeFor(config.defaultLocale).processGroup })]
|
|
526
|
+
: [];
|
|
448
527
|
const fallbackGroupDefault: Group = {
|
|
449
528
|
id: FALLBACK_GROUP_ID,
|
|
450
529
|
label: chromeFor(config.defaultLocale).fallbackGroup,
|
|
@@ -454,7 +533,7 @@ export async function buildModel(
|
|
|
454
533
|
config,
|
|
455
534
|
options,
|
|
456
535
|
textsByLocale,
|
|
457
|
-
|
|
536
|
+
applyStaleTerms,
|
|
458
537
|
labelGroup,
|
|
459
538
|
);
|
|
460
539
|
const shotsDir =
|
|
@@ -463,13 +542,14 @@ export async function buildModel(
|
|
|
463
542
|
? discoverMatrix(shotsDir, config.locales)
|
|
464
543
|
: new Map<string, Map<string, MatrixShots>>();
|
|
465
544
|
|
|
466
|
-
const groups: Group[] = [...config.groups.map(labelGroup), ...flowGroups];
|
|
545
|
+
const groups: Group[] = [...config.groups.map(labelGroup), ...flowGroups, ...processGroups];
|
|
467
546
|
const { chapters, sourceOrder, configGroupOf } = registerChapters(
|
|
468
547
|
screens,
|
|
469
548
|
flowChapters,
|
|
470
549
|
textsByLocale,
|
|
471
550
|
matrix,
|
|
472
551
|
);
|
|
552
|
+
markProcessChapters(chapters, processIds);
|
|
473
553
|
|
|
474
554
|
const fallbackGroup = assignGroups(config, chapters, groups, configGroupOf, fallbackGroupDefault);
|
|
475
555
|
return assembleModel(config, chapters, groups, sourceOrder, fallbackGroup);
|