package markdown import ( "strings" "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", }, { name: "inline code preserved", content: "`console.log('hello')`", maxLength: 100, expected: "console.log('hello')", }, { name: "text with inline code", content: "Use `fmt.Println` to print output.", maxLength: 100, expected: "Use fmt.Println to print output.", }, { name: "image alt text", content: "![alt text](https://example.com/img.png)", maxLength: 100, expected: "alt text", }, { name: "strikethrough text", content: "~~deleted text~~", maxLength: 100, expected: "deleted text", }, { name: "blockquote", content: "> quoted text", maxLength: 100, expected: "quoted text", }, { name: "table cells spaced", content: "| a | b |\n|---|---|\n| 1 | 2 |", maxLength: 100, expected: "a b 1 2", }, { name: "plain URL autolink", content: "https://usememos.com", maxLength: 100, expected: "https://usememos.com", }, { name: "text with plain URL", content: "Check out https://usememos.com for more info.", maxLength: 100, expected: "Check out https://usememos.com for more info.", }, } 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) }) } // Test with tag extension enabled (matches production config). svcWithTags := NewService(WithTagExtension()) tagTests := []struct { name string content string maxLength int expected string }{ { name: "tag only", content: "#todo", maxLength: 100, expected: "#todo", }, { name: "text with tags", content: "Remember to #review the #code", maxLength: 100, expected: "Remember to #review the #code", }, } for _, tt := range tagTests { t.Run(tt.name, func(t *testing.T) { snippet, err := svcWithTags.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 hasLink bool hasCode bool hasTasks bool hasInc bool title string }{ { name: "plain text", content: "Just plain text", hasLink: false, hasCode: false, hasTasks: false, hasInc: false, title: "", }, { name: "with link", content: "Check out [this link](https://example.com)", hasLink: true, hasCode: false, hasTasks: false, hasInc: false, title: "", }, { name: "with inline code", content: "Use `console.log()` to debug", hasLink: false, hasCode: true, hasTasks: false, hasInc: false, title: "", }, { name: "with code block", content: "```go\nfunc main() {}\n```", hasLink: false, hasCode: true, hasTasks: false, hasInc: false, title: "", }, { name: "with completed task", content: "- [x] Completed task", hasLink: false, hasCode: false, hasTasks: true, hasInc: false, title: "", }, { name: "with incomplete task", content: "- [ ] Todo item", hasLink: false, hasCode: false, hasTasks: true, hasInc: true, title: "", }, { name: "mixed tasks", content: "- [x] Done\n- [ ] Not done", hasLink: false, hasCode: false, hasTasks: true, hasInc: true, title: "", }, { name: "everything", content: "# Title\n\n[Link](url)\n\n`code`\n\n- [ ] Task", hasLink: true, hasCode: true, hasTasks: true, hasInc: true, title: "Title", }, { name: "h1 as first node extracts title", content: "# My Article Title\n\nBody text here.", title: "My Article Title", }, { name: "h2 as first node does not extract title", content: "## Sub Heading\n\nBody text.", title: "", }, { name: "h1 not first node does not extract title", content: "Some text\n\n# Heading Later", title: "", }, { name: "h1 with inline formatting extracts plain text", content: "# Title with **bold** and *italic*\n\nBody.", title: "Title with bold and italic", }, { name: "empty content has no title", content: "", title: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { 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") assert.Equal(t, tt.title, props.Title, "Title") }) } } func TestExtractAllTitle(t *testing.T) { svc := NewService(WithTagExtension()) tests := []struct { name string content string title string }{ { name: "h1 first node", content: "# Article Title\n\nContent with #tag", title: "Article Title", }, { name: "no h1", content: "Just text with #tag", title: "", }, { name: "h1 not first", content: "Intro\n\n# Late Heading", title: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { data, err := svc.ExtractAll([]byte(tt.content)) require.NoError(t, err) assert.Equal(t, tt.title, data.Property.Title, "Title") }) } } func TestExtractAllMentions(t *testing.T) { svc := NewService(WithTagExtension(), WithMentionExtension()) data, err := svc.ExtractAll([]byte("Hi @Alice and @bob. Email support@example.com should stay plain. #tag")) require.NoError(t, err) assert.ElementsMatch(t, []string{"alice", "bob"}, data.Mentions) assert.ElementsMatch(t, []string{"tag"}, data.Tags) maxLengthUsername := "a" + strings.Repeat("b", 62) data, err = svc.ExtractAll([]byte("@" + maxLengthUsername)) require.NoError(t, err) assert.Equal(t, []string{maxLengthUsername}, data.Mentions) data, err = svc.ExtractAll([]byte("@" + maxLengthUsername + "c")) require.NoError(t, err) assert.Empty(t, data.Mentions) } func TestExtractAllSkipsTagsInsideLinks(t *testing.T) { svc := NewService(WithTagExtension()) data, err := svc.ExtractAll([]byte( "[release #notes](https://example.com/releases#release-notes)\n\n" + "![preview #image](https://example.com/image#preview)\n\n" + "Outside #memo-tag", )) require.NoError(t, err) assert.ElementsMatch(t, []string{"memo-tag"}, data.Tags) assert.True(t, data.Property.HasLink) } 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", "Work", "WORK"}, }, { 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: "autolink URL fragment not tag", content: "https://github.com/dmtrKovalenko/fff#pi-agent-extension\n\nProject #memo-tag", withExt: true, expected: []string{"memo-tag"}, }, { name: "markdown link text and fragment not tags", content: "[release #notes](https://example.com/releases#release-notes) Outside #memo-tag", withExt: true, expected: []string{"memo-tag"}, }, { name: "reference link text and fragment not tags", content: "[reference #anchor][docs]\n\n[docs]: https://example.com/docs#reference-anchor\n\nOutside #memo-tag", withExt: true, expected: []string{"memo-tag"}, }, { name: "image alt text and fragment not tags", content: "![preview #image](https://example.com/image#preview)\n\nOutside #memo-tag", withExt: true, expected: []string{"memo-tag"}, }, { name: "no extension enabled", content: "Text with #tag", withExt: false, expected: []string{}, }, { name: "Chinese tag", content: "Text with #测试", withExt: true, expected: []string{"测试"}, }, { name: "Chinese tag followed by punctuation", content: "Text #测试。 More text", withExt: true, expected: []string{"测试"}, }, { name: "mixed Chinese and ASCII tag", content: "#测试test123 content", withExt: true, expected: []string{"测试test123"}, }, { name: "Japanese tag", content: "#日本語 content", withExt: true, expected: []string{"日本語"}, }, { name: "Korean tag", content: "#한국어 content", withExt: true, expected: []string{"한국어"}, }, { name: "hierarchical tag with Chinese", content: "#work/测试/项目", withExt: true, expected: []string{"work", "work/测试", "work/测试/项目"}, }, } 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 TestExtractTagsMemosTagV1(t *testing.T) { svc := NewService(WithTagExtension()) tests := []struct { name string content string expected []string }{ {name: "adjacent introducers", content: "#first#second", expected: []string{"first", "second"}}, {name: "failed introducer does not hide next", content: "##tag", expected: []string{"tag"}}, {name: "ATX heading marker", content: "## tag", expected: []string{}}, {name: "number sign keycap", content: "#️⃣", expected: []string{}}, {name: "number sign keycap value", content: "##️⃣", expected: []string{"#️⃣"}}, {name: "number sign keycap continuation", content: "#first#️⃣", expected: []string{"first#️⃣"}}, {name: "exact identity", content: "#Work #work", expected: []string{"Work", "work"}}, {name: "no normalization", content: "#café #cafe\u0301", expected: []string{"café", "cafe\u0301"}}, {name: "ignored spellings deduplicate", content: "#AB #A\u200dB #\u0301AB", expected: []string{"AB"}}, {name: "hierarchy expansion", content: "#book/fiction/history", expected: []string{"book", "book/fiction", "book/fiction/history"}}, {name: "hierarchy exact dedupe", content: "#book/fiction #book", expected: []string{"book", "book/fiction"}}, {name: "literal ampersand", content: "#R&D", expected: []string{"R&D"}}, {name: "named character reference boundary", content: "#R&D", expected: []string{"R"}}, {name: "character reference requires a real opener", content: "~#a!&&)*", expected: []string{"a"}}, {name: "unknown character reference stays literal", content: "#R&bogus;D #Q&&bogus;D", expected: []string{"R&bogus", "Q"}}, {name: "numeric character reference introducer", content: "#tag #other", expected: []string{}}, {name: "named character reference introducer", content: "#tag", expected: []string{}}, {name: "escaped introducer and body", content: "\\#tag #foo\\+bar", expected: []string{"foo"}}, {name: "code contexts", content: "`#inline`\n\n```text\n#fenced\n```\n\n #indented", expected: []string{}}, {name: "blockquote tab paragraph", content: ">\t #tag", expected: []string{"tag"}}, {name: "trailing spaces before tag line", content: "before \n#tag", expected: []string{"tag"}}, {name: "link and image contexts", content: "[#label](https://example.com/#destination) ![#alt](image#fragment)", expected: []string{}}, {name: "invalid link definition stays text", content: "[#use][bad]\n\n[bad]:(", expected: []string{"use"}}, {name: "invalid link definition label stays text", content: "[#label]:(", expected: []string{"label"}}, {name: "email in invalid link definition stays opaque", content: "[#foo@example.com]:(", expected: []string{}}, {name: "code in invalid link definition stays opaque", content: "[`#code`]:(", expected: []string{}}, {name: "URL in invalid link definition stays opaque", content: "[ https://example.com/#url]:(", expected: []string{}}, {name: "balanced bare reference remains a link", content: "[#hidden][valid]\n\n[valid]: path(foo)", expected: []string{}}, {name: "escaped bare destination parenthesis remains a link", content: "[#hidden][valid]\n\n[valid]: path\\(", expected: []string{}}, {name: "angle destination parenthesis remains a link", content: "[#hidden][valid]\n\n[valid]: <(>", expected: []string{}}, { name: "angle autolinks stay opaque", content: " ", expected: []string{}, }, {name: "literal URL context", content: "https://example.com/path#url /path#plain", expected: []string{"plain"}}, { name: "non GFM URL schemes and forms stay text", content: "ftp://example.com/#ftp HTTP://example.com/#upper https://localhost/#local", expected: []string{"ftp", "upper", "local"}, }, { name: "written GFM URL domains and paths", content: "https://example.COM/#hidden www.example.123/#also-hidden https://a_b.foo.example/路径#still-hidden https://foo_bar.example/#visible", expected: []string{"visible"}, }, { name: "written GFM URL Unicode domains", content: "https://點看.com/#hidden www.點看.com/#also-hidden https://a_b.點看.com/#still-hidden " + "https://foo_bar.點看/#visible https://點看.com_/#suffix hellohttps://點看.com/#joined", expected: []string{"visible", "suffix", "joined"}, }, { name: "written GFM URL protocol and www boundaries", content: "1https://點看.com/#digit .https://點看.com/#punctuation ]www.點看.com/#www " + "中https://點看.com/#unicode hellohttps://點看.com/#joined " + "[https://點看.com/#unbalanced\n\n[www.點看.com/#unbalanced-www\n\n" + `\[https://點看.com/#escaped` + "\n\n" + `\[www.點看.com/#escaped-www`, expected: []string{ "digit", "punctuation", "www", "unicode", "joined", "unbalanced", "unbalanced-www", "escaped", "escaped-www", }, }, { name: "written GFM URL domain validation", content: "http://example.com#hidden http://example.com./#also-hidden https://foo.example_/#visible\n" + "-\thttps://foo.example_/#padded\n\n(https://foo.example_/#parenthesized) *https://foo.example_/#emphasized*", expected: []string{"visible", "padded", "parenthesized", "emphasized"}, }, {name: "initial BOM before written GFM URL", content: "\ufeffhttps://example.com/#hidden #shown", expected: []string{"shown"}}, {name: "second initial BOM is ordinary text", content: "\ufeff\ufeffhttps://example.com/#shown", expected: []string{"shown"}}, {name: "mid-document BOM does not start written GFM URL", content: "before\n\n\ufeffhttps://example.com/#shown", expected: []string{"shown"}}, {name: "written URL inside unresolved bracket text", content: "[text https://點看.com/#inside [more https://example.com/#ascii", expected: []string{}}, { name: "written URL inside closed unresolved bracket text", content: "[ https://example.com/#hidden] [x https://example.com/#also-hidden] ![ https://example.com/#image-hidden]", expected: []string{}, }, { name: "separator characters end written URL", content: "https://點看.com/\u00a0#nbsp https://點看.com/\u2003#em-space https://點看.com/\u000b#vertical-tab " + "https://點看.com/\u2028#line-separator https://點看.com/\u2029#paragraph-separator https://點看.com/\u0085#nel-hidden", expected: []string{"nbsp", "em-space", "vertical-tab", "line-separator", "paragraph-separator"}, }, { name: "written GFM email boundaries", content: "foo#mail@example.com foo!#second@example.com foo@example.com/#after", expected: []string{"after"}, }, {name: "invalid GFM email local part", content: "#foo!bar@example.com", expected: []string{"foo"}}, {name: "email after tag hierarchy separator", content: "#foo/bar@example.com", expected: []string{"foo"}}, {name: "GFM email after Unicode tag", content: "#中mail@example.com", expected: []string{"中"}}, {name: "email after padded list tag", content: "-\t#foo/bar@example.com", expected: []string{"foo"}}, {name: "invalid GFM email domain suffix", content: "#foo/bar@example.com_", expected: []string{"foo", "foo/bar"}}, {name: "escaped punctuation is decoded in GFM email autolinks", content: `#foo\+bar@example.com #next\_item@example.com`, expected: []string{}}, {name: "numeric entity joins GFM email", content: `#foo.bar@example.com #foo@example.com`, expected: []string{}}, {name: "named entity joins GFM email", content: `#foo.bar@example.com`, expected: []string{}}, {name: "unknown entity does not join GFM email", content: `#foo&bogus;bar@example.com`, expected: []string{"foo&bogus"}}, {name: "email inside emphasis", content: "_foo@example.com #tag_", expected: []string{"tag"}}, {name: "raw HTML syntax", content: "#text", expected: []string{"text"}}, {name: "inline math", content: "$#math$ #plain", expected: []string{"plain"}}, {name: "inline math exact closing run", content: "$#one$$ #two$ #plain", expected: []string{"plain"}}, {name: "inline math does not reopen within a dollar run", content: "$$#math$ #plain", expected: []string{"math", "plain"}}, {name: "inline math after escaped dollar", content: "\\$$#math$ #plain", expected: []string{"plain"}}, {name: "display math", content: "$$\n#math\n$$\n\n#plain", expected: []string{"plain"}}, {name: "unclosed math flow", content: "$$meta\n#math", expected: []string{}}, {name: "unclosed math flow across blocks", content: "$$meta\n\n#math", expected: []string{}}, {name: "unclosed math flow in quote", content: "> $$meta\n> #math", expected: []string{}}, {name: "flow math stays inside quote", content: "> $$\n> #math\noutside #plain", expected: []string{"plain"}}, {name: "flow math spans quote blocks", content: "> $$\n> #math\n>\n> #still-math\n> $$\noutside #plain", expected: []string{"plain"}}, {name: "flow math stays inside list item", content: "- $$\n #math\n#outside", expected: []string{"outside"}}, {name: "flow math spans list item blank lines", content: "- $$\n \n #math\n $$\n\n#outside", expected: []string{"outside"}}, {name: "dollar in flow metadata stays text", content: "$$meta$x\n#plain", expected: []string{"plain"}}, {name: "math fence inside code is inert", content: "```text\n$$\n```\n\n#plain", expected: []string{"plain"}}, {name: "unmatched inline dollar", content: "text $ #plain", expected: []string{"plain"}}, {name: "formatted text", content: "**#strong** ~~#strike~~", expected: []string{"strong", "strike"}}, {name: "emphasis is a hard boundary", content: "#_foo_", expected: []string{}}, {name: "unmatched underscore", content: "#_", expected: []string{"_"}}, {name: "intraword underscore", content: "#foo_bar", expected: []string{"foo_bar"}}, {name: "no length limit", content: "#" + strings.Repeat("a", 101), expected: []string{strings.Repeat("a", 101)}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tags, err := svc.ExtractTags([]byte(tt.content)) require.NoError(t, err) if len(tt.expected) == 0 { assert.Empty(t, tags) } else { assert.Equal(t, tt.expected, tags) } }) } } func TestExtractAllExpandsTagHierarchy(t *testing.T) { svc := NewService(WithTagExtension()) data, err := svc.ExtractAll([]byte("#book/fiction/history #book")) require.NoError(t, err) assert.Equal(t, []string{"book", "book/fiction", "book/fiction/history"}, data.Tags) } func TestExtractAllResolvesGFMEmailBeforeMention(t *testing.T) { svc := NewService(WithTagExtension(), WithMentionExtension()) data, err := svc.ExtractAll([]byte("#foo/bar_baz@example.com #next/item_@example.com @alice")) require.NoError(t, err) assert.Equal(t, []string{"foo", "next"}, data.Tags) assert.Equal(t, []string{"alice"}, data.Mentions) assert.True(t, data.Property.HasLink) } func TestInvalidLinkDefinitionsRetainInlineContexts(t *testing.T) { svc := NewService(WithTagExtension()) data, err := svc.ExtractAll([]byte("[#foo@example.com]:(\n\n[`#code`]:(")) require.NoError(t, err) assert.Empty(t, data.Tags) assert.True(t, data.Property.HasLink) assert.True(t, data.Property.HasCode) } func TestTagSourceSpelling(t *testing.T) { svc := NewService(WithTagExtension()) content := "#A\u200dB #\u0301foo" rendered, err := svc.RenderMarkdown([]byte(content)) require.NoError(t, err) assert.Equal(t, content, rendered) snippet, err := svc.GenerateSnippet([]byte(content), 100) require.NoError(t, err) assert.Equal(t, content, snippet) renamed, err := svc.RenameTag([]byte(content), "AB", "done") require.NoError(t, err) assert.Equal(t, "#done #\u0301foo", renamed) } func TestRenderHTMLPreservesTagSourceSpelling(t *testing.T) { svc := NewService(WithTagExtension()) html, err := svc.RenderHTML([]byte("#R&D #A\u200dB")) require.NoError(t, err) assert.Equal(t, "

