@aigne/doc-smith 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.
Files changed (39) hide show
  1. package/.github/workflows/ci.yml +46 -0
  2. package/.github/workflows/reviewer.yml +2 -1
  3. package/CHANGELOG.md +10 -0
  4. package/agents/chat.yaml +30 -0
  5. package/agents/check-structure-plan.mjs +1 -1
  6. package/agents/docs-fs.yaml +25 -0
  7. package/agents/exit.mjs +6 -0
  8. package/agents/feedback-refiner.yaml +5 -1
  9. package/agents/find-items-by-paths.mjs +10 -4
  10. package/agents/fs.mjs +60 -0
  11. package/agents/input-generator.mjs +159 -90
  12. package/agents/load-config.mjs +0 -5
  13. package/agents/load-sources.mjs +61 -8
  14. package/agents/publish-docs.mjs +27 -12
  15. package/agents/retranslate.yaml +1 -1
  16. package/agents/team-publish-docs.yaml +2 -2
  17. package/aigne.yaml +1 -0
  18. package/package.json +13 -10
  19. package/prompts/content-detail-generator.md +7 -3
  20. package/prompts/document/custom-components.md +80 -0
  21. package/prompts/document/d2-chart/diy-examples.md +44 -0
  22. package/prompts/document/d2-chart/official-examples.md +708 -0
  23. package/prompts/document/d2-chart/rules.md +48 -0
  24. package/prompts/document/detail-generator.md +12 -15
  25. package/prompts/document/structure-planning.md +1 -3
  26. package/prompts/feedback-refiner.md +81 -60
  27. package/prompts/structure-planning.md +20 -3
  28. package/tests/check-detail-result.test.mjs +3 -4
  29. package/tests/conflict-resolution.test.mjs +237 -0
  30. package/tests/input-generator.test.mjs +940 -0
  31. package/tests/load-sources.test.mjs +627 -3
  32. package/tests/preferences-utils.test.mjs +94 -0
  33. package/tests/save-value-to-config.test.mjs +182 -5
  34. package/tests/utils.test.mjs +49 -0
  35. package/utils/conflict-detector.mjs +72 -1
  36. package/utils/constants.mjs +125 -124
  37. package/utils/kroki-utils.mjs +162 -0
  38. package/utils/markdown-checker.mjs +98 -70
  39. package/utils/utils.mjs +96 -28
@@ -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
- documentPurpose: [
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
- conditions: {
488
- documentPurpose: ["getStarted"],
489
- documentationDepth: ["comprehensive"],
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
- conditions: {
500
- documentPurpose: ["solveProblems"],
501
- documentationDepth: ["essentialOnly"],
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
- conditions: {
513
- targetAudienceTypes: ["endUsers"],
514
- documentationDepth: ["comprehensive"],
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
- conditions: {
525
- targetAudienceTypes: ["decisionMakers"],
526
- documentationDepth: ["essentialOnly"],
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
- conditions: {
537
- readerKnowledgeLevel: ["completeBeginners"],
538
- documentationDepth: ["essentialOnly"],
539
- },
540
- severity: "moderate",
541
- reason: "Complete beginners may need more explanations, not just core content",
542
- action: "filter",
543
- conflictingOptions: {
544
- documentationDepth: ["essentialOnly"],
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 `![](../${TMP_ASSETS_DIR}/d2/${fileName})`;
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 ((match = linkRegex.exec(markdown)) !== null) {
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 ((match = imageRegex.exec(markdown)) !== null) {
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 && node.lang.toLowerCase() === "mermaid") {
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
- // Check for backticks in node labels
393
- const nodeLabelRegex = /[A-Za-z0-9_]+\["([^"]*`[^"]*)"\]|[A-Za-z0-9_]+{"([^}]*`[^}]*)"}/g;
394
- let match;
395
- match = nodeLabelRegex.exec(mermaidContent);
396
- while (match !== null) {
397
- const label = match[1] || match[2];
398
- errorMessages.push(
399
- `Found backticks in Mermaid node label in ${source} at line ${line}: "${label}" - backticks in node labels cause rendering issues in Mermaid diagrams`,
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
- // Check for numbered list format in edge descriptions
405
- const edgeDescriptionRegex = /--\s*"([^"]*)"\s*-->/g;
406
- let edgeMatch;
407
- edgeMatch = edgeDescriptionRegex.exec(mermaidContent);
408
- while (edgeMatch !== null) {
409
- const description = edgeMatch[1];
410
- if (/^\d+\.\s/.test(description)) {
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
- `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`,
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
- // Check for numbered list format in node labels (for both [] and {} syntax)
419
- const nodeLabelWithNumberRegex =
420
- /[A-Za-z0-9_]+\["([^"]*\d+\.\s[^"]*)"\]|[A-Za-z0-9_]+{"([^}]*\d+\.\s[^}]*)"}/g;
421
- let numberMatch;
422
- numberMatch = nodeLabelWithNumberRegex.exec(mermaidContent);
423
- while (numberMatch !== null) {
424
- const label = numberMatch[1] || numberMatch[2];
425
- // Check if the label contains numbered list format
426
- if (/\d+\.\s/.test(label)) {
427
- errorMessages.push(
428
- `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`,
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
- // Check for special characters in node labels that should be quoted
435
- const nodeWithSpecialCharsRegex = /([A-Za-z0-9_]+)\[([^\]]*[(){}:;,\-\s.][^\]]*)\]/g;
436
- let specialCharMatch;
437
- specialCharMatch = nodeWithSpecialCharsRegex.exec(mermaidContent);
438
- while (specialCharMatch !== null) {
439
- const nodeId = specialCharMatch[1];
440
- const label = specialCharMatch[2];
441
-
442
- // Check if label contains special characters but is not quoted
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 unquoted special characters in Mermaid node label in ${source} at line ${line}: "${label}" contains ${foundSpecialChars.join(
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);