concept-atlas-dense-explain 1.2.0 → 3.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.
- package/README.md +8 -5
- package/bin/cli.mjs +212 -78
- package/package.json +1 -1
- package/packaged-skill/SKILL.md +14 -11
- package/packaged-skill/references/atlas-guide.mdx +5 -4
- package/packaged-skill/references/scroll-guide.mdx +16 -2
- package/template/references/atlas-guide.mdx +5 -4
- package/template/references/scroll-guide.mdx +16 -2
- package/template/src/app/App.jsx +76 -43
- package/template/src/app/search.js +14 -5
- package/template/src/app/use-appearance.js +29 -15
- package/template/src/components/FigureScope.jsx +17 -0
- package/template/src/components/MDXComponents.jsx +1063 -193
- package/template/src/components/SkinPicker.jsx +14 -6
- package/template/src/model/concept-schema.js +27 -12
- package/template/src/model/figures.js +61 -0
- package/template/src/model/node-kinds.js +9 -9
- package/template/src/model/normalize-content.js +8 -8
- package/template/src/model/relation-types.js +37 -12
- package/template/src/model/validate-content.js +308 -55
- package/template/src/scroll-main.jsx +12 -2
- package/template/src/styles/core.css +215 -56
- package/template/src/styles/packs/elastic.css +37 -4
- package/template/src/styles/packs/manuscript.css +67 -0
- package/template/src/styles/packs/shadcn.css +33 -4
- package/template/src/views/NodeExplorer.jsx +369 -316
- package/template/src/views/RelationGraph.jsx +191 -118
- package/template/vite.config.js +116 -30
package/README.md
CHANGED
|
@@ -26,23 +26,26 @@ npx concept-atlas-dense-explain validate topic.mdx --mode atlas
|
|
|
26
26
|
npx concept-atlas-dense-explain topic.mdx --mode atlas
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
inputs). Validation errors abort the build; `--no-validate` forces a knowingly
|
|
29
|
+
Existing outputs are overwritten; `--force` is accepted for compatibility and is
|
|
30
|
+
a no-op. `--mode` defaults to `scroll`. Use `-o` to choose the output path (a
|
|
31
|
+
directory when passing several inputs). Validation errors abort the build; `--no-validate` forces a knowingly
|
|
32
32
|
broken build.
|
|
33
33
|
|
|
34
34
|
Mermaid loads from a CDN at runtime by default (fast builds, needs network);
|
|
35
35
|
`--inline-mermaid` bakes it into the HTML for a fully offline single file, and
|
|
36
36
|
`--mermaid-cdn` overrides the CDN URL. Appearance defaults can be baked with
|
|
37
37
|
`--skin`, `--default-mode` and `--style`; readers can still switch in the UI.
|
|
38
|
+
Local figures link by default (small HTML, ship `assets/` beside the output);
|
|
39
|
+
`--inline-assets` bakes them in as base64 for a self-contained file, and a
|
|
40
|
+
per-figure `inline={true|false}` overrides that choice.
|
|
38
41
|
|
|
39
42
|
Run `npx concept-atlas-dense-explain help` for the full flag list.
|
|
40
43
|
|
|
41
44
|
## Links
|
|
42
45
|
|
|
43
46
|
- Repository: https://github.com/wurenrumian/concept-atlas
|
|
44
|
-
- Framework guide: [`docs/FRAMEWORK.md`](https://github.com/wurenrumian/concept-atlas/blob/
|
|
45
|
-
- Usage recipes: [`docs/USAGE.md`](https://github.com/wurenrumian/concept-atlas/blob/
|
|
47
|
+
- Framework guide: [`docs/FRAMEWORK.md`](https://github.com/wurenrumian/concept-atlas/blob/master/docs/FRAMEWORK.md)
|
|
48
|
+
- Usage recipes: [`docs/USAGE.md`](https://github.com/wurenrumian/concept-atlas/blob/master/docs/USAGE.md)
|
|
46
49
|
|
|
47
50
|
## License
|
|
48
51
|
|
package/bin/cli.mjs
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
access,
|
|
4
|
+
constants,
|
|
5
|
+
copyFile,
|
|
6
|
+
cp,
|
|
7
|
+
mkdir,
|
|
8
|
+
readFile,
|
|
9
|
+
rm,
|
|
10
|
+
rename,
|
|
11
|
+
writeFile
|
|
12
|
+
} from 'node:fs/promises';
|
|
13
|
+
import { existsSync, statSync } from 'node:fs';
|
|
4
14
|
import path from 'node:path';
|
|
5
15
|
import { build } from 'vite';
|
|
6
16
|
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import {
|
|
8
|
-
|
|
17
|
+
import {
|
|
18
|
+
validateMdxSource,
|
|
19
|
+
countBySeverity,
|
|
20
|
+
detectFeatures,
|
|
21
|
+
extractPageTitle
|
|
22
|
+
} from '../template/src/model/validate-content.js';
|
|
23
|
+
import {
|
|
24
|
+
SKINS,
|
|
25
|
+
normalizeSkin,
|
|
26
|
+
COMPONENT_STYLES,
|
|
27
|
+
normalizeStyle
|
|
28
|
+
} from '../template/src/model/skins.js';
|
|
9
29
|
|
|
10
30
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
31
|
const templateRoot = path.join(packageRoot, 'template');
|
|
@@ -14,21 +34,50 @@ let buildCounter = 0;
|
|
|
14
34
|
|
|
15
35
|
function usage() {
|
|
16
36
|
console.log('Usage:');
|
|
17
|
-
console.log(
|
|
37
|
+
console.log(
|
|
38
|
+
' npx concept-atlas-dense-explain <input.mdx>... [--mode atlas|scroll] [--skin <id>] [--default-mode dark|light|system] [--style <id>] [-o output.html|dir] [--concurrency N] [--inline-assets] [--inline-mermaid] [--mermaid-cdn <url>] [--json] [--no-validate]'
|
|
39
|
+
);
|
|
18
40
|
console.log(' npx concept-atlas-dense-explain render <input.mdx>... [-o output.html|dir]');
|
|
19
|
-
console.log(
|
|
20
|
-
|
|
21
|
-
|
|
41
|
+
console.log(
|
|
42
|
+
' npx concept-atlas-dense-explain validate <input.mdx> [--mode atlas|scroll] [--strict] [--json]'
|
|
43
|
+
);
|
|
44
|
+
console.log(' npx concept-atlas-dense-explain create <output.mdx> [--mode atlas|scroll]');
|
|
45
|
+
console.log(' npx concept-atlas-dense-explain guide [--mode atlas|scroll] [-o output.mdx]');
|
|
22
46
|
console.log('');
|
|
47
|
+
console.log(
|
|
48
|
+
' --mode defaults to scroll. Existing outputs are overwritten; --force is accepted for compatibility and is a no-op.'
|
|
49
|
+
);
|
|
23
50
|
console.log(' --help shows this text; --version prints the package version.');
|
|
24
|
-
console.log(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
console.log(
|
|
51
|
+
console.log(
|
|
52
|
+
' Multiple inputs build in parallel (default 2 at a time, cap 4); -o is then a directory.'
|
|
53
|
+
);
|
|
54
|
+
console.log(
|
|
55
|
+
' Figures are kept as relative links by default (small HTML; ship the assets/ dir beside it). --inline-assets bakes every local image into the HTML as base64 instead; a per-tag inline={true|false} on <Figure> overrides that for one image. --link-assets is kept as an explicit alias for the default.'
|
|
56
|
+
);
|
|
57
|
+
console.log(
|
|
58
|
+
' Mermaid diagrams load from a CDN at runtime by default (fast builds, needs network); --inline-mermaid bakes Mermaid into the HTML for a fully offline single file; --mermaid-cdn overrides the CDN URL.'
|
|
59
|
+
);
|
|
60
|
+
console.log(
|
|
61
|
+
` --skin bakes a default palette (${SKINS.map(skin => skin.id).join(', ')}); --default-mode bakes a default dark/light mode; --style bakes a default component style (${COMPONENT_STYLES.map(style => style.id).join(', ')}). Readers can still switch in the UI.`
|
|
62
|
+
);
|
|
28
63
|
}
|
|
29
64
|
|
|
30
65
|
async function exists(filePath) {
|
|
31
|
-
try {
|
|
66
|
+
try {
|
|
67
|
+
await access(filePath, constants.F_OK);
|
|
68
|
+
return true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Byte size of an asset, or null when it does not exist. */
|
|
75
|
+
function assetByteSize(filePath) {
|
|
76
|
+
try {
|
|
77
|
+
return statSync(filePath).size;
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
32
81
|
}
|
|
33
82
|
|
|
34
83
|
function flagValue(flags, names) {
|
|
@@ -45,8 +94,29 @@ function fail(message) {
|
|
|
45
94
|
process.exit(1);
|
|
46
95
|
}
|
|
47
96
|
|
|
48
|
-
const VALUE_FLAGS = new Set([
|
|
49
|
-
|
|
97
|
+
const VALUE_FLAGS = new Set([
|
|
98
|
+
'--mode',
|
|
99
|
+
'-o',
|
|
100
|
+
'--output',
|
|
101
|
+
'--concurrency',
|
|
102
|
+
'--skin',
|
|
103
|
+
'--default-mode',
|
|
104
|
+
'--style',
|
|
105
|
+
'--mermaid-cdn'
|
|
106
|
+
]);
|
|
107
|
+
const BOOLEAN_FLAGS = new Set([
|
|
108
|
+
'--force',
|
|
109
|
+
'--json',
|
|
110
|
+
'--strict',
|
|
111
|
+
'--no-validate',
|
|
112
|
+
'--link-assets',
|
|
113
|
+
'--inline-assets',
|
|
114
|
+
'--inline-mermaid',
|
|
115
|
+
'--help',
|
|
116
|
+
'-h',
|
|
117
|
+
'--version',
|
|
118
|
+
'-v'
|
|
119
|
+
]);
|
|
50
120
|
const COMMAND_NAMES = ['help', 'create', 'new', 'render', 'validate', 'guide'];
|
|
51
121
|
|
|
52
122
|
/** Splits argv into flags, flag values and positional arguments. */
|
|
@@ -77,7 +147,10 @@ function extractCommand(argv) {
|
|
|
77
147
|
const rest = [...argv];
|
|
78
148
|
for (let i = 0; i < rest.length; i += 1) {
|
|
79
149
|
const arg = rest[i];
|
|
80
|
-
if (VALUE_FLAGS.has(arg)) {
|
|
150
|
+
if (VALUE_FLAGS.has(arg)) {
|
|
151
|
+
i += 1;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
81
154
|
if (arg.startsWith('-') && arg.length > 1) continue;
|
|
82
155
|
if (COMMAND_NAMES.includes(arg)) {
|
|
83
156
|
rest.splice(i, 1);
|
|
@@ -100,7 +173,9 @@ function resolveOutputs(inputs, explicit) {
|
|
|
100
173
|
if (path.extname(target).toLowerCase() === '.html') {
|
|
101
174
|
fail('`-o` must be a directory when building more than one input.');
|
|
102
175
|
}
|
|
103
|
-
return inputs.map(input =>
|
|
176
|
+
return inputs.map(input =>
|
|
177
|
+
path.join(target, `${path.basename(input, path.extname(input))}.html`)
|
|
178
|
+
);
|
|
104
179
|
}
|
|
105
180
|
|
|
106
181
|
function printDiagnostics(source, options, { json, label = null, quiet = false }) {
|
|
@@ -118,7 +193,9 @@ function printDiagnostics(source, options, { json, label = null, quiet = false }
|
|
|
118
193
|
console.error(`${severity} ${where} ${item.code} ${item.message}`);
|
|
119
194
|
}
|
|
120
195
|
const { error, warning } = countBySeverity(diagnostics);
|
|
121
|
-
const scope = carrier
|
|
196
|
+
const scope = carrier
|
|
197
|
+
? `${carrier} · ${stats.nodes} 节点 / ${stats.relations} 关系`
|
|
198
|
+
: '未识别载体';
|
|
122
199
|
if (error) console.error(`校验失败:${error} 个错误,${warning} 个警告(${scope})`);
|
|
123
200
|
else if (warning) console.error(`校验通过:${warning} 个警告(${scope})`);
|
|
124
201
|
else console.error(`校验通过:无问题(${scope})`);
|
|
@@ -128,7 +205,10 @@ function printDiagnostics(source, options, { json, label = null, quiet = false }
|
|
|
128
205
|
const { command, rest } = extractCommand(args);
|
|
129
206
|
args = rest;
|
|
130
207
|
|
|
131
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
208
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
209
|
+
usage();
|
|
210
|
+
process.exit(0);
|
|
211
|
+
}
|
|
132
212
|
if (args.includes('--version') || args.includes('-v')) {
|
|
133
213
|
const pkg = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
134
214
|
console.log(pkg.version);
|
|
@@ -150,16 +230,17 @@ for (let i = 0; i < args.length; i += 1) {
|
|
|
150
230
|
}
|
|
151
231
|
}
|
|
152
232
|
|
|
153
|
-
if (command === 'help') {
|
|
233
|
+
if (command === 'help') {
|
|
234
|
+
usage();
|
|
235
|
+
process.exit(0);
|
|
236
|
+
}
|
|
154
237
|
|
|
155
238
|
if (command === 'guide') {
|
|
156
|
-
const mode = flagValue(args, ['--mode']) || '
|
|
239
|
+
const mode = flagValue(args, ['--mode']) || 'scroll';
|
|
157
240
|
if (!['atlas', 'scroll'].includes(mode)) fail(`Unknown mode: ${mode}`);
|
|
158
|
-
const output = path.resolve(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
process.exit(1);
|
|
162
|
-
}
|
|
241
|
+
const output = path.resolve(
|
|
242
|
+
flagValue(args, ['-o', '--output']) || `concept-atlas-${mode}-guide.mdx`
|
|
243
|
+
);
|
|
163
244
|
const source = path.join(templateRoot, 'references', `${mode}-guide.mdx`);
|
|
164
245
|
if (!(await exists(source))) {
|
|
165
246
|
console.error(`Guide for mode "${mode}" is missing from the package.`);
|
|
@@ -169,7 +250,7 @@ if (command === 'guide') {
|
|
|
169
250
|
await copyFile(source, output);
|
|
170
251
|
const assetsSource = path.join(templateRoot, 'references', 'assets');
|
|
171
252
|
const assetsTarget = path.join(path.dirname(output), 'assets');
|
|
172
|
-
if (await exists(assetsSource) && path.resolve(assetsSource) !== path.resolve(assetsTarget)
|
|
253
|
+
if ((await exists(assetsSource)) && path.resolve(assetsSource) !== path.resolve(assetsTarget)) {
|
|
173
254
|
await cp(assetsSource, assetsTarget, { recursive: true, force: true });
|
|
174
255
|
console.log(`Copied guide assets to ${assetsTarget}`);
|
|
175
256
|
}
|
|
@@ -180,17 +261,19 @@ if (command === 'guide') {
|
|
|
180
261
|
|
|
181
262
|
if (command === 'create' || command === 'new') {
|
|
182
263
|
const output = args[0] ? path.resolve(args[0]) : null;
|
|
183
|
-
const mode = flagValue(args, ['--mode']) || '
|
|
184
|
-
if (
|
|
264
|
+
const mode = flagValue(args, ['--mode']) || 'scroll';
|
|
265
|
+
if (
|
|
266
|
+
!output ||
|
|
267
|
+
path.extname(output).toLowerCase() !== '.mdx' ||
|
|
268
|
+
!['atlas', 'scroll'].includes(mode)
|
|
269
|
+
) {
|
|
185
270
|
usage();
|
|
186
271
|
process.exit(1);
|
|
187
272
|
}
|
|
188
|
-
if (await exists(output) && !args.includes('--force')) {
|
|
189
|
-
console.error(`Refusing to overwrite ${output}; pass --force to replace it.`);
|
|
190
|
-
process.exit(1);
|
|
191
|
-
}
|
|
192
273
|
await mkdir(path.dirname(output), { recursive: true });
|
|
193
|
-
const template =
|
|
274
|
+
const template =
|
|
275
|
+
mode === 'atlas'
|
|
276
|
+
? `\
|
|
194
277
|
{/* shell 的 title 会成为浏览器标签页标题;页面图标固定为 📃。请把“主题名称”改成真实标题。 */}
|
|
195
278
|
<ExplainPage id="topic-id" title="主题名称" summary="用一句话说明这个主题解决什么问题。">
|
|
196
279
|
<ConceptGraph root="root-node">
|
|
@@ -216,7 +299,8 @@ if (command === 'create' || command === 'new') {
|
|
|
216
299
|
<Relation from="first-branch" to="second-branch" type="depends-on" label="依赖" />
|
|
217
300
|
</ConceptGraph>
|
|
218
301
|
</ExplainPage>
|
|
219
|
-
`
|
|
302
|
+
`
|
|
303
|
+
: `\
|
|
220
304
|
{/* shell 的 title 会成为浏览器标签页标题;页面图标固定为 📃。请把“主题名称”改成真实标题。 */}
|
|
221
305
|
<ScrollDocument>
|
|
222
306
|
<ScrollHeader title="主题名称">用一两句话说明主题、背景和读者应该带走的判断。</ScrollHeader>
|
|
@@ -242,7 +326,9 @@ if (command === 'create' || command === 'new') {
|
|
|
242
326
|
`;
|
|
243
327
|
await writeFile(output, template, 'utf8');
|
|
244
328
|
console.log(`Created ${mode} MDX template: ${output}`);
|
|
245
|
-
console.log(
|
|
329
|
+
console.log(
|
|
330
|
+
`Tip: run "npx concept-atlas-dense-explain guide --mode ${mode}" for a full component reference.`
|
|
331
|
+
);
|
|
246
332
|
process.exit(0);
|
|
247
333
|
}
|
|
248
334
|
|
|
@@ -250,8 +336,11 @@ const parsed = parseFlags(args);
|
|
|
250
336
|
const json = parsed.flags.has('--json');
|
|
251
337
|
const strict = parsed.flags.has('--strict');
|
|
252
338
|
const skipValidate = parsed.flags.has('--no-validate');
|
|
253
|
-
const force = parsed.flags.has('--force');
|
|
254
339
|
const linkAssets = parsed.flags.has('--link-assets');
|
|
340
|
+
const inlineAssets = parsed.flags.has('--inline-assets');
|
|
341
|
+
if (linkAssets && inlineAssets) {
|
|
342
|
+
fail('--link-assets and --inline-assets are mutually exclusive (linking is the default).');
|
|
343
|
+
}
|
|
255
344
|
const inlineMermaid = parsed.flags.has('--inline-mermaid');
|
|
256
345
|
const mermaidCdn = parsed.values.get('--mermaid-cdn') || null;
|
|
257
346
|
const modeFlag = parsed.values.get('--mode') || null;
|
|
@@ -261,18 +350,25 @@ const modeFlag = parsed.values.get('--mode') || null;
|
|
|
261
350
|
let skinFlag = null;
|
|
262
351
|
if (parsed.values.has('--skin')) {
|
|
263
352
|
skinFlag = normalizeSkin(parsed.values.get('--skin'));
|
|
264
|
-
if (!skinFlag)
|
|
353
|
+
if (!skinFlag)
|
|
354
|
+
fail(
|
|
355
|
+
`Unknown skin: ${parsed.values.get('--skin')} (available: ${SKINS.map(skin => skin.id).join(', ')})`
|
|
356
|
+
);
|
|
265
357
|
}
|
|
266
358
|
let defaultModeFlag = null;
|
|
267
359
|
if (parsed.values.has('--default-mode')) {
|
|
268
360
|
const raw = parsed.values.get('--default-mode');
|
|
269
|
-
if (!['dark', 'light', 'system'].includes(raw))
|
|
361
|
+
if (!['dark', 'light', 'system'].includes(raw))
|
|
362
|
+
fail(`Invalid --default-mode: ${raw} (use dark, light or system)`);
|
|
270
363
|
defaultModeFlag = raw;
|
|
271
364
|
}
|
|
272
365
|
let styleFlag = null;
|
|
273
366
|
if (parsed.values.has('--style')) {
|
|
274
367
|
styleFlag = normalizeStyle(parsed.values.get('--style'));
|
|
275
|
-
if (!styleFlag)
|
|
368
|
+
if (!styleFlag)
|
|
369
|
+
fail(
|
|
370
|
+
`Unknown component style: ${parsed.values.get('--style')} (available: ${COMPONENT_STYLES.map(style => style.id).join(', ')})`
|
|
371
|
+
);
|
|
276
372
|
}
|
|
277
373
|
|
|
278
374
|
if (command === 'validate') {
|
|
@@ -281,19 +377,26 @@ if (command === 'validate') {
|
|
|
281
377
|
fail('Provide an existing .mdx file to validate.');
|
|
282
378
|
}
|
|
283
379
|
const source = await readFile(target, 'utf8');
|
|
284
|
-
const result = printDiagnostics(
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
380
|
+
const result = printDiagnostics(
|
|
381
|
+
source,
|
|
382
|
+
{
|
|
383
|
+
filePath: target,
|
|
384
|
+
mode: modeFlag,
|
|
385
|
+
strict,
|
|
386
|
+
inlineAssets,
|
|
387
|
+
assetExists: spec => existsSync(path.resolve(path.dirname(target), spec)),
|
|
388
|
+
assetSize: spec => assetByteSize(path.resolve(path.dirname(target), spec))
|
|
389
|
+
},
|
|
390
|
+
{ json }
|
|
391
|
+
);
|
|
290
392
|
process.exit(countBySeverity(result.diagnostics).error ? 1 : 0);
|
|
291
393
|
}
|
|
292
394
|
|
|
293
395
|
// `render` is the default command, so it may still appear as a leading token.
|
|
294
|
-
const positional =
|
|
295
|
-
|
|
296
|
-
|
|
396
|
+
const positional =
|
|
397
|
+
parsed.positional[0] && parsed.positional[0].toLowerCase() === 'render'
|
|
398
|
+
? parsed.positional.slice(1)
|
|
399
|
+
: parsed.positional;
|
|
297
400
|
const inputs = positional.map(entry => path.resolve(entry));
|
|
298
401
|
|
|
299
402
|
if (!inputs.length) {
|
|
@@ -310,26 +413,25 @@ for (const input of inputs) {
|
|
|
310
413
|
|
|
311
414
|
const outputs = resolveOutputs(inputs, parsed.values.get('-o') || parsed.values.get('--output'));
|
|
312
415
|
|
|
313
|
-
for (const output of outputs) {
|
|
314
|
-
if (await exists(output) && !force) {
|
|
315
|
-
console.error(`Refusing to overwrite ${output}; pass --force to replace it.`);
|
|
316
|
-
process.exit(1);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
416
|
const multi = inputs.length > 1;
|
|
321
417
|
const sources = await Promise.all(inputs.map(input => readFile(input, 'utf8')));
|
|
322
418
|
|
|
323
419
|
// Validate every document before building any of them: a batch should fail as a
|
|
324
420
|
// batch rather than leaving half the targets rendered.
|
|
325
|
-
const validations = sources.map((source, index) =>
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
421
|
+
const validations = sources.map((source, index) =>
|
|
422
|
+
printDiagnostics(
|
|
423
|
+
source,
|
|
424
|
+
{
|
|
425
|
+
filePath: inputs[index],
|
|
426
|
+
mode: modeFlag,
|
|
427
|
+
strict,
|
|
428
|
+
inlineAssets,
|
|
429
|
+
assetExists: spec => existsSync(path.resolve(path.dirname(inputs[index]), spec)),
|
|
430
|
+
assetSize: spec => assetByteSize(path.resolve(path.dirname(inputs[index]), spec))
|
|
431
|
+
},
|
|
432
|
+
json ? { json: false, quiet: true } : { json: false, label: multi ? inputs[index] : null }
|
|
433
|
+
)
|
|
434
|
+
);
|
|
333
435
|
|
|
334
436
|
if (json) {
|
|
335
437
|
const payload = multi
|
|
@@ -338,7 +440,10 @@ if (json) {
|
|
|
338
440
|
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
339
441
|
}
|
|
340
442
|
|
|
341
|
-
const errorCount = validations.reduce(
|
|
443
|
+
const errorCount = validations.reduce(
|
|
444
|
+
(sum, result) => sum + countBySeverity(result.diagnostics).error,
|
|
445
|
+
0
|
|
446
|
+
);
|
|
342
447
|
if (errorCount && !skipValidate) {
|
|
343
448
|
console.error('内容校验未通过,已停止构建。修复后重试,或用 --no-validate 强制构建。');
|
|
344
449
|
process.exit(1);
|
|
@@ -347,38 +452,65 @@ if (errorCount && !skipValidate) {
|
|
|
347
452
|
const jobs = inputs.map((input, index) => {
|
|
348
453
|
const mode = modeFlag || validations[index].carrier;
|
|
349
454
|
if (!mode || !['atlas', 'scroll'].includes(mode)) {
|
|
350
|
-
console.error(
|
|
455
|
+
console.error(
|
|
456
|
+
`Could not detect the MDX carrier for ${input}; choose --mode atlas or --mode scroll.`
|
|
457
|
+
);
|
|
351
458
|
process.exit(1);
|
|
352
459
|
}
|
|
353
|
-
|
|
354
|
-
|
|
460
|
+
// Figures link by default. If the HTML is written outside the MDX's directory
|
|
461
|
+
// its relative image paths no longer resolve, so warn unless --inline-assets
|
|
462
|
+
// bakes them in.
|
|
463
|
+
if (
|
|
464
|
+
!inlineAssets &&
|
|
465
|
+
path.resolve(path.dirname(outputs[index])) !== path.resolve(path.dirname(input))
|
|
466
|
+
) {
|
|
467
|
+
console.error(
|
|
468
|
+
`警告:默认外链图片,但 ${outputs[index]} 不在 ${path.dirname(input)} 内,相对图片路径会失效;请用 --inline-assets,或把 assets/ 一并放到输出目录。`
|
|
469
|
+
);
|
|
355
470
|
}
|
|
356
|
-
return {
|
|
471
|
+
return {
|
|
472
|
+
input,
|
|
473
|
+
output: outputs[index],
|
|
474
|
+
mode,
|
|
475
|
+
title: extractPageTitle(sources[index]),
|
|
476
|
+
features: detectFeatures(sources[index]),
|
|
477
|
+
inlineAssets
|
|
478
|
+
};
|
|
357
479
|
});
|
|
358
480
|
|
|
359
481
|
const limit = clampConcurrency(parsed.values.get('--concurrency'), jobs.length);
|
|
360
482
|
const started = Date.now();
|
|
361
|
-
const results = await runPool(
|
|
483
|
+
const results = await runPool(
|
|
484
|
+
jobs.map(job => () => buildOne(job)),
|
|
485
|
+
limit
|
|
486
|
+
);
|
|
362
487
|
|
|
363
488
|
const failures = results.filter(result => !result.ok);
|
|
364
489
|
const saved = summarizeFeatures(jobs, results);
|
|
365
|
-
console.log(
|
|
490
|
+
console.log(
|
|
491
|
+
`Built ${results.length - failures.length}/${results.length} page(s) with concurrency ${limit} in ${((Date.now() - started) / 1000).toFixed(1)}s${saved ? ` (${saved})` : ''}.`
|
|
492
|
+
);
|
|
366
493
|
if (failures.length) {
|
|
367
|
-
for (const failure of failures)
|
|
494
|
+
for (const failure of failures)
|
|
495
|
+
console.error(`Build failed for ${failure.input}:`, failure.error);
|
|
368
496
|
process.exit(1);
|
|
369
497
|
}
|
|
370
498
|
|
|
371
499
|
async function buildOne(job) {
|
|
372
|
-
const { input, output, mode, title, features,
|
|
500
|
+
const { input, output, mode, title, features, inlineAssets: inline } = job;
|
|
373
501
|
const templateEntry = mode === 'atlas' ? 'index.html' : 'scroll.html';
|
|
374
502
|
// Each build gets its own scratch outDir: the template always writes
|
|
375
503
|
// `index.html`/`scroll.html`, so concurrent builds sharing a directory would
|
|
376
504
|
// overwrite each other before the rename.
|
|
377
|
-
const scratch = path.join(
|
|
505
|
+
const scratch = path.join(
|
|
506
|
+
path.dirname(output),
|
|
507
|
+
`.concept-atlas-${process.pid}-${(buildCounter += 1)}`
|
|
508
|
+
);
|
|
378
509
|
await mkdir(path.dirname(output), { recursive: true });
|
|
379
510
|
await mkdir(scratch, { recursive: true });
|
|
380
511
|
const define = { __ATLAS_FEATURES__: JSON.stringify(features) };
|
|
381
|
-
|
|
512
|
+
// 'false' is the default (link); only --inline-assets sets 'true'.
|
|
513
|
+
define.__ATLAS_INLINE_ASSETS__ = inline ? 'true' : 'false';
|
|
382
514
|
if (title) define.__ATLAS_PAGE_TITLE__ = JSON.stringify(title);
|
|
383
515
|
if (skinFlag) define.__ATLAS_DEFAULT_SKIN__ = JSON.stringify(skinFlag);
|
|
384
516
|
if (defaultModeFlag) define.__ATLAS_DEFAULT_MODE__ = JSON.stringify(defaultModeFlag);
|
|
@@ -396,8 +528,8 @@ async function buildOne(job) {
|
|
|
396
528
|
build: {
|
|
397
529
|
outDir: scratch,
|
|
398
530
|
emptyOutDir: false,
|
|
399
|
-
rollupOptions: { input: path.join(templateRoot, templateEntry) }
|
|
400
|
-
}
|
|
531
|
+
rollupOptions: { input: path.join(templateRoot, templateEntry) }
|
|
532
|
+
}
|
|
401
533
|
});
|
|
402
534
|
// Swap the new build in without a window where no output exists: on
|
|
403
535
|
// POSIX rename replaces atomically; the fallback path (Windows can refuse
|
|
@@ -424,7 +556,9 @@ async function buildOne(job) {
|
|
|
424
556
|
}
|
|
425
557
|
if (hasBackup) await rm(backup, { force: true });
|
|
426
558
|
}
|
|
427
|
-
console.log(
|
|
559
|
+
console.log(
|
|
560
|
+
`Built ${mode} HTML: ${output}${title ? ` [tab: ${title}]` : ''}${describeFeatures(features)}${inline ? ' [figures inlined]' : ' [figures linked]'}`
|
|
561
|
+
);
|
|
428
562
|
return { ok: true, input, output };
|
|
429
563
|
} catch (error) {
|
|
430
564
|
return { ok: false, input, output, error };
|
package/package.json
CHANGED
package/packaged-skill/SKILL.md
CHANGED
|
@@ -24,7 +24,7 @@ This is the only runner — there is nothing to look for. Do not search for a lo
|
|
|
24
24
|
- `references/atlas-guide.mdx` (atlas) or `references/scroll-guide.mdx` (scroll)
|
|
25
25
|
It is real, compilable MDX showing that shell's components and their exact props; search it for a component name instead of guessing, and do not look for component documentation anywhere else. Do not run `guide` for this.
|
|
26
26
|
Run `npx concept-atlas-dense-explain guide --mode <mode> -o <file>` only when you specifically need a project-local copy to compile beside the page, and delete that copy when done.
|
|
27
|
-
3. Start from a skeleton when useful: `npx concept-atlas-dense-explain create <file>.mdx --mode atlas|scroll
|
|
27
|
+
3. Start from a skeleton when useful: `npx concept-atlas-dense-explain create <file>.mdx --mode atlas|scroll` (`--mode` defaults to `scroll`). `create` and `guide` overwrite their output, so never point them at a file you want to keep.
|
|
28
28
|
4. Write the semantic MDX into the user's `.mdx` file (see Authoring rules).
|
|
29
29
|
5. Validate before rendering:
|
|
30
30
|
```bash
|
|
@@ -32,9 +32,9 @@ This is the only runner — there is nothing to look for. Do not search for a lo
|
|
|
32
32
|
npx concept-atlas-dense-explain validate <file>.mdx --json
|
|
33
33
|
```
|
|
34
34
|
Diagnostics are `CODE line:column message`. Fix all `error`s and re-run; address warnings when cheap.
|
|
35
|
-
6. Compile: `npx concept-atlas-dense-explain <file>.mdx --mode atlas|scroll [-o out.html] [--skin <id>] [--default-mode dark|light|system] [--style <id>] [--
|
|
35
|
+
6. Compile: `npx concept-atlas-dense-explain <file>.mdx --mode atlas|scroll [-o out.html] [--skin <id>] [--default-mode dark|light|system] [--style <id>] [--mermaid-cdn <url>]`. Output is a standalone HTML beside the MDX unless `-o` is given. Validation errors abort the build (`--no-validate` forces a knowingly broken build). **Mermaid stays on its runtime CDN by default — do not pass `--inline-mermaid` on your own.** Only add `--inline-mermaid` when the user explicitly asks for a fully offline single file, since it bloats the HTML with the whole Mermaid bundle.
|
|
36
36
|
7. **Appearance (optional)**: pages ship a reader-facing appearance menu — palette (`aurora` indigo, `ember` gold, `verdant` forest, `sakura` pink-plum, `noir` ink), a dark/light toggle, and a component style pack (`manuscript` editorial marginalia, `classic` boxed cards, `shadcn` hairline-bordered minimal UI, `elastic` bordered observability panels). The shipped default is aurora × manuscript × light; choices persist in localStorage across both carriers. Bake different compile-time defaults with `--skin ember --default-mode dark --style classic` (or `CONCEPT_ATLAS_SKIN` / `CONCEPT_ATLAS_DEFAULT_MODE` / `CONCEPT_ATLAS_STYLE` on the repo build); `--default-mode` honors `dark`/`light` and resolves `system` to the carrier default (`light`). Bake a default only when the user asks for one — content MDX never sets appearance.
|
|
37
|
-
8. For several documents, pass them all in one call: `npx concept-atlas-dense-explain a.mdx b.mdx c.mdx -o dist
|
|
37
|
+
8. For several documents, pass them all in one call: `npx concept-atlas-dense-explain a.mdx b.mdx c.mdx -o dist [--concurrency 3]` (`-o` is then a directory; everything validates first, then builds in parallel). Builds bundle only the heavy renderers the content uses: no `<Math>` skips KaTeX's ~1.4MB inlined fonts, and Mermaid stays on a CDN. Never add dummy `<Math>`/`<Mermaid>` nodes to "enable" them.
|
|
38
38
|
9. Report the shell, output path, validation result (errors/warnings), and limitations. Do not claim interactions you did not verify.
|
|
39
39
|
|
|
40
40
|
## Carriers
|
|
@@ -47,12 +47,12 @@ This is the only runner — there is nothing to look for. Do not search for a lo
|
|
|
47
47
|
## Component families
|
|
48
48
|
|
|
49
49
|
- Node semantics: `Overview`, `Definition`, `Mechanism`, `Implementation`, `CodeBlock`, `Boundary`, `Example`, `Counterexample`, `Prerequisite`, `Input`, `Output`, `Glossary`
|
|
50
|
-
- Argument and evidence: `Evidence`, `Invariant`, `FailureMode`, `Tradeoff`, `LearningObjectives`, `KeyQuestion`
|
|
51
|
-
- Learning loop and provenance: `WorkedExample` + `Step`, `Quiz`, `KeyTakeaways`, `Source`, `Confidence`, `Term`
|
|
50
|
+
- Argument and evidence: `Evidence`, `Invariant`, `FailureMode`, `Tradeoff`, `Checklist`, `LearningObjectives`, `KeyQuestion`
|
|
51
|
+
- Learning loop and provenance: `WorkedExample` + `Step`, `Quiz`, `KeyTakeaways`, `Source`, `Confidence`, `LastReviewed`, `Term`
|
|
52
52
|
- Information models: `Flow`, `Timeline`, `Compare`, `DecisionMatrix`, `FrameworkModel`, `MatrixModel`, `FormulaModel`, `PyramidModel`, `FunnelModel`
|
|
53
|
-
- Data and behaviour: `DataTable`, `Metric`, `StateMachine`, `DecisionTree`, `FeedbackLoop`, `CodeDiff`
|
|
54
|
-
- Reading and layout: `Insight`, `Callout`, `Details`, `NoteGrid`, `Tabs`, `Columns`, `Stack`, `Grid`, `Split`, `ScrollGrid`, `ScrollPair`, `ScrollToc`
|
|
55
|
-
- Graphics and extensions: `Mermaid`, `RelationMap`, `RelationPath`, `Math`, `MathBlock`, `Chart`, `Figure` (alias `Image`), `Cite`, `References`
|
|
53
|
+
- Data and behaviour: `DataTable`, `Metric`, `PropertyList`, `TreeView`, `StateMachine`, `DecisionTree`, `FeedbackLoop`, `CodeDiff`
|
|
54
|
+
- Reading and layout: `Insight`, `Callout`, `Details`, `Quote`, `NoteGrid`, `Tabs`, `Columns`, `Stack`, `Grid`, `Split`, `ScrollGrid`, `ScrollPair`, `ScrollToc`
|
|
55
|
+
- Graphics and extensions: `Mermaid`, `RelationMap`, `RelationPath`, `Math`, `MathBlock`, `Chart`, `CodeTabs`, `AnnotatedCode`, `Figure` (alias `Image`), `FigureRef`, `Cite`, `References`
|
|
56
56
|
|
|
57
57
|
## Authoring rules
|
|
58
58
|
|
|
@@ -67,20 +67,23 @@ This is the only runner — there is nothing to look for. Do not search for a lo
|
|
|
67
67
|
- **Learning loop**: `WorkedExample` holds `Step` children; give each step a `reason` (the justification) so it teaches reasoning, not just the result. `Quiz question answer` reveals the answer on click with children as the explanation. `KeyTakeaways items={[...]}` closes a section.
|
|
68
68
|
- **Provenance**: `Source kind="spec|rfc|implementation|experiment|experience" label href` marks one claim's origin inline; `Confidence level="high|medium|low" basis` wraps a claim with how strongly it is established; `Term name` (or children as the definition) gives an inline hover definition.
|
|
69
69
|
- **Data and behaviour**: `DataTable headers={[...]} rows={[[...]]}` is the neutral table (no comparative stance). `Metric label value unit delta trend note` is one headline number — compose several with `Grid`/`ScrollGrid`. `StateMachine states={[{id,label,terminal}]} transitions={[{from,to,event,guard}]}` expresses cycles and guards. `DecisionTree branches={[{condition,outcome,tone,branches}]}` nests branches. `FeedbackLoop type="reinforcing|balancing" nodes={[{label,description}]}` closes a loop. `CodeDiff before after language` shows a change.
|
|
70
|
-
- **
|
|
71
|
-
- **Figure
|
|
70
|
+
- **Structure and code extensions**: `Quote author source href` attributes a quotation. `Checklist title items={[{text,status:'pass'|'fail'|'unknown'|'todo',note}]}` is a verification list. `PropertyList title items={[{name,value,note}]}` is a name/value spec ledger. `TreeView title items={[{label,description,children:[…]}]}` nests a hierarchy. `CodeTabs items={[{label,language,code}]} title caption lineNumbers` gives one collapsible listing per variant. `AnnotatedCode code language title notes={[{line,text}]}` numbers a listing and calls out specific lines. `LastReviewed date by note` is a review stamp.
|
|
71
|
+
- **Figure**: a relative `src` (`./assets/diagram.png`) stays a relative link by default; compile with `--inline-assets` to bake every local image in as base64, or set `inline={true}` / `inline={false}` on one figure to override (precedence: per-figure prop > global flag > link). `http(s)` URLs stay links and warn (`ASSET_REMOTE`). Always set `alt`; a missing relative file warns (`ASSET_MISSING`) and shows a placeholder. Readers can click a figure to open it full-screen (zoom, drag, `Esc`) — mention it for diagram-heavy pages.
|
|
72
|
+
- **Figure numbering**: give a figure an `id` and reference it with `<FigureRef id="..." />` to render "图 N". Numbering follows document order (per node in `atlas`, whole document in `scroll`) and reflows automatically, so never hand-write "图 1"; an explicit `label` still wins. A ref with no matching `id` warns (`FIGURE_REF_UNRESOLVED`).
|
|
73
|
+
- **Figure size & cost**: linking keeps the HTML small but requires the output to sit beside the MDX's `assets/` (the CLI warns when `-o` points elsewhere). Inlining is what makes a screenshot-heavy page large (a page with no heavy renderers otherwise lands near 250KB), and an inlined asset over 512KB warns (`ASSET_LARGE`). Decide the trade-off with the user instead of choosing silently.
|
|
72
74
|
- **Cite/References**: `<Cite id="..." />` renders `[n]` from the matching item's position in `<References items={...} />`. In `scroll`, `References` can sit anywhere. In `atlas`, keep the cites and the `References` block in the same node, because node content only renders when that node is open.
|
|
73
75
|
- Continuous reading is configured on the shell, not with manual CSS: `spacing="compact|comfortable|airy"` for rhythm, `fontSize="compact|normal|large|xlarge"` (or numeric `scale`/`lineHeight`) for text size.
|
|
74
76
|
|
|
75
77
|
## Validation diagnostics
|
|
76
78
|
|
|
77
|
-
`validate` and the build print `CODE line:column message`. Fix these `error`s before building: `UNKNOWN_COMPONENT`, `CARRIER_MISSING`, `CARRIER_CONFLICT`, `CARRIER_MODE_MISMATCH`, `NODE_MISSING_ID`, `DUPLICATE_NODE_ID`, `NODE_MISSING_TITLE`, `MISSING_PARENT`, `GRAPH_ROOT_UNRESOLVED`, `REF_MISSING_ID`, `REF_UNRESOLVED`, `RELATION_FROM_UNRESOLVED`, `RELATION_TO_UNRESOLVED`. Warnings worth fixing: `NODE_MISSING_SUMMARY`, `NODE_NO_CORE_CONTENT`, `UNKNOWN_LEVEL`, `UNKNOWN_KIND`, `MECHANISM_KIND_UNVERIFIED`, `FAILURE_KIND_UNSTRUCTURED`, `FAILURE_MODE_EMPTY`, `UNKNOWN_RELATION_TYPE`, `RELATION_MISSING_LABEL`, `RELATION_SELF`, `PROP_EXPECTS_ARRAY`, `MATH_CHILDREN_BRACES`, `PROSE_EXPRESSION`, `FRONTMATTER_UNSUPPORTED`, `GRAPH_MISSING_ROOT`, `FIGURE_MISSING_SRC`, `ASSET_MISSING`, `REF_SELF`; `NO_ROOT_LEVEL`, `MULTIPLE_ROOT_LEVEL`, `MECHANISM_KIND_UNVERIFIED` and `FAILURE_KIND_UNSTRUCTURED` are warnings that `--strict` promotes to errors.
|
|
79
|
+
`validate` and the build print `CODE line:column message`. Fix these `error`s before building: `UNKNOWN_COMPONENT`, `CARRIER_MISSING`, `CARRIER_CONFLICT`, `CARRIER_MODE_MISMATCH`, `NODE_MISSING_ID`, `DUPLICATE_NODE_ID`, `NODE_MISSING_TITLE`, `MISSING_PARENT`, `GRAPH_ROOT_UNRESOLVED`, `REF_MISSING_ID`, `REF_UNRESOLVED`, `RELATION_FROM_UNRESOLVED`, `RELATION_TO_UNRESOLVED`. Warnings worth fixing: `NODE_MISSING_SUMMARY`, `NODE_NO_CORE_CONTENT`, `UNKNOWN_LEVEL`, `UNKNOWN_KIND`, `MECHANISM_KIND_UNVERIFIED`, `FAILURE_KIND_UNSTRUCTURED`, `FAILURE_MODE_EMPTY`, `UNKNOWN_RELATION_TYPE`, `RELATION_MISSING_LABEL`, `RELATION_SELF`, `PROP_EXPECTS_ARRAY`, `MATH_CHILDREN_BRACES`, `PROSE_EXPRESSION`, `FRONTMATTER_UNSUPPORTED`, `GRAPH_MISSING_ROOT`, `FIGURE_MISSING_SRC`, `ASSET_MISSING`, `ASSET_REMOTE`, `ASSET_LARGE`, `FIGURE_REF_UNRESOLVED`, `FIGURE_REF_MISSING_ID`, `FIGURE_DUPLICATE_ID`, `REF_SELF`; `NO_ROOT_LEVEL`, `MULTIPLE_ROOT_LEVEL`, `MECHANISM_KIND_UNVERIFIED` and `FAILURE_KIND_UNSTRUCTURED` are warnings that `--strict` promotes to errors.
|
|
78
80
|
|
|
79
81
|
## Before you report
|
|
80
82
|
|
|
81
83
|
- All `error` diagnostics resolved (or `--no-validate` explicitly justified).
|
|
82
84
|
- All `ConceptRef`, `Relation` endpoints, and `ConceptGraph root` point at existing node ids.
|
|
83
85
|
- Every node has `title` + `summary`; every `Relation` has a `label`.
|
|
86
|
+
- You left Mermaid on its CDN default and did not pass `--inline-mermaid` unless the user explicitly asked for a fully offline single file.
|
|
84
87
|
- The HTML file exists at the reported path.
|
|
85
88
|
|
|
86
89
|
If `npx` cannot reach the registry, report the blocker. Never copy implementation files into the skill directory.
|
|
@@ -132,10 +132,11 @@
|
|
|
132
132
|
<Chart title="留存趋势" type="line" labels={['第1周','第2周','第3周','第4周']} series={[{name:'留存率',values:[100,72,58,49]}]} />
|
|
133
133
|
</ConceptNode>
|
|
134
134
|
|
|
135
|
-
<ConceptNode id="figure-demo" title="Figure:图片与题注一起出现" level="L2" parent="extension-family" summary="Figure
|
|
136
|
-
<Definition
|
|
137
|
-
<Figure src="./assets/sample-diagram.svg" alt="概念缩放示意"
|
|
138
|
-
<
|
|
135
|
+
<ConceptNode id="figure-demo" title="Figure:图片与题注一起出现" level="L2" parent="extension-family" summary="Figure 把图片、题注编号和说明绑定;默认外链,按需内联。">
|
|
136
|
+
<Definition>相对路径的图片默认保持外链,构建时用 --inline-assets 可全部内联,单张图也可用 `inline={true}` 或 `inline={false}` 覆盖;远程 URL 保持不变。给图一个 id,正文就能用 FigureRef 自动引用“图 N”。</Definition>
|
|
137
|
+
<Figure id="zoom-scale" src="./assets/sample-diagram.svg" alt="概念缩放示意" caption="概念缩放:同一主题可以从 L0 全局逐层下钻到 L4 边界反例。" />
|
|
138
|
+
<Example title="自动图号">正文写“见 <FigureRef id="zoom-scale" />”,编号会随插图自动重排,不必手写“图 1”。</Example>
|
|
139
|
+
<Boundary>内联大图会显著增大 HTML;截图类内容建议控制尺寸,或对单张图设 `inline={false}`。</Boundary>
|
|
139
140
|
</ConceptNode>
|
|
140
141
|
|
|
141
142
|
<ConceptNode id="code-demo" title="CodeBlock:把命令、配置或伪代码放进正文" level="L2" parent="extension-family" summary="与绑定节点的 Implementation 不同,CodeBlock 可以出现在任何位置,并支持标题、题注、换行和行号。">
|