Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion pkg/codingcontext/markdown/markdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,13 @@ func ParseMarkdownFile[T any](path string, frontMatter *T) (Markdown[T], error)
case 1: // Scanning frontmatter
if line == "---" {
state = 2 // End of frontmatter, start scanning content
// From here on, just copy everything as-is to content
} else {
if _, err := frontMatterBytes.WriteString(line + "\n"); err != nil {
return Markdown[T]{}, fmt.Errorf("failed to write frontmatter: %w", err)
}
}
case 2: // Scanning content
case 2: // Scanning content - copy everything as-is
if _, err := content.WriteString(line + "\n"); err != nil {
return Markdown[T]{}, fmt.Errorf("failed to write content: %w", err)
}
Expand Down
98 changes: 98 additions & 0 deletions pkg/codingcontext/markdown/markdown_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"path/filepath"
"strings"
"testing"

"github.com/kitproj/coding-context-cli/pkg/codingcontext/taskparser"
)

func TestParseMarkdownFile(t *testing.T) {
Expand Down Expand Up @@ -272,3 +274,99 @@ This task has no frontmatter.
})
}
}

func TestParseMarkdownFile_MultipleNewlinesAfterFrontmatter(t *testing.T) {
// This test verifies that multiple newlines after the frontmatter
// closing delimiter are handled correctly.
// The parser should:
// 1. Preserve multiple newlines between frontmatter and content
// 2. Strip a single newline (treating it as just a separator)
// 3. Allow the task parser to successfully parse content that starts with newlines
tests := []struct {
name string
content string
wantContent string
}{
{
name: "multiple newlines after frontmatter",
content: `---
{}
---

Start of context
`,
wantContent: "\nStart of context\n", // Content copied as-is after frontmatter
},
{
name: "single newline after frontmatter (baseline)",
content: `---
{}
---
Start of context
`,
wantContent: "Start of context\n", // Content copied as-is after frontmatter (newline after --- is preserved)
},
{
name: "three newlines after frontmatter",
content: `---
{}
---


Start of context
`,
wantContent: "\n\nStart of context\n", // Content copied as-is after frontmatter
},
{
name: "mixed whitespace after frontmatter",
content: `---
{}
---



Start of context
`,
wantContent: " \n\t \n\nStart of context\n", // Content copied as-is, preserving whitespace (newline after --- is preserved)
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create a temporary file
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "test.md")
if err := os.WriteFile(tmpFile, []byte(tt.content), 0o644); err != nil {
t.Fatalf("failed to create temp file: %v", err)
}

// Parse the file
var frontmatter BaseFrontMatter
md, err := ParseMarkdownFile(tmpFile, &frontmatter)
if err != nil {
t.Fatalf("ParseMarkdownFile() error = %v", err)
}

// Check content
if md.Content != tt.wantContent {
t.Errorf("ParseMarkdownFile() content = %q, want %q", md.Content, tt.wantContent)
}

// Verify that the content can be parsed as a task
// This is the actual use case - content is parsed as a task after frontmatter extraction
task, err := taskparser.ParseTask(md.Content)
if err != nil {
t.Fatalf("ParseTask() failed: %v, content = %q", err, md.Content)
}
if len(task) == 0 && strings.TrimSpace(md.Content) != "" {
t.Errorf("ParseTask() returned empty task for non-empty content: %q", md.Content)
}
// Verify that the parsed task content matches the original exactly
// The parser preserves all content including leading newlines
taskContent := task.String()
if taskContent != md.Content {
t.Errorf("ParseTask() then String() = %q, want %q", taskContent, md.Content)
}
})
}
}
3 changes: 2 additions & 1 deletion pkg/codingcontext/taskparser/grammar.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ type Argument struct {
// It can span multiple lines, consuming line content and newlines
// But it will stop before a newline that's followed by a slash (potential command)
type Text struct {
Lines []TextLine `parser:"@@+"`
LeadingNewlines []string `parser:"@Newline*"` // Leading newlines before any content (empty lines at the start)
Lines []TextLine `parser:"@@+"` // At least one line with actual content
}

// TextLine is a single line of text content (not starting with a slash)
Expand Down
5 changes: 5 additions & 0 deletions pkg/codingcontext/taskparser/taskparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,11 @@ func (s *SlashCommand) Params() Params {
// Content returns the text content with all lines concatenated
func (t *Text) Content() string {
var sb strings.Builder
// Write leading newlines first
for _, nl := range t.LeadingNewlines {
sb.WriteString(nl)
}
// Then write all the lines
for _, line := range t.Lines {
for _, tok := range line.NonSlashStart {
sb.WriteString(tok)
Expand Down