Skip to content

Commit e892104

Browse files
lb-vincentlactions-user
authored andcommitted
Add readMarkdownWithLinks filter
1 parent 48ffb65 commit e892104

File tree

4 files changed

+596
-0
lines changed

4 files changed

+596
-0
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
??? note "Plugin Code: readMarkdownWithLinks"
2+
```javascript
3+
--8<-- "plugins/filters/readMarkdownWithLinks/index.js"
4+
```
5+
<div class="result" markdown>
6+
<span>
7+
</span>
8+
</div>
9+
10+
The main use case for this plugin is enhancing LinearB AI code reviews with comprehensive documentation context.
11+
12+
### Basic Usage
13+
```yaml
14+
guidelines: |
15+
{{ "REVIEW_RULES.md" | readMarkdownWithLinks }}
16+
17+
Additional Context:
18+
{{ "README.md" | readMarkdownWithLinks(maxDepth=2) }}
19+
```
20+
21+
## Configuration Options
22+
23+
- `followLinks` (boolean, default: `true`): Whether to follow internal markdown links
24+
- `maxDepth` (number, default: `3`): Maximum depth to follow links to prevent excessive recursion
25+
26+
## API
27+
28+
### `readMarkdownWithLinks(filePath, options)`
29+
30+
Returns the combined content of the main file and all linked files as a formatted string.
31+
32+
### `readMarkdown(filePath, options)`
33+
34+
Returns a structured object containing:
35+
- `path`: Absolute path to the file
36+
- `content`: File content
37+
- `error`: Any error encountered
38+
- `linkedFiles`: Array of linked file objects with the same structure
39+
40+
## Example Output
41+
```
42+
=== main.md ===
43+
# Main Document
44+
Content of main document...
45+
46+
=== related.md ===
47+
# Related Document
48+
Content of related document...
49+
50+
=== subdoc.md ===
51+
# Sub Document
52+
Content of sub document...
53+
```
54+
55+
56+
??? example "gitStream CM Example: readMarkdownWithLinks"
57+
```yaml+jinja
58+
--8<-- "plugins/filters/readMarkdownWithLinks/read_markdown_with_links.cm"
59+
```
60+
<div class="result" markdown>
61+
<span>
62+
</span>
63+
</div>
64+
65+
[Download Source Code](https://github.com/linear-b/gitstream/tree/main/plugins/filters/readMarkdownWithLinks)
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
4+
/**
5+
* Read file function - in GitStream environment this would be provided
6+
* For standalone testing, we use fs.readFileSync
7+
* @param {string} filePath - Path to file to read
8+
* @returns {string|null} File content or null if error
9+
*/
10+
function readFile(filePath) {
11+
try {
12+
// In GitStream, replace this with: return gitstream.readFile(filePath);
13+
return fs.readFileSync(filePath, 'utf8');
14+
} catch (error) {
15+
console.log(`Error reading file ${filePath}: ${error.message}`);
16+
return null;
17+
}
18+
}
19+
20+
/**
21+
* Extract internal markdown links from content
22+
* Matches patterns like [text](./file.md) or [text](../file.md) or [text](file.md)
23+
* @param {string} content - The markdown content to scan for links
24+
* @param {string} basePath - Base directory path for resolving relative links
25+
* @returns {Array} Array of link objects with text, path, and resolvedPath
26+
*/
27+
function extractInternalLinks(content, basePath) {
28+
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
29+
const internalLinks = [];
30+
let match;
31+
32+
while ((match = linkRegex.exec(content)) !== null) {
33+
const linkText = match[1];
34+
const linkPath = match[2];
35+
36+
// Check if it's an internal link (not http/https and ends with .md)
37+
if (!linkPath.startsWith('http') && linkPath.endsWith('.md')) {
38+
const resolvedPath = path.resolve(basePath, linkPath);
39+
internalLinks.push({
40+
text: linkText,
41+
path: linkPath,
42+
resolvedPath: resolvedPath
43+
});
44+
}
45+
}
46+
47+
return internalLinks;
48+
}
49+
50+
/**
51+
* Read markdown file and follow internal links
52+
* @param {string} filePath - Path to the markdown file
53+
* @param {Object} options - Configuration options
54+
* @param {boolean} options.followLinks - Whether to follow internal links (default: true)
55+
* @param {number} options.maxDepth - Maximum depth to follow links (default: 3)
56+
* @param {Set} options.visited - Internal set to track visited files (prevent cycles)
57+
* @param {number} options.currentDepth - Current depth (internal)
58+
* @returns {Object} Object containing content and linked files
59+
*/
60+
function readMarkdown(filePath, options = {}) {
61+
const {
62+
followLinks = true,
63+
maxDepth = 3,
64+
visited = new Set(),
65+
currentDepth = 0
66+
} = options;
67+
68+
// Resolve the absolute path
69+
const absolutePath = path.resolve(filePath);
70+
71+
// Check if we've already visited this file (prevent cycles)
72+
if (visited.has(absolutePath)) {
73+
return {
74+
path: absolutePath,
75+
content: null,
76+
error: 'Circular reference detected',
77+
linkedFiles: []
78+
};
79+
}
80+
81+
// Check depth limit
82+
if (currentDepth >= maxDepth) {
83+
return {
84+
path: absolutePath,
85+
content: readFile(absolutePath),
86+
error: null,
87+
linkedFiles: [],
88+
depthLimitReached: true
89+
};
90+
}
91+
92+
// Mark this file as visited
93+
visited.add(absolutePath);
94+
95+
// Read the main file content
96+
const content = readFile(absolutePath);
97+
if (!content) {
98+
return {
99+
path: absolutePath,
100+
content: null,
101+
error: 'File not found or could not be read',
102+
linkedFiles: []
103+
};
104+
}
105+
106+
const result = {
107+
path: absolutePath,
108+
content: content,
109+
error: null,
110+
linkedFiles: []
111+
};
112+
113+
// If we should follow links, extract and process them
114+
if (followLinks) {
115+
const basePath = path.dirname(absolutePath);
116+
const internalLinks = extractInternalLinks(content, basePath);
117+
118+
for (const link of internalLinks) {
119+
const linkedFileResult = readMarkdown(link.resolvedPath, {
120+
followLinks,
121+
maxDepth,
122+
visited: new Set(visited), // Create a new set for each branch
123+
currentDepth: currentDepth + 1
124+
});
125+
126+
result.linkedFiles.push({
127+
linkText: link.text,
128+
originalPath: link.path,
129+
...linkedFileResult
130+
});
131+
}
132+
}
133+
134+
return result;
135+
}
136+
137+
/**
138+
* @module readMarkdownWithLinks
139+
* @description Reads a markdown file and follows internal links to create a comprehensive document view.
140+
* Uses GitStream's readFile under the hood but extends it to recursively follow markdown link references.
141+
* Prevents circular references and supports configurable depth limits.
142+
* @param {string} filePath - Path to the markdown file to read
143+
* @param {Object} [options={}] - Configuration options for link following
144+
* @param {boolean} [options.followLinks=true] - Whether to follow internal links
145+
* @param {number} [options.maxDepth=3] - Maximum depth to follow links
146+
* @param {boolean} [options.structured=false] - Return structured data instead of combined text
147+
* @returns {string} Combined content of the file and all linked files with headers
148+
* @example {{ "docs/README.md" | readMarkdownWithLinks }}
149+
* @example {{ "docs/README.md" | readMarkdownWithLinks(maxDepth=2) }}
150+
* @license MIT
151+
*/
152+
function readMarkdownWithLinks(filePath, options = {}) {
153+
const {
154+
followLinks = true,
155+
maxDepth = 3,
156+
structured = false
157+
} = options;
158+
159+
const result = readMarkdown(filePath, {
160+
followLinks,
161+
maxDepth,
162+
visited: new Set(),
163+
currentDepth: 0
164+
});
165+
166+
// Return structured data if requested
167+
if (structured) {
168+
return result;
169+
}
170+
171+
// Otherwise return combined content
172+
function combineContent(fileResult, depth = 0) {
173+
const indent = ' '.repeat(depth);
174+
let combined = '';
175+
176+
if (fileResult.content) {
177+
combined += `${indent}=== ${path.basename(fileResult.path)} ===\n`;
178+
combined += fileResult.content + '\n\n';
179+
}
180+
181+
if (fileResult.linkedFiles) {
182+
for (const linkedFile of fileResult.linkedFiles) {
183+
combined += combineContent(linkedFile, depth + 1);
184+
}
185+
}
186+
187+
return combined;
188+
}
189+
190+
return combineContent(result);
191+
}
192+
193+
module.exports = readMarkdownWithLinks;
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# -*- mode: yaml -*-
2+
# Example GitStream configuration using readMarkdownWithLinks for LinearB AI code reviews
3+
# This shows how to enhance AI code reviews with comprehensive documentation context
4+
5+
manifest:
6+
version: 1.0
7+
8+
automations:
9+
# Enhanced AI code review with comprehensive documentation context
10+
ai_review_with_full_docs:
11+
if:
12+
- {{ not pr.draft }}
13+
- {{ pr.files | match(regex=r".*\.(js|ts|py|go|java|cpp|cs)") | some }}
14+
run:
15+
- action: code-review@v1
16+
args:
17+
guidelines: |
18+
Code Review Guidelines:
19+
{{ "REVIEW_RULES.md" | readMarkdownWithLinks | dump }}
20+
21+
Project Documentation Context:
22+
{{ "README.md" | readMarkdownWithLinks(maxDepth=2) | dump }}
23+
24+
Architecture and Design:
25+
{{ "docs/ARCHITECTURE.md" | readMarkdownWithLinks(maxDepth=1) | dump }}
26+
27+
# Context-aware reviews based on changed file areas
28+
contextual_ai_review:
29+
if:
30+
- {{ not pr.draft }}
31+
run:
32+
- action: code-review@v1
33+
args:
34+
guidelines: |
35+
Base Review Guidelines:
36+
{{ "REVIEW_RULES.md" | readMarkdownWithLinks | dump }}
37+
38+
{% if pr.files | match(regex=r"src/api/.*") | some %}
39+
API-Specific Guidelines and Documentation:
40+
{{ "docs/api/README.md" | readMarkdownWithLinks | dump }}
41+
{% endif %}
42+
43+
{% if pr.files | match(regex=r".*test.*") | some %}
44+
Testing Standards and Guidelines:
45+
{{ "docs/testing/README.md" | readMarkdownWithLinks | dump }}
46+
{% endif %}
47+
48+
{% if pr.files | match(regex=r".*security.*") | some %}
49+
Security Guidelines:
50+
{{ "docs/security/GUIDELINES.md" | readMarkdownWithLinks | dump }}
51+
{% endif %}
52+
53+
# Large PR reviews with extensive context
54+
comprehensive_review_large_prs:
55+
if:
56+
- {{ not pr.draft }}
57+
- {{ pr.files | length > 10 }} # Large changes
58+
run:
59+
- action: code-review@v1
60+
args:
61+
guidelines: |
62+
Comprehensive Review Guidelines for Large Changes:
63+
{{ "REVIEW_RULES.md" | readMarkdownWithLinks | dump }}
64+
65+
Full Project Context:
66+
{{ "README.md" | readMarkdownWithLinks(maxDepth=1) | dump }}
67+
68+
Contributing Guidelines:
69+
{{ "CONTRIBUTING.md" | readMarkdownWithLinks | dump }}
70+
71+
Architecture Documentation:
72+
{{ "docs/ARCHITECTURE.md" | readMarkdownWithLinks(maxDepth=2) | dump }}
73+
74+
# First-time contributor guidance
75+
enhanced_review_new_contributors:
76+
if:
77+
- {{ not pr.draft }}
78+
- {{ pr.author_is_new_contributor }}
79+
run:
80+
- action: code-review@v1
81+
args:
82+
guidelines: |
83+
Welcome! Here are our code review guidelines with full context:
84+
{{ "REVIEW_RULES.md" | readMarkdownWithLinks | dump }}
85+
86+
New Contributor Guide:
87+
{{ "docs/ONBOARDING.md" | readMarkdownWithLinks | dump }}
88+
89+
Project Overview:
90+
{{ "README.md" | readMarkdownWithLinks(maxDepth=1) | dump }}
91+
92+
Contributing Guidelines:
93+
{{ "CONTRIBUTING.md" | readMarkdownWithLinks(maxDepth=1) | dump }}

0 commit comments

Comments
 (0)