@aigne/doc-smith 0.2.6 → 0.2.9
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/CHANGELOG.md +26 -0
- package/agents/check-detail-result.mjs +2 -7
- package/agents/check-detail.mjs +4 -6
- package/agents/check-structure-plan.mjs +5 -10
- package/agents/find-item-by-path.mjs +13 -31
- package/agents/input-generator.mjs +15 -35
- package/agents/language-selector.mjs +6 -18
- package/agents/load-config.mjs +2 -2
- package/agents/load-sources.mjs +29 -117
- package/agents/publish-docs.mjs +15 -28
- package/agents/save-docs.mjs +8 -20
- package/agents/save-output.mjs +2 -9
- package/agents/save-single-doc.mjs +2 -2
- package/agents/schema/structure-plan.yaml +1 -1
- package/agents/transform-detail-datasources.mjs +2 -5
- package/biome.json +13 -3
- package/docs-mcp/get-docs-structure.mjs +1 -1
- package/docs-mcp/read-doc-content.mjs +1 -4
- package/package.json +10 -6
- package/tests/check-detail-result.test.mjs +8 -19
- package/tests/load-sources.test.mjs +65 -161
- package/tests/test-all-validation-cases.mjs +71 -37
- package/tests/test-save-docs.mjs +6 -17
- package/utils/constants.mjs +1 -2
- package/utils/file-utils.mjs +205 -0
- package/utils/markdown-checker.mjs +124 -57
- package/utils/mermaid-validator.mjs +5 -10
- package/utils/mermaid-worker-pool.mjs +7 -11
- package/utils/mermaid-worker.mjs +8 -17
- package/utils/utils.mjs +52 -104
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { glob } from "glob";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check if a directory is inside a git repository using git command
|
|
8
|
+
* @param {string} dir - Directory path to check
|
|
9
|
+
* @returns {boolean} True if inside a git repository
|
|
10
|
+
*/
|
|
11
|
+
function isInGitRepository(dir) {
|
|
12
|
+
try {
|
|
13
|
+
execSync("git rev-parse --is-inside-work-tree", {
|
|
14
|
+
cwd: dir,
|
|
15
|
+
stdio: "pipe",
|
|
16
|
+
encoding: "utf8",
|
|
17
|
+
});
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Find git repository root directory using git command
|
|
26
|
+
* @param {string} startDir - Starting directory path
|
|
27
|
+
* @returns {string|null} Git repository root path or null if not found
|
|
28
|
+
*/
|
|
29
|
+
function findGitRoot(startDir) {
|
|
30
|
+
try {
|
|
31
|
+
const gitRoot = execSync("git rev-parse --show-toplevel", {
|
|
32
|
+
cwd: startDir,
|
|
33
|
+
stdio: "pipe",
|
|
34
|
+
encoding: "utf8",
|
|
35
|
+
}).trim();
|
|
36
|
+
return gitRoot;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Convert gitignore patterns to glob-compatible patterns
|
|
44
|
+
* @param {string} pattern - A single gitignore pattern
|
|
45
|
+
* @returns {string[]} Array of glob patterns that match gitignore behavior
|
|
46
|
+
*/
|
|
47
|
+
function gitignoreToGlobPatterns(pattern) {
|
|
48
|
+
const patterns = [];
|
|
49
|
+
|
|
50
|
+
// Remove leading slash (already handled by gitignore parsing)
|
|
51
|
+
const cleanPattern = pattern.replace(/^\//, "");
|
|
52
|
+
|
|
53
|
+
// If pattern doesn't contain wildcards and doesn't end with /
|
|
54
|
+
// it could match both files and directories
|
|
55
|
+
if (!cleanPattern.includes("*") && !cleanPattern.includes("?") && !cleanPattern.endsWith("/")) {
|
|
56
|
+
// Add patterns to match both file and directory
|
|
57
|
+
patterns.push(cleanPattern); // Exact match
|
|
58
|
+
patterns.push(`${cleanPattern}/**`); // Directory contents
|
|
59
|
+
patterns.push(`**/${cleanPattern}`); // Nested exact match
|
|
60
|
+
patterns.push(`**/${cleanPattern}/**`); // Nested directory contents
|
|
61
|
+
} else if (cleanPattern.endsWith("/")) {
|
|
62
|
+
// Directory-only pattern
|
|
63
|
+
const dirPattern = cleanPattern.slice(0, -1);
|
|
64
|
+
patterns.push(`${dirPattern}/**`);
|
|
65
|
+
patterns.push(`**/${dirPattern}/**`);
|
|
66
|
+
} else {
|
|
67
|
+
// Pattern with wildcards or specific file
|
|
68
|
+
patterns.push(cleanPattern);
|
|
69
|
+
if (!cleanPattern.startsWith("**/")) {
|
|
70
|
+
patterns.push(`**/${cleanPattern}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return patterns;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parse .gitignore content into patterns
|
|
79
|
+
* @param {string} content - .gitignore file content
|
|
80
|
+
* @returns {string[]} Array of ignore patterns converted to glob format
|
|
81
|
+
*/
|
|
82
|
+
function parseGitignoreContent(content) {
|
|
83
|
+
const lines = content
|
|
84
|
+
.split("\n")
|
|
85
|
+
.map((line) => line.trim())
|
|
86
|
+
.filter((line) => line && !line.startsWith("#"))
|
|
87
|
+
.map((line) => line.replace(/^\//, "")); // Remove leading slash
|
|
88
|
+
|
|
89
|
+
// Convert each gitignore pattern to glob patterns
|
|
90
|
+
const allPatterns = [];
|
|
91
|
+
for (const line of lines) {
|
|
92
|
+
allPatterns.push(...gitignoreToGlobPatterns(line));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return [...new Set(allPatterns)]; // Remove duplicates
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Load .gitignore patterns from multiple directories (current + all parent directories up to git root)
|
|
100
|
+
* @param {string} dir - Directory path (will search up to find all .gitignore files)
|
|
101
|
+
* @returns {string[]|null} Array of merged ignore patterns or null if no .gitignore found
|
|
102
|
+
*/
|
|
103
|
+
export async function loadGitignore(dir) {
|
|
104
|
+
// First, check if we're in a git repository
|
|
105
|
+
const inGitRepo = isInGitRepository(dir);
|
|
106
|
+
if (!inGitRepo) {
|
|
107
|
+
// Not in a git repository, just check the current directory
|
|
108
|
+
const gitignorePath = path.join(dir, ".gitignore");
|
|
109
|
+
try {
|
|
110
|
+
await access(gitignorePath);
|
|
111
|
+
const gitignoreContent = await readFile(gitignorePath, "utf8");
|
|
112
|
+
const ignorePatterns = parseGitignoreContent(gitignoreContent);
|
|
113
|
+
return ignorePatterns.length > 0 ? ignorePatterns : null;
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// We're in a git repository, collect all .gitignore files from current dir to git root
|
|
120
|
+
const gitRoot = findGitRoot(dir);
|
|
121
|
+
if (!gitRoot) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const allPatterns = [];
|
|
126
|
+
let currentDir = path.resolve(dir);
|
|
127
|
+
|
|
128
|
+
// Collect .gitignore patterns from current directory up to git root
|
|
129
|
+
while (currentDir.startsWith(gitRoot)) {
|
|
130
|
+
const gitignorePath = path.join(currentDir, ".gitignore");
|
|
131
|
+
try {
|
|
132
|
+
await access(gitignorePath);
|
|
133
|
+
const gitignoreContent = await readFile(gitignorePath, "utf8");
|
|
134
|
+
const patterns = parseGitignoreContent(gitignoreContent);
|
|
135
|
+
|
|
136
|
+
// Add patterns with context of which directory they came from
|
|
137
|
+
// Patterns from deeper directories take precedence
|
|
138
|
+
allPatterns.unshift(...patterns);
|
|
139
|
+
} catch {
|
|
140
|
+
// .gitignore doesn't exist in this directory, continue
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Move up one directory
|
|
144
|
+
if (currentDir === gitRoot) {
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
currentDir = path.dirname(currentDir);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return allPatterns.length > 0 ? [...new Set(allPatterns)] : null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get files using glob patterns
|
|
155
|
+
* @param {string} dir - Directory to search
|
|
156
|
+
* @param {string[]} includePatterns - Include patterns
|
|
157
|
+
* @param {string[]} excludePatterns - Exclude patterns
|
|
158
|
+
* @param {string[]} gitignorePatterns - .gitignore patterns
|
|
159
|
+
* @returns {Promise<string[]>} Array of file paths
|
|
160
|
+
*/
|
|
161
|
+
export async function getFilesWithGlob(dir, includePatterns, excludePatterns, gitignorePatterns) {
|
|
162
|
+
// Prepare all ignore patterns
|
|
163
|
+
const allIgnorePatterns = [];
|
|
164
|
+
|
|
165
|
+
if (excludePatterns) {
|
|
166
|
+
allIgnorePatterns.push(...excludePatterns);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (gitignorePatterns) {
|
|
170
|
+
allIgnorePatterns.push(...gitignorePatterns);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Add default exclusions if not already present
|
|
174
|
+
const defaultExclusions = ["node_modules/**", "test/**", "temp/**"];
|
|
175
|
+
for (const exclusion of defaultExclusions) {
|
|
176
|
+
if (!allIgnorePatterns.includes(exclusion)) {
|
|
177
|
+
allIgnorePatterns.push(exclusion);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Convert patterns to be relative to the directory
|
|
182
|
+
const patterns = includePatterns.map((pattern) => {
|
|
183
|
+
// If pattern doesn't start with / or **, make it relative to dir
|
|
184
|
+
if (!pattern.startsWith("/") && !pattern.startsWith("**")) {
|
|
185
|
+
return `**/${pattern}`; // Use ** to search recursively
|
|
186
|
+
}
|
|
187
|
+
return pattern;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const files = await glob(patterns, {
|
|
192
|
+
cwd: dir,
|
|
193
|
+
ignore: allIgnorePatterns.length > 0 ? allIgnorePatterns : undefined,
|
|
194
|
+
absolute: true,
|
|
195
|
+
nodir: true, // Only return files, not directories
|
|
196
|
+
dot: false, // Don't include dot files by default
|
|
197
|
+
gitignore: true, // Enable .gitignore support
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
return files;
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.warn(`Warning: Error during glob search in ${dir}: ${error.message}`);
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { unified } from "unified";
|
|
2
|
-
import remarkParse from "remark-parse";
|
|
3
1
|
import remarkGfm from "remark-gfm";
|
|
4
2
|
import remarkLint from "remark-lint";
|
|
5
|
-
import
|
|
3
|
+
import remarkParse from "remark-parse";
|
|
4
|
+
import { unified } from "unified";
|
|
6
5
|
import { visit } from "unist-util-visit";
|
|
6
|
+
import { VFile } from "vfile";
|
|
7
7
|
import { validateMermaidSyntax } from "./mermaid-validator.mjs";
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -16,10 +16,7 @@ function countTableColumns(line) {
|
|
|
16
16
|
const trimmed = line.trim();
|
|
17
17
|
|
|
18
18
|
// Remove leading and trailing pipes if present
|
|
19
|
-
const content =
|
|
20
|
-
trimmed.startsWith("|") && trimmed.endsWith("|")
|
|
21
|
-
? trimmed.slice(1, -1)
|
|
22
|
-
: trimmed;
|
|
19
|
+
const content = trimmed.startsWith("|") && trimmed.endsWith("|") ? trimmed.slice(1, -1) : trimmed;
|
|
23
20
|
|
|
24
21
|
if (!content.trim()) {
|
|
25
22
|
return 0;
|
|
@@ -65,7 +62,7 @@ function countTableColumns(line) {
|
|
|
65
62
|
* @param {Array} errorMessages - Array to push error messages to
|
|
66
63
|
*/
|
|
67
64
|
function checkDeadLinks(markdown, source, allowedLinks, errorMessages) {
|
|
68
|
-
const linkRegex = /(
|
|
65
|
+
const linkRegex = /(?<!!)\[([^\]]+)\]\(([^)]+)\)/g;
|
|
69
66
|
let match;
|
|
70
67
|
|
|
71
68
|
while ((match = linkRegex.exec(markdown)) !== null) {
|
|
@@ -77,7 +74,7 @@ function checkDeadLinks(markdown, source, allowedLinks, errorMessages) {
|
|
|
77
74
|
if (/^(https?:\/\/|mailto:)/.test(trimLink)) continue;
|
|
78
75
|
|
|
79
76
|
// Preserve anchors
|
|
80
|
-
const [path,
|
|
77
|
+
const [path, _hash] = trimLink.split("#");
|
|
81
78
|
|
|
82
79
|
// Only process relative paths or paths starting with /
|
|
83
80
|
if (!path) continue;
|
|
@@ -85,7 +82,89 @@ function checkDeadLinks(markdown, source, allowedLinks, errorMessages) {
|
|
|
85
82
|
// Check if this link is in the allowed links set
|
|
86
83
|
if (!allowedLinks.has(trimLink)) {
|
|
87
84
|
errorMessages.push(
|
|
88
|
-
`Found a dead link in ${source}: [${match[1]}](${trimLink}), ensure the link exists in the structure plan path
|
|
85
|
+
`Found a dead link in ${source}: [${match[1]}](${trimLink}), ensure the link exists in the structure plan path`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check code block content for indentation consistency issues
|
|
93
|
+
* @param {Array} codeBlockContent - Array of {line, lineNumber} objects from the code block
|
|
94
|
+
* @param {string} source - Source description for error reporting
|
|
95
|
+
* @param {Array} errorMessages - Array to push error messages to
|
|
96
|
+
*/
|
|
97
|
+
function checkCodeBlockIndentation(codeBlockContent, source, errorMessages) {
|
|
98
|
+
if (codeBlockContent.length === 0) return;
|
|
99
|
+
|
|
100
|
+
// Filter out empty lines for base indentation calculation
|
|
101
|
+
const nonEmptyLines = codeBlockContent.filter((item) => item.line.trim().length > 0);
|
|
102
|
+
if (nonEmptyLines.length === 0) return;
|
|
103
|
+
|
|
104
|
+
// Find the base indentation from the first meaningful line
|
|
105
|
+
let baseCodeIndent = null;
|
|
106
|
+
const problematicLines = [];
|
|
107
|
+
|
|
108
|
+
for (const item of nonEmptyLines) {
|
|
109
|
+
const { line, lineNumber } = item;
|
|
110
|
+
const match = line.match(/^(\s*)/);
|
|
111
|
+
const currentIndent = match ? match[1].length : 0;
|
|
112
|
+
const trimmedLine = line.trim();
|
|
113
|
+
|
|
114
|
+
// Skip lines that are clearly comments (common pattern: # comment)
|
|
115
|
+
if (trimmedLine.startsWith("#") && !trimmedLine.includes("=") && !trimmedLine.includes("{")) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Establish base indentation from the first meaningful line
|
|
120
|
+
if (baseCodeIndent === null) {
|
|
121
|
+
baseCodeIndent = currentIndent;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check if current line has less indentation than the base
|
|
126
|
+
// This indicates inconsistent indentation that may cause rendering issues
|
|
127
|
+
if (currentIndent < baseCodeIndent && baseCodeIndent > 0) {
|
|
128
|
+
problematicLines.push({
|
|
129
|
+
lineNumber,
|
|
130
|
+
line: line.trimEnd(),
|
|
131
|
+
currentIndent,
|
|
132
|
+
baseIndent: baseCodeIndent,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Report indentation issues if found
|
|
138
|
+
if (problematicLines.length > 0) {
|
|
139
|
+
// Group consecutive issues to avoid spam
|
|
140
|
+
const groupedIssues = [];
|
|
141
|
+
let currentGroup = [problematicLines[0]];
|
|
142
|
+
|
|
143
|
+
for (let i = 1; i < problematicLines.length; i++) {
|
|
144
|
+
const current = problematicLines[i];
|
|
145
|
+
const previous = problematicLines[i - 1];
|
|
146
|
+
|
|
147
|
+
if (current.lineNumber - previous.lineNumber <= 2) {
|
|
148
|
+
currentGroup.push(current);
|
|
149
|
+
} else {
|
|
150
|
+
groupedIssues.push(currentGroup);
|
|
151
|
+
currentGroup = [current];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
groupedIssues.push(currentGroup);
|
|
155
|
+
|
|
156
|
+
// Report the most significant groups
|
|
157
|
+
for (const group of groupedIssues.slice(0, 2)) {
|
|
158
|
+
// Limit to avoid spam
|
|
159
|
+
const firstIssue = group[0];
|
|
160
|
+
const lineNumbers =
|
|
161
|
+
group.length > 1
|
|
162
|
+
? `lines ${group[0].lineNumber}-${group[group.length - 1].lineNumber}`
|
|
163
|
+
: `line ${firstIssue.lineNumber}`;
|
|
164
|
+
|
|
165
|
+
const issue = `inconsistent indentation: ${firstIssue.currentIndent} spaces (base: ${firstIssue.baseIndent} spaces)`;
|
|
166
|
+
errorMessages.push(
|
|
167
|
+
`Found code block with inconsistent indentation in ${source} at ${lineNumbers}: ${issue}. This may cause rendering issues`,
|
|
89
168
|
);
|
|
90
169
|
}
|
|
91
170
|
}
|
|
@@ -103,10 +182,9 @@ function checkContentStructure(markdown, source, errorMessages) {
|
|
|
103
182
|
|
|
104
183
|
// State variables for different checks
|
|
105
184
|
let inCodeBlock = false;
|
|
106
|
-
let codeBlockIndentLevel = 0;
|
|
107
|
-
let codeBlockStartLine = 0;
|
|
108
185
|
let inAnyCodeBlock = false;
|
|
109
186
|
let anyCodeBlockStartLine = 0;
|
|
187
|
+
let codeBlockContent = [];
|
|
110
188
|
|
|
111
189
|
for (let i = 0; i < lines.length; i++) {
|
|
112
190
|
const line = lines[i];
|
|
@@ -118,17 +196,28 @@ function checkContentStructure(markdown, source, errorMessages) {
|
|
|
118
196
|
// Starting a new code block
|
|
119
197
|
inAnyCodeBlock = true;
|
|
120
198
|
anyCodeBlockStartLine = lineNumber;
|
|
199
|
+
inCodeBlock = true;
|
|
200
|
+
codeBlockContent = [];
|
|
121
201
|
} else {
|
|
122
202
|
// Ending the code block
|
|
123
203
|
inAnyCodeBlock = false;
|
|
204
|
+
|
|
205
|
+
if (inCodeBlock) {
|
|
206
|
+
// Check code block content for indentation issues
|
|
207
|
+
checkCodeBlockIndentation(codeBlockContent, source, errorMessages);
|
|
208
|
+
inCodeBlock = false;
|
|
209
|
+
}
|
|
124
210
|
}
|
|
211
|
+
} else if (inCodeBlock) {
|
|
212
|
+
// Collect code block content for indentation analysis
|
|
213
|
+
codeBlockContent.push({ line, lineNumber });
|
|
125
214
|
}
|
|
126
215
|
}
|
|
127
216
|
|
|
128
217
|
// Check for incomplete code blocks (started but not closed)
|
|
129
218
|
if (inAnyCodeBlock) {
|
|
130
219
|
errorMessages.push(
|
|
131
|
-
`Found incomplete code block in ${source} starting at line ${anyCodeBlockStartLine}: code block opened with \`\`\` but never closed. Please return the complete content
|
|
220
|
+
`Found incomplete code block in ${source} starting at line ${anyCodeBlockStartLine}: code block opened with \`\`\` but never closed. Please return the complete content`,
|
|
132
221
|
);
|
|
133
222
|
}
|
|
134
223
|
|
|
@@ -136,19 +225,15 @@ function checkContentStructure(markdown, source, errorMessages) {
|
|
|
136
225
|
const newlineCount = (markdown.match(/\n/g) || []).length;
|
|
137
226
|
if (newlineCount === 0 && markdown.trim().length > 0) {
|
|
138
227
|
errorMessages.push(
|
|
139
|
-
`Found single line content in ${source}: content appears to be on only one line, check for missing line breaks
|
|
228
|
+
`Found single line content in ${source}: content appears to be on only one line, check for missing line breaks`,
|
|
140
229
|
);
|
|
141
230
|
}
|
|
142
231
|
|
|
143
232
|
// Check if content ends with proper punctuation (indicating completeness)
|
|
144
233
|
const trimmedText = markdown.trim();
|
|
145
|
-
if (
|
|
146
|
-
trimmedText.length > 0 &&
|
|
147
|
-
!trimmedText.endsWith(".") &&
|
|
148
|
-
!trimmedText.endsWith("。")
|
|
149
|
-
) {
|
|
234
|
+
if (trimmedText.length > 0 && !trimmedText.endsWith(".") && !trimmedText.endsWith("。")) {
|
|
150
235
|
errorMessages.push(
|
|
151
|
-
`Found incomplete content in ${source}: content does not end with proper punctuation (. or 。). Please return the complete content
|
|
236
|
+
`Found incomplete content in ${source}: content does not end with proper punctuation (. or 。). Please return the complete content`,
|
|
152
237
|
);
|
|
153
238
|
}
|
|
154
239
|
}
|
|
@@ -161,11 +246,7 @@ function checkContentStructure(markdown, source, errorMessages) {
|
|
|
161
246
|
* @param {Array} [options.allowedLinks] - Set of allowed links for link validation
|
|
162
247
|
* @returns {Promise<Array<string>>} - Array of error messages in check-detail-result format
|
|
163
248
|
*/
|
|
164
|
-
export async function checkMarkdown(
|
|
165
|
-
markdown,
|
|
166
|
-
source = "content",
|
|
167
|
-
options = {}
|
|
168
|
-
) {
|
|
249
|
+
export async function checkMarkdown(markdown, source = "content", options = {}) {
|
|
169
250
|
const file = new VFile({ value: markdown, path: source });
|
|
170
251
|
const errorMessages = [];
|
|
171
252
|
|
|
@@ -224,15 +305,14 @@ export async function checkMarkdown(
|
|
|
224
305
|
// Check for mermaid syntax errors
|
|
225
306
|
mermaidChecks.push(
|
|
226
307
|
validateMermaidSyntax(node.value).catch((error) => {
|
|
227
|
-
const errorMessage =
|
|
228
|
-
error?.message || String(error) || "Unknown mermaid syntax error";
|
|
308
|
+
const errorMessage = error?.message || String(error) || "Unknown mermaid syntax error";
|
|
229
309
|
|
|
230
310
|
// Format mermaid error in check-detail-result style
|
|
231
311
|
const line = node.position?.start?.line || "unknown";
|
|
232
312
|
errorMessages.push(
|
|
233
|
-
`Found Mermaid syntax error in ${source} at line ${line}: ${errorMessage}
|
|
313
|
+
`Found Mermaid syntax error in ${source} at line ${line}: ${errorMessage}`,
|
|
234
314
|
);
|
|
235
|
-
})
|
|
315
|
+
}),
|
|
236
316
|
);
|
|
237
317
|
|
|
238
318
|
// Check for specific mermaid rendering issues
|
|
@@ -240,38 +320,31 @@ export async function checkMarkdown(
|
|
|
240
320
|
const line = node.position?.start?.line || "unknown";
|
|
241
321
|
|
|
242
322
|
// Check for backticks in node labels
|
|
243
|
-
const nodeLabelRegex =
|
|
244
|
-
/[A-Za-z0-9_]+\["([^"]*`[^"]*)"\]|[A-Za-z0-9_]+{"([^}]*`[^}]*)"}/g;
|
|
323
|
+
const nodeLabelRegex = /[A-Za-z0-9_]+\["([^"]*`[^"]*)"\]|[A-Za-z0-9_]+{"([^}]*`[^}]*)"}/g;
|
|
245
324
|
let match;
|
|
246
325
|
while ((match = nodeLabelRegex.exec(mermaidContent)) !== null) {
|
|
247
326
|
const label = match[1] || match[2];
|
|
248
327
|
errorMessages.push(
|
|
249
|
-
`Found backticks in Mermaid node label in ${source} at line ${line}: "${label}" - backticks in node labels cause rendering issues in Mermaid diagrams
|
|
328
|
+
`Found backticks in Mermaid node label in ${source} at line ${line}: "${label}" - backticks in node labels cause rendering issues in Mermaid diagrams`,
|
|
250
329
|
);
|
|
251
330
|
}
|
|
252
331
|
|
|
253
332
|
// Check for numbered list format in edge descriptions
|
|
254
333
|
const edgeDescriptionRegex = /--\s*"([^"]*)"\s*-->/g;
|
|
255
334
|
let edgeMatch;
|
|
256
|
-
while (
|
|
257
|
-
(edgeMatch = edgeDescriptionRegex.exec(mermaidContent)) !== null
|
|
258
|
-
) {
|
|
335
|
+
while ((edgeMatch = edgeDescriptionRegex.exec(mermaidContent)) !== null) {
|
|
259
336
|
const description = edgeMatch[1];
|
|
260
337
|
if (/^\d+\.\s/.test(description)) {
|
|
261
338
|
errorMessages.push(
|
|
262
|
-
`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
|
|
339
|
+
`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`,
|
|
263
340
|
);
|
|
264
341
|
}
|
|
265
342
|
}
|
|
266
343
|
|
|
267
344
|
// Check for special characters in node labels that should be quoted
|
|
268
|
-
const nodeWithSpecialCharsRegex =
|
|
269
|
-
/([A-Za-z0-9_]+)\[([^\]]*[(){}:;,\-\s\.][^\]]*)\]/g;
|
|
345
|
+
const nodeWithSpecialCharsRegex = /([A-Za-z0-9_]+)\[([^\]]*[(){}:;,\-\s.][^\]]*)\]/g;
|
|
270
346
|
let specialCharMatch;
|
|
271
|
-
while (
|
|
272
|
-
(specialCharMatch =
|
|
273
|
-
nodeWithSpecialCharsRegex.exec(mermaidContent)) !== null
|
|
274
|
-
) {
|
|
347
|
+
while ((specialCharMatch = nodeWithSpecialCharsRegex.exec(mermaidContent)) !== null) {
|
|
275
348
|
const nodeId = specialCharMatch[1];
|
|
276
349
|
const label = specialCharMatch[2];
|
|
277
350
|
|
|
@@ -279,15 +352,13 @@ export async function checkMarkdown(
|
|
|
279
352
|
if (!/^".*"$/.test(label)) {
|
|
280
353
|
// List of characters that typically need quoting
|
|
281
354
|
const specialChars = ["(", ")", "{", "}", ":", ";", ",", "-", "."];
|
|
282
|
-
const foundSpecialChars = specialChars.filter((char) =>
|
|
283
|
-
label.includes(char)
|
|
284
|
-
);
|
|
355
|
+
const foundSpecialChars = specialChars.filter((char) => label.includes(char));
|
|
285
356
|
|
|
286
357
|
if (foundSpecialChars.length > 0) {
|
|
287
358
|
errorMessages.push(
|
|
288
359
|
`Found unquoted special characters in Mermaid node label in ${source} at line ${line}: "${label}" contains ${foundSpecialChars.join(
|
|
289
|
-
", "
|
|
290
|
-
)} - node labels with special characters should be quoted like ${nodeId}["${label}"]
|
|
360
|
+
", ",
|
|
361
|
+
)} - node labels with special characters should be quoted like ${nodeId}["${label}"]`,
|
|
291
362
|
);
|
|
292
363
|
}
|
|
293
364
|
}
|
|
@@ -317,7 +388,7 @@ export async function checkMarkdown(
|
|
|
317
388
|
errorMessages.push(
|
|
318
389
|
`Found table separator with mismatched column count in ${source} at line ${
|
|
319
390
|
i + 1
|
|
320
|
-
}: separator has ${separatorColumns} columns but header has ${headerColumns} columns - this causes table rendering issues
|
|
391
|
+
}: separator has ${separatorColumns} columns but header has ${headerColumns} columns - this causes table rendering issues`,
|
|
321
392
|
);
|
|
322
393
|
}
|
|
323
394
|
|
|
@@ -330,7 +401,7 @@ export async function checkMarkdown(
|
|
|
330
401
|
errorMessages.push(
|
|
331
402
|
`Found table data row with mismatched column count in ${source} at line ${
|
|
332
403
|
i + 2
|
|
333
|
-
}: data row has ${dataColumns} columns but separator defines ${separatorColumns} columns - this causes table rendering issues
|
|
404
|
+
}: data row has ${dataColumns} columns but separator defines ${separatorColumns} columns - this causes table rendering issues`,
|
|
334
405
|
);
|
|
335
406
|
}
|
|
336
407
|
}
|
|
@@ -349,7 +420,7 @@ export async function checkMarkdown(
|
|
|
349
420
|
// Format messages in check-detail-result style
|
|
350
421
|
file.messages.forEach((message) => {
|
|
351
422
|
const line = message.line || "unknown";
|
|
352
|
-
const
|
|
423
|
+
const _column = message.column || "unknown";
|
|
353
424
|
const reason = message.reason || "Unknown markdown issue";
|
|
354
425
|
const ruleId = message.ruleId || message.source || "markdown";
|
|
355
426
|
|
|
@@ -366,21 +437,17 @@ export async function checkMarkdown(
|
|
|
366
437
|
// Format error message similar to check-detail-result style
|
|
367
438
|
if (line !== "unknown") {
|
|
368
439
|
errorMessages.push(
|
|
369
|
-
`Found ${errorType} issue in ${source} at line ${line}: ${reason} (${ruleId})
|
|
440
|
+
`Found ${errorType} issue in ${source} at line ${line}: ${reason} (${ruleId})`,
|
|
370
441
|
);
|
|
371
442
|
} else {
|
|
372
|
-
errorMessages.push(
|
|
373
|
-
`Found ${errorType} issue in ${source}: ${reason} (${ruleId})`
|
|
374
|
-
);
|
|
443
|
+
errorMessages.push(`Found ${errorType} issue in ${source}: ${reason} (${ruleId})`);
|
|
375
444
|
}
|
|
376
445
|
});
|
|
377
446
|
|
|
378
447
|
return errorMessages;
|
|
379
448
|
} catch (error) {
|
|
380
449
|
// Handle any unexpected errors during processing
|
|
381
|
-
errorMessages.push(
|
|
382
|
-
`Found markdown processing error in ${source}: ${error.message}`
|
|
383
|
-
);
|
|
450
|
+
errorMessages.push(`Found markdown processing error in ${source}: ${error.message}`);
|
|
384
451
|
return errorMessages;
|
|
385
452
|
}
|
|
386
453
|
}
|
|
@@ -3,10 +3,7 @@
|
|
|
3
3
|
* Provides concurrent-safe validation with isolated worker environments
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
getMermaidWorkerPool,
|
|
8
|
-
shutdownMermaidWorkerPool,
|
|
9
|
-
} from "./mermaid-worker-pool.mjs";
|
|
6
|
+
import { getMermaidWorkerPool, shutdownMermaidWorkerPool } from "./mermaid-worker-pool.mjs";
|
|
10
7
|
|
|
11
8
|
/**
|
|
12
9
|
* Worker-based mermaid validation - DEPRECATED but kept for compatibility
|
|
@@ -55,17 +52,15 @@ export function validateBasicMermaidSyntax(content) {
|
|
|
55
52
|
];
|
|
56
53
|
|
|
57
54
|
const firstLine = trimmedContent.split("\n")[0].trim();
|
|
58
|
-
const hasValidType = validDiagramTypes.some((type) =>
|
|
59
|
-
firstLine.includes(type)
|
|
60
|
-
);
|
|
55
|
+
const hasValidType = validDiagramTypes.some((type) => firstLine.includes(type));
|
|
61
56
|
|
|
62
57
|
if (!hasValidType) {
|
|
63
58
|
throw new Error("Invalid or missing diagram type");
|
|
64
59
|
}
|
|
65
60
|
|
|
66
61
|
// Basic bracket matching
|
|
67
|
-
const openBrackets = (content.match(/[
|
|
68
|
-
const closeBrackets = (content.match(/[\]
|
|
62
|
+
const openBrackets = (content.match(/[[{(]/g) || []).length;
|
|
63
|
+
const closeBrackets = (content.match(/[\]})]/g) || []).length;
|
|
69
64
|
|
|
70
65
|
if (openBrackets !== closeBrackets) {
|
|
71
66
|
throw new Error("Unmatched brackets in diagram");
|
|
@@ -125,7 +120,7 @@ export async function validateMermaidSyntax(content) {
|
|
|
125
120
|
// Fall back to basic validation for environment issues
|
|
126
121
|
console.warn(
|
|
127
122
|
"Worker-based mermaid validation failed, falling back to basic validation:",
|
|
128
|
-
errorMsg
|
|
123
|
+
errorMsg,
|
|
129
124
|
);
|
|
130
125
|
return validateBasicMermaidSyntax(content);
|
|
131
126
|
}
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* Manages worker threads for concurrent mermaid validation
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import { fileURLToPath } from "url";
|
|
10
|
-
import {
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { Worker } from "node:worker_threads";
|
|
11
11
|
|
|
12
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
13
|
const __dirname = dirname(__filename);
|
|
@@ -51,18 +51,14 @@ class SimpleMermaidWorkerPool {
|
|
|
51
51
|
// Handle worker errors more gracefully
|
|
52
52
|
worker.on("error", (error) => {
|
|
53
53
|
if (worker.currentRequest) {
|
|
54
|
-
worker.currentRequest.reject(
|
|
55
|
-
new Error(`Worker error: ${error.message}`)
|
|
56
|
-
);
|
|
54
|
+
worker.currentRequest.reject(new Error(`Worker error: ${error.message}`));
|
|
57
55
|
worker.currentRequest = null;
|
|
58
56
|
}
|
|
59
57
|
});
|
|
60
58
|
|
|
61
|
-
worker.on("exit", (
|
|
59
|
+
worker.on("exit", (_code) => {
|
|
62
60
|
if (worker.currentRequest) {
|
|
63
|
-
worker.currentRequest.reject(
|
|
64
|
-
new Error("Worker exited unexpectedly")
|
|
65
|
-
);
|
|
61
|
+
worker.currentRequest.reject(new Error("Worker exited unexpectedly"));
|
|
66
62
|
worker.currentRequest = null;
|
|
67
63
|
}
|
|
68
64
|
});
|
|
@@ -212,7 +208,7 @@ class SimpleMermaidWorkerPool {
|
|
|
212
208
|
const terminationPromises = this.workers.map(async (worker) => {
|
|
213
209
|
try {
|
|
214
210
|
await worker.terminate();
|
|
215
|
-
} catch (
|
|
211
|
+
} catch (_error) {
|
|
216
212
|
// Ignore termination errors
|
|
217
213
|
}
|
|
218
214
|
});
|