@aigne/doc-smith 0.2.6 → 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 +19 -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 +2 -2
- package/agents/input-generator.mjs +12 -11
- package/agents/language-selector.mjs +6 -18
- package/agents/load-config.mjs +2 -2
- package/agents/load-sources.mjs +13 -40
- package/agents/publish-docs.mjs +8 -13
- 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/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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { checkMarkdown } from "../utils/markdown-checker.mjs";
|
|
4
3
|
import checkDetailResult from "../agents/check-detail-result.mjs";
|
|
4
|
+
import { checkMarkdown } from "../utils/markdown-checker.mjs";
|
|
5
5
|
import { shutdownValidation } from "../utils/mermaid-validator.mjs";
|
|
6
6
|
|
|
7
7
|
// Mock structure plan for link validation
|
|
@@ -164,25 +164,6 @@ This content ends properly.
|
|
|
164
164
|
},
|
|
165
165
|
|
|
166
166
|
// ========== CODE BLOCK VALIDATION CASES ==========
|
|
167
|
-
{
|
|
168
|
-
category: "💻 CODE BLOCK VALIDATION",
|
|
169
|
-
name: "Inconsistent code block indentation",
|
|
170
|
-
expectPass: false,
|
|
171
|
-
expectedErrors: ["code block with inconsistent indentation"],
|
|
172
|
-
content: `# Test Document
|
|
173
|
-
|
|
174
|
-
Here's incorrectly indented code:
|
|
175
|
-
|
|
176
|
-
\`\`\`javascript
|
|
177
|
-
function test() {
|
|
178
|
-
return "content not properly indented";
|
|
179
|
-
}
|
|
180
|
-
\`\`\`
|
|
181
|
-
|
|
182
|
-
This content ends properly.
|
|
183
|
-
`,
|
|
184
|
-
},
|
|
185
|
-
|
|
186
167
|
{
|
|
187
168
|
category: "💻 CODE BLOCK VALIDATION",
|
|
188
169
|
name: "Incomplete code block",
|
|
@@ -216,6 +197,68 @@ This content ends properly.
|
|
|
216
197
|
`,
|
|
217
198
|
},
|
|
218
199
|
|
|
200
|
+
{
|
|
201
|
+
category: "💻 CODE BLOCK VALIDATION",
|
|
202
|
+
name: "Code block with inconsistent indentation (user case)",
|
|
203
|
+
expectPass: false,
|
|
204
|
+
expectedErrors: ["code block with inconsistent indentation"],
|
|
205
|
+
content: `# API Response Handling
|
|
206
|
+
|
|
207
|
+
You can retrieve the response body in various formats:
|
|
208
|
+
|
|
209
|
+
* **\`response.content\`**: Accesses the raw response body as bytes. This is useful for non-text data like images or binary files.
|
|
210
|
+
\`\`\`python
|
|
211
|
+
import requests
|
|
212
|
+
|
|
213
|
+
r = requests.get('https://httpbin.org/image/png')
|
|
214
|
+
print(type(r.content))
|
|
215
|
+
# Expected output: <class 'bytes'>
|
|
216
|
+
\`\`\`
|
|
217
|
+
|
|
218
|
+
* **\`response.text\`**: Accesses the response body as Unicode text. Requests automatically guesses the encoding, or you can explicitly set \`response.encoding\`.
|
|
219
|
+
\`\`\`python
|
|
220
|
+
import requests
|
|
221
|
+
|
|
222
|
+
r = requests.get('https://httpbin.org/get')
|
|
223
|
+
print(type(r.text))
|
|
224
|
+
# Expected output: <class 'str'>
|
|
225
|
+
print(r.text)
|
|
226
|
+
# Expected output: {"args": {}, "headers": ..., "origin": "...", "url": "https://httpbin.org/get"}
|
|
227
|
+
\`\`\`
|
|
228
|
+
|
|
229
|
+
* **\`response.json(**kwargs)\`**: Decodes the response body as JSON into a Python object (dictionary, list, etc.). This method raises \`requests.exceptions.JSONDecodeError\` if the content is not valid JSON.
|
|
230
|
+
\`\`\`python
|
|
231
|
+
import requests
|
|
232
|
+
|
|
233
|
+
r = requests.get('https://httpbin.org/json')
|
|
234
|
+
print(type(r.json()))
|
|
235
|
+
# Expected output: <class 'dict'>
|
|
236
|
+
print(r.json())
|
|
237
|
+
# Expected output: {'slideshow': {'author': 'Yours Truly', 'date': 'date of publication', 'slides': [...], 'title': 'Sample Slide Show'}}
|
|
238
|
+
\`\`\`
|
|
239
|
+
|
|
240
|
+
**Status and Error Handling**
|
|
241
|
+
|
|
242
|
+
* **\`response.ok\`**: A boolean property that returns \`True\` if \`status_code\` is less than 400, indicating no client or server error. It does *not* necessarily mean \`200 OK\`.
|
|
243
|
+
|
|
244
|
+
* **\`response.raise_for_status()\`**: Raises an \`HTTPError\` if the HTTP request returned an unsuccessful status code (4xx or 5xx). This is a convenient way to check for errors and is typically used after a request to ensure it was successful.
|
|
245
|
+
|
|
246
|
+
\`\`\`python
|
|
247
|
+
import requests
|
|
248
|
+
from requests.exceptions import HTTPError
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
r = requests.get('https://httpbin.org/status/404')
|
|
252
|
+
r.raise_for_status() # This will raise an HTTPError for 404
|
|
253
|
+
except HTTPError as e:
|
|
254
|
+
print(f"HTTP Error occurred: {e}")
|
|
255
|
+
# Expected output: HTTP Error occurred: 404 Client Error: NOT FOUND for url: https://httpbin.org/status/404
|
|
256
|
+
\`\`\`
|
|
257
|
+
|
|
258
|
+
This document ends properly.
|
|
259
|
+
`,
|
|
260
|
+
},
|
|
261
|
+
|
|
219
262
|
// ========== CONTENT STRUCTURE CASES ==========
|
|
220
263
|
{
|
|
221
264
|
category: "📝 CONTENT STRUCTURE",
|
|
@@ -598,11 +641,8 @@ async function runValidationTests() {
|
|
|
598
641
|
|
|
599
642
|
// Check if expected error types are present
|
|
600
643
|
if (testCase.expectedErrors) {
|
|
601
|
-
const foundExpectedErrors = testCase.expectedErrors.every(
|
|
602
|
-
(
|
|
603
|
-
errors.some((error) =>
|
|
604
|
-
error.toLowerCase().includes(expectedError.toLowerCase())
|
|
605
|
-
)
|
|
644
|
+
const foundExpectedErrors = testCase.expectedErrors.every((expectedError) =>
|
|
645
|
+
errors.some((error) => error.toLowerCase().includes(expectedError.toLowerCase())),
|
|
606
646
|
);
|
|
607
647
|
|
|
608
648
|
if (foundExpectedErrors) {
|
|
@@ -618,7 +658,7 @@ async function runValidationTests() {
|
|
|
618
658
|
console.log(
|
|
619
659
|
`❌ FAIL - Expected ${expectPass ? "PASS" : "FAIL"} but got ${
|
|
620
660
|
hasErrors ? "FAIL" : "PASS"
|
|
621
|
-
}
|
|
661
|
+
}`,
|
|
622
662
|
);
|
|
623
663
|
failedTests++;
|
|
624
664
|
}
|
|
@@ -643,7 +683,7 @@ async function runValidationTests() {
|
|
|
643
683
|
console.log("✅ Direct call and wrapper consistent");
|
|
644
684
|
} else {
|
|
645
685
|
console.log(
|
|
646
|
-
`⚠️ Inconsistent results: direct=${errors.length}, wrapper=${wrapperErrors.length}
|
|
686
|
+
`⚠️ Inconsistent results: direct=${errors.length}, wrapper=${wrapperErrors.length}`,
|
|
647
687
|
);
|
|
648
688
|
}
|
|
649
689
|
} catch (error) {
|
|
@@ -653,15 +693,13 @@ async function runValidationTests() {
|
|
|
653
693
|
}
|
|
654
694
|
|
|
655
695
|
// Final summary
|
|
656
|
-
console.log(
|
|
696
|
+
console.log(`\n${"=".repeat(80)}`);
|
|
657
697
|
console.log("📊 TEST SUMMARY");
|
|
658
698
|
console.log("=".repeat(80));
|
|
659
699
|
console.log(`Total Tests: ${totalTests}`);
|
|
660
700
|
console.log(`Passed: ${passedTests} ✅`);
|
|
661
701
|
console.log(`Failed: ${failedTests} ❌`);
|
|
662
|
-
console.log(
|
|
663
|
-
`Success Rate: ${((passedTests / totalTests) * 100).toFixed(1)}%`
|
|
664
|
-
);
|
|
702
|
+
console.log(`Success Rate: ${((passedTests / totalTests) * 100).toFixed(1)}%`);
|
|
665
703
|
|
|
666
704
|
console.log("\n🔍 VALIDATION COVERAGE:");
|
|
667
705
|
console.log("✅ Link validation (dead links, allowed links)");
|
|
@@ -674,13 +712,9 @@ async function runValidationTests() {
|
|
|
674
712
|
console.log("✅ Edge cases and error conditions");
|
|
675
713
|
|
|
676
714
|
if (failedTests === 0) {
|
|
677
|
-
console.log(
|
|
678
|
-
"\n🎉 ALL TESTS PASSED! Validation system is working correctly."
|
|
679
|
-
);
|
|
715
|
+
console.log("\n🎉 ALL TESTS PASSED! Validation system is working correctly.");
|
|
680
716
|
} else {
|
|
681
|
-
console.log(
|
|
682
|
-
`\n⚠️ ${failedTests} test(s) failed. Please review the validation logic.`
|
|
683
|
-
);
|
|
717
|
+
console.log(`\n⚠️ ${failedTests} test(s) failed. Please review the validation logic.`);
|
|
684
718
|
}
|
|
685
719
|
|
|
686
720
|
// Shutdown worker pool to ensure clean exit
|
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
|
};
|
|
@@ -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
|
});
|