@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.
- package/CHANGELOG.md +76 -0
- package/agents/clear/choose-contents.mjs +4 -4
- package/agents/clear/clear-auth-tokens.mjs +8 -8
- package/agents/clear/clear-deployment-config.mjs +2 -2
- package/agents/clear/clear-document-config.mjs +3 -3
- package/agents/clear/clear-document-structure.mjs +10 -10
- package/agents/clear/clear-generated-docs.mjs +103 -14
- package/agents/clear/clear-media-description.mjs +7 -7
- package/agents/evaluate/document-structure.yaml +3 -1
- package/agents/evaluate/document.yaml +3 -1
- package/agents/evaluate/index.yaml +1 -3
- package/agents/generate/check-diagram.mjs +1 -1
- package/agents/generate/check-need-generate-structure.mjs +2 -7
- package/agents/generate/draw-diagram.yaml +4 -0
- package/agents/generate/generate-structure.yaml +117 -65
- package/agents/generate/index.yaml +3 -3
- package/agents/generate/{merge-d2-diagram.yaml → merge-diagram.yaml} +7 -6
- package/agents/generate/update-document-structure.yaml +1 -1
- package/agents/generate/user-review-document-structure.mjs +1 -0
- package/agents/generate/utils/merge-document-structures.mjs +30 -0
- package/agents/init/check.mjs +3 -1
- package/agents/init/index.mjs +37 -7
- package/agents/media/load-media-description.mjs +12 -24
- package/agents/publish/publish-docs.mjs +3 -8
- package/agents/schema/document-execution-structure.yaml +1 -1
- package/agents/schema/document-structure-item.yaml +23 -0
- package/agents/schema/document-structure-refine-item.yaml +20 -0
- package/agents/schema/document-structure.yaml +1 -1
- package/agents/translate/index.yaml +1 -4
- package/agents/translate/record-translation-history.mjs +6 -2
- package/agents/translate/translate-multilingual.yaml +1 -1
- package/agents/update/batch-generate-document.yaml +1 -1
- package/agents/update/batch-update-document.yaml +1 -1
- package/agents/update/check-document.mjs +35 -13
- package/agents/update/check-generate-diagram.mjs +26 -0
- package/agents/update/generate-diagram.yaml +29 -0
- package/agents/update/generate-document.yaml +17 -30
- package/agents/update/handle-document-update.yaml +10 -1
- package/agents/update/save-and-translate-document.mjs +18 -47
- package/agents/update/update-document-detail.yaml +2 -1
- package/agents/update/update-single-document.yaml +1 -1
- package/agents/update/user-review-document.mjs +6 -5
- package/agents/utils/choose-docs.mjs +2 -1
- package/agents/utils/load-sources.mjs +62 -45
- package/agents/utils/{save-docs.mjs → post-generate.mjs} +2 -51
- package/agents/utils/save-doc-translation.mjs +27 -0
- package/agents/utils/{save-single-doc.mjs → save-doc.mjs} +17 -12
- package/agents/utils/save-sidebar.mjs +59 -0
- package/agents/utils/{transform-detail-datasources.mjs → transform-detail-data-sources.mjs} +7 -7
- package/aigne.yaml +16 -8
- package/package.json +2 -1
- package/prompts/common/document/content-rules-core.md +6 -6
- package/prompts/common/document/media-file-list-usage-rules.md +12 -0
- package/prompts/common/document/openapi-usage-rules.md +36 -0
- package/prompts/common/document/role-and-personality.md +1 -2
- package/prompts/common/document-structure/conflict-resolution-guidance.md +2 -2
- package/prompts/common/document-structure/document-structure-rules.md +8 -8
- package/prompts/common/document-structure/output-constraints.md +3 -3
- package/prompts/detail/custom/custom-components.md +38 -3
- package/prompts/detail/d2-diagram/rules.md +11 -14
- package/prompts/detail/d2-diagram/system-prompt.md +0 -14
- package/prompts/detail/d2-diagram/user-prompt.md +39 -0
- package/prompts/detail/generate/document-rules.md +3 -3
- package/prompts/detail/generate/system-prompt.md +2 -6
- package/prompts/detail/generate/user-prompt.md +20 -61
- package/prompts/detail/update/system-prompt.md +2 -6
- package/prompts/detail/update/user-prompt.md +7 -6
- package/prompts/evaluate/document.md +0 -4
- package/prompts/structure/check-document-structure.md +4 -4
- package/prompts/structure/generate/system-prompt.md +0 -31
- package/prompts/structure/generate/user-prompt.md +68 -29
- package/prompts/structure/review/structure-review-system.md +79 -0
- package/prompts/structure/update/system-prompt.md +1 -1
- package/prompts/structure/update/user-prompt.md +4 -4
- package/prompts/translate/code-block.md +13 -3
- package/prompts/translate/translate-document.md +1 -1
- package/types/document-structure-schema.mjs +3 -3
- package/utils/docs-finder-utils.mjs +48 -0
- package/utils/extract-api.mjs +32 -0
- package/utils/file-utils.mjs +56 -101
- package/utils/history-utils.mjs +20 -8
- package/utils/load-config.mjs +1 -1
- package/utils/markdown-checker.mjs +35 -1
- package/utils/utils.mjs +67 -65
- package/agents/generate/document-structure-tools/generate-sub-structure.mjs +0 -131
- package/agents/generate/generate-structure-without-tools.yaml +0 -65
- 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
|
+
}
|
package/utils/file-utils.mjs
CHANGED
|
@@ -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 (
|
|
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 (
|
|
391
|
-
return
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
|
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
|
|
439
|
-
if (!
|
|
466
|
+
export async function getRemoteFileContent(fileUrl) {
|
|
467
|
+
if (!fileUrl) return null;
|
|
440
468
|
try {
|
|
441
|
-
const res = await fetch(
|
|
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: ${
|
|
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 (
|
|
473
|
-
const content = await
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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;
|
package/utils/history-utils.mjs
CHANGED
|
@@ -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,
|
|
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
|
|
129
|
-
if (
|
|
130
|
-
entry.
|
|
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.
|
|
156
|
+
* @param {string[]} [params.docPaths] - Document path list for updates.
|
|
157
157
|
*/
|
|
158
|
-
export function recordUpdate({ operation, feedback,
|
|
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,
|
|
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
|
-
|
|
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: [] };
|
package/utils/load-config.mjs
CHANGED
|
@@ -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}:  - 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
|
|
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 
|
|
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
|
|
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<
|
|
91
|
+
* @returns {Promise<{ path: string, success: boolean, error?: string }>}
|
|
94
92
|
*/
|
|
95
|
-
export async function
|
|
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
|
-
//
|
|
111
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
126
|
+
// Add labels front matter to translation content if labels are provided
|
|
127
|
+
let finalTranslationContent = processContent({
|
|
128
|
+
content: translation,
|
|
129
|
+
});
|
|
147
130
|
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
1101
|
-
|
|
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)) {
|