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 ;
0 commit comments