#R&D #A\u200dB

\n", html) } func TestRenderHTMLRejectsUnclosedReferenceDestination(t *testing.T) { svc := NewService(WithTagExtension()) html, err := svc.RenderHTML([]byte("[#use][bad]\n\n[bad]:(")) require.NoError(t, err) assert.Equal(t, "

[#use][bad]

\n

[bad]:(

\n", html) } func TestRenderHTMLRecognizesGFMEmails(t *testing.T) { svc := NewService(WithTagExtension()) tests := []struct { content string expected string }{ { content: "foo#mail@example.com", expected: "

foo#mail@example.com

\n", }, { content: "#foo/bar_baz@example.com", expected: "

#foo/bar_baz@example.com

\n", }, { content: "_foo@example.com #tag_", expected: "

foo@example.com #tag

\n", }, { content: "foo@bar.com@baz.example", expected: "

foo@bar.com@baz.example

\n", }, { content: "foo@bar.com+abc@def.com", expected: "

foo@bar.com+abc@def.com

\n", }, { content: `#foo\+bar@example.com`, expected: "

#foo+bar@example.com

\n", }, { content: `#foo\@example\.com`, expected: "

#foo@example.com

\n", }, { content: `#foo.bar@example.com`, expected: "

#foo.bar@example.com

\n", }, { content: `#foo.bar@example.com`, expected: "

#foo.bar@example.com

\n", }, { content: `#foo@example.com`, expected: "

#foo@example.com

\n", }, } for _, test := range tests { html, err := svc.RenderHTML([]byte(test.content)) require.NoError(t, err) assert.Equal(t, test.expected, html) } } func TestRenderHTMLRecognizesGFMEmailWithoutTagExtension(t *testing.T) { svc := NewService() html, err := svc.RenderHTML([]byte("mail@example.com")) require.NoError(t, err) assert.Equal(t, "

mail@example.com

\n", html) } func TestRenderHTMLRecognizesGFMURLsInsideUnresolvedBracketText(t *testing.T) { svc := NewService(WithTagExtension()) for _, content := range []string{ "[ https://example.com/#hidden]", "[x https://example.com/#hidden]", "![ https://example.com/#hidden]", "[text https://example.com/#hidden", } { html, err := svc.RenderHTML([]byte(content)) require.NoError(t, err) assert.Contains(t, html, " $$meta\n> x < y\n> $$", "- $$meta\n x < y\n $$", "- $$meta\n \n x < y\n $$", "- $$meta\n x < y\n $$\n- next", "10. $$meta\n x < y\n $$", } { rendered, err := svc.RenderMarkdown([]byte(content)) require.NoError(t, err) assert.Equal(t, content, rendered) } html, err := svc.RenderHTML([]byte("$x < y$\n\n$$meta\nx < y\n$$")) require.NoError(t, err) assert.Equal(t, "

$x < y$

\n$$meta\nx < y\n$$", html) } func TestRenderMarkdownPreservesLineBreakAfterTag(t *testing.T) { svc := NewService(WithTagExtension()) for _, content := range []string{"#tag\nnext", "#tag \nnext"} { rendered, err := svc.RenderMarkdown([]byte(content)) require.NoError(t, err) assert.Equal(t, content, rendered) } } func TestRenameTagSkipsTagsInsideLinks(t *testing.T) { svc := NewService(WithTagExtension()) result, err := svc.RenameTag( []byte("[release #notes](https://example.com/releases#release-notes)\n\nOutside #notes"), "notes", "done", ) require.NoError(t, err) assert.Equal(t, "[release #notes](https://example.com/releases#release-notes)\n\nOutside #done", result) } func TestRenameTagOnlyChangesRecognizedSourceSpans(t *testing.T) { svc := NewService(WithTagExtension()) for _, content := range []string{ "#old", " user@example.com #old", "#old\n$$\nx\n$$\nnext", } { result, err := svc.RenameTag([]byte(content), "old", "new") require.NoError(t, err) assert.Equal(t, strings.ReplaceAll(content, "#old", "#new"), result) } } func TestRenameTagReplacesEveryExactSourceSpan(t *testing.T) { svc := NewService(WithTagExtension()) content := "#old #o\u200dld #old#old [#old](url) https://example.com/#old #Old" result, err := svc.RenameTag([]byte(content), "old", "new") require.NoError(t, err) assert.Equal(t, "#new #new #new#new [#old](url) https://example.com/#old #Old", result) } func TestUniquePreserveCase(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", "TAG", "Tag"}, }, { name: "mixed", input: []string{"Work", "work", "Important", "work"}, expected: []string{"Work", "work", "Important"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := uniquePreserveCase(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 ...", }, { name: "CJK characters without spaces", input: "这是一个很长的中文句子没有空格的情况下也要正确处理", maxLength: 15, expected: "这是一个很长的中文句子没有空格 ...", }, { name: "mixed CJK and Latin", input: "这是中文mixed with English文字", maxLength: 10, expected: "这是中文mixed ...", }, } 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) } } }