Skip to content

Commit 6be2ce1

Browse files
authored
Merge branch 'dev' into copilot/fix-6f56c3cd-1667-400f-8551-f983d131e664
2 parents 59855ce + 2dd4a98 commit 6be2ce1

File tree

8 files changed

+326
-17
lines changed

8 files changed

+326
-17
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
---
2+
title: Markdown Script Include
3+
sidebar:
4+
order: 51
5+
description: Learn how to include external files in markdown scripts using the @include directive
6+
keywords: markdown scripts, include, file inclusion, @include directive
7+
hero:
8+
image:
9+
alt: A minimalistic 8-bit style icon showing two document rectangles with arrows pointing from one to another, representing file inclusion. The first document has an "@include" symbol, and content flows to the second document. Small geometric shapes indicate text content. The image uses five corporate colors on a transparent background, is highly simplified and geometric, and measures 128 by 128 pixels. No people, words, or realistic effects are present.
10+
file: ./markdown-include.png
11+
12+
---
13+
14+
Markdown scripts support including external files using the `@include` directive, allowing you to compose larger prompts from reusable components.
15+
16+
## Basic Usage
17+
18+
Use the `@include "filepath"` directive to insert the contents of an external file into your markdown script:
19+
20+
```markdown title="main.genai.md"
21+
---
22+
title: "My Script"
23+
description: "Script with included content"
24+
---
25+
26+
# Welcome
27+
28+
This is the main content.
29+
30+
@include "templates/greeting.md"
31+
32+
More content after the include.
33+
```
34+
35+
```markdown title="templates/greeting.md"
36+
## Hello World
37+
38+
This greeting template can be reused across multiple scripts.
39+
40+
Welcome to GenAIScript!
41+
```
42+
43+
## File Resolution
44+
45+
File paths in `@include` directives are resolved relative to the directory containing the markdown script:
46+
47+
- `@include "file.txt"` - file in the same directory as the script
48+
- `@include "templates/header.md"` - file in a subdirectory
49+
- `@include "../shared/common.md"` - file in a parent directory
50+
51+
## Multiple Includes
52+
53+
You can use multiple `@include` directives in the same markdown script:
54+
55+
```markdown title="complex.genai.md"
56+
# Complex Script
57+
58+
@include "header.md"
59+
60+
## Main Content
61+
62+
This is the main content section.
63+
64+
@include "examples/code-sample.md"
65+
66+
## Conclusion
67+
68+
@include "footer.md"
69+
```
70+
71+
## Error Handling
72+
73+
If an included file cannot be found or read, the `@include` directive is replaced with an HTML comment containing the error message:
74+
75+
```markdown
76+
<!-- Error including missing-file.txt: File not found: /path/to/missing-file.txt -->
77+
```
78+
79+
This ensures that your script continues to work even when included files are missing, while providing clear feedback about what went wrong.
80+
81+
## Use Cases
82+
83+
The `@include` directive is useful for:
84+
85+
- **Reusable templates**: Share common greetings, instructions, or examples across multiple scripts
86+
- **Modular prompts**: Break large prompts into manageable, focused files
87+
- **Content organization**: Keep related content in separate files for better maintainability
88+
- **Team collaboration**: Allow team members to work on different parts of a prompt independently
89+
90+
## Comparison with importTemplate
91+
92+
The `@include` directive is different from the programmatic `importTemplate` function:
93+
94+
| Feature | `@include` directive | `importTemplate` function |
95+
|---------|---------------------|---------------------------|
96+
| **Usage** | Markdown scripts (`.genai.md`) | JavaScript/TypeScript scripts (`.genai.mjs`, `.genai.mts`) |
97+
| **Processing** | Content replacement during parsing | Runtime template rendering |
98+
| **Variables** | No variable interpolation | Supports Mustache/Jinja variables |
99+
| **Error handling** | HTML comments for missing files | Runtime errors |
100+
| **File types** | Any text file | Template files with variable syntax |
101+
102+
Choose `@include` for simple content inclusion in markdown scripts, and `importTemplate` for dynamic template rendering with variables in JavaScript/TypeScript scripts.

