Skip to content

Commit 4b19eef

Browse files
Copilotpelikhan
andcommitted
Extract include expander to separate file (Phase 5)
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
1 parent d7741b4 commit 4b19eef

2 files changed

Lines changed: 178 additions & 171 deletions

File tree

pkg/parser/frontmatter.go

Lines changed: 0 additions & 171 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
package parser
22

33
import (
4-
"bufio"
5-
"bytes"
64
"fmt"
75
"os"
8-
"path/filepath"
96
"strings"
107

118
"github.com/githubnext/gh-aw/pkg/logger"
@@ -452,171 +449,3 @@ func processImportsFromFrontmatterWithManifestAndSource(frontmatter map[string]a
452449
ImportInputs: importInputs,
453450
}, nil
454451
}
455-
456-
// ExpandIncludes recursively expands @include and @import directives until no more remain
457-
// This matches the bash expand_includes function behavior
458-
func ExpandIncludes(content, baseDir string, extractTools bool) (string, error) {
459-
expandedContent, _, err := ExpandIncludesWithManifest(content, baseDir, extractTools)
460-
return expandedContent, err
461-
}
462-
463-
// ExpandIncludesWithManifest recursively expands @include and @import directives and returns list of included files
464-
func ExpandIncludesWithManifest(content, baseDir string, extractTools bool) (string, []string, error) {
465-
const maxDepth = 10
466-
currentContent := content
467-
visited := make(map[string]bool)
468-
469-
for depth := 0; depth < maxDepth; depth++ {
470-
// Process includes in current content
471-
processedContent, err := processIncludesWithVisited(currentContent, baseDir, extractTools, visited)
472-
if err != nil {
473-
return "", nil, err
474-
}
475-
476-
// For tools mode, check if we still have @include or @import directives
477-
if extractTools {
478-
if !strings.Contains(processedContent, "@include") && !strings.Contains(processedContent, "@import") {
479-
// No more includes to process for tools mode
480-
currentContent = processedContent
481-
break
482-
}
483-
} else {
484-
// For content mode, check if content changed
485-
if processedContent == currentContent {
486-
// No more includes to process
487-
break
488-
}
489-
}
490-
491-
currentContent = processedContent
492-
}
493-
494-
// Convert visited map to slice of file paths (make them relative to baseDir if possible)
495-
var includedFiles []string
496-
for filePath := range visited {
497-
// Try to make path relative to baseDir for cleaner output
498-
relPath, err := filepath.Rel(baseDir, filePath)
499-
if err == nil && !strings.HasPrefix(relPath, "..") {
500-
includedFiles = append(includedFiles, relPath)
501-
} else {
502-
includedFiles = append(includedFiles, filePath)
503-
}
504-
}
505-
506-
if extractTools {
507-
// For tools mode, merge all extracted JSON objects
508-
mergedTools, err := mergeToolsFromJSON(currentContent)
509-
return mergedTools, includedFiles, err
510-
}
511-
512-
return currentContent, includedFiles, nil
513-
}
514-
515-
// ExpandIncludesForEngines recursively expands @include and @import directives to extract engine configurations
516-
func ExpandIncludesForEngines(content, baseDir string) ([]string, error) {
517-
return expandIncludesForField(content, baseDir, extractEngineFromContent, "")
518-
}
519-
520-
// ExpandIncludesForSafeOutputs recursively expands @include and @import directives to extract safe-outputs configurations
521-
func ExpandIncludesForSafeOutputs(content, baseDir string) ([]string, error) {
522-
return expandIncludesForField(content, baseDir, extractSafeOutputsFromContent, "{}")
523-
}
524-
525-
// expandIncludesForField recursively expands includes to extract a specific frontmatter field
526-
func expandIncludesForField(content, baseDir string, extractFunc func(string) (string, error), emptyValue string) ([]string, error) {
527-
const maxDepth = 10
528-
var results []string
529-
currentContent := content
530-
531-
for depth := 0; depth < maxDepth; depth++ {
532-
// Process includes in current content to extract the field
533-
processedResults, processedContent, err := processIncludesForField(currentContent, baseDir, extractFunc, emptyValue)
534-
if err != nil {
535-
return nil, err
536-
}
537-
538-
// Add found results to the list
539-
results = append(results, processedResults...)
540-
541-
// Check if content changed
542-
if processedContent == currentContent {
543-
// No more includes to process
544-
break
545-
}
546-
547-
currentContent = processedContent
548-
}
549-
550-
return results, nil
551-
}
552-
553-
// ProcessIncludesForEngines processes import directives to extract engine configurations
554-
func ProcessIncludesForEngines(content, baseDir string) ([]string, string, error) {
555-
return processIncludesForField(content, baseDir, extractEngineFromContent, "")
556-
}
557-
558-
// ProcessIncludesForSafeOutputs processes import directives to extract safe-outputs configurations
559-
func ProcessIncludesForSafeOutputs(content, baseDir string) ([]string, string, error) {
560-
return processIncludesForField(content, baseDir, extractSafeOutputsFromContent, "{}")
561-
}
562-
563-
// processIncludesForField processes import directives to extract a specific frontmatter field
564-
func processIncludesForField(content, baseDir string, extractFunc func(string) (string, error), emptyValue string) ([]string, string, error) {
565-
scanner := bufio.NewScanner(strings.NewReader(content))
566-
var result bytes.Buffer
567-
var results []string
568-
569-
for scanner.Scan() {
570-
line := scanner.Text()
571-
572-
// Parse import directive
573-
directive := ParseImportDirective(line)
574-
if directive != nil {
575-
isOptional := directive.IsOptional
576-
includePath := directive.Path
577-
578-
// Handle section references (file.md#Section) - for frontmatter fields, we ignore sections
579-
var filePath string
580-
if strings.Contains(includePath, "#") {
581-
parts := strings.SplitN(includePath, "#", 2)
582-
filePath = parts[0]
583-
// Note: section references are ignored for frontmatter field extraction
584-
} else {
585-
filePath = includePath
586-
}
587-
588-
// Resolve file path
589-
fullPath, err := ResolveIncludePath(filePath, baseDir, nil)
590-
if err != nil {
591-
if isOptional {
592-
// For optional includes, skip extraction
593-
continue
594-
}
595-
// For required includes, fail compilation with an error
596-
return nil, "", fmt.Errorf("failed to resolve required include '%s': %w", filePath, err)
597-
}
598-
599-
// Read the included file
600-
fileContent, err := os.ReadFile(fullPath)
601-
if err != nil {
602-
// For any processing errors, fail compilation
603-
return nil, "", fmt.Errorf("failed to read included file '%s': %w", fullPath, err)
604-
}
605-
606-
// Extract the field using the provided extraction function
607-
fieldJSON, err := extractFunc(string(fileContent))
608-
if err != nil {
609-
return nil, "", fmt.Errorf("failed to extract field from '%s': %w", fullPath, err)
610-
}
611-
612-
if fieldJSON != "" && fieldJSON != emptyValue {
613-
results = append(results, fieldJSON)
614-
}
615-
} else {
616-
// Regular line, just pass through
617-
result.WriteString(line + "\n")
618-
}
619-
}
620-
621-
return results, result.String(), nil
622-
}

