@aigne/doc-smith 0.9.7-beta → 0.9.7-beta.1

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,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.7-beta.1](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.9.7-beta...v0.9.7-beta.1) (2025-11-27)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **ci:** ensure pipeline fails on test failures ([#328](https://github.com/AIGNE-io/aigne-doc-smith/issues/328)) ([56fb4d5](https://github.com/AIGNE-io/aigne-doc-smith/commit/56fb4d5259a21a6cf76f67c6ac5a43a7c1403b99))
9
+ * supports admonition syntax ([#338](https://github.com/AIGNE-io/aigne-doc-smith/issues/338)) ([db19661](https://github.com/AIGNE-io/aigne-doc-smith/commit/db19661e1c2d654181d932e0834e7e011a3ad8a5))
10
+
3
11
  ## [0.9.7-beta](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.9.6...v0.9.7-beta) (2025-11-25)
4
12
 
5
13
 
@@ -36,5 +36,4 @@ output_schema:
36
36
  type: string
37
37
  description: Merged content of the document with D2 diagram
38
38
  required:
39
- - content
40
-
39
+ - content
@@ -87,7 +87,8 @@ export default async function checkDocument(
87
87
  contentValidationFailed = true;
88
88
  }
89
89
  }
90
- const languages = translates.map((x) => x.language);
90
+ const translateList = Array.isArray(translates) ? translates : [];
91
+ const languages = translateList.map((x) => x.language);
91
92
  const lackLanguages = new Set(languages);
92
93
  const skills = [];
93
94
 
@@ -140,7 +141,7 @@ export default async function checkDocument(
140
141
 
141
142
  const result = await options.context.invoke(teamAgent, {
142
143
  ...rest,
143
- translates: translates.filter((x) => lackLanguages.has(x.language)),
144
+ translates: translateList.filter((x) => lackLanguages.has(x.language)),
144
145
  locale,
145
146
  docsDir,
146
147
  path,
@@ -1,4 +1,4 @@
1
- import { DIAGRAM_PLACEHOLDER } from "../../utils/d2-utils.mjs";
1
+ import { DIAGRAM_PLACEHOLDER, replacePlaceholderWithD2 } from "../../utils/d2-utils.mjs";
2
2
 
3
3
  const DEFAULT_DIAGRAMMING_EFFORT = 5;
4
4
  const MIN_DIAGRAMMING_EFFORT = 0;
@@ -68,7 +68,10 @@ export default async function checkGenerateDiagram(
68
68
 
69
69
  if (diagramSourceCode && !skipGenerateDiagram) {
70
70
  if (content.includes(DIAGRAM_PLACEHOLDER)) {
71
- content = content.replace(DIAGRAM_PLACEHOLDER, diagramSourceCode);
71
+ content = replacePlaceholderWithD2({
72
+ content,
73
+ diagramSourceCode,
74
+ });
72
75
  } else {
73
76
  const mergeAgent = options.context?.agents?.["mergeDiagramToDocument"];
74
77
  ({ content } = await options.context.invoke(mergeAgent, {
@@ -1,7 +1,11 @@
1
1
  import { AIAgent } from "@aigne/core";
2
2
  import { pick } from "@aigne/core/utils/type-utils.js";
3
3
  import z from "zod";
4
- import { DIAGRAM_PLACEHOLDER, replacePlaceholder } from "../../../utils/d2-utils.mjs";
4
+ import {
5
+ DIAGRAM_PLACEHOLDER,
6
+ replaceD2WithPlaceholder,
7
+ replacePlaceholderWithD2,
8
+ } from "../../../utils/d2-utils.mjs";
5
9
  import { userContextAt } from "../../../utils/utils.mjs";
6
10
 
7
11
  async function getIntentType(input, options) {
@@ -64,7 +68,7 @@ async function addDiagram(input, options) {
64
68
  async function updateDiagram(input, options) {
65
69
  const contentContext = userContextAt(options, `currentContents.${input.path}`);
66
70
  const currentContent = contentContext.get();
67
- let [content, previousDiagramContent] = replacePlaceholder({
71
+ let [content, previousDiagramContent] = replaceD2WithPlaceholder({
68
72
  content: currentContent,
69
73
  });
70
74
  const generateAgent = options.context?.agents?.["generateDiagram"];
@@ -74,7 +78,10 @@ async function updateDiagram(input, options) {
74
78
  previousDiagramContent,
75
79
  feedback: input.feedback,
76
80
  });
77
- content = content.replace(DIAGRAM_PLACEHOLDER, diagramSourceCode);
81
+ content = replacePlaceholderWithD2({
82
+ content,
83
+ diagramSourceCode,
84
+ });
78
85
  contentContext.set(content);
79
86
  await saveDoc(input, options, { content });
80
87
  return { content };
@@ -83,7 +90,7 @@ async function updateDiagram(input, options) {
83
90
  async function deleteDiagram(input, options) {
84
91
  const contentContext = userContextAt(options, `currentContents.${input.path}`);
85
92
  const currentContent = contentContext.get();
86
- const [documentContent] = replacePlaceholder({
93
+ const [documentContent] = replaceD2WithPlaceholder({
87
94
  content: currentContent,
88
95
  });
89
96
  const instructions = `<role>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/doc-smith",
3
- "version": "0.9.7-beta",
3
+ "version": "0.9.7-beta.1",
4
4
  "description": "AI-driven documentation generation tool built on the AIGNE Framework",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -24,10 +24,10 @@
24
24
  "author": "Arcblock <blocklet@arcblock.io> https://github.com/blocklet",
25
25
  "license": "Elastic-2.0",
26
26
  "dependencies": {
27
- "@aigne/cli": "^1.54.1",
28
- "@aigne/core": "^1.67.0",
29
- "@aigne/publish-docs": "^0.13.1",
30
- "@blocklet/payment-broker-client": "^1.22.12",
27
+ "@aigne/cli": "^1.56.0",
28
+ "@aigne/core": "^1.69.0",
29
+ "@aigne/publish-docs": "^0.14.1",
30
+ "@blocklet/payment-broker-client": "^1.22.24",
31
31
  "@terrastruct/d2": "^0.1.33",
32
32
  "chalk": "^5.5.0",
33
33
  "debug": "^4.4.1",
@@ -66,10 +66,12 @@
66
66
  "@biomejs/biome": "^2.2.4"
67
67
  },
68
68
  "scripts": {
69
- "test": "bun test",
70
- "test:coverage2": "bun test --coverage",
71
- "test:coverage": "bun test --coverage --coverage-reporter=lcov --coverage-reporter=text",
72
- "test:watch": "bun test --watch",
69
+ "test": "bun test --preload ./tests/setup/mock-process-exit.mjs",
70
+ "test:coverage2": "bun test --preload ./tests/setup/mock-process-exit.mjs --coverage",
71
+ "test:coverage": "bun test --preload ./tests/setup/mock-process-exit.mjs --coverage --coverage-reporter=lcov --coverage-reporter=text",
72
+ "test:deploy": "bun test --preload ./tests/setup/mock-process-exit.mjs tests/utils/deploy.test.mjs --coverage --coverage-reporter=text",
73
+ "test:user-review": "bun test --preload ./tests/setup/mock-process-exit.mjs tests/agents/update/user-review-document.test.mjs --coverage --coverage-reporter=lcov --coverage-reporter=text",
74
+ "test:watch": "bun test --preload ./tests/setup/mock-process-exit.mjs --watch",
73
75
  "lint": "biome lint && biome format",
74
76
  "update:deps": "npx -y taze major -r -w -f -n '/@abtnode|@aigne|@arcblock|@blocklet|@did-connect|@did-pay|@did-space|@nft-store|@nft-studio|@ocap/' && pnpm install && pnpm run deduplicate",
75
77
  "deduplicate": "pnpm dedupe",
@@ -0,0 +1,65 @@
1
+ <markdown_syntax_rules>
2
+
3
+ ## Markdown Syntax Standard
4
+
5
+ ### Allowed Syntax
6
+
7
+ **Inline** (used within text):
8
+
9
+ - Emphasis: `**bold**`, `*italic*`, `~~strikethrough~~`
10
+ - Links: `[text](url)`
11
+ - Images: `![alt](url)`
12
+ - Inline Code: `` `code` ``
13
+
14
+ **Block** (standalone blocks):
15
+
16
+ - Headings: `#`, `##`, `###`, etc.
17
+ - Lists: `- item`, `1. item`
18
+ - Task Lists: `- [ ]`, `- [x]`
19
+ - Blockquotes: `> quote`
20
+ - Code Block: ` ```language ... ``` `
21
+ - Tables: `| col | col |` with `|---|---|` separator
22
+ - Horizontal Rule: `---`
23
+ - Admonition: `:::severity ... :::`
24
+
25
+ ### Prohibited Syntax
26
+
27
+ The following are **strictly forbidden**:
28
+
29
+ - Footnotes: `[^1]: note text`
30
+ - Math/LaTeX: `$inline$`, `$$ block $$`
31
+ - Highlight: `==highlighted==`
32
+ - Subscript/Superscript: `H~2~O`, `X^2^`
33
+ - Abbreviations: `*[HTML]: Hyper Text Markup Language`
34
+
35
+ ### Link Rules
36
+
37
+ - Links must reference valid external URLs or paths from the document structure
38
+ - Use absolute paths from the documentation structure for internal links
39
+
40
+ ### Table Formatting Rules
41
+
42
+ - Separator row (`|---|---|---|`) must match the exact column count of header row
43
+ - Each row must have the same number of columns
44
+ - Use tables for predefined values (e.g., status types, options) or term definitions
45
+ - Validate table structure before output
46
+
47
+ ### Code Block Rules
48
+
49
+ - Ensure code blocks are properly closed
50
+ - Generate complete, syntactically correct code (JSON, etc.)
51
+ - Perform self-validation to ensure all code blocks, lists, and tables are properly closed without truncation
52
+
53
+ ### Block-Level Elements
54
+
55
+ Block-level elements are standalone content blocks that must be visually separated from surrounding content.
56
+
57
+ Block-Level Element List:
58
+
59
+ - Admonition: `:::severity ... :::`
60
+ - Code Block: ` ```language ... ``` `
61
+ - Custom Components: `<x-cards>`, `<x-card>`, `<x-field-group>`, etc.
62
+
63
+ **Spacing Rule:** Always insert a blank line before and after any block-level element when it is **adjacent to** other Markdown content (headings, paragraphs, lists, etc.).
64
+
65
+ </markdown_syntax_rules>
@@ -0,0 +1,94 @@
1
+ <admonition_syntax_rules>
2
+
3
+ ## Admonition Syntax Rules
4
+
5
+ Admonition is a Markdown block extension used to highlight important information. Use it sparingly.
6
+
7
+ ### Syntax Structure
8
+
9
+ ```
10
+ :::severity
11
+ content
12
+ :::
13
+ ```
14
+
15
+ ### Syntax Rules
16
+
17
+ The `severity` is **required** and must be one of the following:
18
+
19
+ - `success`: Positive outcome or best practice
20
+ - `info`: General tips
21
+ - `warning`: Cautions or potential issues
22
+ - `error`: Critical risks or breaking operations
23
+
24
+ The `content` is **required** and MUST strictly comply with the rules below:
25
+
26
+ - The `content` MUST be plain text only
27
+ - The `content` MUST be a single paragraph (no line breaks).
28
+ - Nesting any blocks or Admonitions is forbidden.
29
+ - Recommended length: within 200 characters.
30
+
31
+ ### Usage Guidelines
32
+
33
+ - Use sparingly, only for messages that truly require user attention
34
+ - Do not use Admonition if the content needs any Markdown syntax from <markdown_syntax_rules> — use regular paragraphs instead
35
+ - Keep the text short, clear, and actionable
36
+ - Choose the severity level according to the importance of the message
37
+
38
+ ### Good Examples
39
+
40
+ 1. All four severity types:
41
+
42
+ ```md
43
+ :::success
44
+ Your configuration is complete.
45
+ :::
46
+
47
+ :::info
48
+ Environment variables can override this setting.
49
+ :::
50
+
51
+ :::warning
52
+ This API will be removed in v3.0.
53
+ :::
54
+
55
+ :::error
56
+ Never commit API keys to version control.
57
+ :::
58
+ ```
59
+
60
+ ### Bad Examples
61
+ 1. Contains Markdown Syntax:
62
+
63
+ ```md
64
+ :::info
65
+ No **bold**, *italic*, or `inline code` allowed.
66
+ :::
67
+
68
+ :::warning
69
+ No [links](https://example.com) allowed.
70
+ :::
71
+
72
+ :::info
73
+ - No lists
74
+ - Or bullet points
75
+ :::
76
+
77
+ :::error
78
+ ```sh
79
+ npm i
80
+ ```
81
+ :::
82
+ ```
83
+
84
+ 2. Multi-paragraph:
85
+
86
+ ```md
87
+ :::warning
88
+ No multi-paragraph.
89
+
90
+ Like this.
91
+ :::
92
+ ```
93
+
94
+ </admonition_syntax_rules>
@@ -18,7 +18,7 @@ XCard is individual link display card, suitable for displaying individual links
18
18
  ### Children
19
19
 
20
20
  - Must be written within `<x-card>...</x-card>` children.
21
- - **Plain Text Only**: All markdown formatting is prohibited, including inline formats like `code`, **bold**, _italic_, [links](), and block-level formats like headers (# ##), lists (- \*), code blocks (```), tables (|), and any other markdown syntax. Only plain text content is allowed.
21
+ - Plain Text Only: Do not use any Markdown syntax (see `<markdown_syntax_rules>` for the full list).
22
22
 
23
23
  ### Good Examples
24
24
 
@@ -17,6 +17,11 @@ XFieldDesc is rich field description. Used to provide rich text descriptions for
17
17
  ### Usage Rules
18
18
 
19
19
  - **Parent Requirement**: Must be child of `<x-field>`: `<x-field-desc>` can only appear as a child element of `<x-field>` components
20
+ - **Avoid Redundant Information**: Do not repeat information in `<x-field-desc>` that is already expressed by the parent `<x-field>` attributes. Specifically:
21
+ - **Required Status**: Do not mention "required" or "optional" in descriptions since `data-required` attribute already indicates this
22
+ - **Default Values**: Do not repeat default values in descriptions since `data-default` attribute already shows this
23
+ - **Deprecated Status**: Do not mention "deprecated" in descriptions since `data-deprecated` attribute already indicates this
24
+ - Focus descriptions on the field's purpose, format, constraints, example values, and usage guidance instead
20
25
 
21
26
  ### Good Examples
22
27
 
@@ -83,4 +88,33 @@ XFieldDesc is rich field description. Used to provide rich text descriptions for
83
88
  </x-field-group>
84
89
  ```
85
90
 
91
+ - Example 7: Redundant required information in description (violates "Avoid Redundant Information" rule)
92
+ ```md
93
+ <x-field-group>
94
+ <x-field data-name="api_key" data-type="string" data-required="true">
95
+ <x-field-desc markdown>Your **API key** for authentication. **This field is required.**</x-field-desc>
96
+ </x-field>
97
+ <x-field data-name="timeout" data-type="number" data-required="false" data-default="5000">
98
+ <x-field-desc markdown>Request timeout in milliseconds. **Optional**, defaults to `5000`.</x-field-desc>
99
+ </x-field>
100
+ <x-field data-name="old_api" data-type="string" data-deprecated="true">
101
+ <x-field-desc markdown>Old API endpoint. **This field is deprecated.**</x-field-desc>
102
+ </x-field>
103
+ </x-field-group>
104
+ ```
105
+ **Correct approach:**
106
+ ```md
107
+ <x-field-group>
108
+ <x-field data-name="api_key" data-type="string" data-required="true">
109
+ <x-field-desc markdown>Your **API key** for authentication. Generate one from the `Settings > API Keys` section.</x-field-desc>
110
+ </x-field>
111
+ <x-field data-name="timeout" data-type="number" data-required="false" data-default="5000">
112
+ <x-field-desc markdown>Request timeout in milliseconds.</x-field-desc>
113
+ </x-field>
114
+ <x-field data-name="old_api" data-type="string" data-deprecated="true">
115
+ <x-field-desc markdown>Old API endpoint. Use the new endpoint instead.</x-field-desc>
116
+ </x-field>
117
+ </x-field-group>
118
+ ```
119
+
86
120
  </x-field-desc-usage-rules>
@@ -15,7 +15,6 @@ XFieldGroup is `<x-field>` grouping container. Used to group multiple related `<
15
15
 
16
16
  - **Top-Level Only**: Used only at the top level for grouping related `<x-field>` elements. Cannot be nested inside other `<x-field>` or `<x-field-group>` elements
17
17
  - **Structured Data Only**: Use `<x-field-group>` for fields **other than simple types** (`string`, `number`, `boolean`, `symbol`), e.g., Properties, Context, Parameters, Return values. For simple-type fields, use plain Markdown text.
18
- - **Spacing Around**: Always insert a blank line before and after `<x-field-group>` when it’s adjacent to Markdown content.
19
18
 
20
19
  ### Good Examples
21
20
 
@@ -78,18 +77,4 @@ XFieldGroup is `<x-field>` grouping container. Used to group multiple related `<
78
77
  </x-field-group>
79
78
  ```
80
79
 
81
- - Example 6: Missing blank line before x-field-group (violates "Spacing Around" rule)
82
- ```md
83
- **Parameters**
84
- <x-field-group>
85
- <x-field data-name="initialState" data-type="any" data-required="false">
86
- <x-field-desc markdown>The initial state value.</x-field-desc>
87
- </x-field>
88
- </x-field-group>
89
-
90
- `useReducer` returns an array with two items:
91
- <x-field-group>
92
- <x-field data-name="dispatch" data-type="function" data-desc="A function that you can call with an action to update the state."></x-field>
93
- </x-field-group>
94
- ```
95
80
  </x-field-group-usage-rules>
@@ -10,7 +10,7 @@ XField is structured data field, suitable for displaying API parameters, return
10
10
  - `data-default` (optional): Default value for the field
11
11
  - `data-required` (optional): Whether the field is required ("true" or "false")
12
12
  - `data-deprecated` (optional): Whether the field is deprecated ("true" or "false")
13
- - `data-desc` (optional): Simple description of the field (plain text only)
13
+ - `data-desc` (optional): Simple description of the field. Do not use any Markdown syntax (see `<markdown_syntax_rules>` for the full list).
14
14
 
15
15
  ### Children
16
16
 
@@ -1,6 +1,8 @@
1
1
 
2
2
  <document_rules>
3
3
 
4
+ {% include "../../common/document/markdown-syntax-rules.md" %}
5
+
4
6
  Documentation Generation Rules:
5
7
  - **Opening Hook Requirement:** The document must begin with a compelling, relaxed, and concise introductory paragraph (The "Hook").
6
8
  - **Hook Content:** This paragraph must clearly state the specific outcome, knowledge, or skill the reader will gain upon completing the document (Preferably 50 words or less).
@@ -13,9 +15,6 @@ Documentation Generation Rules:
13
15
  - Since API names are already specified in document titles, avoid repeating them in subheadings—use sub-API names directly
14
16
  - Include links to related documents in the introduction using Markdown format to help users navigate to relevant content
15
17
  - Add links to further reading materials in the summary section using Markdown format
16
- - **Markdown Syntax Constraint**: Use only GitHub Flavored Markdown (GFM) syntax by default. Prohibited extensions include: custom blocks `:::`, footnotes `[^1]: notes`, math formulas `$$ LaTeX`, highlighted text `==code==`, and other non-GFM syntax unless explicitly defined in custom component rules
17
- - Use proper Markdown link syntax, for example: [Next Chapter Title](next_chapter_path)
18
- - **Ensure next_chapter_path references either external URLs or valid paths from the documentation structure**—use absolute paths from the documentation structure
19
18
  - When detailDataSource includes third-party links, incorporate them appropriately throughout the document
20
19
  - Structure each section with: title, introduction, code examples, response data samples, and explanatory notes. Place explanations directly after code examples without separate "Example Description" subheadings
21
20
  - Maintain content completeness and logical flow so users can follow the documentation seamlessly
@@ -23,10 +22,6 @@ Documentation Generation Rules:
23
22
  - All interface and method documentation must include **response data examples**
24
23
  - **Use `<x-field-group>` for all structured data**: Represent objects with nested `<x-field>` elements, and expand each structure to the **deepest relevant level**.
25
24
  - **Enhance field descriptions with example values**: For structured data defined using `<x-field-group>`, extract example values from type definitions, comments, or test cases to make documentation more practical and user-friendly.
26
- - **Use Markdown tables** for predefined values (e.g., status types, options) or term definitions to improve clarity and allow side-by-side comparison.
27
- - Validate output Markdown for completeness, ensuring tables are properly formatted
28
- - **Content Integrity**: Generate complete, syntactically correct code blocks (JSON, etc.). Perform self-validation to ensure all code blocks, lists, and tables are properly closed without truncation
29
- - **Markdown Syntax Validation**: Ensure correct Markdown formatting, particularly table separators (e.g., `|---|---|---|`) that match column counts
30
25
  - Use README files for reference only—extract the most current and comprehensive information directly from source code
31
26
  - Omit tag information from document headers as it's processed programmatically
32
27
  - Parse `jsx` syntax correctly when present in code samples
@@ -42,6 +42,8 @@ Documentation content generation rules:
42
42
 
43
43
  {% include "../custom/code-block-usage-rules.md" %}
44
44
 
45
+ {% include "../custom/admonition-usage-rules.md" %}
46
+
45
47
  {% include "../../common/document/media-file-list-usage-rules.md" %}
46
48
 
47
49
  {% include "../../common/document/openapi-usage-rules.md" %}
@@ -68,6 +68,8 @@ Documentation content optimization rules:
68
68
 
69
69
  {% include "../custom/code-block-usage-rules.md" %}
70
70
 
71
+ {% include "../custom/admonition-usage-rules.md" %}
72
+
71
73
  {% include "../../common/document/media-file-list-usage-rules.md" %}
72
74
 
73
75
  {% include "../../common/document/openapi-usage-rules.md" %}
@@ -0,0 +1,20 @@
1
+ <admonition_rules>
2
+
3
+ Admonition blocks use the following format:
4
+
5
+ ```
6
+ :::severity
7
+
8
+ text
9
+
10
+ :::
11
+ ```
12
+
13
+ Admonition Translation Rules:
14
+
15
+ - **Translate** the text content only
16
+ - **Do not translate** the severity keyword (success, info, warning, error)
17
+ - **Preserve** the `:::` syntax and block structure
18
+
19
+ </admonition_rules>
20
+
@@ -24,6 +24,8 @@ Translation Requirements:
24
24
  - Translate Descriptions Only in <x-field>: All `<x-field>` component attributes must maintain the original format. Only translate the description content within `data-desc` attribute or `<x-field-desc>` elements.
25
25
 
26
26
  {% include "./code-block.md" %}
27
+
28
+ {% include "./admonition.md" %}
27
29
  </translation_rules>
28
30
 
29
31
  {% if feedback %}
@@ -1,5 +1,5 @@
1
1
  // Default file patterns for inclusion and exclusion
2
- export const DEFAULT_INCLUDE_PATTERNS = [
2
+ export const DEFAULT_INCLUDE_PATTERNS = Object.freeze([
3
3
  // Python
4
4
  "*.py",
5
5
  "*.pyi",
@@ -100,9 +100,9 @@ export const DEFAULT_INCLUDE_PATTERNS = [
100
100
  "*.mkv",
101
101
  "*.webm",
102
102
  "*.m4v",
103
- ];
103
+ ]);
104
104
 
105
- export const DEFAULT_EXCLUDE_PATTERNS = [
105
+ export const DEFAULT_EXCLUDE_PATTERNS = Object.freeze([
106
106
  "**/doc-smith/**",
107
107
  "**/.aigne/**",
108
108
  "**/vendor/**",
@@ -130,7 +130,7 @@ export const DEFAULT_EXCLUDE_PATTERNS = [
130
130
  "**/bun.lockb",
131
131
  "**/bun.lock",
132
132
  "**/bun.lockb",
133
- ];
133
+ ]);
134
134
 
135
135
  // Supported languages for documentation
136
136
  export const SUPPORTED_LANGUAGES = [
@@ -206,7 +206,12 @@ export function wrapCode({ content }) {
206
206
  return `\`\`\`d2\n${content}\n\`\`\``;
207
207
  }
208
208
 
209
- export function replacePlaceholder({ content }) {
209
+ /**
210
+ * Replaces D2 code block with DIAGRAM_PLACEHOLDER.
211
+ * @param {string} content - Document content containing D2 code block
212
+ * @returns {Array} - [contentWithPlaceholder, originalCodeBlock]
213
+ */
214
+ export function replaceD2WithPlaceholder({ content }) {
210
215
  const [firstMatch] = Array.from(content.matchAll(codeBlockRegex));
211
216
  if (firstMatch) {
212
217
  const matchContent = firstMatch[0];
@@ -216,3 +221,37 @@ export function replacePlaceholder({ content }) {
216
221
 
217
222
  return [content, ""];
218
223
  }
224
+
225
+ /**
226
+ * Replaces DIAGRAM_PLACEHOLDER with D2 code block, ensuring proper spacing.
227
+ * @param {string} content - Document content containing DIAGRAM_PLACEHOLDER
228
+ * @param {string} diagramSourceCode - D2 diagram source code (without markdown wrapper)
229
+ * @returns {string} - Content with placeholder replaced by code block
230
+ */
231
+ export function replacePlaceholderWithD2({ content, diagramSourceCode }) {
232
+ if (!content || !diagramSourceCode) {
233
+ return content;
234
+ }
235
+
236
+ const placeholderIndex = content.indexOf(DIAGRAM_PLACEHOLDER);
237
+ if (placeholderIndex === -1) {
238
+ return content;
239
+ }
240
+
241
+ // Check if placeholder has newlines around it
242
+ const beforePlaceholder = content.substring(0, placeholderIndex);
243
+ const afterPlaceholder = content.substring(placeholderIndex + DIAGRAM_PLACEHOLDER.length);
244
+
245
+ const codeBlock = wrapCode({ content: diagramSourceCode });
246
+
247
+ // Add newlines if missing
248
+ let replacement = codeBlock;
249
+ if (beforePlaceholder && !beforePlaceholder.endsWith("\n")) {
250
+ replacement = `\n${replacement}`;
251
+ }
252
+ if (afterPlaceholder && !afterPlaceholder.startsWith("\n")) {
253
+ replacement = `${replacement}\n`;
254
+ }
255
+
256
+ return content.replace(DIAGRAM_PLACEHOLDER, replacement);
257
+ }