@aigne/doc-smith 0.2.5 → 0.2.8
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/README.md +1 -0
- package/agents/check-detail-result.mjs +13 -139
- package/agents/check-detail.mjs +4 -6
- package/agents/check-structure-plan.mjs +56 -12
- package/agents/detail-generator-and-translate.yaml +7 -1
- package/agents/detail-regenerator.yaml +3 -1
- package/agents/docs-generator.yaml +2 -1
- package/agents/find-item-by-path.mjs +64 -15
- package/agents/input-generator.mjs +31 -11
- package/agents/language-selector.mjs +89 -0
- package/agents/load-config.mjs +2 -2
- package/agents/load-sources.mjs +13 -40
- package/agents/publish-docs.mjs +47 -161
- package/agents/retranslate.yaml +74 -0
- package/agents/save-docs.mjs +19 -21
- package/agents/save-output.mjs +2 -9
- package/agents/save-single-doc.mjs +20 -1
- package/agents/schema/structure-plan.yaml +1 -1
- package/agents/structure-planning.yaml +6 -0
- package/agents/transform-detail-datasources.mjs +2 -5
- package/agents/translate.yaml +3 -0
- package/aigne.yaml +5 -1
- 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 +20 -7
- package/prompts/check-structure-planning-result.md +4 -7
- package/prompts/content-detail-generator.md +1 -2
- package/prompts/structure-planning.md +7 -2
- package/prompts/translator.md +4 -0
- package/tests/check-detail-result.test.mjs +8 -19
- package/tests/load-sources.test.mjs +65 -161
- package/tests/test-all-validation-cases.mjs +741 -0
- package/tests/test-save-docs.mjs +6 -17
- package/utils/constants.mjs +1 -2
- package/utils/markdown-checker.mjs +453 -0
- package/utils/mermaid-validator.mjs +153 -0
- package/utils/mermaid-worker-pool.mjs +250 -0
- package/utils/mermaid-worker.mjs +233 -0
- package/utils/utils.mjs +162 -114
package/tests/test-save-docs.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { mkdir, readdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { dirname } from "node:path";
|
|
5
4
|
import saveDocs from "../agents/save-docs.mjs";
|
|
6
5
|
|
|
7
6
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -81,12 +80,8 @@ async function testSaveDocs() {
|
|
|
81
80
|
"_sidebar.md",
|
|
82
81
|
];
|
|
83
82
|
|
|
84
|
-
const missingFiles = expectedFiles.filter(
|
|
85
|
-
|
|
86
|
-
);
|
|
87
|
-
const extraFiles = remainingFiles.filter(
|
|
88
|
-
(file) => !expectedFiles.includes(file)
|
|
89
|
-
);
|
|
83
|
+
const missingFiles = expectedFiles.filter((file) => !remainingFiles.includes(file));
|
|
84
|
+
const extraFiles = remainingFiles.filter((file) => !expectedFiles.includes(file));
|
|
90
85
|
|
|
91
86
|
if (missingFiles.length === 0 && extraFiles.length === 0) {
|
|
92
87
|
console.log("\n✅ Test passed! All files are as expected.");
|
|
@@ -101,14 +96,8 @@ async function testSaveDocs() {
|
|
|
101
96
|
}
|
|
102
97
|
|
|
103
98
|
// Verify that invalid files were deleted
|
|
104
|
-
const deletedFiles = [
|
|
105
|
-
|
|
106
|
-
"another-old-file.md",
|
|
107
|
-
"old-translation.zh.md",
|
|
108
|
-
];
|
|
109
|
-
const stillExist = deletedFiles.filter((file) =>
|
|
110
|
-
remainingFiles.includes(file)
|
|
111
|
-
);
|
|
99
|
+
const deletedFiles = ["old-file.md", "another-old-file.md", "old-translation.zh.md"];
|
|
100
|
+
const stillExist = deletedFiles.filter((file) => remainingFiles.includes(file));
|
|
112
101
|
|
|
113
102
|
if (stillExist.length === 0) {
|
|
114
103
|
console.log("✅ All invalid files were successfully deleted.");
|
package/utils/constants.mjs
CHANGED
|
@@ -99,8 +99,7 @@ export const DOCUMENT_STYLES = {
|
|
|
99
99
|
// Predefined target audiences
|
|
100
100
|
export const TARGET_AUDIENCES = {
|
|
101
101
|
actionFirst: "Developers, Implementation Engineers, DevOps",
|
|
102
|
-
conceptFirst:
|
|
103
|
-
"Architects, Technical Leads, Developers interested in principles",
|
|
102
|
+
conceptFirst: "Architects, Technical Leads, Developers interested in principles",
|
|
104
103
|
generalUsers: "General Users",
|
|
105
104
|
custom: "Enter your own target audience",
|
|
106
105
|
};
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import remarkGfm from "remark-gfm";
|
|
2
|
+
import remarkLint from "remark-lint";
|
|
3
|
+
import remarkParse from "remark-parse";
|
|
4
|
+
import { unified } from "unified";
|
|
5
|
+
import { visit } from "unist-util-visit";
|
|
6
|
+
import { VFile } from "vfile";
|
|
7
|
+
import { validateMermaidSyntax } from "./mermaid-validator.mjs";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parse table row and count actual columns
|
|
11
|
+
* Properly handles content within cells, including pipes that are part of content
|
|
12
|
+
* @param {string} line - The table row line to analyze
|
|
13
|
+
* @returns {number} - Number of actual table columns
|
|
14
|
+
*/
|
|
15
|
+
function countTableColumns(line) {
|
|
16
|
+
const trimmed = line.trim();
|
|
17
|
+
|
|
18
|
+
// Remove leading and trailing pipes if present
|
|
19
|
+
const content = trimmed.startsWith("|") && trimmed.endsWith("|") ? trimmed.slice(1, -1) : trimmed;
|
|
20
|
+
|
|
21
|
+
if (!content.trim()) {
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const columns = [];
|
|
26
|
+
let currentColumn = "";
|
|
27
|
+
let i = 0;
|
|
28
|
+
let inCode = false;
|
|
29
|
+
|
|
30
|
+
while (i < content.length) {
|
|
31
|
+
const char = content[i];
|
|
32
|
+
const prevChar = i > 0 ? content[i - 1] : "";
|
|
33
|
+
|
|
34
|
+
if (char === "`") {
|
|
35
|
+
// Toggle code span state
|
|
36
|
+
inCode = !inCode;
|
|
37
|
+
currentColumn += char;
|
|
38
|
+
} else if (char === "|" && !inCode && prevChar !== "\\") {
|
|
39
|
+
// This is a column separator (not escaped and not in code)
|
|
40
|
+
columns.push(currentColumn.trim());
|
|
41
|
+
currentColumn = "";
|
|
42
|
+
} else {
|
|
43
|
+
currentColumn += char;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
i++;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Add the last column
|
|
50
|
+
if (currentColumn.length > 0 || content.endsWith("|")) {
|
|
51
|
+
columns.push(currentColumn.trim());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return columns.length;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check for dead links in markdown content
|
|
59
|
+
* @param {string} markdown - The markdown content
|
|
60
|
+
* @param {string} source - Source description for error reporting
|
|
61
|
+
* @param {Set} allowedLinks - Set of allowed links
|
|
62
|
+
* @param {Array} errorMessages - Array to push error messages to
|
|
63
|
+
*/
|
|
64
|
+
function checkDeadLinks(markdown, source, allowedLinks, errorMessages) {
|
|
65
|
+
const linkRegex = /(?<!!)\[([^\]]+)\]\(([^)]+)\)/g;
|
|
66
|
+
let match;
|
|
67
|
+
|
|
68
|
+
while ((match = linkRegex.exec(markdown)) !== null) {
|
|
69
|
+
const link = match[2];
|
|
70
|
+
const trimLink = link.trim();
|
|
71
|
+
|
|
72
|
+
// Only check links that processContent would process
|
|
73
|
+
// Exclude external links and mailto
|
|
74
|
+
if (/^(https?:\/\/|mailto:)/.test(trimLink)) continue;
|
|
75
|
+
|
|
76
|
+
// Preserve anchors
|
|
77
|
+
const [path, _hash] = trimLink.split("#");
|
|
78
|
+
|
|
79
|
+
// Only process relative paths or paths starting with /
|
|
80
|
+
if (!path) continue;
|
|
81
|
+
|
|
82
|
+
// Check if this link is in the allowed links set
|
|
83
|
+
if (!allowedLinks.has(trimLink)) {
|
|
84
|
+
errorMessages.push(
|
|
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`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check content structure and formatting issues
|
|
175
|
+
* @param {string} markdown - The markdown content
|
|
176
|
+
* @param {string} source - Source description for error reporting
|
|
177
|
+
* @param {Array} errorMessages - Array to push error messages to
|
|
178
|
+
*/
|
|
179
|
+
function checkContentStructure(markdown, source, errorMessages) {
|
|
180
|
+
const lines = markdown.split("\n");
|
|
181
|
+
const allCodeBlockRegex = /^\s*```(?:\w+)?$/;
|
|
182
|
+
|
|
183
|
+
// State variables for different checks
|
|
184
|
+
let inCodeBlock = false;
|
|
185
|
+
let inAnyCodeBlock = false;
|
|
186
|
+
let anyCodeBlockStartLine = 0;
|
|
187
|
+
let codeBlockContent = [];
|
|
188
|
+
|
|
189
|
+
for (let i = 0; i < lines.length; i++) {
|
|
190
|
+
const line = lines[i];
|
|
191
|
+
const lineNumber = i + 1;
|
|
192
|
+
|
|
193
|
+
// Check for any code block markers (for incomplete code block detection)
|
|
194
|
+
if (allCodeBlockRegex.test(line)) {
|
|
195
|
+
if (!inAnyCodeBlock) {
|
|
196
|
+
// Starting a new code block
|
|
197
|
+
inAnyCodeBlock = true;
|
|
198
|
+
anyCodeBlockStartLine = lineNumber;
|
|
199
|
+
inCodeBlock = true;
|
|
200
|
+
codeBlockContent = [];
|
|
201
|
+
} else {
|
|
202
|
+
// Ending the code block
|
|
203
|
+
inAnyCodeBlock = false;
|
|
204
|
+
|
|
205
|
+
if (inCodeBlock) {
|
|
206
|
+
// Check code block content for indentation issues
|
|
207
|
+
checkCodeBlockIndentation(codeBlockContent, source, errorMessages);
|
|
208
|
+
inCodeBlock = false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
} else if (inCodeBlock) {
|
|
212
|
+
// Collect code block content for indentation analysis
|
|
213
|
+
codeBlockContent.push({ line, lineNumber });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Check for incomplete code blocks (started but not closed)
|
|
218
|
+
if (inAnyCodeBlock) {
|
|
219
|
+
errorMessages.push(
|
|
220
|
+
`Found incomplete code block in ${source} starting at line ${anyCodeBlockStartLine}: code block opened with \`\`\` but never closed. Please return the complete content`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check single line content (this needs to be done after the loop)
|
|
225
|
+
const newlineCount = (markdown.match(/\n/g) || []).length;
|
|
226
|
+
if (newlineCount === 0 && markdown.trim().length > 0) {
|
|
227
|
+
errorMessages.push(
|
|
228
|
+
`Found single line content in ${source}: content appears to be on only one line, check for missing line breaks`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Check if content ends with proper punctuation (indicating completeness)
|
|
233
|
+
const trimmedText = markdown.trim();
|
|
234
|
+
if (trimmedText.length > 0 && !trimmedText.endsWith(".") && !trimmedText.endsWith("。")) {
|
|
235
|
+
errorMessages.push(
|
|
236
|
+
`Found incomplete content in ${source}: content does not end with proper punctuation (. or 。). Please return the complete content`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Check markdown content for formatting issues and mermaid syntax errors
|
|
243
|
+
* @param {string} markdown - The markdown content to check
|
|
244
|
+
* @param {string} [source] - Source description for error reporting (e.g., "result")
|
|
245
|
+
* @param {Object} [options] - Additional options for validation
|
|
246
|
+
* @param {Array} [options.allowedLinks] - Set of allowed links for link validation
|
|
247
|
+
* @returns {Promise<Array<string>>} - Array of error messages in check-detail-result format
|
|
248
|
+
*/
|
|
249
|
+
export async function checkMarkdown(markdown, source = "content", options = {}) {
|
|
250
|
+
const file = new VFile({ value: markdown, path: source });
|
|
251
|
+
const errorMessages = [];
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
// Extract allowed links from options
|
|
255
|
+
const { allowedLinks } = options;
|
|
256
|
+
|
|
257
|
+
// Create unified processor with markdown parsing and linting
|
|
258
|
+
// Use individual rules instead of presets to have better control
|
|
259
|
+
const processor = unified()
|
|
260
|
+
.use(remarkParse)
|
|
261
|
+
.use(remarkGfm)
|
|
262
|
+
.use(remarkLint)
|
|
263
|
+
// Add specific useful rules, avoiding overly strict formatting ones
|
|
264
|
+
.use(remarkLint, [
|
|
265
|
+
// Content quality rules (keep these)
|
|
266
|
+
"no-duplicate-headings",
|
|
267
|
+
"no-duplicate-definitions",
|
|
268
|
+
"no-unused-definitions",
|
|
269
|
+
"no-undefined-references",
|
|
270
|
+
|
|
271
|
+
// Structural rules (keep these)
|
|
272
|
+
"no-heading-content-indent",
|
|
273
|
+
"no-heading-indent",
|
|
274
|
+
"no-multiple-toplevel-headings",
|
|
275
|
+
|
|
276
|
+
// Link rules (keep these)
|
|
277
|
+
"no-reference-like-url",
|
|
278
|
+
"no-unneeded-full-reference-image",
|
|
279
|
+
"no-unneeded-full-reference-link",
|
|
280
|
+
"code-block-style",
|
|
281
|
+
|
|
282
|
+
// Skip overly strict formatting rules that don't affect rendering:
|
|
283
|
+
// - final-newline (missing newline at end)
|
|
284
|
+
// - list-item-indent (flexible list spacing)
|
|
285
|
+
// - table-cell-padding (flexible table spacing)
|
|
286
|
+
// - emphasis-marker (allow both * and _)
|
|
287
|
+
// - strong-marker (allow both ** and __)
|
|
288
|
+
]);
|
|
289
|
+
|
|
290
|
+
// Parse markdown content to AST
|
|
291
|
+
const ast = processor.parse(file);
|
|
292
|
+
|
|
293
|
+
// 1. Check dead links if allowedLinks is provided
|
|
294
|
+
if (allowedLinks) {
|
|
295
|
+
checkDeadLinks(markdown, source, allowedLinks, errorMessages);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 2. Check content structure and formatting issues
|
|
299
|
+
checkContentStructure(markdown, source, errorMessages);
|
|
300
|
+
|
|
301
|
+
// Check mermaid code blocks and other custom validations
|
|
302
|
+
const mermaidChecks = [];
|
|
303
|
+
visit(ast, "code", (node) => {
|
|
304
|
+
if (node.lang && node.lang.toLowerCase() === "mermaid") {
|
|
305
|
+
// Check for mermaid syntax errors
|
|
306
|
+
mermaidChecks.push(
|
|
307
|
+
validateMermaidSyntax(node.value).catch((error) => {
|
|
308
|
+
const errorMessage = error?.message || String(error) || "Unknown mermaid syntax error";
|
|
309
|
+
|
|
310
|
+
// Format mermaid error in check-detail-result style
|
|
311
|
+
const line = node.position?.start?.line || "unknown";
|
|
312
|
+
errorMessages.push(
|
|
313
|
+
`Found Mermaid syntax error in ${source} at line ${line}: ${errorMessage}`,
|
|
314
|
+
);
|
|
315
|
+
}),
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
// Check for specific mermaid rendering issues
|
|
319
|
+
const mermaidContent = node.value;
|
|
320
|
+
const line = node.position?.start?.line || "unknown";
|
|
321
|
+
|
|
322
|
+
// Check for backticks in node labels
|
|
323
|
+
const nodeLabelRegex = /[A-Za-z0-9_]+\["([^"]*`[^"]*)"\]|[A-Za-z0-9_]+{"([^}]*`[^}]*)"}/g;
|
|
324
|
+
let match;
|
|
325
|
+
while ((match = nodeLabelRegex.exec(mermaidContent)) !== null) {
|
|
326
|
+
const label = match[1] || match[2];
|
|
327
|
+
errorMessages.push(
|
|
328
|
+
`Found backticks in Mermaid node label in ${source} at line ${line}: "${label}" - backticks in node labels cause rendering issues in Mermaid diagrams`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Check for numbered list format in edge descriptions
|
|
333
|
+
const edgeDescriptionRegex = /--\s*"([^"]*)"\s*-->/g;
|
|
334
|
+
let edgeMatch;
|
|
335
|
+
while ((edgeMatch = edgeDescriptionRegex.exec(mermaidContent)) !== null) {
|
|
336
|
+
const description = edgeMatch[1];
|
|
337
|
+
if (/^\d+\.\s/.test(description)) {
|
|
338
|
+
errorMessages.push(
|
|
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`,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Check for special characters in node labels that should be quoted
|
|
345
|
+
const nodeWithSpecialCharsRegex = /([A-Za-z0-9_]+)\[([^\]]*[(){}:;,\-\s.][^\]]*)\]/g;
|
|
346
|
+
let specialCharMatch;
|
|
347
|
+
while ((specialCharMatch = nodeWithSpecialCharsRegex.exec(mermaidContent)) !== null) {
|
|
348
|
+
const nodeId = specialCharMatch[1];
|
|
349
|
+
const label = specialCharMatch[2];
|
|
350
|
+
|
|
351
|
+
// Check if label contains special characters but is not quoted
|
|
352
|
+
if (!/^".*"$/.test(label)) {
|
|
353
|
+
// List of characters that typically need quoting
|
|
354
|
+
const specialChars = ["(", ")", "{", "}", ":", ";", ",", "-", "."];
|
|
355
|
+
const foundSpecialChars = specialChars.filter((char) => label.includes(char));
|
|
356
|
+
|
|
357
|
+
if (foundSpecialChars.length > 0) {
|
|
358
|
+
errorMessages.push(
|
|
359
|
+
`Found unquoted special characters in Mermaid node label in ${source} at line ${line}: "${label}" contains ${foundSpecialChars.join(
|
|
360
|
+
", ",
|
|
361
|
+
)} - node labels with special characters should be quoted like ${nodeId}["${label}"]`,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Check table separators in original text (since AST normalizes them)
|
|
370
|
+
const lines = markdown.split("\n");
|
|
371
|
+
for (let i = 0; i < lines.length; i++) {
|
|
372
|
+
const line = lines[i];
|
|
373
|
+
|
|
374
|
+
// Check for table separator lines (lines with | and -)
|
|
375
|
+
if (/^\s*\|.*-.*\|\s*$/.test(line)) {
|
|
376
|
+
// Count separator columns
|
|
377
|
+
const separatorColumns = countTableColumns(line);
|
|
378
|
+
|
|
379
|
+
// Check if previous line looks like a table header
|
|
380
|
+
if (i > 0) {
|
|
381
|
+
const prevLine = lines[i - 1];
|
|
382
|
+
if (/^\s*\|.*\|\s*$/.test(prevLine)) {
|
|
383
|
+
// Count header columns
|
|
384
|
+
const headerColumns = countTableColumns(prevLine);
|
|
385
|
+
|
|
386
|
+
// Check for column count mismatch
|
|
387
|
+
if (separatorColumns !== headerColumns) {
|
|
388
|
+
errorMessages.push(
|
|
389
|
+
`Found table separator with mismatched column count in ${source} at line ${
|
|
390
|
+
i + 1
|
|
391
|
+
}: separator has ${separatorColumns} columns but header has ${headerColumns} columns - this causes table rendering issues`,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Also check if next line exists and has different column count
|
|
396
|
+
if (i + 1 < lines.length) {
|
|
397
|
+
const nextLine = lines[i + 1];
|
|
398
|
+
if (/^\s*\|.*\|\s*$/.test(nextLine)) {
|
|
399
|
+
const dataColumns = countTableColumns(nextLine);
|
|
400
|
+
if (separatorColumns !== dataColumns) {
|
|
401
|
+
errorMessages.push(
|
|
402
|
+
`Found table data row with mismatched column count in ${source} at line ${
|
|
403
|
+
i + 2
|
|
404
|
+
}: data row has ${dataColumns} columns but separator defines ${separatorColumns} columns - this causes table rendering issues`,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Wait for all mermaid checks to complete
|
|
415
|
+
await Promise.all(mermaidChecks);
|
|
416
|
+
|
|
417
|
+
// Run markdown linting rules
|
|
418
|
+
await processor.run(ast, file);
|
|
419
|
+
|
|
420
|
+
// Format messages in check-detail-result style
|
|
421
|
+
file.messages.forEach((message) => {
|
|
422
|
+
const line = message.line || "unknown";
|
|
423
|
+
const _column = message.column || "unknown";
|
|
424
|
+
const reason = message.reason || "Unknown markdown issue";
|
|
425
|
+
const ruleId = message.ruleId || message.source || "markdown";
|
|
426
|
+
|
|
427
|
+
// Categorize different types of issues
|
|
428
|
+
let errorType = "markdown formatting";
|
|
429
|
+
if (ruleId.includes("table")) {
|
|
430
|
+
errorType = "table";
|
|
431
|
+
} else if (ruleId.includes("code")) {
|
|
432
|
+
errorType = "code block";
|
|
433
|
+
} else if (ruleId.includes("link")) {
|
|
434
|
+
errorType = "link";
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Format error message similar to check-detail-result style
|
|
438
|
+
if (line !== "unknown") {
|
|
439
|
+
errorMessages.push(
|
|
440
|
+
`Found ${errorType} issue in ${source} at line ${line}: ${reason} (${ruleId})`,
|
|
441
|
+
);
|
|
442
|
+
} else {
|
|
443
|
+
errorMessages.push(`Found ${errorType} issue in ${source}: ${reason} (${ruleId})`);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
return errorMessages;
|
|
448
|
+
} catch (error) {
|
|
449
|
+
// Handle any unexpected errors during processing
|
|
450
|
+
errorMessages.push(`Found markdown processing error in ${source}: ${error.message}`);
|
|
451
|
+
return errorMessages;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simplified Mermaid validation using Worker Thread pool
|
|
3
|
+
* Provides concurrent-safe validation with isolated worker environments
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getMermaidWorkerPool, shutdownMermaidWorkerPool } from "./mermaid-worker-pool.mjs";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Worker-based mermaid validation - DEPRECATED but kept for compatibility
|
|
10
|
+
* This function now delegates to the worker pool implementation
|
|
11
|
+
* @param {string} content - Mermaid diagram content
|
|
12
|
+
* @returns {boolean} - True if syntax is valid
|
|
13
|
+
* @throws {Error} - If syntax is invalid
|
|
14
|
+
* @deprecated Use validateMermaidSyntax instead which uses worker pool
|
|
15
|
+
*/
|
|
16
|
+
export async function validateMermaidWithOfficialParser(content) {
|
|
17
|
+
// Delegate to the new worker-based implementation
|
|
18
|
+
return await validateMermaidSyntax(content);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Basic mermaid syntax validation fallback
|
|
23
|
+
* Used when worker validation fails due to environment issues
|
|
24
|
+
* @param {string} content - Mermaid diagram content
|
|
25
|
+
* @returns {boolean} - True if basic validation passes
|
|
26
|
+
* @throws {Error} - If validation fails
|
|
27
|
+
*/
|
|
28
|
+
export function validateBasicMermaidSyntax(content) {
|
|
29
|
+
const trimmedContent = content.trim();
|
|
30
|
+
|
|
31
|
+
if (!trimmedContent) {
|
|
32
|
+
throw new Error("Empty mermaid diagram");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Check for valid diagram type
|
|
36
|
+
const validDiagramTypes = [
|
|
37
|
+
"flowchart",
|
|
38
|
+
"graph",
|
|
39
|
+
"sequenceDiagram",
|
|
40
|
+
"classDiagram",
|
|
41
|
+
"stateDiagram",
|
|
42
|
+
"entityRelationshipDiagram",
|
|
43
|
+
"erDiagram",
|
|
44
|
+
"journey",
|
|
45
|
+
"gantt",
|
|
46
|
+
"pie",
|
|
47
|
+
"requirement",
|
|
48
|
+
"gitgraph",
|
|
49
|
+
"mindmap",
|
|
50
|
+
"timeline",
|
|
51
|
+
"quadrantChart",
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const firstLine = trimmedContent.split("\n")[0].trim();
|
|
55
|
+
const hasValidType = validDiagramTypes.some((type) => firstLine.includes(type));
|
|
56
|
+
|
|
57
|
+
if (!hasValidType) {
|
|
58
|
+
throw new Error("Invalid or missing diagram type");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Basic bracket matching
|
|
62
|
+
const openBrackets = (content.match(/[[{(]/g) || []).length;
|
|
63
|
+
const closeBrackets = (content.match(/[\]})]/g) || []).length;
|
|
64
|
+
|
|
65
|
+
if (openBrackets !== closeBrackets) {
|
|
66
|
+
throw new Error("Unmatched brackets in diagram");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Basic quote matching
|
|
70
|
+
const singleQuotes = (content.match(/'/g) || []).length;
|
|
71
|
+
const doubleQuotes = (content.match(/"/g) || []).length;
|
|
72
|
+
|
|
73
|
+
if (singleQuotes % 2 !== 0) {
|
|
74
|
+
throw new Error("Unmatched single quotes in diagram");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (doubleQuotes % 2 !== 0) {
|
|
78
|
+
throw new Error("Unmatched double quotes in diagram");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Main validation function using simplified worker pool for concurrency safety
|
|
86
|
+
* @param {string} content - Mermaid diagram content
|
|
87
|
+
* @returns {Promise<boolean>} - True if validation passes
|
|
88
|
+
* @throws {Error} - If validation fails
|
|
89
|
+
*/
|
|
90
|
+
export async function validateMermaidSyntax(content) {
|
|
91
|
+
if (!content || !content.trim()) {
|
|
92
|
+
throw new Error("Empty mermaid diagram");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
// Use simplified worker pool for validation
|
|
97
|
+
const workerPool = getMermaidWorkerPool({
|
|
98
|
+
poolSize: 2, // Reduced pool size
|
|
99
|
+
timeout: 10000, // Reduced timeout
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const result = await workerPool.validate(content);
|
|
103
|
+
return result;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
// If worker validation fails, check if it's an environment issue
|
|
106
|
+
const errorMsg = error.message || String(error);
|
|
107
|
+
|
|
108
|
+
if (
|
|
109
|
+
errorMsg.includes("Worker error") ||
|
|
110
|
+
errorMsg.includes("Worker exited") ||
|
|
111
|
+
errorMsg.includes("Worker pool") ||
|
|
112
|
+
errorMsg.includes("timeout") ||
|
|
113
|
+
errorMsg.includes("Cannot resolve module") ||
|
|
114
|
+
errorMsg.includes("window is not defined") ||
|
|
115
|
+
errorMsg.includes("canvas") ||
|
|
116
|
+
errorMsg.includes("Web APIs") ||
|
|
117
|
+
errorMsg.includes("getComputedTextLength") ||
|
|
118
|
+
errorMsg.includes("document is not defined")
|
|
119
|
+
) {
|
|
120
|
+
// Fall back to basic validation for environment issues
|
|
121
|
+
console.warn(
|
|
122
|
+
"Worker-based mermaid validation failed, falling back to basic validation:",
|
|
123
|
+
errorMsg,
|
|
124
|
+
);
|
|
125
|
+
return validateBasicMermaidSyntax(content);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// If it's a genuine syntax error, re-throw it
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get worker pool statistics for monitoring
|
|
135
|
+
* @returns {Object} - Pool statistics
|
|
136
|
+
*/
|
|
137
|
+
export function getValidationStats() {
|
|
138
|
+
try {
|
|
139
|
+
const workerPool = getMermaidWorkerPool();
|
|
140
|
+
return workerPool.getStats();
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return { error: error.message };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Shutdown the validation worker pool
|
|
148
|
+
* Call this when shutting down the application
|
|
149
|
+
* @returns {Promise<void>}
|
|
150
|
+
*/
|
|
151
|
+
export async function shutdownValidation() {
|
|
152
|
+
await shutdownMermaidWorkerPool();
|
|
153
|
+
}
|