@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +1 -0
  3. package/agents/check-detail-result.mjs +13 -139
  4. package/agents/check-detail.mjs +4 -6
  5. package/agents/check-structure-plan.mjs +56 -12
  6. package/agents/detail-generator-and-translate.yaml +7 -1
  7. package/agents/detail-regenerator.yaml +3 -1
  8. package/agents/docs-generator.yaml +2 -1
  9. package/agents/find-item-by-path.mjs +64 -15
  10. package/agents/input-generator.mjs +31 -11
  11. package/agents/language-selector.mjs +89 -0
  12. package/agents/load-config.mjs +2 -2
  13. package/agents/load-sources.mjs +13 -40
  14. package/agents/publish-docs.mjs +47 -161
  15. package/agents/retranslate.yaml +74 -0
  16. package/agents/save-docs.mjs +19 -21
  17. package/agents/save-output.mjs +2 -9
  18. package/agents/save-single-doc.mjs +20 -1
  19. package/agents/schema/structure-plan.yaml +1 -1
  20. package/agents/structure-planning.yaml +6 -0
  21. package/agents/transform-detail-datasources.mjs +2 -5
  22. package/agents/translate.yaml +3 -0
  23. package/aigne.yaml +5 -1
  24. package/biome.json +13 -3
  25. package/docs-mcp/get-docs-structure.mjs +1 -1
  26. package/docs-mcp/read-doc-content.mjs +1 -4
  27. package/package.json +20 -7
  28. package/prompts/check-structure-planning-result.md +4 -7
  29. package/prompts/content-detail-generator.md +1 -2
  30. package/prompts/structure-planning.md +7 -2
  31. package/prompts/translator.md +4 -0
  32. package/tests/check-detail-result.test.mjs +8 -19
  33. package/tests/load-sources.test.mjs +65 -161
  34. package/tests/test-all-validation-cases.mjs +741 -0
  35. package/tests/test-save-docs.mjs +6 -17
  36. package/utils/constants.mjs +1 -2
  37. package/utils/markdown-checker.mjs +453 -0
  38. package/utils/mermaid-validator.mjs +153 -0
  39. package/utils/mermaid-worker-pool.mjs +250 -0
  40. package/utils/mermaid-worker.mjs +233 -0
  41. package/utils/utils.mjs +162 -114
@@ -1,7 +1,6 @@
1
- import { writeFile, mkdir, readdir } from "node:fs/promises";
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
- (file) => !remainingFiles.includes(file)
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
- "old-file.md",
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.");
@@ -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
+ }