concept-atlas-dense-explain 0.6.0 → 0.7.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/bin/cli.mjs +22 -9
- package/package.json +1 -1
- package/skill/SKILL.md +8 -7
- package/template/index.html +1 -0
- package/template/scripts/build.mjs +69 -17
- package/template/scroll.html +1 -0
- package/template/src/components/MDXComponents.jsx +2 -1
- package/template/src/model/skins.js +18 -1
- package/template/src/model/validate-content.js +25 -0
- package/template/src/styles/concept-explain.css +10 -3904
- package/template/src/styles/core.css +3948 -0
- package/template/src/styles/motion.css +189 -0
- package/template/src/styles/packs/elastic.css +517 -0
- package/template/src/styles/packs/manuscript.css +426 -0
- package/template/src/styles/packs/shadcn.css +517 -0
- package/template/src/styles/skins.css +561 -0
- package/template/src/styles/tokens.css +80 -37
- package/template/src/views/RelationGraph.jsx +31 -14
- package/template/vite.config.js +89 -6
package/bin/cli.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { existsSync } from 'node:fs';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { build } from 'vite';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import { validateMdxSource, countBySeverity, detectFeatures } from '../template/src/model/validate-content.js';
|
|
7
|
+
import { validateMdxSource, countBySeverity, detectFeatures, extractPageTitle } from '../template/src/model/validate-content.js';
|
|
8
8
|
import { SKINS, normalizeSkin, COMPONENT_STYLES, normalizeStyle } from '../template/src/model/skins.js';
|
|
9
9
|
|
|
10
10
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -14,7 +14,7 @@ let buildCounter = 0;
|
|
|
14
14
|
|
|
15
15
|
function usage() {
|
|
16
16
|
console.log('Usage:');
|
|
17
|
-
console.log(' npx concept-atlas-dense-explain <input.mdx>... [--mode atlas|scroll] [--skin <id>] [--default-mode dark|light|system] [--style <id>] [-o output.html|dir] [--force] [--concurrency N] [--link-assets] [--json] [--no-validate]');
|
|
17
|
+
console.log(' npx concept-atlas-dense-explain <input.mdx>... [--mode atlas|scroll] [--skin <id>] [--default-mode dark|light|system] [--style <id>] [-o output.html|dir] [--force] [--concurrency N] [--link-assets] [--inline-mermaid] [--mermaid-cdn <url>] [--json] [--no-validate]');
|
|
18
18
|
console.log(' npx concept-atlas-dense-explain render <input.mdx>... [-o output.html|dir]');
|
|
19
19
|
console.log(' npx concept-atlas-dense-explain validate <input.mdx> [--mode atlas|scroll] [--strict] [--json]');
|
|
20
20
|
console.log(' npx concept-atlas-dense-explain create <output.mdx> [--mode atlas|scroll] [--force]');
|
|
@@ -22,6 +22,7 @@ function usage() {
|
|
|
22
22
|
console.log('');
|
|
23
23
|
console.log(' Multiple inputs build in parallel (default 2 at a time, cap 4); -o is then a directory.');
|
|
24
24
|
console.log(' --link-assets keeps figures as relative links instead of inlining them as base64.');
|
|
25
|
+
console.log(' 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.');
|
|
25
26
|
console.log(` --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.`);
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -43,7 +44,7 @@ function fail(message) {
|
|
|
43
44
|
process.exit(1);
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
const VALUE_FLAGS = new Set(['--mode', '-o', '--output', '--concurrency', '--skin', '--default-mode', '--style']);
|
|
47
|
+
const VALUE_FLAGS = new Set(['--mode', '-o', '--output', '--concurrency', '--skin', '--default-mode', '--style', '--mermaid-cdn']);
|
|
47
48
|
|
|
48
49
|
/** Splits argv into flags, flag values and positional arguments. */
|
|
49
50
|
function parseFlags(argv) {
|
|
@@ -144,6 +145,7 @@ if (command === 'create' || command === 'new') {
|
|
|
144
145
|
}
|
|
145
146
|
await mkdir(path.dirname(output), { recursive: true });
|
|
146
147
|
const template = mode === 'atlas' ? `\
|
|
148
|
+
{/* shell 的 title 会成为浏览器标签页标题;页面图标固定为 📃。请把“主题名称”改成真实标题。 */}
|
|
147
149
|
<ExplainPage id="topic-id" title="主题名称" summary="用一句话说明这个主题解决什么问题。">
|
|
148
150
|
<ConceptGraph root="root-node">
|
|
149
151
|
<ConceptNode id="root-node" title="核心概念" level="L0" summary="给读者建立整体认知。">
|
|
@@ -168,6 +170,7 @@ if (command === 'create' || command === 'new') {
|
|
|
168
170
|
</ConceptGraph>
|
|
169
171
|
</ExplainPage>
|
|
170
172
|
` : `\
|
|
173
|
+
{/* shell 的 title 会成为浏览器标签页标题;页面图标固定为 📃。请把“主题名称”改成真实标题。 */}
|
|
171
174
|
<ScrollDocument>
|
|
172
175
|
<ScrollHeader title="主题名称">用一两句话说明主题、背景和读者应该带走的判断。</ScrollHeader>
|
|
173
176
|
|
|
@@ -201,6 +204,8 @@ const strict = parsed.flags.has('--strict');
|
|
|
201
204
|
const skipValidate = parsed.flags.has('--no-validate');
|
|
202
205
|
const force = parsed.flags.has('--force');
|
|
203
206
|
const linkAssets = parsed.flags.has('--link-assets');
|
|
207
|
+
const inlineMermaid = parsed.flags.has('--inline-mermaid');
|
|
208
|
+
const mermaidCdn = parsed.values.get('--mermaid-cdn') || null;
|
|
204
209
|
const modeFlag = parsed.values.get('--mode') || null;
|
|
205
210
|
|
|
206
211
|
// Compile-time appearance defaults. Invalid values fail fast with the valid
|
|
@@ -300,7 +305,7 @@ const jobs = inputs.map((input, index) => {
|
|
|
300
305
|
if (linkAssets && path.resolve(path.dirname(outputs[index])) !== path.resolve(path.dirname(input))) {
|
|
301
306
|
console.error(`警告:--link-assets 下 ${outputs[index]} 不在 ${path.dirname(input)} 内,相对图片路径会失效。`);
|
|
302
307
|
}
|
|
303
|
-
return { input, output: outputs[index], mode, features: detectFeatures(sources[index]), linkAssets };
|
|
308
|
+
return { input, output: outputs[index], mode, title: extractPageTitle(sources[index]), features: detectFeatures(sources[index]), linkAssets };
|
|
304
309
|
});
|
|
305
310
|
|
|
306
311
|
const limit = clampConcurrency(parsed.values.get('--concurrency'), jobs.length);
|
|
@@ -316,7 +321,7 @@ if (failures.length) {
|
|
|
316
321
|
}
|
|
317
322
|
|
|
318
323
|
async function buildOne(job) {
|
|
319
|
-
const { input, output, mode, features, linkAssets: link } = job;
|
|
324
|
+
const { input, output, mode, title, features, linkAssets: link } = job;
|
|
320
325
|
const templateEntry = mode === 'atlas' ? 'index.html' : 'scroll.html';
|
|
321
326
|
// Each build gets its own scratch outDir: the template always writes
|
|
322
327
|
// `index.html`/`scroll.html`, so concurrent builds sharing a directory would
|
|
@@ -326,9 +331,14 @@ async function buildOne(job) {
|
|
|
326
331
|
await mkdir(scratch, { recursive: true });
|
|
327
332
|
const define = { __ATLAS_FEATURES__: JSON.stringify(features) };
|
|
328
333
|
if (link) define.__ATLAS_INLINE_ASSETS__ = 'false';
|
|
334
|
+
if (title) define.__ATLAS_PAGE_TITLE__ = JSON.stringify(title);
|
|
329
335
|
if (skinFlag) define.__ATLAS_DEFAULT_SKIN__ = JSON.stringify(skinFlag);
|
|
330
336
|
if (defaultModeFlag) define.__ATLAS_DEFAULT_MODE__ = JSON.stringify(defaultModeFlag);
|
|
331
337
|
if (styleFlag) define.__ATLAS_DEFAULT_STYLE__ = JSON.stringify(styleFlag);
|
|
338
|
+
if (features.mermaid) {
|
|
339
|
+
define.__ATLAS_MERMAID_MODE__ = JSON.stringify(inlineMermaid ? 'inline' : 'cdn');
|
|
340
|
+
if (mermaidCdn) define.__ATLAS_MERMAID_CDN_URL__ = JSON.stringify(mermaidCdn);
|
|
341
|
+
}
|
|
332
342
|
try {
|
|
333
343
|
await build({
|
|
334
344
|
root: templateRoot,
|
|
@@ -343,7 +353,7 @@ async function buildOne(job) {
|
|
|
343
353
|
});
|
|
344
354
|
await rm(output, { force: true });
|
|
345
355
|
await rename(path.join(scratch, templateEntry), output);
|
|
346
|
-
console.log(`Built ${mode} HTML: ${output}${describeFeatures(features)}${link ? ' [figures linked]' : ''}`);
|
|
356
|
+
console.log(`Built ${mode} HTML: ${output}${title ? ` [tab: ${title}]` : ''}${describeFeatures(features)}${link ? ' [figures linked]' : ''}`);
|
|
347
357
|
return { ok: true, input, output };
|
|
348
358
|
} catch (error) {
|
|
349
359
|
return { ok: false, input, output, error };
|
|
@@ -354,9 +364,10 @@ async function buildOne(job) {
|
|
|
354
364
|
|
|
355
365
|
/** Saves the mermaid/KaTeX payload when a document never renders them. */
|
|
356
366
|
function describeFeatures(features) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
367
|
+
const tags = [];
|
|
368
|
+
if (features.mermaid) tags.push(inlineMermaid ? 'mermaid inline' : 'mermaid cdn');
|
|
369
|
+
if (features.math) tags.push('KaTeX');
|
|
370
|
+
return tags.length ? ` [${tags.join(', ')}]` : '';
|
|
360
371
|
}
|
|
361
372
|
|
|
362
373
|
function summarizeFeatures(jobs, results) {
|
|
@@ -365,7 +376,9 @@ function summarizeFeatures(jobs, results) {
|
|
|
365
376
|
if (!pages.length) return '';
|
|
366
377
|
const droppedMath = pages.filter(job => !job.features.math).length;
|
|
367
378
|
const droppedMermaid = pages.filter(job => !job.features.mermaid).length;
|
|
379
|
+
const cdnMermaid = pages.filter(job => job.features.mermaid && !inlineMermaid).length;
|
|
368
380
|
const parts = [];
|
|
381
|
+
if (cdnMermaid) parts.push(`Mermaid via CDN on ${cdnMermaid}/${pages.length}`);
|
|
369
382
|
if (droppedMath) parts.push(`KaTeX dropped on ${droppedMath}/${pages.length}`);
|
|
370
383
|
if (droppedMermaid) parts.push(`Mermaid dropped on ${droppedMermaid}/${pages.length}`);
|
|
371
384
|
return parts.join(', ');
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -17,7 +17,7 @@ If the user only wants the prompt/methodology and not files, still choose a shel
|
|
|
17
17
|
npx concept-atlas-dense-explain guide --mode atlas -o ./concept-atlas-atlas-guide.mdx
|
|
18
18
|
npx concept-atlas-dense-explain guide --mode scroll -o ./concept-atlas-scroll-guide.mdx
|
|
19
19
|
```
|
|
20
|
-
It is a real, compilable MDX file
|
|
20
|
+
It is a real, compilable MDX file that demonstrates the components for that shell and their exact props. Search it for a component name to copy the correct prop shape instead of guessing. Delete it when done.
|
|
21
21
|
3. Start from a skeleton when useful: `npx concept-atlas-dense-explain create <file>.mdx --mode atlas|scroll`. `create` and `guide` refuse to overwrite an existing file unless `--force` is passed.
|
|
22
22
|
4. Write the semantic MDX into the user's `.mdx` file (see Authoring rules).
|
|
23
23
|
5. Validate before rendering:
|
|
@@ -26,15 +26,16 @@ If the user only wants the prompt/methodology and not files, still choose a shel
|
|
|
26
26
|
npx concept-atlas-dense-explain validate <file>.mdx --json
|
|
27
27
|
```
|
|
28
28
|
Every diagnostic is `CODE line:column message`. Fix all `error`s and re-run; warnings are quality signals you should also address when cheap.
|
|
29
|
-
6. Compile: `npx concept-atlas-dense-explain <file>.mdx --mode atlas|scroll [-o out.html] [--skin <id>] [--default-mode dark|light|system] [--style <id>]`. Output is a standalone HTML beside the MDX unless `-o` is given. Validation errors abort the build; use `--no-validate` only to force a knowingly broken build.
|
|
30
|
-
7. **Appearance (optional)**: pages ship with a reader-facing appearance menu — palette (`aurora` cool
|
|
31
|
-
8. For several documents, pass them all in one call: `npx concept-atlas-dense-explain a.mdx b.mdx c.mdx -o dist --force [--concurrency 3]`. `-o` is then a directory. The batch validates everything first and builds in parallel. Builds only bundle the heavy renderers the content uses: a page with no `<Math
|
|
29
|
+
6. Compile: `npx concept-atlas-dense-explain <file>.mdx --mode atlas|scroll [-o out.html] [--skin <id>] [--default-mode dark|light|system] [--style <id>] [--inline-mermaid] [--mermaid-cdn <url>]`. Output is a standalone HTML beside the MDX unless `-o` is given. Validation errors abort the build; use `--no-validate` only to force a knowingly broken build. Mermaid loads from a CDN at runtime by default (fast build, small HTML, needs network); pass `--inline-mermaid` when the user needs a fully offline single file.
|
|
30
|
+
7. **Appearance (optional)**: pages ship with a reader-facing appearance menu — palette (`aurora` cool indigo, `ember` warm gold, `verdant` forest green, `sakura` pink-plum, `noir` achromatic 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. You can bake different compile-time defaults: `--skin ember --default-mode dark --style classic` (or env `CONCEPT_ATLAS_SKIN` / `CONCEPT_ATLAS_DEFAULT_MODE` / `CONCEPT_ATLAS_STYLE` on the repo build). `--default-mode` only honors `dark`/`light`; `system` is accepted by the CLI but resolves to the carrier default (`light`). Bake a default only when the user asks for one. Content MDX never sets appearance — it is carrier/tooling territory, not content.
|
|
31
|
+
8. For several documents, pass them all in one call: `npx concept-atlas-dense-explain a.mdx b.mdx c.mdx -o dist --force [--concurrency 3]`. `-o` is then a directory. The batch validates everything first and builds in parallel. Builds only bundle the heavy renderers the content uses: a page with no `<Math>` skips KaTeX (its ~1.4MB inlined fonts), and Mermaid is served from a CDN by default rather than bundled. Do not add dummy `<Math>`/`<Mermaid>` nodes to "enable" them — write the components only when the content needs them. Add `--link-assets` when the page carries many screenshots and size matters.
|
|
32
32
|
9. Report the shell, output path, validation result (errors/warnings), and limitations. Do not claim interactions you did not verify.
|
|
33
33
|
|
|
34
34
|
## Carriers
|
|
35
35
|
|
|
36
36
|
- `atlas`: `ExplainPage` → `ConceptGraph` → `ConceptNode`, plus `Children`/`ConceptRef` and cross-branch `Relation`s. Exactly one `L0` root, several `L1` branches, depth to `L3`/`L4`. Shared components live inside nodes.
|
|
37
37
|
- `scroll`: `ScrollDocument` → `ScrollHeader` + `ScrollSection` (+ `ScrollProse`, `ScrollGrid`). Shared components live inside sections. The shell auto-builds a table of contents and reading progress from section titles — do not hand-build navigation.
|
|
38
|
+
- The browser tab comes from the shell, not the build flags: `ExplainPage title="..."` (atlas) or `ScrollHeader title="..."` (scroll) becomes the `<title>`, so give it a real, specific document name — never leave a placeholder like "主题名称". The favicon is a fixed 📃 document emoji on every generated page.
|
|
38
39
|
- Never make one MDX file both shells. When switching shells, convert only the outer structure.
|
|
39
40
|
|
|
40
41
|
## Component families
|
|
@@ -42,8 +43,8 @@ If the user only wants the prompt/methodology and not files, still choose a shel
|
|
|
42
43
|
- Node semantics: `Overview`, `Definition`, `Mechanism`, `Implementation`, `Boundary`, `Example`, `Counterexample`, `Prerequisite`, `Input`, `Output`, `Glossary`
|
|
43
44
|
- Argument and evidence: `Evidence`, `Invariant`, `FailureMode`, `Tradeoff`, `LearningObjectives`, `KeyQuestion`
|
|
44
45
|
- Information models: `Flow`, `Timeline`, `Compare`, `DecisionMatrix`, `FrameworkModel`, `MatrixModel`, `FormulaModel`, `PyramidModel`, `FunnelModel`
|
|
45
|
-
- Reading and layout: `Insight`, `Callout`, `Details`, `NoteGrid`, `Tabs`, `Columns`, `Stack`, `Grid`, `Split`, `ScrollGrid`
|
|
46
|
-
- Graphics and extensions: `Mermaid`, `RelationMap`, `RelationPath`, `Math`, `MathBlock`, `Chart`, `Figure
|
|
46
|
+
- Reading and layout: `Insight`, `Callout`, `Details`, `NoteGrid`, `Tabs`, `Columns`, `Stack`, `Grid`, `Split`, `ScrollGrid`, `ScrollPair`, `ScrollToc`
|
|
47
|
+
- Graphics and extensions: `Mermaid`, `RelationMap`, `RelationPath`, `Math`, `MathBlock`, `Chart`, `Figure` (alias `Image`), `Cite`, `References`
|
|
47
48
|
|
|
48
49
|
## Authoring rules
|
|
49
50
|
|
|
@@ -61,7 +62,7 @@ If the user only wants the prompt/methodology and not files, still choose a shel
|
|
|
61
62
|
|
|
62
63
|
## Validation diagnostics
|
|
63
64
|
|
|
64
|
-
`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_UNRESOLVED`, `RELATION_FROM_UNRESOLVED`, `RELATION_TO_UNRESOLVED`. Warnings worth fixing: `NODE_MISSING_SUMMARY`, `NODE_NO_CORE_CONTENT`, `UNKNOWN_LEVEL`, `UNKNOWN_RELATION_TYPE`, `RELATION_MISSING_LABEL`, `PROP_EXPECTS_ARRAY`, `MATH_CHILDREN_BRACES`, `GRAPH_MISSING_ROOT`, `ASSET_MISSING
|
|
65
|
+
`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_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` and `MULTIPLE_ROOT_LEVEL` are warnings that `--strict` promotes to errors.
|
|
65
66
|
|
|
66
67
|
## Before you report
|
|
67
68
|
|
package/template/index.html
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E📃%3C/text%3E%3C/svg%3E" />
|
|
6
7
|
<title>Concept Atlas · 概念缩放式知识图谱</title>
|
|
7
8
|
<script>
|
|
8
9
|
(function () {
|
|
@@ -3,11 +3,21 @@ import path from 'path';
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { normalizeSkin, normalizeStyle } from '../src/model/skins.js';
|
|
6
|
+
import { extractPageTitle, detectFeatures } from '../src/model/validate-content.js';
|
|
6
7
|
|
|
7
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
9
|
const __dirname = path.dirname(__filename);
|
|
9
10
|
const rootDir = path.resolve(__dirname, '..');
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Mermaid is served from a CDN at runtime by default so its ~2100-module
|
|
14
|
+
* transform stays out of the build. Set CONCEPT_ATLAS_INLINE_MERMAID=1 to bake
|
|
15
|
+
* it back into the HTML for a fully offline single file; CONCEPT_ATLAS_MERMAID_CDN
|
|
16
|
+
* overrides the CDN URL.
|
|
17
|
+
*/
|
|
18
|
+
const INLINE_MERMAID = ['1', 'true', 'yes'].includes((process.env.CONCEPT_ATLAS_INLINE_MERMAID || '').toLowerCase());
|
|
19
|
+
const MERMAID_CDN_URL = process.env.CONCEPT_ATLAS_MERMAID_CDN || '';
|
|
20
|
+
|
|
11
21
|
/**
|
|
12
22
|
* Optional compile-time appearance defaults, read from the environment and
|
|
13
23
|
* forwarded as `define`s. The carrier HTML plugin replaces the placeholders
|
|
@@ -24,15 +34,66 @@ function appearanceDefines() {
|
|
|
24
34
|
return define;
|
|
25
35
|
}
|
|
26
36
|
|
|
37
|
+
/**
|
|
38
|
+
* The demo document mounted by each carrier. Unlike the npm template (which
|
|
39
|
+
* aliases the user's MDX), the repository hardcodes its demos in main.jsx /
|
|
40
|
+
* scroll-main.jsx, so the build reads them to derive the tab title and the set
|
|
41
|
+
* of optional renderers actually needed.
|
|
42
|
+
*/
|
|
43
|
+
function demoSourceFor(entry) {
|
|
44
|
+
const demos = entry === 'scroll.html'
|
|
45
|
+
? ['content/scroll-reading-demo.mdx']
|
|
46
|
+
: ['content/components-demo.mdx', 'content/compile-runtime.mdx'];
|
|
47
|
+
const demo = demos.map(name => path.resolve(rootDir, name)).find(file => fs.existsSync(file));
|
|
48
|
+
return demo ? { demo, source: fs.readFileSync(demo, 'utf8') } : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Builds one standalone carrier HTML. `__ATLAS_FEATURES__` mirrors the CLI's
|
|
53
|
+
* per-document stubbing so a demo that never uses <Math>/<Mermaid> skips the
|
|
54
|
+
* KaTeX fonts and Mermaid module graph instead of bundling them unconditionally.
|
|
55
|
+
*/
|
|
56
|
+
async function buildCarrier(entry, baseDefine) {
|
|
57
|
+
const define = { ...baseDefine };
|
|
58
|
+
const demo = demoSourceFor(entry);
|
|
59
|
+
if (demo) {
|
|
60
|
+
const title = extractPageTitle(demo.source);
|
|
61
|
+
if (title) {
|
|
62
|
+
define.__ATLAS_PAGE_TITLE__ = JSON.stringify(title);
|
|
63
|
+
console.log(`🔖 ${entry} 标签页标题:${title}`);
|
|
64
|
+
}
|
|
65
|
+
const features = detectFeatures(demo.source);
|
|
66
|
+
define.__ATLAS_FEATURES__ = JSON.stringify(features);
|
|
67
|
+
if (features.mermaid) {
|
|
68
|
+
define.__ATLAS_MERMAID_MODE__ = JSON.stringify(INLINE_MERMAID ? 'inline' : 'cdn');
|
|
69
|
+
if (MERMAID_CDN_URL) define.__ATLAS_MERMAID_CDN_URL__ = JSON.stringify(MERMAID_CDN_URL);
|
|
70
|
+
if (!INLINE_MERMAID) console.log(`🌐 ${entry} Mermaid 运行时从 CDN 加载(--inline-mermaid 可内联)`);
|
|
71
|
+
}
|
|
72
|
+
const dropped = [features.math ? null : 'KaTeX', features.mermaid ? null : 'Mermaid'].filter(Boolean);
|
|
73
|
+
if (dropped.length) console.log(`⚡ ${entry} 省略未使用的渲染器:${dropped.join('、')}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await build({
|
|
77
|
+
root: rootDir,
|
|
78
|
+
define,
|
|
79
|
+
build: {
|
|
80
|
+
outDir: 'dist',
|
|
81
|
+
// dist is cleared once up front; parallel carriers must not wipe each
|
|
82
|
+
// other's output mid-build.
|
|
83
|
+
emptyOutDir: false,
|
|
84
|
+
rollupOptions: { input: path.resolve(rootDir, entry) },
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
27
89
|
async function runBuild() {
|
|
28
90
|
console.log('🚀 开始构建 Concept Atlas 知识讲解页面...');
|
|
29
91
|
|
|
30
92
|
try {
|
|
31
|
-
//
|
|
93
|
+
// Clear dist once, then let every carrier write into it concurrently.
|
|
32
94
|
const distDir = path.resolve(rootDir, 'dist');
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
95
|
+
fs.rmSync(distDir, { recursive: true, force: true });
|
|
96
|
+
fs.mkdirSync(distDir, { recursive: true });
|
|
36
97
|
|
|
37
98
|
const mode = process.env.CONCEPT_ATLAS_MODE;
|
|
38
99
|
const carriers = mode === 'atlas' ? ['index.html'] : mode === 'scroll' ? ['scroll.html'] : ['index.html', 'scroll.html'];
|
|
@@ -41,19 +102,10 @@ async function runBuild() {
|
|
|
41
102
|
console.log(`🎨 默认外观:skin=${define.__ATLAS_DEFAULT_SKIN__ || '(carrier 默认)'} mode=${define.__ATLAS_DEFAULT_MODE__ || '(carrier 默认)'}`);
|
|
42
103
|
}
|
|
43
104
|
|
|
44
|
-
// vite-plugin-singlefile supports one HTML input per build
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
root: rootDir,
|
|
49
|
-
define,
|
|
50
|
-
build: {
|
|
51
|
-
outDir: 'dist',
|
|
52
|
-
emptyOutDir: index === 0,
|
|
53
|
-
rollupOptions: { input: path.resolve(rootDir, entry) },
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
}
|
|
105
|
+
// vite-plugin-singlefile supports one HTML input per build and emits no
|
|
106
|
+
// shared assets, so the carriers are independent and build in parallel
|
|
107
|
+
// instead of one full pass after another.
|
|
108
|
+
await Promise.all(carriers.map(entry => buildCarrier(entry, define)));
|
|
57
109
|
|
|
58
110
|
console.log(`✅ 构建成功!产物已生成到 ${carriers.map(entry => `dist/${entry}`).join(' 和 ')}。`);
|
|
59
111
|
} catch (err) {
|
package/template/scroll.html
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E📃%3C/text%3E%3C/svg%3E" />
|
|
6
7
|
<title>Concept Atlas · 连续阅读示例</title>
|
|
7
8
|
<script>
|
|
8
9
|
(function () {
|
|
@@ -70,7 +70,7 @@ function configureMermaid() {
|
|
|
70
70
|
lineColor: read('--mermaid-line', MERMAID_FALLBACKS.lineColor),
|
|
71
71
|
secondaryColor: nodeBg,
|
|
72
72
|
tertiaryColor: read('--mermaid-canvas', MERMAID_FALLBACKS.tertiaryColor),
|
|
73
|
-
fontFamily: 'Plus Jakarta Sans, sans-serif
|
|
73
|
+
fontFamily: read('--font-sans', "'Plus Jakarta Sans', sans-serif"),
|
|
74
74
|
},
|
|
75
75
|
});
|
|
76
76
|
}
|
|
@@ -942,6 +942,7 @@ export function Chart({ title = '图表', type = 'bar', data = [], series = [],
|
|
|
942
942
|
<g key={seriesIndex}>
|
|
943
943
|
<polyline
|
|
944
944
|
className="chart-line"
|
|
945
|
+
pathLength="1"
|
|
945
946
|
style={chartColor(seriesIndex)}
|
|
946
947
|
points={entry.values.map((value, idx) => `${xFor(idx)},${yFor(value)}`).join(' ')}
|
|
947
948
|
/>
|
|
@@ -13,6 +13,21 @@ export const SKINS = [
|
|
|
13
13
|
id: 'ember',
|
|
14
14
|
label: 'Ember · 炉火',
|
|
15
15
|
swatch: { dark: '#1c1812', light: '#f6f1e7', accent: '#d99a4e' }
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'verdant',
|
|
19
|
+
label: 'Verdant · 苔原',
|
|
20
|
+
swatch: { dark: '#0f1813', light: '#f3f7f2', accent: '#10b981' }
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'sakura',
|
|
24
|
+
label: 'Sakura · 樱雾',
|
|
25
|
+
swatch: { dark: '#1e131d', light: '#fbf3f7', accent: '#ec4899' }
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'noir',
|
|
29
|
+
label: 'Noir · 墨白',
|
|
30
|
+
swatch: { dark: '#141416', light: '#f5f5f4', accent: '#e63946' }
|
|
16
31
|
}
|
|
17
32
|
];
|
|
18
33
|
|
|
@@ -32,7 +47,9 @@ export function normalizeSkin(value) {
|
|
|
32
47
|
*/
|
|
33
48
|
export const COMPONENT_STYLES = [
|
|
34
49
|
{ id: 'manuscript', label: 'Manuscript · 评注手稿' },
|
|
35
|
-
{ id: 'classic', label: 'Classic · 经典卡片' }
|
|
50
|
+
{ id: 'classic', label: 'Classic · 经典卡片' },
|
|
51
|
+
{ id: 'shadcn', label: 'shadcn · 极简界面' },
|
|
52
|
+
{ id: 'elastic', label: 'Elastic · 观测面板' }
|
|
36
53
|
];
|
|
37
54
|
|
|
38
55
|
export const DEFAULT_STYLE = 'manuscript';
|
|
@@ -598,3 +598,28 @@ export function detectFeatures(source) {
|
|
|
598
598
|
mermaid: used.has('Mermaid'),
|
|
599
599
|
};
|
|
600
600
|
}
|
|
601
|
+
|
|
602
|
+
/** JSX string literals decode these five entities; a single pass avoids
|
|
603
|
+
* double-decoding sequences like `&lt;`. */
|
|
604
|
+
const ENTITY_MAP = { amp: '&', lt: '<', gt: '>', quot: '"', '#39': "'" };
|
|
605
|
+
const decodeEntities = text => text.replace(/&(amp|lt|gt|quot|#39);/g, (_, entity) => ENTITY_MAP[entity]);
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Reads the browser-tab title from the MDX source without rendering: the atlas
|
|
609
|
+
* shell declares it on <ExplainPage title="...">, the scroll shell on
|
|
610
|
+
* <ScrollHeader title="...">. Only quoted string props count — a `{...}`
|
|
611
|
+
* expression title cannot be known at build time. Returns null when no static
|
|
612
|
+
* title exists, so the build keeps the carrier's default <title>.
|
|
613
|
+
*/
|
|
614
|
+
export function extractPageTitle(source) {
|
|
615
|
+
const tags = tokenize(maskIgnored(source));
|
|
616
|
+
for (const shell of ['ExplainPage', 'ScrollHeader']) {
|
|
617
|
+
const tag = tags.find(item => item.name === shell && item.kind !== 'close');
|
|
618
|
+
if (!tag) continue;
|
|
619
|
+
const attr = attrsToMap(tag.attrs).title;
|
|
620
|
+
if (attr && attr.hasValue && attr.quoted && attr.value.trim()) {
|
|
621
|
+
return decodeEntities(attr.value).trim();
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return null;
|
|
625
|
+
}
|