Skip to content
Merged
42 changes: 40 additions & 2 deletions pkg/parser/frontmatter_content.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func ExtractFrontmatterFromContent(content string) (*FrontmatterResult, error) {
}

// Check if file starts with frontmatter delimiter.
if strings.TrimSpace(firstLine) != "---" {
if !isFrontmatterDelimiterLine(firstLine) {
log.Print("No frontmatter delimiter found, returning content as markdown")
// No frontmatter, return entire content as markdown
return &FrontmatterResult{
Expand Down Expand Up @@ -62,7 +62,7 @@ func ExtractFrontmatterFromContent(content string) (*FrontmatterResult, error) {
}
}

if strings.TrimSpace(content[lineStart:lineEnd]) == "---" {
if isFrontmatterDelimiterLine(content[lineStart:lineEnd]) {
frontmatterEndStart = lineStart
markdownStart = nextCursor
break
Expand Down Expand Up @@ -122,6 +122,44 @@ func ExtractFrontmatterFromContent(content string) (*FrontmatterResult, error) {
}, nil
}

// isFrontmatterDelimiterLine returns true when a line consists of "---" with optional surrounding whitespace.
func isFrontmatterDelimiterLine(line string) bool {
// Fast path for common delimiters.
if line == "---" || line == "---\r" {
return true
}

// Fast path for ASCII-trimmable whitespace.
start, end := 0, len(line)
for start < end {
switch line[start] {
case ' ', '\t', '\n', '\r', '\v', '\f':
start++
default:
goto leftTrimmed
}
}
leftTrimmed:
if start >= end {
return false
}
for end > start {
switch line[end-1] {
case ' ', '\t', '\n', '\r', '\v', '\f':
end--
default:
goto rightTrimmed
}
}
rightTrimmed:
if end-start == 3 && line[start] == '-' && line[start+1] == '-' && line[start+2] == '-' {
return true
}

// Fallback keeps previous behavior for uncommon Unicode whitespace.
return strings.TrimSpace(line) == "---"
}

// ExtractFrontmatterFromBuiltinFile is a caching wrapper around ExtractFrontmatterFromContent
// for builtin virtual files. Because builtin files are registered once at startup and never
// change, the parsed FrontmatterResult is identical across calls. This function caches the
Expand Down
8 changes: 8 additions & 0 deletions pkg/parser/frontmatter_extraction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ This is a test workflow with empty frontmatter.`,
},
wantMarkdown: "# Content",
},
{
name: "frontmatter delimiters with surrounding whitespace",
content: " \t--- \t\r\non: push\r\n \t--- \t\r\n\r\n# Test Workflow\r\n",
wantYAML: map[string]any{
"on": "push",
},
wantMarkdown: "# Test Workflow",
},
}

for _, tt := range tests {
Expand Down
7 changes: 4 additions & 3 deletions pkg/workflow/compiler_orchestrator_frontmatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,21 @@ func (c *Compiler) parseFrontmatterSection(markdownPath string) (*frontmatterPar
// Intentionally not wrapping to avoid exposing internal path details
return nil, fmt.Errorf("failed to read file: %v", err) //nolint:errorlint // intentionally not wrapping to avoid exposing os.PathError
}
contentString := string(content)

log.Printf("File size: %d bytes", len(content))

// Parse frontmatter and markdown
orchestratorFrontmatterLog.Printf("Parsing frontmatter from file: %s", cleanPath)
result, err := parser.ExtractFrontmatterFromContent(string(content))
result, err := parser.ExtractFrontmatterFromContent(contentString)
if err != nil {
orchestratorFrontmatterLog.Printf("Frontmatter extraction failed: %v", err)
// Use FrontmatterStart from result if available, otherwise default to line 2 (after opening ---)
frontmatterStart := 2
if result != nil && result.FrontmatterStart > 0 {
frontmatterStart = result.FrontmatterStart
}
return nil, c.createFrontmatterError(cleanPath, string(content), err, frontmatterStart)
return nil, c.createFrontmatterError(cleanPath, contentString, err, frontmatterStart)
}

if len(result.Frontmatter) == 0 {
Expand All @@ -62,7 +63,7 @@ func (c *Compiler) parseFrontmatterSection(markdownPath string) (*frontmatterPar
}

// Preprocess schedule fields to convert human-friendly format to cron expressions
if err := c.preprocessScheduleFields(result.Frontmatter, cleanPath, string(content)); err != nil {
if err := c.preprocessScheduleFields(result.Frontmatter, cleanPath, contentString); err != nil {
orchestratorFrontmatterLog.Printf("Schedule preprocessing failed: %v", err)
return nil, err
}
Expand Down
Loading