import { describe, it, expect } from 'vitest' import { preprocessMarkdown, preprocessVideoLinks, preprocessHashtags, preprocessItemMentions, removeMarkdownSyntax, truncateMarkdown, } from './preprocessMarkdown' // ============================================================================ // convertNakedUrls (tested via preprocessMarkdown) // ============================================================================ describe('convertNakedUrls (via preprocessMarkdown)', () => { describe('Happy Path', () => { it('converts a naked URL to markdown link', () => { const result = preprocessMarkdown('Check https://example.com out') expect(result).toContain('[example.com](https://example.com)') }) it('removes www from display text', () => { const result = preprocessMarkdown('Visit https://www.example.com') expect(result).toContain('[example.com](https://www.example.com)') }) it('converts multiple naked URLs', () => { const result = preprocessMarkdown('See https://a.com and https://b.com') expect(result).toContain('[a.com](https://a.com)') expect(result).toContain('[b.com](https://b.com)') }) it('preserves query parameters in URL', () => { const result = preprocessMarkdown('Link https://example.com?a=1&b=2 here') expect(result).toContain('](https://example.com?a=1&b=2)') }) it('converts http URLs', () => { const result = preprocessMarkdown('Old http://example.com link') expect(result).toContain('[example.com](http://example.com)') }) }) describe('Skip - Already Linked', () => { it('does not convert URL already in markdown link', () => { const input = '[my link](https://example.com)' const result = preprocessMarkdown(input) // Should not double-wrap expect(result).toBe(input) }) it('does not convert URL in autolink syntax', () => { const input = '' const result = preprocessMarkdown(input) // Autolinks are preserved (may be converted to video embeds if matching) expect(result).not.toContain('](https://example.com)') }) }) describe('Edge Cases', () => { it('handles URL at end of sentence with period', () => { const result = preprocessMarkdown('Visit https://example.com.') // Note: Current implementation includes trailing period in URL // This documents actual behavior - may be improved later expect(result).toContain('[example.com.](https://example.com.)') }) it('handles URL in parentheses', () => { const result = preprocessMarkdown('(https://example.com)') expect(result).toContain('[example.com](https://example.com)') }) it('handles URL at line start', () => { const result = preprocessMarkdown('https://example.com is great') expect(result).toContain('[example.com](https://example.com)') }) it('handles URL with path', () => { const result = preprocessMarkdown('See https://example.com/path/to/page') expect(result).toContain('[example.com/path/to/page](https://example.com/path/to/page)') }) }) }) // ============================================================================ // preprocessVideoLinks // ============================================================================ describe('preprocessVideoLinks', () => { describe('YouTube - Happy Path', () => { it('converts YouTube autolink (standard)', () => { const result = preprocessVideoLinks('') expect(result).toBe('') }) it('converts YouTube autolink (short URL)', () => { const result = preprocessVideoLinks('') expect(result).toBe('') }) it('converts YouTube markdown link (standard)', () => { const result = preprocessVideoLinks('[Video](https://youtube.com/watch?v=abc123)') expect(result).toBe('') }) it('converts YouTube markdown link (short URL)', () => { const result = preprocessVideoLinks('[Watch](https://youtu.be/xyz789)') expect(result).toBe('') }) it('handles YouTube without www', () => { const result = preprocessVideoLinks('') expect(result).toBe('') }) }) describe('YouTube - Edge Cases', () => { it('extracts only video-id, ignores extra params', () => { const result = preprocessVideoLinks('') expect(result).toBe('') }) it('handles http (non-https) YouTube links', () => { const result = preprocessVideoLinks('') expect(result).toBe('') }) it('handles short URL with query params', () => { const result = preprocessVideoLinks('') expect(result).toBe('') }) }) describe('Rumble - Happy Path', () => { it('converts Rumble autolink', () => { const result = preprocessVideoLinks('') expect(result).toBe('') }) it('converts Rumble markdown link', () => { const result = preprocessVideoLinks('[Rumble Video](https://rumble.com/embed/xyz123)') expect(result).toBe('') }) }) describe('Non-Video Links', () => { it('does not convert non-video autolinks', () => { const input = '' expect(preprocessVideoLinks(input)).toBe(input) }) it('does not convert non-video markdown links', () => { const input = '[Example](https://example.com)' expect(preprocessVideoLinks(input)).toBe(input) }) }) describe('Mixed Content', () => { it('converts video in mixed content', () => { const result = preprocessVideoLinks('Check this: and more text') expect(result).toContain('') expect(result).toContain('Check this:') expect(result).toContain('and more text') }) it('converts multiple videos', () => { const result = preprocessVideoLinks(' and ') expect(result).toContain('video-id="a"') expect(result).toContain('video-id="b"') }) }) }) // ============================================================================ // preprocessHashtags // ============================================================================ describe('preprocessHashtags', () => { describe('Happy Path', () => { it('converts simple hashtag', () => { const result = preprocessHashtags('Hello #world') expect(result).toBe('Hello #world') }) it('converts multiple hashtags', () => { const result = preprocessHashtags('#one #two #three') expect(result).toContain('data-label="one"') expect(result).toContain('data-label="two"') expect(result).toContain('data-label="three"') }) it('converts hashtag with numbers', () => { const result = preprocessHashtags('#test123') expect(result).toContain('data-label="test123"') }) it('converts hashtag with underscore', () => { const result = preprocessHashtags('#my_tag') expect(result).toContain('data-label="my_tag"') }) it('converts hashtag with hyphen', () => { const result = preprocessHashtags('#my-tag') expect(result).toContain('data-label="my-tag"') }) it('converts hashtag with German umlauts', () => { const result = preprocessHashtags('#München') expect(result).toContain('data-label="München"') }) it('converts hashtag with French accents', () => { const result = preprocessHashtags('#café') expect(result).toContain('data-label="café"') }) }) describe('Skip - Inside Links', () => { it('does not convert hashtag in link text', () => { const input = '[#tag](#anchor)' const result = preprocessHashtags(input) expect(result).not.toContain('data-hashtag') }) it('does not convert hashtag in link URL', () => { const input = '[section](#section-heading)' const result = preprocessHashtags(input) expect(result).not.toContain('data-hashtag') }) }) describe('Edge Cases', () => { it('handles concurrent hashtags without space', () => { const result = preprocessHashtags('#tag1#tag2') // First should convert, second might not (depends on lookbehind) expect(result).toContain('data-label="tag1"') }) it('does not convert lone # symbol', () => { const result = preprocessHashtags('Just #') expect(result).not.toContain('data-hashtag') expect(result).toBe('Just #') }) it('handles hashtag at start of text', () => { const result = preprocessHashtags('#first thing') expect(result).toContain('data-label="first"') }) it('handles hashtag at end of text', () => { const result = preprocessHashtags('last is #final') expect(result).toContain('data-label="final"') }) }) }) // ============================================================================ // preprocessItemMentions // ============================================================================ describe('preprocessItemMentions', () => { describe('Happy Path', () => { it('converts standard mention format', () => { const result = preprocessItemMentions('Hello [@Alice](/item/abc-123)') expect(result).toBe( 'Hello @Alice', ) }) it('converts mention with layer (legacy format)', () => { const result = preprocessItemMentions('[@Bob](/item/people/def-456)') expect(result).toContain('data-id="def-456"') expect(result).toContain('data-label="Bob"') }) it('converts relative path format with leading slash', () => { // Note: Relative paths without leading slash are not supported by regex // The regex requires /item/ or item/ with UUID pattern (hex + dashes only) const result = preprocessItemMentions('[@Name](/item/abc-def-123)') expect(result).toContain('data-id="abc-def-123"') }) it('converts multiple mentions', () => { const result = preprocessItemMentions('[@A](/item/1) and [@B](/item/2)') expect(result).toContain('data-id="1"') expect(result).toContain('data-id="2"') }) it('handles UUID with uppercase letters', () => { const result = preprocessItemMentions('[@Name](/item/ABC-DEF-123)') expect(result).toContain('data-id="ABC-DEF-123"') }) it('handles label with spaces', () => { // Note: UUID must be hex chars + dashes only (no letters like 'uuid') const result = preprocessItemMentions('[@Max Müller](/item/abc-def-123)') expect(result).toContain('data-label="Max Müller"') }) }) describe('Skip - Non-Matching', () => { it('does not convert non-item links', () => { const input = '[@Name](/other/path)' const result = preprocessItemMentions(input) expect(result).toBe(input) }) it('does not convert regular links (no @)', () => { const input = '[Name](/item/123)' const result = preprocessItemMentions(input) expect(result).toBe(input) }) }) }) // ============================================================================ // removeMarkdownSyntax // ============================================================================ describe('removeMarkdownSyntax', () => { describe('Happy Path', () => { it('removes bold syntax', () => { expect(removeMarkdownSyntax('**bold**')).toBe('bold') }) it('removes italic syntax (asterisk)', () => { expect(removeMarkdownSyntax('*italic*')).toBe('italic') }) it('removes italic syntax (underscore)', () => { expect(removeMarkdownSyntax('_italic_')).toBe('italic') }) it('removes headers', () => { expect(removeMarkdownSyntax('# Heading')).toBe('Heading') expect(removeMarkdownSyntax('## Subheading')).toBe('Subheading') }) it('removes links, keeps text', () => { expect(removeMarkdownSyntax('[text](https://example.com)')).toBe('text') }) it('removes images completely', () => { expect(removeMarkdownSyntax('![alt](image.png)')).toBe('') }) it('removes inline code', () => { expect(removeMarkdownSyntax('`code`')).toBe('code') }) it('removes blockquotes', () => { expect(removeMarkdownSyntax('> quote')).toBe('quote') }) }) describe('Preserve Special Elements', () => { it('preserves @mentions', () => { const result = removeMarkdownSyntax('Hello [@Alice](/item/123)') expect(result).toContain('[@Alice](/item/123)') }) it('preserves hashtags', () => { const result = removeMarkdownSyntax('Hello #world') expect(result).toContain('#world') }) }) }) // ============================================================================ // truncateMarkdown // ============================================================================ describe('truncateMarkdown', () => { describe('Happy Path', () => { it('returns unchanged if under limit', () => { expect(truncateMarkdown('Short text', 100)).toBe('Short text') }) it('truncates and adds ellipsis if over limit', () => { const text = 'A'.repeat(150) const result = truncateMarkdown(text, 100) expect(result).toHaveLength(103) // 100 chars + '...' expect(result.endsWith('...')).toBe(true) }) it('respects exact limit', () => { const result = truncateMarkdown('Hello World', 5) expect(result).toBe('Hello...') }) }) describe('Atomic Token Preservation', () => { it('preserves complete hashtag', () => { const result = truncateMarkdown('A'.repeat(95) + ' #tag', 100) // Should either include complete #tag or cut before it if (result.includes('#')) { expect(result).toContain('#tag') } }) it('preserves complete mention', () => { const result = truncateMarkdown('A'.repeat(90) + ' [@Alice](/item/123)', 100) // Should either include complete mention or cut before it if (result.includes('@')) { expect(result).toContain('[@Alice](/item/123)') } }) it('preserves complete link', () => { const result = truncateMarkdown('See [link](url) more text here', 8) // Visible text "See link" is 8 chars expect(result).toContain('[link](url)') }) }) describe('Edge Cases', () => { it('does not count newlines toward limit', () => { const result = truncateMarkdown('Line1\n\nLine2', 10) expect(result).toContain('Line1') expect(result).toContain('Line2') }) it('handles empty text', () => { expect(truncateMarkdown('', 100)).toBe('') }) it('handles limit of 0', () => { expect(truncateMarkdown('Text', 0)).toBe('...') }) it('handles negative limit gracefully', () => { // Should not throw expect(() => truncateMarkdown('Text', -1)).not.toThrow() }) }) }) // ============================================================================ // preprocessMarkdown (Full Pipeline) // ============================================================================ describe('preprocessMarkdown', () => { describe('Happy Path', () => { it('processes complete content with all features', () => { const input = 'Check https://example.com #tag [@Alice](/item/123)' const result = preprocessMarkdown(input) // URL converted expect(result).toContain('[example.com](https://example.com)') // Hashtag converted expect(result).toContain('data-hashtag') // Mention converted expect(result).toContain('data-item-mention') }) it('processes video links', () => { const result = preprocessMarkdown('') expect(result).toContain('video-embed') }) it('converts email addresses to mailto links', () => { const result = preprocessMarkdown('Contact test@example.com') expect(result).toContain('[test@example.com](mailto:test@example.com)') }) }) describe('Edge Cases', () => { it('handles empty string', () => { expect(preprocessMarkdown('')).toBe('') }) it('handles null input', () => { // @ts-expect-error testing null input expect(preprocessMarkdown(null)).toBe('') }) it('handles undefined input', () => { // @ts-expect-error testing undefined input expect(preprocessMarkdown(undefined)).toBe('') }) it('preserves whitespace', () => { expect(preprocessMarkdown(' ')).toBe(' ') }) it('handles very long text without timeout', () => { const longText = 'A'.repeat(10000) const start = performance.now() preprocessMarkdown(longText) const elapsed = performance.now() - start expect(elapsed).toBeLessThan(1000) // Should complete in <1s }) }) describe('Error Handling', () => { it('handles malformed markdown without throwing', () => { expect(() => preprocessMarkdown('[unclosed link')).not.toThrow() expect(() => preprocessMarkdown('**unclosed bold')).not.toThrow() }) it('handles malformed URLs without throwing', () => { expect(() => preprocessMarkdown('http:/broken')).not.toThrow() }) }) })