@aigne/doc-smith 0.8.15-beta.1 → 0.8.15-beta.10

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 (87) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/agents/clear/choose-contents.mjs +4 -4
  3. package/agents/clear/clear-auth-tokens.mjs +8 -8
  4. package/agents/clear/clear-deployment-config.mjs +2 -2
  5. package/agents/clear/clear-document-config.mjs +3 -3
  6. package/agents/clear/clear-document-structure.mjs +10 -10
  7. package/agents/clear/clear-generated-docs.mjs +103 -14
  8. package/agents/clear/clear-media-description.mjs +7 -7
  9. package/agents/evaluate/document-structure.yaml +3 -1
  10. package/agents/evaluate/document.yaml +3 -1
  11. package/agents/evaluate/index.yaml +1 -3
  12. package/agents/generate/check-diagram.mjs +1 -1
  13. package/agents/generate/check-need-generate-structure.mjs +2 -7
  14. package/agents/generate/draw-diagram.yaml +4 -0
  15. package/agents/generate/generate-structure.yaml +117 -65
  16. package/agents/generate/index.yaml +3 -3
  17. package/agents/generate/{merge-d2-diagram.yaml → merge-diagram.yaml} +7 -6
  18. package/agents/generate/update-document-structure.yaml +1 -1
  19. package/agents/generate/user-review-document-structure.mjs +1 -0
  20. package/agents/generate/utils/merge-document-structures.mjs +30 -0
  21. package/agents/init/check.mjs +3 -1
  22. package/agents/init/index.mjs +37 -7
  23. package/agents/media/load-media-description.mjs +12 -24
  24. package/agents/publish/publish-docs.mjs +3 -8
  25. package/agents/schema/document-execution-structure.yaml +1 -1
  26. package/agents/schema/document-structure-item.yaml +23 -0
  27. package/agents/schema/document-structure-refine-item.yaml +20 -0
  28. package/agents/schema/document-structure.yaml +1 -1
  29. package/agents/translate/index.yaml +1 -4
  30. package/agents/translate/record-translation-history.mjs +6 -2
  31. package/agents/translate/translate-multilingual.yaml +1 -1
  32. package/agents/update/batch-generate-document.yaml +1 -1
  33. package/agents/update/batch-update-document.yaml +1 -1
  34. package/agents/update/check-document.mjs +35 -13
  35. package/agents/update/check-generate-diagram.mjs +26 -0
  36. package/agents/update/generate-diagram.yaml +29 -0
  37. package/agents/update/generate-document.yaml +17 -30
  38. package/agents/update/handle-document-update.yaml +10 -1
  39. package/agents/update/save-and-translate-document.mjs +18 -47
  40. package/agents/update/update-document-detail.yaml +2 -1
  41. package/agents/update/update-single-document.yaml +1 -1
  42. package/agents/update/user-review-document.mjs +6 -5
  43. package/agents/utils/choose-docs.mjs +2 -1
  44. package/agents/utils/load-sources.mjs +62 -45
  45. package/agents/utils/{save-docs.mjs → post-generate.mjs} +2 -51
  46. package/agents/utils/save-doc-translation.mjs +27 -0
  47. package/agents/utils/{save-single-doc.mjs → save-doc.mjs} +17 -12
  48. package/agents/utils/save-sidebar.mjs +59 -0
  49. package/agents/utils/{transform-detail-datasources.mjs → transform-detail-data-sources.mjs} +7 -7
  50. package/aigne.yaml +16 -8
  51. package/package.json +2 -1
  52. package/prompts/common/document/content-rules-core.md +6 -6
  53. package/prompts/common/document/media-file-list-usage-rules.md +12 -0
  54. package/prompts/common/document/openapi-usage-rules.md +36 -0
  55. package/prompts/common/document/role-and-personality.md +1 -2
  56. package/prompts/common/document-structure/conflict-resolution-guidance.md +2 -2
  57. package/prompts/common/document-structure/document-structure-rules.md +8 -8
  58. package/prompts/common/document-structure/output-constraints.md +3 -3
  59. package/prompts/detail/custom/custom-components.md +38 -3
  60. package/prompts/detail/d2-diagram/rules.md +11 -14
  61. package/prompts/detail/d2-diagram/system-prompt.md +0 -14
  62. package/prompts/detail/d2-diagram/user-prompt.md +39 -0
  63. package/prompts/detail/generate/document-rules.md +3 -3
  64. package/prompts/detail/generate/system-prompt.md +2 -6
  65. package/prompts/detail/generate/user-prompt.md +20 -61
  66. package/prompts/detail/update/system-prompt.md +2 -6
  67. package/prompts/detail/update/user-prompt.md +7 -6
  68. package/prompts/evaluate/document.md +0 -4
  69. package/prompts/structure/check-document-structure.md +4 -4
  70. package/prompts/structure/generate/system-prompt.md +0 -31
  71. package/prompts/structure/generate/user-prompt.md +68 -29
  72. package/prompts/structure/review/structure-review-system.md +79 -0
  73. package/prompts/structure/update/system-prompt.md +1 -1
  74. package/prompts/structure/update/user-prompt.md +4 -4
  75. package/prompts/translate/code-block.md +13 -3
  76. package/prompts/translate/translate-document.md +1 -1
  77. package/types/document-structure-schema.mjs +3 -3
  78. package/utils/docs-finder-utils.mjs +48 -0
  79. package/utils/extract-api.mjs +32 -0
  80. package/utils/file-utils.mjs +56 -101
  81. package/utils/history-utils.mjs +20 -8
  82. package/utils/load-config.mjs +1 -1
  83. package/utils/markdown-checker.mjs +35 -1
  84. package/utils/utils.mjs +67 -65
  85. package/agents/generate/document-structure-tools/generate-sub-structure.mjs +0 -131
  86. package/agents/generate/generate-structure-without-tools.yaml +0 -65
  87. package/prompts/common/document/media-handling-rules.md +0 -9
