Skip to content

Commit 0517d04

Browse files
fix: Preserve leading newlines in task parser content (#200)
The task parser wasn't properly handling newlines after the end of frontmatter marker. For instance: ```markdown --- {} --- content ``` Would fail with a task parsing with an error about unexpected newlines. Co-authored-by: Alex Collins <alexec@users.noreply.github.com>
1 parent 5e3c6b3 commit 0517d04

4 files changed

Lines changed: 107 additions & 2 deletions

File tree

pkg/codingcontext/markdown/markdown.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,13 @@ func ParseMarkdownFile[T any](path string, frontMatter *T) (Markdown[T], error)
5555
case 1: // Scanning frontmatter
5656
if line == "---" {
5757
state = 2 // End of frontmatter, start scanning content
58+
// From here on, just copy everything as-is to content
5859
} else {
5960
if _, err := frontMatterBytes.WriteString(line + "\n"); err != nil {
6061
return Markdown[T]{}, fmt.Errorf("failed to write frontmatter: %w", err)
6162
}
6263
}
63-
case 2: // Scanning content
64+
case 2: // Scanning content - copy everything as-is
6465
if _, err := content.WriteString(line + "\n"); err != nil {
6566
return Markdown[T]{}, fmt.Errorf("failed to write content: %w", err)
6667
}

pkg/codingcontext/markdown/markdown_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"path/filepath"
66
"strings"
77
"testing"
8+
9+
"github.com/kitproj/coding-context-cli/pkg/codingcontext/taskparser"
810
)
911

1012
func TestParseMarkdownFile(t *testing.T) {
@@ -272,3 +274,99 @@ This task has no frontmatter.
272274
})
273275
}
274276
}
277+
278+
func TestParseMarkdownFile_MultipleNewlinesAfterFrontmatter(t *testing.T) {
279+
// This test verifies that multiple newlines after the frontmatter
280+
// closing delimiter are handled correctly.
281+
// The parser should:
282+
// 1. Preserve multiple newlines between frontmatter and content
283+
// 2. Strip a single newline (treating it as just a separator)
284+
// 3. Allow the task parser to successfully parse content that starts with newlines
285+
tests := []struct {
286+
name string
287+
content string
288+
wantContent string
289+
}{
290+
{
291+
name: "multiple newlines after frontmatter",
292+
content: `---
293+
{}
294+
---
295+
296+
Start of context
297+
`,
298+
wantContent: "\nStart of context\n", // Content copied as-is after frontmatter
299+
},
300+
{
301+
name: "single newline after frontmatter (baseline)",
302+
content: `---
303+
{}
304+
---
305+
Start of context
306+
`,
307+
wantContent: "Start of context\n", // Content copied as-is after frontmatter (newline after --- is preserved)
308+
},
309+
{
310+
name: "three newlines after frontmatter",
311+
content: `---
312+
{}
313+
---
314+
315+
316+
Start of context
317+
`,
318+
wantContent: "\n\nStart of context\n", // Content copied as-is after frontmatter
319+
},
320+
{
321+
name: "mixed whitespace after frontmatter",
322+
content: `---
323+
{}
324+
---
325+
326+
327+
328+
Start of context
329+
`,
330+
wantContent: " \n\t \n\nStart of context\n", // Content copied as-is, preserving whitespace (newline after --- is preserved)
331+
},
332+
}
333+
334+
for _, tt := range tests {
335+
t.Run(tt.name, func(t *testing.T) {
336+
// Create a temporary file
337+
tmpDir := t.TempDir()
338+
tmpFile := filepath.Join(tmpDir, "test.md")
339+
if err := os.WriteFile(tmpFile, []byte(tt.content), 0o644); err != nil {
340+
t.Fatalf("failed to create temp file: %v", err)
341+
}
342+
343+
// Parse the file
344+
var frontmatter BaseFrontMatter
345+
md, err := ParseMarkdownFile(tmpFile, &frontmatter)
346+
if err != nil {
347+
t.Fatalf("ParseMarkdownFile() error = %v", err)
348+
}
349+
350+
// Check content
351+
if md.Content != tt.wantContent {
352+
t.Errorf("ParseMarkdownFile() content = %q, want %q", md.Content, tt.wantContent)
353+
}
354+
355+
// Verify that the content can be parsed as a task
356+
// This is the actual use case - content is parsed as a task after frontmatter extraction
357+
task, err := taskparser.ParseTask(md.Content)
358+
if err != nil {
359+
t.Fatalf("ParseTask() failed: %v, content = %q", err, md.Content)
360+
}
361+
if len(task) == 0 && strings.TrimSpace(md.Content) != "" {
362+
t.Errorf("ParseTask() returned empty task for non-empty content: %q", md.Content)
363+
}
364+
// Verify that the parsed task content matches the original exactly
365+
// The parser preserves all content including leading newlines
366+
taskContent := task.String()
367+
if taskContent != md.Content {
368+
t.Errorf("ParseTask() then String() = %q, want %q", taskContent, md.Content)
369+
}
370+
})
371+
}
372+
}

pkg/codingcontext/taskparser/grammar.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ type Argument struct {
3838
// It can span multiple lines, consuming line content and newlines
3939
// But it will stop before a newline that's followed by a slash (potential command)
4040
type Text struct {
41-
Lines []TextLine `parser:"@@+"`
41+
LeadingNewlines []string `parser:"@Newline*"` // Leading newlines before any content (empty lines at the start)
42+
Lines []TextLine `parser:"@@+"` // At least one line with actual content
4243
}
4344

4445
// TextLine is a single line of text content (not starting with a slash)

pkg/codingcontext/taskparser/taskparser.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,11 @@ func (s *SlashCommand) Params() Params {
160160
// Content returns the text content with all lines concatenated
161161
func (t *Text) Content() string {
162162
var sb strings.Builder
163+
// Write leading newlines first
164+
for _, nl := range t.LeadingNewlines {
165+
sb.WriteString(nl)
166+
}
167+
// Then write all the lines
163168
for _, line := range t.Lines {
164169
for _, tok := range line.NonSlashStart {
165170
sb.WriteString(tok)

0 commit comments

Comments
 (0)