package markdown import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNewService(t *testing.T) { svc := NewService() assert.NotNil(t, svc) } func TestValidateContent(t *testing.T) { svc := NewService() tests := []struct { name string content string wantErr bool }{ { name: "valid markdown", content: "# Hello\n\nThis is **bold** text.", wantErr: false, }, { name: "empty content", content: "", wantErr: false, }, { name: "complex markdown", content: "# Title\n\n- List item 1\n- List item 2\n\n```go\ncode block\n```", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := svc.ValidateContent([]byte(tt.content)) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) } }) } } func TestGenerateSnippet(t *testing.T) { svc := NewService() tests := []struct { name string content string maxLength int expected string }{ { name: "simple text", content: "Hello world", maxLength: 100, expected: "Hello world", }, { name: "text with formatting", content: "This is **bold** and *italic* text.", maxLength: 100, expected: "This is bold and italic text.", }, { name: "truncate long text", content: "This is a very long piece of text that should be truncated at a word boundary.", maxLength: 30, expected: "This is a very long piece of ...", }, { name: "heading and paragraph", content: "# My Title\n\nThis is the first paragraph.", maxLength: 100, expected: "My Title This is the first paragraph.", }, { name: "code block removed", content: "Text before\n\n```go\ncode\n```\n\nText after", maxLength: 100, expected: "Text before Text after", }, { name: "list items", content: "- Item 1\n- Item 2\n- Item 3", maxLength: 100, expected: "Item 1 Item 2 Item 3", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { snippet, err := svc.GenerateSnippet([]byte(tt.content), tt.maxLength) require.NoError(t, err) assert.Equal(t, tt.expected, snippet) }) } } func TestExtractProperties(t *testing.T) { tests := []struct { name string content string withExt bool hasLink bool hasCode bool hasTasks bool hasInc bool }{ { name: "plain text", content: "Just plain text", withExt: false, hasLink: false, hasCode: false, hasTasks: false, hasInc: false, }, { name: "with link", content: "Check out [this link](https://example.com)", withExt: false, hasLink: true, hasCode: false, hasTasks: false, hasInc: false, }, { name: "with inline code", content: "Use `console.log()` to debug", withExt: false, hasLink: false, hasCode: true, hasTasks: false, hasInc: false, }, { name: "with code block", content: "```go\nfunc main() {}\n```", withExt: false, hasLink: false, hasCode: true, hasTasks: false, hasInc: false, }, { name: "with completed task", content: "- [x] Completed task", withExt: false, hasLink: false, hasCode: false, hasTasks: true, hasInc: false, }, { name: "with incomplete task", content: "- [ ] Todo item", withExt: false, hasLink: false, hasCode: false, hasTasks: true, hasInc: true, }, { name: "mixed tasks", content: "- [x] Done\n- [ ] Not done", withExt: false, hasLink: false, hasCode: false, hasTasks: true, hasInc: true, }, { name: "with referenced content", content: "See [[memos/1]] for details", withExt: true, hasLink: true, hasCode: false, hasTasks: false, hasInc: false, }, { name: "everything", content: "# Title\n\n[Link](url)\n\n`code`\n\n- [ ] Task", withExt: false, hasLink: true, hasCode: true, hasTasks: true, hasInc: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var svc Service if tt.withExt { svc = NewService(WithWikilinkExtension()) } else { svc = NewService() } props, err := svc.ExtractProperties([]byte(tt.content)) require.NoError(t, err) assert.Equal(t, tt.hasLink, props.HasLink, "HasLink") assert.Equal(t, tt.hasCode, props.HasCode, "HasCode") assert.Equal(t, tt.hasTasks, props.HasTaskList, "HasTaskList") assert.Equal(t, tt.hasInc, props.HasIncompleteTasks, "HasIncompleteTasks") }) } } func TestExtractTags(t *testing.T) { tests := []struct { name string content string withExt bool expected []string }{ { name: "no tags", content: "Just plain text", withExt: false, expected: []string{}, }, { name: "single tag", content: "Text with #tag", withExt: true, expected: []string{"tag"}, }, { name: "multiple tags", content: "Text with #tag1 and #tag2", withExt: true, expected: []string{"tag1", "tag2"}, }, { name: "duplicate tags", content: "#work is important. #Work #WORK", withExt: true, expected: []string{"work"}, // Deduplicated and lowercased }, { name: "tags with hyphens and underscores", content: "Tags: #work-notes #2024_plans", withExt: true, expected: []string{"work-notes", "2024_plans"}, }, { name: "tags at end of sentence", content: "This is important #urgent.", withExt: true, expected: []string{"urgent"}, }, { name: "headings not tags", content: "## Heading\n\n# Title\n\nText with #realtag", withExt: true, expected: []string{"realtag"}, }, { name: "numeric tag", content: "Issue #123", withExt: true, expected: []string{"123"}, }, { name: "tag in list", content: "- Item 1 #todo\n- Item 2 #done", withExt: true, expected: []string{"todo", "done"}, }, { name: "no extension enabled", content: "Text with #tag", withExt: false, expected: []string{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var svc Service if tt.withExt { svc = NewService(WithTagExtension()) } else { svc = NewService() } tags, err := svc.ExtractTags([]byte(tt.content)) require.NoError(t, err) assert.ElementsMatch(t, tt.expected, tags) }) } } func TestUniqueLowercase(t *testing.T) { tests := []struct { name string input []string expected []string }{ { name: "empty", input: []string{}, expected: []string{}, }, { name: "unique items", input: []string{"tag1", "tag2", "tag3"}, expected: []string{"tag1", "tag2", "tag3"}, }, { name: "duplicates", input: []string{"tag", "TAG", "Tag"}, expected: []string{"tag"}, }, { name: "mixed", input: []string{"Work", "work", "Important", "work"}, expected: []string{"work", "important"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := uniqueLowercase(tt.input) assert.ElementsMatch(t, tt.expected, result) }) } } func TestTruncateAtWord(t *testing.T) { tests := []struct { name string input string maxLength int expected string }{ { name: "no truncation needed", input: "short", maxLength: 10, expected: "short", }, { name: "exact length", input: "exactly ten", maxLength: 11, expected: "exactly ten", }, { name: "truncate at word", input: "this is a long sentence", maxLength: 10, expected: "this is a ...", }, { name: "truncate very long word", input: "supercalifragilisticexpialidocious", maxLength: 10, expected: "supercalif ...", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := truncateAtWord(tt.input, tt.maxLength) assert.Equal(t, tt.expected, result) }) } } // Benchmark tests. func BenchmarkGenerateSnippet(b *testing.B) { svc := NewService() content := []byte(`# Large Document This is a large document with multiple paragraphs and formatting. ## Section 1 Here is some **bold** text and *italic* text with [links](https://example.com). - List item 1 - List item 2 - List item 3 ## Section 2 More content here with ` + "`inline code`" + ` and other elements. ` + "```go\nfunc example() {\n return true\n}\n```") b.ResetTimer() for i := 0; i < b.N; i++ { _, err := svc.GenerateSnippet(content, 200) if err != nil { b.Fatal(err) } } } func BenchmarkExtractProperties(b *testing.B) { svc := NewService() content := []byte("# Title\n\n[Link](url)\n\n`code`\n\n- [ ] Task\n- [x] Done") b.ResetTimer() for i := 0; i < b.N; i++ { _, err := svc.ExtractProperties(content) if err != nil { b.Fatal(err) } } }