@@ -0,0 +1,32 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { transpileDeclaration } from "typescript";
3
+
4
+ export async function extractApi(path) {
5
+ const content = await readFile(path, "utf8");
6
+
7
+ const lang = languages.find((lang) => lang.match(path, content));
8
+ if (lang) {
9
+ return lang.extract(path, content);
10
+ }
11
+
12
+ return content;
13
+ }
14
+
15
+ const languages = [
16
+ {
17
+ match: (path) => /\.m?(js|ts)x?$/.test(path),
18
+ extract: extractJsApi,
19
+ },
20
+ ];
21
+
22
+ async function extractJsApi(_path, content) {
23
+ const res = transpileDeclaration(content, {
24
+ compilerOptions: {
25
+ declaration: true,
26
+ emitDeclarationOnly: true,
27
+ allowJs: true,
28
+ },
29
+ });
30
+
31
+ return res.outputText.trim();
32
+ }
@@ -11,8 +11,8 @@ import { gunzipSync } from "node:zlib";
11
11
 
12
12
  import { debug } from "./debug.mjs";
13
13
  import { isGlobPattern } from "./utils.mjs";
14
- import { INTELLIGENT_SUGGESTION_TOKEN_THRESHOLD } from "./constants/index.mjs";
15
14
  import { uploadFiles } from "./upload-files.mjs";
15
+ import { extractApi } from "./extract-api.mjs";
16
16
 
17
17
  /**
18
18
  * Check if a directory is inside a git repository using git command
@@ -286,7 +286,7 @@ export async function loadFilesFromPaths(sourcesPath, options = {}) {
286
286
  continue;
287
287
  }
288
288
 
289
- if (checkIsRemoteFile(dir)) {
289
+ if (isRemoteFile(dir)) {
290
290
  allFiles.push(dir);
291
291
  continue;
292
292
  }
@@ -387,8 +387,8 @@ export async function loadFilesFromPaths(sourcesPath, options = {}) {
387
387
  * @returns {Promise<boolean>} True if file appears to be a text file
388
388
  */