pkg/parser/include_expander.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package parser
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
)
11+
12+
// ExpandIncludes recursively expands @include and @import directives until no more remain
13+
// This matches the bash expand_includes function behavior
14+
func ExpandIncludes(content, baseDir string, extractTools bool) (string, error) {
15+
expandedContent, _, err := ExpandIncludesWithManifest(content, baseDir, extractTools)
16+
return expandedContent, err
17+
}
18+
19+
// ExpandIncludesWithManifest recursively expands @include and @import directives and returns list of included files
20+
func ExpandIncludesWithManifest(content, baseDir string, extractTools bool) (string, []string, error) {
21+
const maxDepth = 10
22+
currentContent := content
23+
visited := make(map[string]bool)
24+
25+
for depth := 0; depth < maxDepth; depth++ {
26+
// Process includes in current content
27+
processedContent, err := processIncludesWithVisited(currentContent, baseDir, extractTools, visited)
28+
if err != nil {
29+
return "", nil, err
30+
}
31+
32+
// For tools mode, check if we still have @include or @import directives
33+
if extractTools {
34+
if !strings.Contains(processedContent, "@include") && !strings.Contains(processedContent, "@import") {
35+
// No more includes to process for tools mode
36+
currentContent = processedContent
37+
break
38+
}
39+
} else {
40+
// For content mode, check if content changed
41+
if processedContent == currentContent {
42+
// No more includes to process
43+
break
44+
}
45+
}
46+
47+
currentContent = processedContent
48+
}
49+
50+
// Convert visited map to slice of file paths (make them relative to baseDir if possible)
51+
var includedFiles []string
52+
for filePath := range visited {
53+
// Try to make path relative to baseDir for cleaner output
54+
relPath, err := filepath.Rel(baseDir, filePath)
55+
if err == nil && !strings.HasPrefix(relPath, "..") {
56+
includedFiles = append(includedFiles, relPath)
57+
} else {
58+
includedFiles = append(includedFiles, filePath)
59+
}
60+
}
61+
62+
if extractTools {
63+
// For tools mode, merge all extracted JSON objects
64+
mergedTools, err := mergeToolsFromJSON(currentContent)
65+
return mergedTools, includedFiles, err
66+
}
67+
68+
return currentContent, includedFiles, nil
69+
}
70+
71+
// ExpandIncludesForEngines recursively expands @include and @import directives to extract engine configurations
72+
func ExpandIncludesForEngines(content, baseDir string) ([]string, error) {
73+
return expandIncludesForField(content, baseDir, extractEngineFromContent, "")
74+
}
75+
76+
// ExpandIncludesForSafeOutputs recursively expands @include and @import directives to extract safe-outputs configurations
77+
func ExpandIncludesForSafeOutputs(content, baseDir string) ([]string, error) {
78+
return expandIncludesForField(content, baseDir, extractSafeOutputsFromContent, "{}")
79+
}
80+
81+
// expandIncludesForField recursively expands includes to extract a specific frontmatter field
82+
func expandIncludesForField(content, baseDir string, extractFunc func(string) (string, error), emptyValue string) ([]string, error) {
83+
const maxDepth = 10
84+
var results []string
85+
currentContent := content
86+
87+
for depth := 0; depth < maxDepth; depth++ {
88+
// Process includes in current content to extract the field
89+
processedResults, processedContent, err := processIncludesForField(currentContent, baseDir, extractFunc, emptyValue)
90+
if err != nil {
91+
return nil, err
92+
}
93+
94+
// Add found results to the list
95+
results = append(results, processedResults...)
96+
97+
// Check if content changed
98+
if processedContent == currentContent {
99+
// No more includes to process
100+
break
101+
}
102+
103+
currentContent = processedContent
104+
}
105+
106+
return results, nil
107+
}
108+
109+
// ProcessIncludesForEngines processes import directives to extract engine configurations
110+
func ProcessIncludesForEngines(content, baseDir string) ([]string, string, error) {
111+
return processIncludesForField(content, baseDir, extractEngineFromContent, "")
112+
}
113+
114+
// ProcessIncludesForSafeOutputs processes import directives to extract safe-outputs configurations
115+
func ProcessIncludesForSafeOutputs(content, baseDir string) ([]string, string, error) {
116+
return processIncludesForField(content, baseDir, extractSafeOutputsFromContent, "{}")
117+
}
118+
119+
// processIncludesForField processes import directives to extract a specific frontmatter field
120+
func processIncludesForField(content, baseDir string, extractFunc func(string) (string, error), emptyValue string) ([]string, string, error) {
121+
scanner := bufio.NewScanner(strings.NewReader(content))
122+
var result bytes.Buffer
123+
var results []string
124+
125+
for scanner.Scan() {
126+
line := scanner.Text()
127+
128+
// Parse import directive
129+
directive := ParseImportDirective(line)
130+
if directive != nil {
131+
isOptional := directive.IsOptional
132+
includePath := directive.Path
133+
134+
// Handle section references (file.md#Section) - for frontmatter fields, we ignore sections
135+
var filePath string
136+
if strings.Contains(includePath, "#") {
137+
parts := strings.SplitN(includePath, "#", 2)
138+
filePath = parts[0]
139+
// Note: section references are ignored for frontmatter field extraction
140+
} else {
141+
filePath = includePath
142+
}
143+
144+
// Resolve file path
145+
fullPath, err := ResolveIncludePath(filePath, baseDir, nil)
146+
if err != nil {
147+
if isOptional {
148+
// For optional includes, skip extraction
149+
continue
150+
}
151+
// For required includes, fail compilation with an error
152+
return nil, "", fmt.Errorf("failed to resolve required include '%s': %w", filePath, err)
153+
}
154+
155+
// Read the included file
156+
fileContent, err := os.ReadFile(fullPath)
157+
if err != nil {
158+
// For any processing errors, fail compilation
159+
return nil, "", fmt.Errorf("failed to read included file '%s': %w", fullPath, err)
160+
}
161+
162+
// Extract the field using the provided extraction function
163+
fieldJSON, err := extractFunc(string(fileContent))
164+
if err != nil {
165+
return nil, "", fmt.Errorf("failed to extract field from '%s': %w", fullPath, err)
166+
}
167+
168+
if fieldJSON != "" && fieldJSON != emptyValue {
169+
results = append(results, fieldJSON)
170+
}
171+
} else {
172+
// Regular line, just pass through
173+
result.WriteString(line + "\n")
174+
}
175+
}
176+
177+
return results, result.String(), nil
178+
}

0 commit comments

Comments
 (0)