@aigne/doc-smith 0.6.0 → 0.7.1
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/.aigne/doc-smith/config.yaml +70 -0
- package/.aigne/doc-smith/output/structure-plan.json +152 -0
- package/.aigne/doc-smith/preferences.yml +31 -0
- package/.aigne/doc-smith/upload-cache.yaml +288 -0
- package/.github/workflows/ci.yml +46 -0
- package/.github/workflows/reviewer.yml +2 -1
- package/CHANGELOG.md +17 -0
- package/README.md +33 -15
- package/agents/chat.yaml +30 -0
- package/agents/check-structure-plan.mjs +1 -1
- package/agents/docs-fs.yaml +25 -0
- package/agents/exit.mjs +6 -0
- package/agents/feedback-refiner.yaml +5 -1
- package/agents/find-items-by-paths.mjs +10 -4
- package/agents/fs.mjs +60 -0
- package/agents/input-generator.mjs +150 -91
- package/agents/load-config.mjs +0 -5
- package/agents/load-sources.mjs +61 -8
- package/agents/publish-docs.mjs +27 -12
- package/agents/retranslate.yaml +1 -1
- package/agents/team-publish-docs.yaml +2 -2
- package/aigne.yaml +1 -0
- package/docs/_sidebar.md +17 -0
- package/docs/advanced-how-it-works.md +104 -0
- package/docs/advanced-how-it-works.zh.md +104 -0
- package/docs/advanced-quality-assurance.md +64 -0
- package/docs/advanced-quality-assurance.zh.md +64 -0
- package/docs/advanced.md +28 -0
- package/docs/advanced.zh.md +28 -0
- package/docs/changelog.md +272 -0
- package/docs/changelog.zh.md +272 -0
- package/docs/cli-reference.md +185 -0
- package/docs/cli-reference.zh.md +185 -0
- package/docs/configuration-interactive-setup.md +82 -0
- package/docs/configuration-interactive-setup.zh.md +82 -0
- package/docs/configuration-language-support.md +64 -0
- package/docs/configuration-language-support.zh.md +64 -0
- package/docs/configuration-llm-setup.md +90 -0
- package/docs/configuration-llm-setup.zh.md +90 -0
- package/docs/configuration-preferences.md +122 -0
- package/docs/configuration-preferences.zh.md +123 -0
- package/docs/configuration.md +173 -0
- package/docs/configuration.zh.md +173 -0
- package/docs/features-generate-documentation.md +82 -0
- package/docs/features-generate-documentation.zh.md +82 -0
- package/docs/features-publish-your-docs.md +98 -0
- package/docs/features-publish-your-docs.zh.md +98 -0
- package/docs/features-translate-documentation.md +83 -0
- package/docs/features-translate-documentation.zh.md +83 -0
- package/docs/features-update-and-refine.md +86 -0
- package/docs/features-update-and-refine.zh.md +86 -0
- package/docs/features.md +56 -0
- package/docs/features.zh.md +56 -0
- package/docs/getting-started.md +74 -0
- package/docs/getting-started.zh.md +74 -0
- package/docs/overview.md +48 -0
- package/docs/overview.zh.md +48 -0
- package/media.md +19 -0
- package/package.json +13 -10
- package/prompts/content-detail-generator.md +7 -3
- package/prompts/document/custom-components.md +80 -0
- package/prompts/document/d2-chart/diy-examples.md +44 -0
- package/prompts/document/d2-chart/official-examples.md +708 -0
- package/prompts/document/d2-chart/rules.md +48 -0
- package/prompts/document/detail-generator.md +12 -15
- package/prompts/document/structure-planning.md +1 -3
- package/prompts/feedback-refiner.md +81 -60
- package/prompts/structure-planning.md +20 -3
- package/tests/check-detail-result.test.mjs +3 -4
- package/tests/conflict-resolution.test.mjs +237 -0
- package/tests/input-generator.test.mjs +940 -0
- package/tests/load-sources.test.mjs +627 -3
- package/tests/preferences-utils.test.mjs +94 -0
- package/tests/save-value-to-config.test.mjs +182 -5
- package/tests/utils.test.mjs +49 -0
- package/utils/conflict-detector.mjs +72 -1
- package/utils/constants.mjs +125 -124
- package/utils/kroki-utils.mjs +162 -0
- package/utils/markdown-checker.mjs +98 -70
- package/utils/utils.mjs +96 -28
package/utils/constants.mjs
CHANGED
|
@@ -343,79 +343,8 @@ export const SUPPORTED_FILE_EXTENSIONS = [".txt", ".md", ".json", ".yaml", ".yml
|
|
|
343
343
|
export const CONFLICT_RULES = {
|
|
344
344
|
// Internal conflicts within the same question (multi-select conflicts)
|
|
345
345
|
internalConflicts: {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
conflictItems: ["getStarted", "findAnswers"],
|
|
349
|
-
severity: "severe",
|
|
350
|
-
reason:
|
|
351
|
-
"Quick start guide (skips complex cases) conflicts with comprehensive API reference (skips beginner explanations)",
|
|
352
|
-
suggestion: "Choose one as primary goal, or consider creating layered documentation",
|
|
353
|
-
},
|
|
354
|
-
{
|
|
355
|
-
conflictItems: ["getStarted", "understandSystem"],
|
|
356
|
-
severity: "severe",
|
|
357
|
-
reason: "30-minute quick start conflicts with deep architectural concept explanations",
|
|
358
|
-
suggestion: "Consider creating separate quick start and architecture design docs",
|
|
359
|
-
},
|
|
360
|
-
{
|
|
361
|
-
conflictItems: ["completeTasks", "understandSystem"],
|
|
362
|
-
severity: "moderate",
|
|
363
|
-
reason:
|
|
364
|
-
"Practical task guidance and theoretical concept explanation have different focuses",
|
|
365
|
-
suggestion:
|
|
366
|
-
"Can be handled through layered document structure: concepts first, then practice",
|
|
367
|
-
},
|
|
368
|
-
{
|
|
369
|
-
conflictItems: ["getStarted", "solveProblems"],
|
|
370
|
-
severity: "moderate",
|
|
371
|
-
reason:
|
|
372
|
-
"Quick start (success cases) and troubleshooting (error scenarios) have different focuses",
|
|
373
|
-
suggestion: "Create separate tutorial and troubleshooting guides",
|
|
374
|
-
},
|
|
375
|
-
{
|
|
376
|
-
conflictItems: ["findAnswers", "solveProblems"],
|
|
377
|
-
severity: "moderate",
|
|
378
|
-
reason: "API reference and diagnostic documentation have different organizational logic",
|
|
379
|
-
suggestion: "Add troubleshooting section to reference documentation",
|
|
380
|
-
},
|
|
381
|
-
],
|
|
382
|
-
targetAudienceTypes: [
|
|
383
|
-
{
|
|
384
|
-
conflictItems: ["endUsers", "developers"],
|
|
385
|
-
severity: "severe",
|
|
386
|
-
reason:
|
|
387
|
-
"Non-technical users (avoid technical terms) conflict with developers (code-first approach)",
|
|
388
|
-
suggestion:
|
|
389
|
-
"Create separate documentation for different audiences or use layered content design",
|
|
390
|
-
},
|
|
391
|
-
{
|
|
392
|
-
conflictItems: ["endUsers", "devops"],
|
|
393
|
-
severity: "severe",
|
|
394
|
-
reason: "Non-technical users and operations technical personnel have very different needs",
|
|
395
|
-
suggestion: "Consider creating separate user guides and operations documentation",
|
|
396
|
-
},
|
|
397
|
-
{
|
|
398
|
-
conflictItems: ["endUsers", "decisionMakers"],
|
|
399
|
-
severity: "severe",
|
|
400
|
-
reason:
|
|
401
|
-
"Non-technical users (simple language) and decision makers (architecture diagrams) have different needs",
|
|
402
|
-
suggestion: "Create high-level overview for management and user operation manuals",
|
|
403
|
-
},
|
|
404
|
-
{
|
|
405
|
-
conflictItems: ["developers", "decisionMakers"],
|
|
406
|
-
severity: "moderate",
|
|
407
|
-
reason:
|
|
408
|
-
"Developers (code details) and decision makers (high-level overview) have different focus areas",
|
|
409
|
-
suggestion: "Use progressive disclosure: high-level first, then details",
|
|
410
|
-
},
|
|
411
|
-
{
|
|
412
|
-
conflictItems: ["supportTeams", "decisionMakers"],
|
|
413
|
-
severity: "moderate",
|
|
414
|
-
reason:
|
|
415
|
-
"Support teams (problem diagnosis) and decision makers (architecture decisions) have different focus areas",
|
|
416
|
-
suggestion: "Include operational considerations in decision documentation",
|
|
417
|
-
},
|
|
418
|
-
],
|
|
346
|
+
// Note: Most conflicts can be resolved through intelligent document structure planning
|
|
347
|
+
// Only keeping conflicts that represent fundamental incompatibilities
|
|
419
348
|
},
|
|
420
349
|
|
|
421
350
|
// Cross-question conflicts (conflicts between different questions)
|
|
@@ -483,66 +412,138 @@ export const CONFLICT_RULES = {
|
|
|
483
412
|
readerKnowledgeLevel: ["emergencyTroubleshooting"],
|
|
484
413
|
},
|
|
485
414
|
},
|
|
415
|
+
],
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
// Conflict resolution rules - defines how to handle conflicts when users select conflicting options
|
|
419
|
+
export const CONFLICT_RESOLUTION_RULES = {
|
|
420
|
+
// Document purpose conflicts that can be resolved through structure planning
|
|
421
|
+
documentPurpose: [
|
|
486
422
|
{
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
},
|
|
491
|
-
severity: "severe",
|
|
492
|
-
reason: "Quick start tutorials contradict comprehensive coverage documentation",
|
|
493
|
-
action: "filter",
|
|
494
|
-
conflictingOptions: {
|
|
495
|
-
documentationDepth: ["comprehensive"],
|
|
496
|
-
},
|
|
423
|
+
conflictItems: ["getStarted", "findAnswers"],
|
|
424
|
+
strategy: "layered_structure",
|
|
425
|
+
description: "Quick start and API reference conflict, resolved through layered structure",
|
|
497
426
|
},
|
|
498
427
|
{
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
severity: "moderate",
|
|
504
|
-
reason:
|
|
505
|
-
"Troubleshooting usually needs to cover edge cases, basic content alone may not be sufficient",
|
|
506
|
-
action: "filter",
|
|
507
|
-
conflictingOptions: {
|
|
508
|
-
documentationDepth: ["essentialOnly"],
|
|
509
|
-
},
|
|
428
|
+
conflictItems: ["getStarted", "understandSystem"],
|
|
429
|
+
strategy: "separate_sections",
|
|
430
|
+
description:
|
|
431
|
+
"Quick start and system understanding conflict, resolved through separate sections",
|
|
510
432
|
},
|
|
511
433
|
{
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
severity: "moderate",
|
|
517
|
-
reason: "Non-technical users typically do not need comprehensive technical coverage",
|
|
518
|
-
action: "filter",
|
|
519
|
-
conflictingOptions: {
|
|
520
|
-
documentationDepth: ["comprehensive"],
|
|
521
|
-
},
|
|
434
|
+
conflictItems: ["completeTasks", "understandSystem"],
|
|
435
|
+
strategy: "concepts_then_practice",
|
|
436
|
+
description:
|
|
437
|
+
"Task guidance and system understanding conflict, resolved through concepts-then-practice structure",
|
|
522
438
|
},
|
|
523
439
|
{
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
severity: "moderate",
|
|
529
|
-
reason: "Decision makers may need more comprehensive information to make decisions",
|
|
530
|
-
action: "filter",
|
|
531
|
-
conflictingOptions: {
|
|
532
|
-
documentationDepth: ["essentialOnly"],
|
|
533
|
-
},
|
|
440
|
+
conflictItems: ["findAnswers", "solveProblems"],
|
|
441
|
+
strategy: "reference_with_troubleshooting",
|
|
442
|
+
description:
|
|
443
|
+
"API reference and problem solving conflict, resolved through reference with troubleshooting",
|
|
534
444
|
},
|
|
445
|
+
],
|
|
446
|
+
|
|
447
|
+
// Target audience conflicts that can be resolved through structure planning
|
|
448
|
+
targetAudienceTypes: [
|
|
535
449
|
{
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
450
|
+
conflictItems: ["endUsers", "developers"],
|
|
451
|
+
strategy: "separate_user_paths",
|
|
452
|
+
description: "End users and developers conflict, resolved through separate user paths",
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
conflictItems: ["endUsers", "devops"],
|
|
456
|
+
strategy: "role_based_sections",
|
|
457
|
+
description: "End users and DevOps conflict, resolved through role-based sections",
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
conflictItems: ["developers", "decisionMakers"],
|
|
461
|
+
strategy: "progressive_disclosure",
|
|
462
|
+
description:
|
|
463
|
+
"Developers and decision makers conflict, resolved through progressive disclosure",
|
|
546
464
|
},
|
|
547
465
|
],
|
|
548
466
|
};
|
|
467
|
+
|
|
468
|
+
// Resolution strategy descriptions
|
|
469
|
+
export const RESOLUTION_STRATEGIES = {
|
|
470
|
+
layered_structure: (items) =>
|
|
471
|
+
`Detected "${items.join('" and "')}" purpose conflict. Resolution strategy: Create layered document structure
|
|
472
|
+
- Quick start section: Uses "get started" style - optimizes for speed, key steps, working examples, skips complex edge cases
|
|
473
|
+
- API reference section: Uses "find answers" style - comprehensive coverage, searchability, rich examples, skips narrative flow
|
|
474
|
+
- Ensure sections complement rather than conflict with each other`,
|
|
475
|
+
|
|
476
|
+
separate_sections: (items) =>
|
|
477
|
+
`Detected "${items.join('" and "')}" purpose conflict. Resolution strategy: Create separate sections
|
|
478
|
+
- Quick start section: Uses "get started" style - focuses on practical operations, completable within 30 minutes
|
|
479
|
+
- System understanding section: Uses "understand system" style - dedicated to explaining architecture, concepts, design decision rationale
|
|
480
|
+
- Meet different depth needs through clear section separation`,
|
|
481
|
+
|
|
482
|
+
concepts_then_practice: (items) =>
|
|
483
|
+
`Detected "${items.join('" and "')}" purpose conflict. Resolution strategy: Use progressive "concepts-then-practice" structure
|
|
484
|
+
- Concepts section: Uses "understand system" style - first explains core concepts and architecture principles
|
|
485
|
+
- Practice section: Uses "complete tasks" style - then provides specific step guidance and practical scenarios
|
|
486
|
+
- Ensure smooth transition between theory and practice`,
|
|
487
|
+
|
|
488
|
+
reference_with_troubleshooting: (items) =>
|
|
489
|
+
`Detected "${items.join('" and "')}" purpose conflict. Resolution strategy: Integrate troubleshooting into API reference
|
|
490
|
+
- API reference section: Uses "find answers" style - comprehensive feature documentation and parameter descriptions
|
|
491
|
+
- Troubleshooting section: Uses "solve problems" style - add common issues and diagnostic methods for each feature
|
|
492
|
+
- Create dedicated problem diagnosis index for quick location`,
|
|
493
|
+
|
|
494
|
+
separate_user_paths: (items) =>
|
|
495
|
+
`Detected "${items.join('" and "')}" audience conflict. Resolution strategy: Create separate user paths
|
|
496
|
+
- User guide path: Uses "end users" style - simple language, UI operations, screenshot instructions, business outcome oriented
|
|
497
|
+
- Developer guide path: Uses "developers" style - code-first, technical precision, SDK examples, configuration snippets
|
|
498
|
+
- Provide clear path navigation for users to choose appropriate entry point`,
|
|
499
|
+
|
|
500
|
+
role_based_sections: (items) =>
|
|
501
|
+
`Detected "${items.join('" and "')}" audience conflict. Resolution strategy: Organize content by role
|
|
502
|
+
- Create dedicated sections for different roles, each section uses corresponding audience style
|
|
503
|
+
- Ensure content depth and expression precisely match the needs and background of corresponding audience
|
|
504
|
+
- Provide cross-references between sections to facilitate collaborative understanding between roles`,
|
|
505
|
+
|
|
506
|
+
progressive_disclosure: (items) =>
|
|
507
|
+
`Detected "${items.join('" and "')}" audience conflict. Resolution strategy: Use progressive information disclosure
|
|
508
|
+
- Overview level: Uses "decision makers" style - high-level architecture diagrams, decision points, business value
|
|
509
|
+
- Detail level: Uses "developers" style - technical implementation details, code examples, best practices
|
|
510
|
+
- Ensure smooth transition from strategic to tactical`,
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
export const D2_CONFIG = `vars: {
|
|
514
|
+
d2-config: {
|
|
515
|
+
layout-engine: elk
|
|
516
|
+
theme-id: 0
|
|
517
|
+
theme-overrides: {
|
|
518
|
+
N1: "#2AA7A1"
|
|
519
|
+
N2: "#73808C"
|
|
520
|
+
|
|
521
|
+
N4: "#FFFFFF"
|
|
522
|
+
N5: "#FAFBFC"
|
|
523
|
+
|
|
524
|
+
N7: "#ffffff"
|
|
525
|
+
|
|
526
|
+
B1: "#8EDDD9"
|
|
527
|
+
B2: "#C9DCE6"
|
|
528
|
+
B3: "#EEF9F9"
|
|
529
|
+
B4: "#F7F8FA"
|
|
530
|
+
B5: "#FCFDFD"
|
|
531
|
+
B6: "#E3E9F0"
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
AA2: "#9EB7C5"
|
|
535
|
+
AA4: "#E3EBF2"
|
|
536
|
+
AA5: "#F6FAFC"
|
|
537
|
+
|
|
538
|
+
AB4: "#B8F1F6"
|
|
539
|
+
AB5: "#E3F8FA"
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}`;
|
|
543
|
+
|
|
544
|
+
export const KROKI_CONCURRENCY = 5;
|
|
545
|
+
export const FILE_CONCURRENCY = 3;
|
|
546
|
+
export const TMP_DIR = ".tmp";
|
|
547
|
+
export const TMP_DOCS_DIR = "docs";
|
|
548
|
+
|
|
549
|
+
export const TMP_ASSETS_DIR = "assets";
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import { glob } from "glob";
|
|
5
|
+
import pMap from "p-map";
|
|
6
|
+
import { joinURL } from "ufo";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
D2_CONFIG,
|
|
10
|
+
FILE_CONCURRENCY,
|
|
11
|
+
KROKI_CONCURRENCY,
|
|
12
|
+
TMP_ASSETS_DIR,
|
|
13
|
+
TMP_DIR,
|
|
14
|
+
} from "./constants.mjs";
|
|
15
|
+
import { getContentHash } from "./utils.mjs";
|
|
16
|
+
|
|
17
|
+
export async function getChart({ chart = "d2", format = "svg", content, strict }) {
|
|
18
|
+
const baseUrl = "https://chart.abtnet.io";
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch(joinURL(baseUrl, chart, format), {
|
|
22
|
+
method: "POST",
|
|
23
|
+
body: content,
|
|
24
|
+
headers: {
|
|
25
|
+
Accept: "image/svg+xml",
|
|
26
|
+
"Content-Type": "text/plain",
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
if (strict && !res.ok) {
|
|
30
|
+
throw new Error(`Failed to fetch chart: ${res.status} ${res.statusText}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const data = await res.text();
|
|
34
|
+
return data;
|
|
35
|
+
} catch (err) {
|
|
36
|
+
if (strict) throw err;
|
|
37
|
+
|
|
38
|
+
console.error("Failed to generate chart from:", baseUrl, err);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function getD2Svg({ content, strict = false }) {
|
|
44
|
+
const svgContent = await getChart({
|
|
45
|
+
chart: "d2",
|
|
46
|
+
format: "svg",
|
|
47
|
+
content,
|
|
48
|
+
strict,
|
|
49
|
+
});
|
|
50
|
+
return svgContent;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Helper: save d2 svg assets alongside document
|
|
54
|
+
export async function saveD2Assets({ markdown, docsDir }) {
|
|
55
|
+
const codeBlockRegex = /```d2\n([\s\S]*?)```/g;
|
|
56
|
+
|
|
57
|
+
const { replaced } = await runIterator({
|
|
58
|
+
input: markdown,
|
|
59
|
+
regexp: codeBlockRegex,
|
|
60
|
+
replace: true,
|
|
61
|
+
fn: async ([_match, _code]) => {
|
|
62
|
+
const assetDir = path.join(docsDir, "../", TMP_ASSETS_DIR, "d2");
|
|
63
|
+
await fs.ensureDir(assetDir);
|
|
64
|
+
const d2Content = [D2_CONFIG, _code].join("\n");
|
|
65
|
+
const fileName = `${getContentHash(d2Content)}.svg`;
|
|
66
|
+
const svgPath = path.join(assetDir, fileName);
|
|
67
|
+
|
|
68
|
+
if (await fs.pathExists(svgPath)) {
|
|
69
|
+
if (process.env.DEBUG) {
|
|
70
|
+
console.log("Found assets cache, skipping generation", svgPath);
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
if (process.env.DEBUG) {
|
|
74
|
+
console.log("start generate d2 chart", svgPath);
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const svg = await getD2Svg({ content: d2Content });
|
|
78
|
+
if (svg) {
|
|
79
|
+
await fs.writeFile(svgPath, svg, { encoding: "utf8" });
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (process.env.DEBUG) {
|
|
83
|
+
console.warn("Failed to generate D2 chart:", error);
|
|
84
|
+
}
|
|
85
|
+
return _code;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return ``;
|
|
89
|
+
},
|
|
90
|
+
options: { concurrency: KROKI_CONCURRENCY },
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return replaced;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function beforePublishHook({ docsDir }) {
|
|
97
|
+
// Example: process each markdown file (replace with your logic)
|
|
98
|
+
const mdFilePaths = await glob("**/*.md", { cwd: docsDir });
|
|
99
|
+
await pMap(
|
|
100
|
+
mdFilePaths,
|
|
101
|
+
async (filePath) => {
|
|
102
|
+
let finalContent = await fs.readFile(path.join(docsDir, filePath), { encoding: "utf8" });
|
|
103
|
+
finalContent = await saveD2Assets({ markdown: finalContent, docsDir });
|
|
104
|
+
|
|
105
|
+
await fs.writeFile(path.join(docsDir, filePath), finalContent, { encoding: "utf8" });
|
|
106
|
+
},
|
|
107
|
+
{ concurrency: FILE_CONCURRENCY },
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function runIterator({ input, regexp, fn = () => {}, options, replace = false }) {
|
|
112
|
+
if (!input) return input;
|
|
113
|
+
const matches = [...input.matchAll(regexp)];
|
|
114
|
+
const results = [];
|
|
115
|
+
await pMap(
|
|
116
|
+
matches,
|
|
117
|
+
async (...args) => {
|
|
118
|
+
const resultItem = await fn(...args);
|
|
119
|
+
results.push(resultItem);
|
|
120
|
+
},
|
|
121
|
+
options,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
let replaced = input;
|
|
125
|
+
if (replace) {
|
|
126
|
+
let index = 0;
|
|
127
|
+
replaced = replaced.replace(regexp, () => {
|
|
128
|
+
return results[index++];
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
results,
|
|
134
|
+
replaced,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function checkD2Content({ content }) {
|
|
139
|
+
await ensureTmpDir();
|
|
140
|
+
const assetDir = path.join(".aigne", "doc-smith", TMP_DIR, TMP_ASSETS_DIR, "d2");
|
|
141
|
+
await fs.ensureDir(assetDir);
|
|
142
|
+
const d2Content = [D2_CONFIG, content].join("\n");
|
|
143
|
+
const fileName = `${getContentHash(d2Content)}.svg`;
|
|
144
|
+
const svgPath = path.join(assetDir, fileName);
|
|
145
|
+
if (await fs.pathExists(svgPath)) {
|
|
146
|
+
if (process.env.DEBUG) {
|
|
147
|
+
console.log("Found assets cache, skipping generation", svgPath);
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const svg = await getD2Svg({ content: d2Content, strict: true });
|
|
153
|
+
await fs.writeFile(svgPath, svg, { encoding: "utf8" });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function ensureTmpDir() {
|
|
157
|
+
const tmpDir = path.join(".aigne", "doc-smith", TMP_DIR);
|
|
158
|
+
if (!(await fs.pathExists(path.join(tmpDir, ".gitignore")))) {
|
|
159
|
+
await fs.ensureDir(tmpDir);
|
|
160
|
+
await fs.writeFile(path.join(tmpDir, ".gitignore"), "**/*", { encoding: "utf8" });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import pMap from "p-map";
|
|
3
4
|
import remarkGfm from "remark-gfm";
|
|
4
5
|
import remarkLint from "remark-lint";
|
|
5
6
|
import remarkParse from "remark-parse";
|
|
6
7
|
import { unified } from "unified";
|
|
7
8
|
import { visit } from "unist-util-visit";
|
|
8
9
|
import { VFile } from "vfile";
|
|
10
|
+
import { KROKI_CONCURRENCY } from "./constants.mjs";
|
|
11
|
+
import { checkD2Content } from "./kroki-utils.mjs";
|
|
9
12
|
import { validateMermaidSyntax } from "./mermaid-validator.mjs";
|
|
10
13
|
|
|
11
14
|
/**
|
|
@@ -67,7 +70,10 @@ function checkDeadLinks(markdown, source, allowedLinks, errorMessages) {
|
|
|
67
70
|
const linkRegex = /(?<!!)\[([^\]]+)\]\(([^)]+)\)/g;
|
|
68
71
|
let match;
|
|
69
72
|
|
|
70
|
-
while (
|
|
73
|
+
while (true) {
|
|
74
|
+
match = linkRegex.exec(markdown);
|
|
75
|
+
if (match === null) break;
|
|
76
|
+
|
|
71
77
|
const link = match[2];
|
|
72
78
|
const trimLink = link.trim();
|
|
73
79
|
|
|
@@ -173,7 +179,9 @@ function checkLocalImages(markdown, source, errorMessages, markdownFilePath, bas
|
|
|
173
179
|
const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
|
174
180
|
let match;
|
|
175
181
|
|
|
176
|
-
while (
|
|
182
|
+
while (true) {
|
|
183
|
+
match = imageRegex.exec(markdown);
|
|
184
|
+
if (match === null) break;
|
|
177
185
|
const imagePath = match[2].trim();
|
|
178
186
|
const altText = match[1];
|
|
179
187
|
|
|
@@ -370,91 +378,102 @@ export async function checkMarkdown(markdown, source = "content", options = {})
|
|
|
370
378
|
|
|
371
379
|
// Check mermaid code blocks and other custom validations
|
|
372
380
|
const mermaidChecks = [];
|
|
381
|
+
const d2ChecksList = [];
|
|
373
382
|
visit(ast, "code", (node) => {
|
|
374
|
-
if (node.lang
|
|
375
|
-
// Check for mermaid syntax errors
|
|
376
|
-
mermaidChecks.push(
|
|
377
|
-
validateMermaidSyntax(node.value).catch((error) => {
|
|
378
|
-
const errorMessage = error?.message || String(error) || "Unknown mermaid syntax error";
|
|
379
|
-
|
|
380
|
-
// Format mermaid error in check-detail-result style
|
|
381
|
-
const line = node.position?.start?.line || "unknown";
|
|
382
|
-
errorMessages.push(
|
|
383
|
-
`Found Mermaid syntax error in ${source} at line ${line}: ${errorMessage}`,
|
|
384
|
-
);
|
|
385
|
-
}),
|
|
386
|
-
);
|
|
387
|
-
|
|
388
|
-
// Check for specific mermaid rendering issues
|
|
389
|
-
const mermaidContent = node.value;
|
|
383
|
+
if (node.lang) {
|
|
390
384
|
const line = node.position?.start?.line || "unknown";
|
|
391
385
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
386
|
+
if (node.lang.toLowerCase() === "mermaid") {
|
|
387
|
+
// Check for mermaid syntax errors
|
|
388
|
+
mermaidChecks.push(
|
|
389
|
+
validateMermaidSyntax(node.value).catch((error) => {
|
|
390
|
+
const errorMessage =
|
|
391
|
+
error?.message || String(error) || "Unknown mermaid syntax error";
|
|
392
|
+
|
|
393
|
+
// Format mermaid error in check-detail-result style
|
|
394
|
+
errorMessages.push(
|
|
395
|
+
`Found Mermaid syntax error in ${source} at line ${line}: ${errorMessage}`,
|
|
396
|
+
);
|
|
397
|
+
}),
|
|
400
398
|
);
|
|
401
|
-
match = nodeLabelRegex.exec(mermaidContent);
|
|
402
|
-
}
|
|
403
399
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
400
|
+
// Check for specific mermaid rendering issues
|
|
401
|
+
const mermaidContent = node.value;
|
|
402
|
+
|
|
403
|
+
// Check for backticks in node labels
|
|
404
|
+
const nodeLabelRegex = /[A-Za-z0-9_]+\["([^"]*`[^"]*)"\]|[A-Za-z0-9_]+{"([^}]*`[^}]*)"}/g;
|
|
405
|
+
let match;
|
|
406
|
+
match = nodeLabelRegex.exec(mermaidContent);
|
|
407
|
+
while (match !== null) {
|
|
408
|
+
const label = match[1] || match[2];
|
|
411
409
|
errorMessages.push(
|
|
412
|
-
`
|
|
410
|
+
`Found backticks in Mermaid node label in ${source} at line ${line}: "${label}" - backticks in node labels cause rendering issues in Mermaid diagrams`,
|
|
413
411
|
);
|
|
412
|
+
match = nodeLabelRegex.exec(mermaidContent);
|
|
414
413
|
}
|
|
415
|
-
edgeMatch = edgeDescriptionRegex.exec(mermaidContent);
|
|
416
|
-
}
|
|
417
414
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
);
|
|
415
|
+
// Check for numbered list format in edge descriptions
|
|
416
|
+
const edgeDescriptionRegex = /--\s*"([^"]*)"\s*-->/g;
|
|
417
|
+
let edgeMatch;
|
|
418
|
+
edgeMatch = edgeDescriptionRegex.exec(mermaidContent);
|
|
419
|
+
while (edgeMatch !== null) {
|
|
420
|
+
const description = edgeMatch[1];
|
|
421
|
+
if (/^\d+\.\s/.test(description)) {
|
|
422
|
+
errorMessages.push(
|
|
423
|
+
`Unsupported markdown: list - Found numbered list format in Mermaid edge description in ${source} at line ${line}: "${description}" - numbered lists in edge descriptions are not supported`,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
edgeMatch = edgeDescriptionRegex.exec(mermaidContent);
|
|
430
427
|
}
|
|
431
|
-
numberMatch = nodeLabelWithNumberRegex.exec(mermaidContent);
|
|
432
|
-
}
|
|
433
428
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
if (!/^".*"$/.test(label)) {
|
|
444
|
-
// List of characters that typically need quoting
|
|
445
|
-
const specialChars = ["(", ")", "{", "}", ":", ";", ",", "-", "."];
|
|
446
|
-
const foundSpecialChars = specialChars.filter((char) => label.includes(char));
|
|
447
|
-
|
|
448
|
-
if (foundSpecialChars.length > 0) {
|
|
429
|
+
// Check for numbered list format in node labels (for both [] and {} syntax)
|
|
430
|
+
const nodeLabelWithNumberRegex =
|
|
431
|
+
/[A-Za-z0-9_]+\["([^"]*\d+\.\s[^"]*)"\]|[A-Za-z0-9_]+{"([^}]*\d+\.\s[^}]*)"}/g;
|
|
432
|
+
let numberMatch;
|
|
433
|
+
numberMatch = nodeLabelWithNumberRegex.exec(mermaidContent);
|
|
434
|
+
while (numberMatch !== null) {
|
|
435
|
+
const label = numberMatch[1] || numberMatch[2];
|
|
436
|
+
// Check if the label contains numbered list format
|
|
437
|
+
if (/\d+\.\s/.test(label)) {
|
|
449
438
|
errorMessages.push(
|
|
450
|
-
`Found
|
|
451
|
-
", ",
|
|
452
|
-
)} - node labels with special characters should be quoted like ${nodeId}["${label}"]`,
|
|
439
|
+
`Unsupported markdown: list - Found numbered list format in Mermaid node label in ${source} at line ${line}: "${label}" - numbered lists in node labels cause rendering issues`,
|
|
453
440
|
);
|
|
454
441
|
}
|
|
442
|
+
numberMatch = nodeLabelWithNumberRegex.exec(mermaidContent);
|
|
455
443
|
}
|
|
444
|
+
|
|
445
|
+
// Check for special characters in node labels that should be quoted
|
|
446
|
+
const nodeWithSpecialCharsRegex = /([A-Za-z0-9_]+)\[([^\]]*[(){}:;,\-\s.][^\]]*)\]/g;
|
|
447
|
+
let specialCharMatch;
|
|
456
448
|
specialCharMatch = nodeWithSpecialCharsRegex.exec(mermaidContent);
|
|
449
|
+
while (specialCharMatch !== null) {
|
|
450
|
+
const nodeId = specialCharMatch[1];
|
|
451
|
+
const label = specialCharMatch[2];
|
|
452
|
+
|
|
453
|
+
// Check if label contains special characters but is not quoted
|
|
454
|
+
if (!/^".*"$/.test(label)) {
|
|
455
|
+
// List of characters that typically need quoting
|
|
456
|
+
const specialChars = ["(", ")", "{", "}", ":", ";", ",", "-", "."];
|
|
457
|
+
const foundSpecialChars = specialChars.filter((char) => label.includes(char));
|
|
458
|
+
|
|
459
|
+
if (foundSpecialChars.length > 0) {
|
|
460
|
+
errorMessages.push(
|
|
461
|
+
`Found unquoted special characters in Mermaid node label in ${source} at line ${line}: "${label}" contains ${foundSpecialChars.join(
|
|
462
|
+
", ",
|
|
463
|
+
)} - node labels with special characters should be quoted like ${nodeId}["${label}"]`,
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
specialCharMatch = nodeWithSpecialCharsRegex.exec(mermaidContent);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (node.lang.toLowerCase() === "d2") {
|
|
471
|
+
d2ChecksList.push({
|
|
472
|
+
content: node.value,
|
|
473
|
+
line,
|
|
474
|
+
});
|
|
457
475
|
}
|
|
476
|
+
// TODO: @zhanghan need to check correctness of every code language
|
|
458
477
|
}
|
|
459
478
|
});
|
|
460
479
|
|
|
@@ -505,6 +524,15 @@ export async function checkMarkdown(markdown, source = "content", options = {})
|
|
|
505
524
|
|
|
506
525
|
// Wait for all mermaid checks to complete
|
|
507
526
|
await Promise.all(mermaidChecks);
|
|
527
|
+
await pMap(
|
|
528
|
+
d2ChecksList,
|
|
529
|
+
async ({ content, line }) =>
|
|
530
|
+
checkD2Content({ content }).catch((err) => {
|
|
531
|
+
const errorMessage = err?.message || String(err) || "Unknown d2 syntax error";
|
|
532
|
+
errorMessages.push(`Found D2 syntax error in ${source} at line ${line}: ${errorMessage}`);
|
|
533
|
+
}),
|
|
534
|
+
{ concurrency: KROKI_CONCURRENCY },
|
|
535
|
+
);
|
|
508
536
|
|
|
509
537
|
// Run markdown linting rules
|
|
510
538
|
await processor.run(ast, file);
|