389
389
  async function isTextFile(filePath) {
390
- if (checkIsRemoteFile(filePath)) {
391
- return checkIsHttpTextFile(filePath);
390
+ if (isRemoteFile(filePath)) {
391
+ return isRemoteTextFile(filePath);
392
392
  }
393
393
 
394
394
  try {
@@ -400,14 +400,42 @@ async function isTextFile(filePath) {
400
400
  }
401
401
  }
402
402
 
403
- export function checkIsRemoteFile(filepath) {
404
- if (filepath.startsWith("http://") || filepath.startsWith("https://")) {
405
- return true;
403
+ /**
404
+ * Check if a string is an HTTP/HTTPS URL
405
+ * @param {string} fileUrl - The string to check
406
+ * @returns {boolean} - True if the string starts with http:// or https://
407
+ */
408
+ export function isRemoteFile(fileUrl) {
409
+ if (typeof fileUrl !== "string") return false;
410
+
411
+ try {
412
+ const url = new URL(fileUrl);
413
+ // Only accept http and https url
414
+ if (["http:", "https:"].includes(url.protocol)) {
415
+ return true;
416
+ }
417
+ // other protocol will be treated as bad url
418
+ return false;
419
+ } catch {
420
+ return false;
406
421
  }
407
- return false;
408
422
  }
409
423
 
410
- export async function checkIsHttpTextFile(fileUrl) {
424
+ export async function isRemoteFileAvailable(fileUrl) {
425
+ if (!isRemoteFile(fileUrl)) return false;
426
+
427
+ try {
428
+ const res = await fetch(fileUrl, {
429
+ method: "HEAD",
430
+ });
431
+ return res.ok;
432
+ } catch (error) {
433
+ debug(`Failed to check HTTP file availability: ${fileUrl} - ${error.message}`);
434
+ return false;
435
+ }
436
+ }
437
+
438
+ export async function isRemoteTextFile(fileUrl) {
411
439
  try {
412
440
  const res = await fetch(fileUrl, {
413
441
  method: "HEAD",
@@ -435,14 +463,14 @@ export async function checkIsHttpTextFile(fileUrl) {
435
463
  }
436
464
  }
437
465
 
438
- export async function getHttpFileContent(file) {
439
- if (!file) return null;
466
+ export async function getRemoteFileContent(fileUrl) {
467
+ if (!fileUrl) return null;
440
468
  try {
441
- const res = await fetch(file);
469
+ const res = await fetch(fileUrl);
442
470
  const text = await res.text();
443
471
  return text;
444
472
  } catch (error) {
445
- debug(`Failed to fetch HTTP file content: ${file} - ${error.message}`);
473
+ debug(`Failed to fetch HTTP file content: ${fileUrl} - ${error.message}`);
446
474
  return null;
447
475
  }
448
476
  }
@@ -469,8 +497,8 @@ export async function readFileContents(files, baseDir = process.cwd(), options =
469
497
  }
470
498
 
471
499
  try {
472
- if (checkIsRemoteFile(file)) {
473
- const content = await getHttpFileContent(file);
500
+ if (isRemoteFile(file)) {
501
+ const content = await getRemoteFileContent(file);
474
502
  if (content) {
475
503
  return {
476
504
  sourceId: file,
@@ -480,7 +508,9 @@ export async function readFileContents(files, baseDir = process.cwd(), options =
480
508
 
481
509
  return null;
482
510
  } else {
483
- const content = await readFile(file, "utf8");
511
+ const content = await extractApi(file);
512
+ if (!content) return null;
513
+
484
514
  const relativePath = path.relative(baseDir, file);
485
515
  return {
486
516
  sourceId: relativePath,
@@ -499,6 +529,11 @@ export async function readFileContents(files, baseDir = process.cwd(), options =
499
529
  return results.filter((result) => result !== null);
500
530
  }
501
531
 
532
+ export function calculateTokens(text) {
533
+ const tokens = encode(text);
534
+ return tokens.length;
535
+ }
536
+
502
537
  /**
503
538
  * Calculate total lines and tokens from file contents
504
539
  * @param {Array<{content: string}>} sourceFiles - Array of objects containing content property
@@ -524,97 +559,17 @@ export function calculateFileStats(sourceFiles) {
524
559
  }
525
560
 
526
561
  /**
527
- * Build sources content string based on context size
528
- * For large contexts, only include core project files to avoid token limit issues
562
+ * Build sources content string
529
563
  * @param {Array<{sourceId: string, content: string}>} sourceFiles - Array of source file objects
530
- * @param {boolean} isLargeContext - Whether the context is large
531
564
  * @returns {string} Concatenated sources content with sourceId comments
532
565
  */
533
- export function buildSourcesContent(sourceFiles, isLargeContext = false) {
534
- // Define core file patterns that represent project structure and key information
535
- const coreFilePatterns = [
536
- // Configuration files
537
- /package\.json$/,
538
- /tsconfig\.json$/,
539
- /jsconfig\.json$/,
540
- /\.env\.example$/,
541
- /Cargo\.toml$/,
542
- /go\.mod$/,
543
- /pom\.xml$/,
544
- /build\.gradle$/,
545
- /Gemfile$/,
546
- /requirements\.txt$/,
547
- /Pipfile$/,
548
- /composer\.json$/,
549
- /pyproject\.toml$/,
550
-
551
- // Documentation
552
- /README\.md$/i,
553
- /CHANGELOG\.md$/i,
554
- /CONTRIBUTING\.md$/i,
555
- /\.github\/.*\.md$/i,
556
-
557
- // Entry points and main files
558
- /index\.(js|ts|jsx|tsx|py|go|rs|java|rb|php)$/,
559
- /main\.(js|ts|jsx|tsx|py|go|rs|java|rb|php)$/,
560
- /app\.(js|ts|jsx|tsx|py)$/,
561
- /server\.(js|ts|jsx|tsx|py)$/,
562
-
563
- // API definitions
564
- /api\/.*\.(js|ts|jsx|tsx|py|go|rs|java|rb|php)$/,
565
- /routes\/.*\.(js|ts|jsx|tsx|py|go|rs|java|rb|php)$/,
566
- /controllers\/.*\.(js|ts|jsx|tsx|py|go|rs|java|rb|php)$/,
567
-
568
- // Type definitions and schemas
569
- /types\.(ts|d\.ts)$/,
570
- /schema\.(js|ts|jsx|tsx|py|go|rs|java|rb|php)$/,
571
- /.*\.d\.ts$/,
572
-
573
- // Core utilities
574
- /utils\/.*\.(js|ts|jsx|tsx|py|go|rs|java|rb|php)$/,
575
- /lib\/.*\.(js|ts|jsx|tsx|py|go|rs|java|rb|php)$/,
576
- /helpers\/.*\.(js|ts|jsx|tsx|py|go|rs|java|rb|php)$/,
577
- ];
578
-
579
- // Function to check if a file is a core file
580
- const isCoreFile = (filePath) => {
581
- return coreFilePatterns.some((pattern) => pattern.test(filePath));
582
- };
583
-
566
+ export function buildSourcesContent(sourceFiles) {
584
567
  // Build sources string
585
568
  let allSources = "";
586
569
 
587
- if (isLargeContext) {
588
- // Only include core files for large contexts
589
- const coreFiles = sourceFiles.filter((source) => isCoreFile(source.sourceId));
590
-
591
- // Determine which files to use and set appropriate message
592
- const filesToInclude = coreFiles.length > 0 ? coreFiles : sourceFiles;
593
- const noteMessage =
594
- coreFiles.length > 0
595
- ? "// Note: Context is large, showing only core project files.\n"
596
- : "// Note: Context is large, showing a sample of files.\n";
597
-
598
- allSources += noteMessage;
599
- let accumulatedTokens = 0;
600
-
601
- for (const source of filesToInclude) {
602
- const fileContent = `// sourceId: ${source.sourceId}\n${source.content}\n`;
603
- const fileTokens = encode(fileContent);
604
-
605
- // Check if adding this file would exceed the token limit
606
- if (accumulatedTokens + fileTokens.length > INTELLIGENT_SUGGESTION_TOKEN_THRESHOLD) {
607
- break;
608
- }
609
-
610
- allSources += fileContent;
611
- accumulatedTokens += fileTokens.length;
612
- }
613
- } else {
614
- // Include all files for normal contexts
615
- for (const source of sourceFiles) {
616
- allSources += `// sourceId: ${source.sourceId}\n${source.content}\n`;
617
- }
570
+ // Include all files for normal contexts
571
+ for (const source of sourceFiles) {
572
+ allSources += `\n// sourceId: ${source.sourceId}\n${source.content}\n`;
618
573
  }
619
574
 
620
575
  return allSources;
@@ -99,7 +99,7 @@ function recordUpdateGit({ feedback }) {
99
99
  /**
100
100
  * Records an update in the YAML file.
101
101
  */
102
- function recordUpdateYaml({ operation, feedback, documentPath = null }) {
102
+ function recordUpdateYaml({ operation, feedback, docPaths = null }) {
103
103
  try {
104
104
  const docSmithDir = join(process.cwd(), DOC_SMITH_DIR);
105
105
  if (!existsSync(docSmithDir)) {
@@ -125,9 +125,9 @@ function recordUpdateYaml({ operation, feedback, documentPath = null }) {
125
125
  feedback,
126
126
  };
127
127
 
128
- // Add document path if provided
129
- if (documentPath) {
130
- entry.documentPath = documentPath;
128
+ // Add document paths if provided
129
+ if (Array.isArray(docPaths) && docPaths.length > 0) {
130
+ entry.docPaths = docPaths;
131
131
  }
132
132
 
133
133
  // Add to beginning (newest first)
@@ -153,14 +153,14 @@ function recordUpdateYaml({ operation, feedback, documentPath = null }) {
153
153
  * @param {Object} params
154
154
  * @param {string} params.operation - The type of operation (e.g., 'document_update', 'structure_update', 'translation_update').
155
155
  * @param {string} params.feedback - The user's feedback text.
156
- * @param {string} params.documentPath - The document path.
156
+ * @param {string[]} [params.docPaths] - Document path list for updates.
157
157
  */
158
- export function recordUpdate({ operation, feedback, documentPath = null }) {
158
+ export function recordUpdate({ operation, feedback, docPaths = null }) {
159
159
  // Skip if no feedback
160
160
  if (!feedback?.trim()) return;
161
161
 
162
162
  // Always record in YAML
163
- recordUpdateYaml({ operation, feedback, documentPath });
163
+ recordUpdateYaml({ operation, feedback, docPaths });
164
164
 
165
165
  // Also record in git if git is available and not in a git repository
166
166
  if (isGitAvailable() && !isInGitRepository(process.cwd())) {
@@ -183,7 +183,19 @@ export function getHistory() {
183
183
 
184
184
  try {
185
185
  const content = readFileSync(historyPath, "utf8");
186
- return parse(content) || { entries: [] };
186
+ const parsed = parse(content) || { entries: [] };
187
+ const entries = Array.isArray(parsed.entries) ? parsed.entries : [];
188
+
189
+ const normalized = entries.map((entry) => {
190
+ if (!entry) return entry;
191
+ // Normalize legacy entries: documentPath -> docPaths: [documentPath]
192
+ if (!entry.docPaths && entry.documentPath) {
193
+ return { ...entry, docPaths: [entry.documentPath] };
194
+ }
195
+ return entry;
196
+ });
197
+
198
+ return { ...parsed, entries: normalized };
187
199
  } catch (error) {
188
200
  console.warn("Could not read the history:", error.message);
189
201
  return { entries: [] };
@@ -28,7 +28,7 @@ export default async function loadConfig({ config, appUrl }) {
28
28
  }
29
29
 
30
30
  // Parse new configuration fields and convert keys to actual content
31
- const processedConfig = processConfigFields(parsedConfig);
31
+ const processedConfig = await processConfigFields(parsedConfig);
32
32
 
33
33
  return {
34
34
  lastGitHead: parsedConfig.lastGitHead || "",
@@ -3,9 +3,12 @@ import path from "node:path";
3
3
  import remarkGfm from "remark-gfm";
4
4
  import remarkLint from "remark-lint";
5
5
  import remarkParse from "remark-parse";
6
+ import { isRelative } from "ufo";
6
7
  import { unified } from "unified";
7
8
  import { visit } from "unist-util-visit";
8
9
  import { VFile } from "vfile";
10
+
11
+ import { isRemoteFile, isRemoteFileAvailable } from "./file-utils.mjs";
9
12
  import { validateMermaidSyntax } from "./mermaid-validator.mjs";
10
13
 
11
14
  /**
@@ -232,6 +235,34 @@ function checkLocalImages(markdown, source, errorMessages, markdownFilePath, bas
232
235
  }
233
236
  }
234
237
 
238
+ async function checkRemoteImages(markdown, source, errorMessages) {
239
+ const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
240
+ let match;
241
+
242
+ while (true) {
243
+ match = imageRegex.exec(markdown);
244
+ if (match === null) break;
245
+ const imagePath = match[2].trim();
246
+ const altText = match[1];
247
+
248
+ if (isRelative(imagePath)) continue;
249
+ if (imagePath.startsWith("/")) continue;
250
+
251
+ // Skip data URLs
252
+ if (/^data:/.test(imagePath)) continue;
253
+
254
+ if (isRemoteFile(imagePath)) {
255
+ const isAvailable = await isRemoteFileAvailable(imagePath);
256
+ if (isAvailable) continue;
257
+ else {
258
+ errorMessages.push(
259
+ `Found invalid remote image in ${source}: ![${altText}](${imagePath}) - only valid media resources can be used`,
260
+ );
261
+ }
262
+ }
263
+ }
264
+ }
265
+
235
266
  /**
236
267
  * Check content structure and formatting issues
237
268
  * @param {string} markdown - The markdown content
@@ -370,7 +401,10 @@ export async function checkMarkdown(markdown, source = "content", options = {})
370
401
  // 2. Check local images existence
371
402
  checkLocalImages(markdown, source, errorMessages, filePath, baseDir);
372
403
 
373
- // 3. Check content structure and formatting issues
404
+ // 3. Check remote images existence
405
+ await checkRemoteImages(markdown, source, errorMessages);
406
+
407
+ // 4. Check content structure and formatting issues
374
408
  checkContentStructure(markdown, source, errorMessages);
375
409
 
376
410
  // Check mermaid code blocks and other custom validations
package/utils/utils.mjs CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  SUPPORTED_LANGUAGES,
19
19
  TARGET_AUDIENCES,
20
20
  } from "./constants/index.mjs";
21
+ import { isRemoteFile, getRemoteFileContent } from "./file-utils.mjs";
21
22
 
22
23
  /**
23
24
  * Normalize path to absolute path for consistent comparison
@@ -47,16 +48,6 @@ export function isGlobPattern(pattern) {
47
48
  return /[*?[\]]|(\*\*)/.test(pattern);
48
49
  }
49
50
 
50
- /**
51
- * Check if a string is an HTTP/HTTPS URL
52
- * @param {string} url - The string to check
53
- * @returns {boolean} - True if the string starts with http:// or https://
54
- */
55
- export function isHttp(url) {
56
- if (typeof url !== "string") return false;
57
- return url.startsWith("http://") || url.startsWith("https://");
58
- }
59
-
60
51
  export function processContent({ content }) {
61
52
  // Match markdown regular links [text](link), exclude images ![text](link)
62
53
  return content.replace(/(?<!!)\[([^\]]+)\]\(([^)]+)\)/g, (match, text, link) => {
@@ -81,77 +72,72 @@ export function processContent({ content }) {
81
72
  });
82
73
  }
83
74
 
75
+ // Helper function to generate filename based on language
76
+ export function getFileName(docPath, language) {
77
+ // Flatten path: remove leading /, replace all / with -
78
+ const flatName = docPath.replace(/^\//, "").replace(/\//g, "-");
79
+ const isEnglish = language === "en";
80
+ return isEnglish ? `${flatName}.md` : `${flatName}.${language}.md`;
81
+ }
82
+
84
83
  /**
85
- * Save a single document and its translations to files
84
+ * Save a single document to files
86
85
  * @param {Object} params
87
86
  * @param {string} params.path - Relative path (without extension)
88
87
  * @param {string} params.content - Main document content
89
88
  * @param {string} params.docsDir - Root directory
90
89
  * @param {string} params.locale - Main content language (e.g., 'en', 'zh', 'fr')
91
- * @param {Array<{language: string, translation: string}>} [params.translates] - Translation content
92
90
  * @param {Array<string>} [params.labels] - Document labels for front matter
93
- * @returns {Promise<Array<{ path: string, success: boolean, error?: string }>>}
91
+ * @returns {Promise<{ path: string, success: boolean, error?: string }>}
94
92
  */
95
- export async function saveDocWithTranslations({
96
- path: docPath,
97
- content,
98
- docsDir,
99
- locale,
100
- translates = [],
101
- labels,
102
- isTranslate = false,
103
- }) {
104
- const results = [];
93
+ export async function saveDoc({ path: docPath, content, docsDir, locale, labels }) {
105
94
  try {
106
- // Flatten path: remove leading /, replace all / with -
107
- const flatName = docPath.replace(/^\//, "").replace(/\//g, "-");
108
95
  await fs.mkdir(docsDir, { recursive: true });
96
+ const mainFileName = getFileName(docPath, locale);
97
+ const mainFilePath = path.join(docsDir, mainFileName);
109
98
 
110
- // Helper function to generate filename based on language
111
- const getFileName = (language) => {
112
- const isEnglish = language === "en";
113
- return isEnglish ? `${flatName}.md` : `${flatName}.${language}.md`;
114
- };
115
-
116
- // Save main content with appropriate filename based on locale (skip if isTranslate is true)
117
- if (!isTranslate) {
118
- const mainFileName = getFileName(locale);
119
- const mainFilePath = path.join(docsDir, mainFileName);
120
-
121
- // Add labels front matter if labels are provided
122
- let finalContent = processContent({ content });
99
+ // Add labels front matter if labels are provided
100
+ let finalContent = processContent({ content });
123
101
 
124
- if (labels && labels.length > 0) {
125
- const frontMatter = `---\nlabels: ${JSON.stringify(labels)}\n---\n\n`;
126
- finalContent = frontMatter + finalContent;
127
- }
128
-
129
- await fs.writeFile(mainFilePath, finalContent, "utf8");
130
- results.push({ path: mainFilePath, success: true });
102
+ if (labels && labels.length > 0) {
103
+ const frontMatter = `---\nlabels: ${JSON.stringify(labels)}\n---\n\n`;
104
+ finalContent = frontMatter + finalContent;
131
105
  }
132
106
 
133
- // Process all translations
134
- for (const translate of translates) {
135
- const translateFileName = getFileName(translate.language);
136
- const translatePath = path.join(docsDir, translateFileName);
107
+ await fs.writeFile(mainFilePath, finalContent, "utf8");
108
+ return { path: mainFilePath, success: true };
109
+ } catch (err) {
110
+ return { path: docPath, success: false, error: err.message };
111
+ }
112
+ }
137
113
 
138
- // Add labels front matter to translation content if labels are provided
139
- let finalTranslationContent = processContent({
140
- content: translate.translation,
141
- });
114
+ export async function saveDocTranslation({
115
+ path: docPath,
116
+ docsDir,
117
+ translation,
118
+ language,
119
+ labels,
120
+ }) {
121
+ try {
122
+ await fs.mkdir(docsDir, { recursive: true });
123
+ const translateFileName = getFileName(docPath, language);
124
+ const translatePath = path.join(docsDir, translateFileName);
142
125
 
143
- if (labels && labels.length > 0) {
144
- const frontMatter = `---\nlabels: ${JSON.stringify(labels)}\n---\n\n`;
145
- finalTranslationContent = frontMatter + finalTranslationContent;
146
- }
126
+ // Add labels front matter to translation content if labels are provided
127
+ let finalTranslationContent = processContent({
128
+ content: translation,
129
+ });
147
130
 
148
- await fs.writeFile(translatePath, finalTranslationContent, "utf8");
149
- results.push({ path: translatePath, success: true });
131
+ if (labels && labels.length > 0) {
132
+ const frontMatter = `---\nlabels: ${JSON.stringify(labels)}\n---\n\n`;
133
+ finalTranslationContent = frontMatter + finalTranslationContent;
150
134
  }
135
+
136
+ await fs.writeFile(translatePath, finalTranslationContent, "utf8");
137
+ return { path: translatePath, success: true };
151
138
  } catch (err) {
152
- results.push({ path: docPath, success: false, error: err.message });
139
+ return { path: docPath, success: false, error: err.message };
153
140
  }
154
- return results;
155
141
  }
156
142
 
157
143
  /**
@@ -963,7 +949,7 @@ export function processTargetAudience(targetAudienceTypes, existingTargetAudienc
963
949
  * @param {Object} config - Parsed configuration
964
950
  * @returns {Object} Processed configuration with content fields
965
951
  */
966
- export function processConfigFields(config) {
952
+ export async function processConfigFields(config) {
967
953
  const processed = {};
968
954
  const allRulesContent = [];
969
955
 
@@ -995,7 +981,15 @@ export function processConfigFields(config) {
995
981
  if (typeof config.rules === "string") {
996
982
  const existingRules = config.rules.trim();
997
983
  if (existingRules) {
998
- allRulesContent.push(existingRules);
984
+ // load rules from remote url
985
+ if (isRemoteFile(existingRules)) {
986
+ const remoteFileContent = await getRemoteFileContent(existingRules);
987
+ if (remoteFileContent) {
988
+ allRulesContent.push(remoteFileContent);
989
+ }
990
+ } else {
991
+ allRulesContent.push(existingRules);
992
+ }
999
993
  }
1000
994
  } else if (Array.isArray(config.rules)) {
1001
995
  // Handle array of rules - join them with newlines
@@ -1054,6 +1048,12 @@ export function processConfigFields(config) {
1054
1048
  }
1055
1049
  }
1056
1050
 
1051
+ if (config.glossary) {
1052
+ if (isRemoteFile(config.glossary)) {
1053
+ processed.glossary = await getRemoteFileContent(config.glossary);
1054
+ }
1055
+ }
1056
+
1057
1057
  // Detect and handle conflicts in user selections
1058
1058
  const conflicts = detectResolvableConflicts(config);
1059
1059
  if (conflicts.length > 0) {
@@ -1097,8 +1097,10 @@ export function processConfigFields(config) {
1097
1097
  * @returns {Promise<any>} - The processed configuration with file content loaded in place of references.
1098
1098
  */
1099
1099
  export async function resolveFileReferences(obj, basePath = process.cwd()) {
1100
- if (typeof obj === "string" && obj.startsWith("@")) {
1101
- return await loadFileContent(obj.slice(1), basePath);
1100
+ if (typeof obj === "string") {
1101
+ if (obj.startsWith("@")) {
1102
+ return await loadFileContent(obj.slice(1), basePath);
1103
+ }
1102
1104
  }
1103
1105
 
1104
1106
  if (Array.isArray(obj)) {