@aigne/doc-smith 0.5.1 → 0.6.0

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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.0](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.5.1...v0.6.0) (2025-08-27)
4
+
5
+
6
+ ### Features
7
+
8
+ * complete support for media processing before publish ([#63](https://github.com/AIGNE-io/aigne-doc-smith/issues/63)) ([5257ca1](https://github.com/AIGNE-io/aigne-doc-smith/commit/5257ca1756f47487b65a1813949e547b6fc51aca))
9
+
3
10
  ## [0.5.1](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.5.0...v0.5.1) (2025-08-26)
4
11
 
5
12
 
@@ -1,6 +1,6 @@
1
1
  import { checkMarkdown } from "../utils/markdown-checker.mjs";
2
2
 
3
- export default async function checkDetailResult({ structurePlan, reviewContent }) {
3
+ export default async function checkDetailResult({ structurePlan, reviewContent, docsDir }) {
4
4
  let isApproved = true;
5
5
  const detailFeedback = [];
6
6
 
@@ -24,6 +24,7 @@ export default async function checkDetailResult({ structurePlan, reviewContent }
24
24
  try {
25
25
  const markdownErrors = await checkMarkdown(reviewContent, "result", {
26
26
  allowedLinks,
27
+ baseDir: docsDir,
27
28
  });
28
29
 
29
30
  if (markdownErrors.length > 0) {
@@ -85,6 +85,7 @@ export default async function checkDetail(
85
85
  const validationResult = await checkDetailResult({
86
86
  structurePlan,
87
87
  reviewContent: fileContent,
88
+ docsDir,
88
89
  });
89
90
 
90
91
  if (!validationResult.isApproved) {
@@ -86,17 +86,55 @@ export default async function loadSources({
86
86
  }
87
87
 
88
88
  files = [...new Set(files)];
89
+
90
+ // Define media file extensions
91
+ const mediaExtensions = [
92
+ ".jpg",
93
+ ".jpeg",
94
+ ".png",
95
+ ".gif",
96
+ ".bmp",
97
+ ".webp",
98
+ ".svg",
99
+ ".mp4",
100
+ ".mov",
101
+ ".avi",
102
+ ".mkv",
103
+ ".webm",
104
+ ".m4v",
105
+ ];
106
+
107
+ // Separate source files from media files
108
+ const sourceFiles = [];
109
+ const mediaFiles = [];
89
110
  let allSources = "";
90
- const sourceFiles = await Promise.all(
111
+
112
+ await Promise.all(
91
113
  files.map(async (file) => {
92
- const content = await readFile(file, "utf8");
93
- // Convert absolute path to relative path from project root
94
- const relativePath = path.relative(process.cwd(), file);
95
- allSources += `// sourceId: ${relativePath}\n${content}\n`;
96
- return {
97
- sourceId: relativePath,
98
- content,
99
- };
114
+ const ext = path.extname(file).toLowerCase();
115
+
116
+ if (mediaExtensions.includes(ext)) {
117
+ // This is a media file
118
+ const relativePath = path.relative(docsDir, file);
119
+ const fileName = path.basename(file);
120
+ const description = path.parse(fileName).name;
121
+
122
+ mediaFiles.push({
123
+ name: fileName,
124
+ path: relativePath,
125
+ description,
126
+ });
127
+ } else {
128
+ // This is a source file
129
+ const content = await readFile(file, "utf8");
130
+ const relativePath = path.relative(process.cwd(), file);
131
+ allSources += `// sourceId: ${relativePath}\n${content}\n`;
132
+
133
+ sourceFiles.push({
134
+ sourceId: relativePath,
135
+ content,
136
+ });
137
+ }
100
138
  }),
101
139
  );
102
140
 
@@ -164,6 +202,17 @@ export default async function loadSources({
164
202
  }
165
203
  }
166
204
 
205
+ // Generate assets content from media files
206
+ let assetsContent = "# Available Media Assets for Documentation\n\n";
207
+
208
+ if (mediaFiles.length > 0) {
209
+ const mediaMarkdown = mediaFiles
210
+ .map((file) => `![${file.description}](${file.path})`)
211
+ .join("\n\n");
212
+
213
+ assetsContent += mediaMarkdown;
214
+ }
215
+
167
216
  // Count words and lines in allSources
168
217
  let totalWords = 0;
169
218
  let totalLines = 0;
@@ -188,6 +237,7 @@ export default async function loadSources({
188
237
  modifiedFiles,
189
238
  totalWords,
190
239
  totalLines,
240
+ assetsContent,
191
241
  };
192
242
  }
193
243
 
@@ -257,6 +307,10 @@ loadSources.output_schema = {
257
307
  items: { type: "string" },
258
308
  description: "Array of modified files since last generation",
259
309
  },
310
+ assetsContent: {
311
+ type: "string",
312
+ description: "Markdown content for available media assets",
313
+ },
260
314
  },
261
315
  };
262
316
 
@@ -98,6 +98,8 @@ export default async function publishDocs(
98
98
  boardName: projectInfo.name,
99
99
  boardDesc: projectInfo.description,
100
100
  boardCover: projectInfo.icon,
101
+ mediaFolder: docsDir,
102
+ cacheFilePath: join(".aigne", "doc-smith", "upload-cache.yaml"),
101
103
  boardMeta,
102
104
  });
103
105
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/doc-smith",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -12,13 +12,13 @@
12
12
  "author": "Arcblock <blocklet@arcblock.io> https://github.com/blocklet",
13
13
  "license": "MIT",
14
14
  "dependencies": {
15
- "@aigne/aigne-hub": "^0.6.9",
16
- "@aigne/anthropic": "^0.11.9",
17
- "@aigne/cli": "^1.41.0",
18
- "@aigne/core": "^1.55.0",
19
- "@aigne/gemini": "^0.9.9",
20
- "@aigne/openai": "^0.12.3",
21
- "@aigne/publish-docs": "^0.5.7",
15
+ "@aigne/aigne-hub": "^0.6.10",
16
+ "@aigne/anthropic": "^0.11.10",
17
+ "@aigne/cli": "^1.41.1",
18
+ "@aigne/core": "^1.55.1",
19
+ "@aigne/gemini": "^0.9.10",
20
+ "@aigne/openai": "^0.12.4",
21
+ "@aigne/publish-docs": "^0.6.0",
22
22
  "chalk": "^5.5.0",
23
23
  "dompurify": "^3.2.6",
24
24
  "glob": "^11.0.3",
@@ -16,6 +16,11 @@
16
16
  {{ detailDataSources }}
17
17
 
18
18
  {{ additionalInformation }}
19
+
20
+ <media_list>
21
+ {{ assetsContent }}
22
+ </media_list>
23
+
19
24
  </datasources>
20
25
 
21
26
  <terms>
@@ -93,7 +98,6 @@ parentId: {{parentId}}
93
98
  - 媒体资源以 markdown 格式提供,示例:![资源描述](https://xxxx)
94
99
  - 在生成结果中以 markdown 格式展示图片
95
100
  - 根据资源描述,在上下文相关的位置,合理的展示图片,让结果展示效果更丰富
96
- - 只使用完整的远程图片URL(如 https://example.com/image.jpg),禁止使用相对路径(如 ./images/photo.png 或 ../assets/logo.png),确保发布后图片能正常访问
97
101
 
98
102
  </media_rules>
99
103
 
@@ -27,6 +27,7 @@
27
27
  - **代码块原子性**:将每个代码块(例如 ```mermaid ... ```)视为一个**不可分割的原子单元**。必须一次性完整生成,从开始标记(```mermaid)到结束标记(```)之间的所有内容都不能省略或截断。
28
28
  - **确保 Markdown 语法**:Markdown 格式正确,特别是表格的分隔线(例如 `|---|---|---|`),需要与表格数据列数一致。
29
29
  - README 文件只做参考,你需要从代码中获取最新、最完整的信息
30
+ - 忽略详情顶部的标签信息,这是程序处理的,不需要在生成时输出
30
31
  </document_rules>
31
32
 
32
33
  <TONE_STYLE>
@@ -45,10 +45,59 @@ describe("checkDetailResult", () => {
45
45
  expect(result.detailFeedback).toContain("dead link");
46
46
  });
47
47
 
48
- test("should approve content with image syntax", async () => {
48
+ test("should approve content with external image syntax", async () => {
49
49
  const structurePlan = [];
50
50
  const reviewContent =
51
- "This is an image ![MCP Go Logo](/logo.png).\n\nThis has proper structure.";
51
+ "This is an image ![MCP Go Logo](https://example.com/logo.png).\n\nThis has proper structure.";
52
+ const result = await checkDetailResult({ structurePlan, reviewContent });
53
+ expect(result.isApproved).toBe(true);
54
+ expect(result.detailFeedback).toBe("");
55
+ });
56
+
57
+ test("should approve content with valid local image path", async () => {
58
+ const structurePlan = [];
59
+ const reviewContent =
60
+ "This is a valid image ![Test Image](./README.md).\n\nThis has proper structure.";
61
+ const docsDir = "/Users/lban/arcblock/code/aigne-doc-smith";
62
+ const result = await checkDetailResult({ structurePlan, reviewContent, docsDir });
63
+ expect(result.isApproved).toBe(true);
64
+ expect(result.detailFeedback).toBe("");
65
+ });
66
+
67
+ test("should reject content with invalid local image path", async () => {
68
+ const structurePlan = [];
69
+ const reviewContent =
70
+ "This is an invalid image ![Non-existent Image](./nonexistent.png).\n\nThis has proper structure.";
71
+ const docsDir = "/Users/lban/arcblock/code/aigne-doc-smith";
72
+ const result = await checkDetailResult({ structurePlan, reviewContent, docsDir });
73
+ expect(result.isApproved).toBe(false);
74
+ expect(result.detailFeedback).toContain("Found invalid local image");
75
+ expect(result.detailFeedback).toContain("only valid media resources can be used");
76
+ });
77
+
78
+ test("should approve content with absolute image path that exists", async () => {
79
+ const structurePlan = [];
80
+ const reviewContent =
81
+ "This is an absolute image ![Test Image](/Users/lban/arcblock/code/aigne-doc-smith/README.md).\n\nThis has proper structure.";
82
+ const result = await checkDetailResult({ structurePlan, reviewContent });
83
+ expect(result.isApproved).toBe(true);
84
+ expect(result.detailFeedback).toBe("");
85
+ });
86
+
87
+ test("should reject content with absolute image path that doesn't exist", async () => {
88
+ const structurePlan = [];
89
+ const reviewContent =
90
+ "This is an invalid absolute image ![Non-existent Image](/path/to/nonexistent.png).\n\nThis has proper structure.";
91
+ const result = await checkDetailResult({ structurePlan, reviewContent });
92
+ expect(result.isApproved).toBe(false);
93
+ expect(result.detailFeedback).toContain("Found invalid local image");
94
+ expect(result.detailFeedback).toContain("only valid media resources can be used");
95
+ });
96
+
97
+ test("should approve content with external image URL", async () => {
98
+ const structurePlan = [];
99
+ const reviewContent =
100
+ "This is an external image ![External Image](https://example.com/image.png).\n\nThis has proper structure.";
52
101
  const result = await checkDetailResult({ structurePlan, reviewContent });
53
102
  expect(result.isApproved).toBe(true);
54
103
  expect(result.detailFeedback).toBe("");
@@ -91,7 +91,7 @@ export async function getAccessToken(appUrl) {
91
91
  source: `AIGNE DocSmith connect to Discuss Kit`,
92
92
  closeOnSuccess: true,
93
93
  appName: "AIGNE DocSmith",
94
- appLogo: "https://www.aigne.io/image-bin/uploads/a7910a71364ee15a27e86f869ad59009.svg",
94
+ appLogo: "https://docsmith.aigne.io/image-bin/uploads/a7910a71364ee15a27e86f869ad59009.svg",
95
95
  openPage: (pageUrl) => open(pageUrl),
96
96
  });
97
97
 
@@ -86,15 +86,27 @@ export const DEFAULT_INCLUDE_PATTERNS = [
86
86
  "*.yml",
87
87
  "*Dockerfile",
88
88
  "*Makefile",
89
+ // Media files
90
+ "*.jpg",
91
+ "*.jpeg",
92
+ "*.png",
93
+ "*.gif",
94
+ "*.bmp",
95
+ "*.webp",
96
+ "*.svg",
97
+ "*.mp4",
98
+ "*.mov",
99
+ "*.avi",
100
+ "*.mkv",
101
+ "*.webm",
102
+ "*.m4v",
89
103
  ];
90
104
 
91
105
  export const DEFAULT_EXCLUDE_PATTERNS = [
92
106
  "**/aigne-docs/**",
93
107
  "**/doc-smith/**",
94
108
  "**/.aigne/**",
95
- "**/assets/**",
96
109
  "**/data/**",
97
- "**/images/**",
98
110
  "**/public/**",
99
111
  "**/static/**",
100
112
  "**/vendor/**",
@@ -1,3 +1,5 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
1
3
  import remarkGfm from "remark-gfm";
2
4
  import remarkLint from "remark-lint";
3
5
  import remarkParse from "remark-parse";
@@ -159,6 +161,72 @@ function checkCodeBlockIndentation(codeBlockContent, codeBlockIndent, source, er
159
161
  }
160
162
  }
161
163
 
164
+ /**
165
+ * Check for local images and verify their existence
166
+ * @param {string} markdown - The markdown content
167
+ * @param {string} source - Source description for error reporting
168
+ * @param {Array} errorMessages - Array to push error messages to
169
+ * @param {string} [markdownFilePath] - Path to the markdown file for resolving relative paths
170
+ * @param {string} [baseDir] - Base directory for resolving relative paths (alternative to markdownFilePath)
171
+ */
172
+ function checkLocalImages(markdown, source, errorMessages, markdownFilePath, baseDir) {
173
+ const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
174
+ let match;
175
+
176
+ while ((match = imageRegex.exec(markdown)) !== null) {
177
+ const imagePath = match[2].trim();
178
+ const altText = match[1];
179
+
180
+ // Skip external URLs (http/https)
181
+ if (/^https?:\/\//.test(imagePath)) continue;
182
+
183
+ // Skip data URLs
184
+ if (/^data:/.test(imagePath)) continue;
185
+
186
+ // Check if it's a local path
187
+ if (!imagePath.startsWith("/") && !imagePath.includes("://")) {
188
+ // It's a relative local path, check if file exists
189
+ try {
190
+ let resolvedPath;
191
+ if (markdownFilePath) {
192
+ // Resolve relative to the markdown file's directory
193
+ const markdownDir = path.dirname(markdownFilePath);
194
+ resolvedPath = path.resolve(markdownDir, imagePath);
195
+ } else if (baseDir) {
196
+ // Resolve relative to the provided base directory
197
+ resolvedPath = path.resolve(baseDir, imagePath);
198
+ } else {
199
+ // Fallback to current working directory
200
+ resolvedPath = path.resolve(imagePath);
201
+ }
202
+
203
+ if (!fs.existsSync(resolvedPath)) {
204
+ errorMessages.push(
205
+ `Found invalid local image in ${source}: ![${altText}](${imagePath}) - only valid media resources can be used`,
206
+ );
207
+ }
208
+ } catch {
209
+ errorMessages.push(
210
+ `Found invalid local image in ${source}: ![${altText}](${imagePath}) - only valid media resources can be used`,
211
+ );
212
+ }
213
+ } else if (imagePath.startsWith("/")) {
214
+ // Absolute local path
215
+ try {
216
+ if (!fs.existsSync(imagePath)) {
217
+ errorMessages.push(
218
+ `Found invalid local image in ${source}: ![${altText}](${imagePath}) - only valid media resources can be used`,
219
+ );
220
+ }
221
+ } catch {
222
+ errorMessages.push(
223
+ `Found invalid local image in ${source}: ![${altText}](${imagePath}) - only valid media resources can be used`,
224
+ );
225
+ }
226
+ }
227
+ }
228
+ }
229
+
162
230
  /**
163
231
  * Check content structure and formatting issues
164
232
  * @param {string} markdown - The markdown content
@@ -224,7 +292,7 @@ function checkContentStructure(markdown, source, errorMessages) {
224
292
  }
225
293
 
226
294
  // Check if content ends with proper punctuation (indicating completeness)
227
- const validEndingPunctuation = [".", "。", ")", "|"];
295
+ const validEndingPunctuation = [".", "。", ")", "|", "*"];
228
296
  const trimmedText = markdown.trim();
229
297
  const hasValidEnding = validEndingPunctuation.some((punct) => trimmedText.endsWith(punct));
230
298
 
@@ -241,6 +309,8 @@ function checkContentStructure(markdown, source, errorMessages) {
241
309
  * @param {string} [source] - Source description for error reporting (e.g., "result")
242
310
  * @param {Object} [options] - Additional options for validation
243
311
  * @param {Array} [options.allowedLinks] - Set of allowed links for link validation
312
+ * @param {string} [options.filePath] - Path to the markdown file for resolving relative image paths
313
+ * @param {string} [options.baseDir] - Base directory for resolving relative image paths (alternative to filePath)
244
314
  * @returns {Promise<Array<string>>} - Array of error messages in check-detail-result format
245
315
  */
246
316
  export async function checkMarkdown(markdown, source = "content", options = {}) {
@@ -248,8 +318,8 @@ export async function checkMarkdown(markdown, source = "content", options = {})
248
318
  const errorMessages = [];
249
319
 
250
320
  try {
251
- // Extract allowed links from options
252
- const { allowedLinks } = options;
321
+ // Extract allowed links, file path, and base directory from options
322
+ const { allowedLinks, filePath, baseDir } = options;
253
323
 
254
324
  // Create unified processor with markdown parsing and linting
255
325
  // Use individual rules instead of presets to have better control
@@ -292,7 +362,10 @@ export async function checkMarkdown(markdown, source = "content", options = {})
292
362
  checkDeadLinks(markdown, source, allowedLinks, errorMessages);
293
363
  }
294
364
 
295
- // 2. Check content structure and formatting issues
365
+ // 2. Check local images existence
366
+ checkLocalImages(markdown, source, errorMessages, filePath, baseDir);
367
+
368
+ // 3. Check content structure and formatting issues
296
369
  checkContentStructure(markdown, source, errorMessages);
297
370
 
298
371
  // Check mermaid code blocks and other custom validations
@@ -319,30 +392,35 @@ export async function checkMarkdown(markdown, source = "content", options = {})
319
392
  // Check for backticks in node labels
320
393
  const nodeLabelRegex = /[A-Za-z0-9_]+\["([^"]*`[^"]*)"\]|[A-Za-z0-9_]+{"([^}]*`[^}]*)"}/g;
321
394
  let match;
322
- while ((match = nodeLabelRegex.exec(mermaidContent)) !== null) {
395
+ match = nodeLabelRegex.exec(mermaidContent);
396
+ while (match !== null) {
323
397
  const label = match[1] || match[2];
324
398
  errorMessages.push(
325
399
  `Found backticks in Mermaid node label in ${source} at line ${line}: "${label}" - backticks in node labels cause rendering issues in Mermaid diagrams`,
326
400
  );
401
+ match = nodeLabelRegex.exec(mermaidContent);
327
402
  }
328
403
 
329
404
  // Check for numbered list format in edge descriptions
330
405
  const edgeDescriptionRegex = /--\s*"([^"]*)"\s*-->/g;
331
406
  let edgeMatch;
332
- while ((edgeMatch = edgeDescriptionRegex.exec(mermaidContent)) !== null) {
407
+ edgeMatch = edgeDescriptionRegex.exec(mermaidContent);
408
+ while (edgeMatch !== null) {
333
409
  const description = edgeMatch[1];
334
410
  if (/^\d+\.\s/.test(description)) {
335
411
  errorMessages.push(
336
412
  `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`,
337
413
  );
338
414
  }
415
+ edgeMatch = edgeDescriptionRegex.exec(mermaidContent);
339
416
  }
340
417
 
341
418
  // Check for numbered list format in node labels (for both [] and {} syntax)
342
419
  const nodeLabelWithNumberRegex =
343
420
  /[A-Za-z0-9_]+\["([^"]*\d+\.\s[^"]*)"\]|[A-Za-z0-9_]+{"([^}]*\d+\.\s[^}]*)"}/g;
344
421
  let numberMatch;
345
- while ((numberMatch = nodeLabelWithNumberRegex.exec(mermaidContent)) !== null) {
422
+ numberMatch = nodeLabelWithNumberRegex.exec(mermaidContent);
423
+ while (numberMatch !== null) {
346
424
  const label = numberMatch[1] || numberMatch[2];
347
425
  // Check if the label contains numbered list format
348
426
  if (/\d+\.\s/.test(label)) {
@@ -350,12 +428,14 @@ export async function checkMarkdown(markdown, source = "content", options = {})
350
428
  `Unsupported markdown: list - Found numbered list format in Mermaid node label in ${source} at line ${line}: "${label}" - numbered lists in node labels cause rendering issues`,
351
429
  );
352
430
  }
431
+ numberMatch = nodeLabelWithNumberRegex.exec(mermaidContent);
353
432
  }
354
433
 
355
434
  // Check for special characters in node labels that should be quoted
356
435
  const nodeWithSpecialCharsRegex = /([A-Za-z0-9_]+)\[([^\]]*[(){}:;,\-\s.][^\]]*)\]/g;
357
436
  let specialCharMatch;
358
- while ((specialCharMatch = nodeWithSpecialCharsRegex.exec(mermaidContent)) !== null) {
437
+ specialCharMatch = nodeWithSpecialCharsRegex.exec(mermaidContent);
438
+ while (specialCharMatch !== null) {
359
439
  const nodeId = specialCharMatch[1];
360
440
  const label = specialCharMatch[2];
361
441
 
@@ -373,6 +453,7 @@ export async function checkMarkdown(markdown, source = "content", options = {})
373
453
  );
374
454
  }
375
455
  }
456
+ specialCharMatch = nodeWithSpecialCharsRegex.exec(mermaidContent);
376
457
  }
377
458
  }
378
459
  });
@@ -431,7 +512,6 @@ export async function checkMarkdown(markdown, source = "content", options = {})
431
512
  // Format messages in check-detail-result style
432
513
  file.messages.forEach((message) => {
433
514
  const line = message.line || "unknown";
434
- const _column = message.column || "unknown";
435
515
  const reason = message.reason || "Unknown markdown issue";
436
516
  const ruleId = message.ruleId || message.source || "markdown";
437
517
 
package/utils/utils.mjs CHANGED
@@ -149,8 +149,8 @@ export function getCurrentGitHead() {
149
149
  * @param {string} gitHead - The current git HEAD commit hash
150
150
  */
151
151
  export async function saveGitHeadToConfig(gitHead) {
152
- if (!gitHead) {
153
- return; // Skip if no git HEAD available
152
+ if (!gitHead || process.env.NODE_ENV === 'test' || process.env.BUN_TEST) {
153
+ return; // Skip if no git HEAD available or in test environment
154
154
  }
155
155
 
156
156
  try {