docs/src/content/docs/reference/scripts/markdown-scripts.mdx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,33 @@ Please analyze the uploaded files and provide:
219219
220220
Focus on technical aspects and provide actionable insights.`} lang="markdown" title="analyze-files.genai.md" />
221221

222+
## Including External Files
223+
224+
Markdown scripts support including content from external files using the `@include` directive. This allows you to compose larger prompts from reusable components:
225+
226+
<Code code={`---
227+
title: "Documentation Generator"
228+
description: "Generates documentation with templates"
229+
---
230+
231+
# Documentation Generation
232+
233+
@include "templates/header.md"
234+
235+
Please generate documentation for the following code:
236+
237+
@include "examples/code-sample.md"
238+
239+
@include "templates/footer.md"`} lang="markdown" title="doc-generator.genai.md" />
240+
241+
The `@include "filepath"` directive:
242+
- Resolves file paths relative to the script's directory
243+
- Supports subdirectories: `@include "templates/header.md"`
244+
- Replaces missing files with error comments
245+
- Processes multiple includes in the same script
246+
247+
For more details, see the [Markdown Script Include](/genaiscript/reference/scripts/markdown-include) documentation.
248+
222249
## Parameters and Variables
223250

224251
You can define script parameters in the frontmatter and reference them in the content:

packages/core/src/markdownscript.ts

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,75 @@ import { deleteUndefinedValues } from "./cleaners.js";
88
import { JSON5Stringify } from "./json5.js";
99
import type { PromptArgs } from "./types.js";
1010
import { genaiscriptDebug } from "./debug.js";
11+
import { resolve } from "node:path";
1112
const dbg = genaiscriptDebug("md");
1213

14+
/**
15+
* Processes @include directives in markdown text by replacing them with file contents.
16+
*
17+
* @param text - The markdown text containing @include directives
18+
* @param readText - Function to read file contents
19+
* @param baseDir - Base directory for resolving relative paths
20+
* @returns The processed text with @include directives replaced
21+
*/
22+
async function processIncludeDirectives(
23+
text: string,
24+
readText: (filepath: string) => Promise<string>,
25+
baseDir: string,
26+
): Promise<string> {
27+
const includeRegex = /@include\s+"([^"]+)"/g;
28+
let result = text;
29+
let match;
30+
31+
while ((match = includeRegex.exec(text)) !== null) {
32+
const [fullMatch, filepath] = match;
33+
try {
34+
// Resolve the file path relative to baseDir
35+
const resolvedPath = resolve(baseDir, filepath);
36+
dbg(`processing @include directive: ${filepath} -> ${resolvedPath}`);
37+
38+
const includedContent = await readText(resolvedPath);
39+
result = result.replace(fullMatch, includedContent);
40+
} catch (error) {
41+
// If file reading fails, replace with a comment indicating the error
42+
const errorMsg = `<!-- Error including ${filepath}: ${error.message} -->`;
43+
dbg(`failed to include ${filepath}: ${error.message}`);
44+
result = result.replace(fullMatch, errorMsg);
45+
}
46+
}
47+
48+
return result;
49+
}
50+
1351
/**
1452
* Parses a markdown script file with frontmatter and transpiles it to GenAIScript.
1553
*
16-
* @param filename - The name of the file being processed
1754
* @param text - The raw text of the document, including optional frontmatter and content body
55+
* @param options - Optional configuration including file reading capabilities and base directory
1856
* @returns The transpiled JavaScript source code
1957
*
2058
* The parsing process:
2159
* - Splits the document into frontmatter and content using splitMarkdown
2260
* - Converts frontmatter to PromptArgs metadata
61+
* - Processes @include directives to inline file contents
2362
* - Converts content body to $ calls for the prompt using unified/remark AST processing
2463
*/
25-
export async function markdownScriptParse(text: string) {
26-
const { frontmatter = "", content = "" } = splitMarkdown(text);
64+
export async function markdownScriptParse(
65+
text: string,
66+
options?: {
67+
readText?: (filepath: string) => Promise<string>;
68+
baseDir?: string;
69+
},
70+
) {
71+
const { readText, baseDir = "." } = options || {};
72+
73+
// Process @include directives before splitting markdown
74+
let processedText = text;
75+
if (readText) {
76+
processedText = await processIncludeDirectives(text, readText, baseDir);
77+
}
78+
79+
const { frontmatter = "", content = "" } = splitMarkdown(processedText);
2780

2881
// Parse frontmatter as YAML and convert to PromptArgs
2982
const fm = frontmatter ? YAMLParse(frontmatter) : {};

packages/core/src/template.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { markdownScriptParse } from "./markdownscript.js";
1616
import { readJSON } from "./fs.js";
1717
import type { PromptArgs, PromptScript, McpServersConfig, McpServerConfig, McpAgentServersConfig, McpAgentServerConfig } from "./types.js";
1818
import { basename, resolve, dirname } from "node:path";
19+
import { readText } from "./fs.js";
1920

2021
/**
2122
* Extracts a template ID from the given filename by removing specific extensions
@@ -153,7 +154,10 @@ async function parsePromptTemplateCore(filename: string, content: string) {
153154
let jsSource: string;
154155
let meta: ReturnType<typeof parsePromptScriptMeta>;
155156
if (GENAI_MD_REGEX.test(filename)) {
156-
const res = await markdownScriptParse(content);
157+
const res = await markdownScriptParse(content, {
158+
readText,
159+
baseDir: dirname(filename),
160+
});
157161
meta = res.meta;
158162
jsSource = res.jsSource;
159163
} else {

0 commit comments

Comments
 (0)