@aigne/doc-smith 0.2.5 → 0.2.6

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.
@@ -0,0 +1,254 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Simplified Mermaid Worker Pool
5
+ * Manages worker threads for concurrent mermaid validation
6
+ */
7
+
8
+ import { Worker } from "worker_threads";
9
+ import { fileURLToPath } from "url";
10
+ import { dirname, join } from "path";
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = dirname(__filename);
14
+
15
+ class SimpleMermaidWorkerPool {
16
+ constructor(options = {}) {
17
+ this.poolSize = options.poolSize || 3;
18
+ this.timeout = options.timeout || 15000; // Reduced timeout
19
+
20
+ this.workers = [];
21
+ this.availableWorkers = [];
22
+ this.requestQueue = [];
23
+ this.nextRequestId = 1;
24
+ this.isShuttingDown = false;
25
+ }
26
+
27
+ /**
28
+ * Initialize worker pool
29
+ */
30
+ async initialize() {
31
+ if (this.workers.length > 0) return; // Already initialized
32
+
33
+ const workerPath = join(__dirname, "mermaid-worker.mjs");
34
+
35
+ for (let i = 0; i < this.poolSize; i++) {
36
+ await this.createWorker(workerPath, i);
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Create a single worker
42
+ */
43
+ async createWorker(workerPath, workerId) {
44
+ return new Promise((resolve, reject) => {
45
+ try {
46
+ const worker = new Worker(workerPath);
47
+ worker.workerId = workerId;
48
+ worker.isAvailable = true;
49
+ worker.currentRequest = null;
50
+
51
+ // Handle worker errors more gracefully
52
+ worker.on("error", (error) => {
53
+ if (worker.currentRequest) {
54
+ worker.currentRequest.reject(
55
+ new Error(`Worker error: ${error.message}`)
56
+ );
57
+ worker.currentRequest = null;
58
+ }
59
+ });
60
+
61
+ worker.on("exit", (code) => {
62
+ if (worker.currentRequest) {
63
+ worker.currentRequest.reject(
64
+ new Error("Worker exited unexpectedly")
65
+ );
66
+ worker.currentRequest = null;
67
+ }
68
+ });
69
+
70
+ worker.on("message", (data) => {
71
+ this.handleWorkerMessage(worker, data);
72
+ });
73
+
74
+ this.workers.push(worker);
75
+ this.availableWorkers.push(worker);
76
+
77
+ resolve(worker);
78
+ } catch (error) {
79
+ reject(error);
80
+ }
81
+ });
82
+ }
83
+
84
+ /**
85
+ * Handle worker message
86
+ */
87
+ handleWorkerMessage(worker, data) {
88
+ if (!worker.currentRequest) return;
89
+
90
+ const { resolve, reject, timeoutId } = worker.currentRequest;
91
+
92
+ // Clear timeout
93
+ if (timeoutId) {
94
+ clearTimeout(timeoutId);
95
+ }
96
+
97
+ // Reset worker state
98
+ worker.currentRequest = null;
99
+ worker.isAvailable = true;
100
+
101
+ // Move worker back to available pool
102
+ const workerIndex = this.workers.indexOf(worker);
103
+ if (workerIndex > -1 && !this.availableWorkers.includes(worker)) {
104
+ this.availableWorkers.push(worker);
105
+ }
106
+
107
+ // Process queued requests
108
+ this.processQueue();
109
+
110
+ // Handle response
111
+ if (data.error) {
112
+ reject(new Error(data.error));
113
+ } else {
114
+ resolve(data.result);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Process queued requests
120
+ */
121
+ processQueue() {
122
+ while (this.requestQueue.length > 0 && this.availableWorkers.length > 0) {
123
+ const queuedRequest = this.requestQueue.shift();
124
+ const worker = this.availableWorkers.shift();
125
+
126
+ this.executeRequest(worker, queuedRequest);
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Execute a request on a worker
132
+ */
133
+ executeRequest(worker, request) {
134
+ const { content, resolve, reject } = request;
135
+ const requestId = this.nextRequestId++;
136
+
137
+ // Set timeout
138
+ const timeoutId = setTimeout(() => {
139
+ worker.currentRequest = null;
140
+ worker.isAvailable = true;
141
+ if (!this.availableWorkers.includes(worker)) {
142
+ this.availableWorkers.push(worker);
143
+ }
144
+ reject(new Error(`Validation timeout after ${this.timeout}ms`));
145
+ }, this.timeout);
146
+
147
+ // Store request info
148
+ worker.currentRequest = { resolve, reject, timeoutId };
149
+ worker.isAvailable = false;
150
+
151
+ // Send request
152
+ worker.postMessage({
153
+ id: requestId,
154
+ content: content,
155
+ });
156
+ }
157
+
158
+ /**
159
+ * Validate content using worker pool
160
+ */
161
+ async validate(content) {
162
+ if (this.isShuttingDown) {
163
+ throw new Error("Worker pool is shutting down");
164
+ }
165
+
166
+ // Initialize if needed
167
+ await this.initialize();
168
+
169
+ return new Promise((resolve, reject) => {
170
+ const request = { content, resolve, reject };
171
+
172
+ // If worker available, use it immediately
173
+ if (this.availableWorkers.length > 0) {
174
+ const worker = this.availableWorkers.shift();
175
+ this.executeRequest(worker, request);
176
+ } else {
177
+ // Queue the request
178
+ this.requestQueue.push(request);
179
+ }
180
+ });
181
+ }
182
+
183
+ /**
184
+ * Get pool statistics
185
+ */
186
+ getStats() {
187
+ return {
188
+ poolSize: this.poolSize,
189
+ totalWorkers: this.workers.length,
190
+ availableWorkers: this.availableWorkers.length,
191
+ busyWorkers: this.workers.length - this.availableWorkers.length,
192
+ queuedRequests: this.requestQueue.length,
193
+ isShuttingDown: this.isShuttingDown,
194
+ };
195
+ }
196
+
197
+ /**
198
+ * Shutdown the pool
199
+ */
200
+ async shutdown() {
201
+ if (this.isShuttingDown) return;
202
+
203
+ this.isShuttingDown = true;
204
+
205
+ // Reject all queued requests
206
+ while (this.requestQueue.length > 0) {
207
+ const request = this.requestQueue.shift();
208
+ request.reject(new Error("Worker pool is shutting down"));
209
+ }
210
+
211
+ // Terminate all workers
212
+ const terminationPromises = this.workers.map(async (worker) => {
213
+ try {
214
+ await worker.terminate();
215
+ } catch (error) {
216
+ // Ignore termination errors
217
+ }
218
+ });
219
+
220
+ await Promise.allSettled(terminationPromises);
221
+
222
+ // Clear arrays
223
+ this.workers.length = 0;
224
+ this.availableWorkers.length = 0;
225
+ }
226
+ }
227
+
228
+ // Global pool instance
229
+ let globalPool = null;
230
+
231
+ /**
232
+ * Get global worker pool
233
+ */
234
+ export function getMermaidWorkerPool(options = {}) {
235
+ if (!globalPool) {
236
+ globalPool = new SimpleMermaidWorkerPool(options);
237
+ }
238
+ return globalPool;
239
+ }
240
+
241
+ /**
242
+ * Shutdown global pool
243
+ */
244
+ export async function shutdownMermaidWorkerPool() {
245
+ if (globalPool) {
246
+ await globalPool.shutdown();
247
+ globalPool = null;
248
+ }
249
+ }
250
+
251
+ // Note: We don't add global process event listeners here to avoid preventing clean exit
252
+ // The application should call shutdownMermaidWorkerPool() explicitly when needed
253
+
254
+ export { SimpleMermaidWorkerPool };
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Simplified Mermaid Validation Worker
5
+ * Runs in isolated Worker thread to avoid global state conflicts
6
+ */
7
+
8
+ import { parentPort } from "worker_threads";
9
+
10
+ /**
11
+ * Validate mermaid syntax using official parser in isolated environment
12
+ */
13
+ async function validateMermaidWithOfficialParser(content) {
14
+ const trimmedContent = content.trim();
15
+ if (!content || !trimmedContent) {
16
+ throw new Error("Empty mermaid diagram");
17
+ }
18
+
19
+ try {
20
+ // Import dependencies
21
+ const { JSDOM } = await import("jsdom");
22
+ const DOMPurifyModule = await import("dompurify");
23
+
24
+ // Create isolated DOM environment
25
+ const { window } = new JSDOM(`<!DOCTYPE html><html><body></body></html>`, {
26
+ pretendToBeVisual: true,
27
+ resources: "usable",
28
+ });
29
+
30
+ // Setup globals (safe in worker - no conflicts)
31
+ global.window = window;
32
+ global.document = window.document;
33
+
34
+ // Only set navigator if it doesn't exist
35
+ if (!global.navigator) {
36
+ global.navigator = {
37
+ userAgent: "node.js",
38
+ platform: "node",
39
+ cookieEnabled: false,
40
+ onLine: true,
41
+ };
42
+ }
43
+
44
+ global.DOMParser = window.DOMParser;
45
+ global.XMLSerializer = window.XMLSerializer;
46
+ global.HTMLElement = window.HTMLElement;
47
+ global.HTMLDivElement = window.HTMLDivElement;
48
+ global.SVGElement = window.SVGElement;
49
+ global.Element = window.Element;
50
+ global.Node = window.Node;
51
+
52
+ // Initialize DOMPurify with the JSDOM window
53
+ const dompurify = DOMPurifyModule.default(window);
54
+
55
+ // Verify DOMPurify is working before proceeding
56
+ if (typeof dompurify.sanitize !== "function") {
57
+ throw new Error(
58
+ "DOMPurify initialization failed - sanitize method not available"
59
+ );
60
+ }
61
+
62
+ // Test DOMPurify functionality
63
+ dompurify.sanitize("<p>test</p>");
64
+
65
+ // Step 5: Comprehensively set up DOMPurify in all possible global locations
66
+ global.DOMPurify = dompurify;
67
+ window.DOMPurify = dompurify;
68
+
69
+ // For ES module interception, we need to ensure DOMPurify is available
70
+ // in all the ways mermaid might try to access it
71
+ if (typeof globalThis !== "undefined") {
72
+ globalThis.DOMPurify = dompurify;
73
+ }
74
+
75
+ // Set up on the global scope itself
76
+ if (typeof self !== "undefined") {
77
+ self.DOMPurify = dompurify;
78
+ }
79
+
80
+ // CRITICAL: Override the DOMPurify constructor/factory to always use our window
81
+ // This is the key to solving the issue: mermaid imports DOMPurify directly
82
+ const originalDOMPurifyFactory = DOMPurifyModule.default;
83
+ try {
84
+ // This might work: intercept the factory function itself
85
+ if (
86
+ typeof originalDOMPurifyFactory === "function" &&
87
+ !originalDOMPurifyFactory.sanitize
88
+ ) {
89
+ // This means DOMPurify.default is a factory function, not an instance
90
+ // We need to make sure when mermaid calls DOMPurify.sanitize, it works
91
+ const factoryResult = originalDOMPurifyFactory(window);
92
+
93
+ // Copy methods from our working instance to the factory result
94
+ Object.assign(originalDOMPurifyFactory, factoryResult);
95
+ }
96
+ } catch (factoryError) {
97
+ // If factory modification fails, that's OK - we have other fallbacks
98
+ }
99
+
100
+ // Import and setup mermaid
101
+ const mermaid = await import("mermaid");
102
+
103
+ mermaid.default.initialize({
104
+ startOnLoad: false,
105
+ theme: "default",
106
+ securityLevel: "loose",
107
+ htmlLabels: false,
108
+ });
109
+
110
+ // Parse content
111
+ await mermaid.default.parse(trimmedContent);
112
+
113
+ return true;
114
+ } catch (error) {
115
+ const errorMessage = error.message || String(error);
116
+
117
+ // Keep parse errors as-is for useful info
118
+ if (errorMessage.includes("Parse error")) {
119
+ throw new Error(errorMessage);
120
+ }
121
+
122
+ if (errorMessage.includes("Expecting ")) {
123
+ throw new Error(
124
+ "Syntax error: " + errorMessage.replace(/^.*Expecting /, "Expected ")
125
+ );
126
+ }
127
+
128
+ if (errorMessage.includes("Lexical error")) {
129
+ throw new Error("Syntax error: invalid characters or tokens");
130
+ }
131
+
132
+ if (errorMessage.includes("No diagram type detected")) {
133
+ throw new Error("Syntax error: invalid or unrecognized diagram type");
134
+ }
135
+
136
+ throw new Error(errorMessage);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Basic validation fallback
142
+ */
143
+ function validateBasicMermaidSyntax(content) {
144
+ const trimmedContent = content.trim();
145
+
146
+ if (!trimmedContent) {
147
+ throw new Error("Empty mermaid diagram");
148
+ }
149
+
150
+ const validDiagramTypes = [
151
+ "flowchart",
152
+ "graph",
153
+ "sequenceDiagram",
154
+ "classDiagram",
155
+ "stateDiagram",
156
+ "entityRelationshipDiagram",
157
+ "erDiagram",
158
+ "journey",
159
+ "gantt",
160
+ "pie",
161
+ "requirement",
162
+ "gitgraph",
163
+ "mindmap",
164
+ "timeline",
165
+ "quadrantChart",
166
+ ];
167
+
168
+ const firstLine = trimmedContent.split("\n")[0].trim();
169
+ const hasValidType = validDiagramTypes.some((type) =>
170
+ firstLine.includes(type)
171
+ );
172
+
173
+ if (!hasValidType) {
174
+ throw new Error("Invalid or missing diagram type");
175
+ }
176
+
177
+ // Basic bracket matching
178
+ const openBrackets = (content.match(/[\[\{\(]/g) || []).length;
179
+ const closeBrackets = (content.match(/[\]\}\)]/g) || []).length;
180
+
181
+ if (openBrackets !== closeBrackets) {
182
+ throw new Error("Unmatched brackets in diagram");
183
+ }
184
+
185
+ return true;
186
+ }
187
+
188
+ /**
189
+ * Main validation with fallback
190
+ */
191
+ async function validateMermaidSyntax(content) {
192
+ try {
193
+ return await validateMermaidWithOfficialParser(content);
194
+ } catch (officialError) {
195
+ const errorMsg = officialError.message || String(officialError);
196
+
197
+ // Check if it's an environment issue
198
+ if (
199
+ errorMsg.includes("Cannot resolve module") ||
200
+ errorMsg.includes("window is not defined") ||
201
+ errorMsg.includes("canvas") ||
202
+ errorMsg.includes("Web APIs") ||
203
+ errorMsg.includes("getComputedTextLength") ||
204
+ errorMsg.includes("document is not defined") ||
205
+ errorMsg.includes("DOMPurify")
206
+ ) {
207
+ // Fall back to basic validation
208
+ return validateBasicMermaidSyntax(content);
209
+ }
210
+
211
+ // Re-throw syntax errors
212
+ throw officialError;
213
+ }
214
+ }
215
+
216
+ // Worker message handler
217
+ if (parentPort) {
218
+ parentPort.on("message", async (data) => {
219
+ const { id, content } = data;
220
+
221
+ try {
222
+ if (!id || !content) {
223
+ throw new Error("Missing id or content");
224
+ }
225
+
226
+ const result = await validateMermaidSyntax(content);
227
+
228
+ parentPort.postMessage({
229
+ id,
230
+ success: true,
231
+ result,
232
+ });
233
+ } catch (error) {
234
+ parentPort.postMessage({
235
+ id,
236
+ error: error.message || String(error),
237
+ });
238
+ }
239
+ });
240
+ }
241
+
242
+ export { validateMermaidSyntax };
package/utils/utils.mjs CHANGED
@@ -83,6 +83,7 @@ export async function saveDocWithTranslations({
83
83
  locale,
84
84
  translates = [],
85
85
  labels,
86
+ isTranslate = false,
86
87
  }) {
87
88
  const results = [];
88
89
  try {
@@ -96,20 +97,22 @@ export async function saveDocWithTranslations({
96
97
  return isEnglish ? `${flatName}.md` : `${flatName}.${language}.md`;
97
98
  };
98
99
 
99
- // Save main content with appropriate filename based on locale
100
- const mainFileName = getFileName(locale);
101
- const mainFilePath = path.join(docsDir, mainFileName);
100
+ // Save main content with appropriate filename based on locale (skip if isTranslate is true)
101
+ if (!isTranslate) {
102
+ const mainFileName = getFileName(locale);
103
+ const mainFilePath = path.join(docsDir, mainFileName);
102
104
 
103
- // Add labels front matter if labels are provided
104
- let finalContent = processContent({ content });
105
- if (labels && labels.length > 0) {
106
- const frontMatter = `---\nlabels: ${JSON.stringify(labels)}\n---\n\n`;
107
- finalContent = frontMatter + finalContent;
108
- }
105
+ // Add labels front matter if labels are provided
106
+ let finalContent = processContent({ content });
107
+ if (labels && labels.length > 0) {
108
+ const frontMatter = `---\nlabels: ${JSON.stringify(labels)}\n---\n\n`;
109
+ finalContent = frontMatter + finalContent;
110
+ }
109
111
 
110
- await fs.writeFile(mainFilePath, finalContent, "utf8");
111
- results.push({ path: mainFilePath, success: true });
112
- console.log(chalk.green(`Saved: ${chalk.cyan(mainFilePath)}`));
112
+ await fs.writeFile(mainFilePath, finalContent, "utf8");
113
+ results.push({ path: mainFilePath, success: true });
114
+ console.log(chalk.green(`Saved: ${chalk.cyan(mainFilePath)}`));
115
+ }
113
116
 
114
117
  // Process all translations
115
118
  for (const translate of translates) {
@@ -368,10 +371,11 @@ export async function loadConfigFromFile() {
368
371
  * Save value to config.yaml file
369
372
  * @param {string} key - The config key to save
370
373
  * @param {string} value - The value to save
374
+ * @param {string} [comment] - Optional comment to add above the key
371
375
  */
372
- export async function saveValueToConfig(key, value) {
373
- if (!value) {
374
- return; // Skip if no value provided
376
+ export async function saveValueToConfig(key, value, comment) {
377
+ if (value === undefined) {
378
+ return; // Skip if value is undefined
375
379
  }
376
380
 
377
381
  try {
@@ -389,17 +393,38 @@ export async function saveValueToConfig(key, value) {
389
393
  }
390
394
 
391
395
  // Check if key already exists in the file
392
- const keyRegex = new RegExp(`^${key}:\\s*.*$`, "m");
393
- const newKeyLine = `${key}: ${value}`;
396
+ const lines = fileContent.split("\n");
397
+ const keyRegex = new RegExp(`^${key}:\\s*.*$`);
398
+ const newKeyLine = `${key}: "${value}"`;
399
+
400
+ const keyIndex = lines.findIndex((line) => keyRegex.test(line));
394
401
 
395
- if (keyRegex.test(fileContent)) {
402
+ if (keyIndex !== -1) {
396
403
  // Replace existing key line
397
- fileContent = fileContent.replace(keyRegex, newKeyLine);
404
+ lines[keyIndex] = newKeyLine;
405
+ fileContent = lines.join("\n");
406
+
407
+ // Add comment if provided and not already present
408
+ if (
409
+ comment &&
410
+ keyIndex > 0 &&
411
+ !lines[keyIndex - 1].trim().startsWith("# ")
412
+ ) {
413
+ // Add comment above the key if it doesn't already have one
414
+ lines.splice(keyIndex, 0, `# ${comment}`);
415
+ fileContent = lines.join("\n");
416
+ }
398
417
  } else {
399
418
  // Add key to the end of file
400
419
  if (fileContent && !fileContent.endsWith("\n")) {
401
420
  fileContent += "\n";
402
421
  }
422
+
423
+ // Add comment if provided
424
+ if (comment) {
425
+ fileContent += `# ${comment}\n`;
426
+ }
427
+
403
428
  fileContent += newKeyLine + "\n";
404
429
  }
405
430
 
@@ -655,3 +680,78 @@ function getDirectoryContents(dirPath, searchTerm = "") {
655
680
  return [];
656
681
  }
657
682
  }
683
+
684
+ /**
685
+ * Get GitHub repository information
686
+ * @param {string} repoUrl - The repository URL
687
+ * @returns {Promise<Object>} - Repository information
688
+ */
689
+ export async function getGitHubRepoInfo(repoUrl) {
690
+ try {
691
+ // Extract owner and repo from GitHub URL
692
+ const match = repoUrl.match(
693
+ /github\.com[\/:]([^\/]+)\/([^\/]+?)(?:\.git)?$/
694
+ );
695
+ if (!match) return null;
696
+
697
+ const [, owner, repo] = match;
698
+ const apiUrl = `https://api.github.com/repos/${owner}/${repo}`;
699
+
700
+ const response = await fetch(apiUrl);
701
+ if (!response.ok) return null;
702
+
703
+ const data = await response.json();
704
+ return {
705
+ name: data.name,
706
+ description: data.description || "",
707
+ icon: data.owner?.avatar_url || "",
708
+ };
709
+ } catch (error) {
710
+ console.warn("Failed to fetch GitHub repository info:", error.message);
711
+ return null;
712
+ }
713
+ }
714
+
715
+ /**
716
+ * Get project information automatically without user confirmation
717
+ * @returns {Promise<Object>} - Project information including name, description, icon, and fromGitHub flag
718
+ */
719
+ export async function getProjectInfo() {
720
+ let repoInfo = null;
721
+ let defaultName = path.basename(process.cwd());
722
+ let defaultDescription = "";
723
+ let defaultIcon = "";
724
+ let fromGitHub = false;
725
+
726
+ // Check if we're in a git repository
727
+ try {
728
+ const gitRemote = execSync("git remote get-url origin", {
729
+ encoding: "utf8",
730
+ stdio: ["pipe", "pipe", "ignore"],
731
+ }).trim();
732
+
733
+ // Extract repository name from git remote URL
734
+ const repoName = gitRemote.split("/").pop().replace(".git", "");
735
+ defaultName = repoName;
736
+
737
+ // If it's a GitHub repository, try to get additional info
738
+ if (gitRemote.includes("github.com")) {
739
+ repoInfo = await getGitHubRepoInfo(gitRemote);
740
+ if (repoInfo) {
741
+ defaultDescription = repoInfo.description;
742
+ defaultIcon = repoInfo.icon;
743
+ fromGitHub = true;
744
+ }
745
+ }
746
+ } catch (error) {
747
+ // Not in git repository or no origin remote, use current directory name
748
+ console.warn("No git repository found, using current directory name");
749
+ }
750
+
751
+ return {
752
+ name: defaultName,
753
+ description: defaultDescription,
754
+ icon: defaultIcon,
755
+ fromGitHub,
756
+ };
757
+ }