diff --git a/src/lib/utils/note-links.test.ts b/src/lib/utils/note-links.test.ts new file mode 100644 index 0000000..67f0f9b --- /dev/null +++ b/src/lib/utils/note-links.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from 'vitest'; +import { extractNoteLinks } from './note-links.js'; + +describe('extractNoteLinks', () => { + it('should extract a single note-link reference', () => { + expect(extractNoteLinks('See [Grocery List](crumb-note://abc-123)')).toEqual(['abc-123']); + }); + + it('should extract multiple distinct references', () => { + const content = '[A](crumb-note://id-1) and [B](crumb-note://id-2)'; + expect(extractNoteLinks(content)).toEqual(['id-1', 'id-2']); + }); + + it('should deduplicate repeated references', () => { + const content = '[A](crumb-note://id-1) again: [A again](crumb-note://id-1)'; + expect(extractNoteLinks(content)).toEqual(['id-1']); + }); + + it('should return an empty array for content with no note-links', () => { + expect(extractNoteLinks('Just [a normal link](https://example.com)')).toEqual([]); + }); + + it('should return an empty array for empty content', () => { + expect(extractNoteLinks('')).toEqual([]); + }); + + it('should not match crumb-note:// occurring inside a code block', () => { + const content = '```\n[A](crumb-note://id-1)\n```'; + expect(extractNoteLinks(content)).toEqual([]); + }); + + it('should not match crumb-note:// occurring inside inline code', () => { + expect(extractNoteLinks('Use `crumb-note://id-1` as an example')).toEqual([]); + }); +}); diff --git a/src/lib/utils/note-links.ts b/src/lib/utils/note-links.ts new file mode 100644 index 0000000..f6154ac --- /dev/null +++ b/src/lib/utils/note-links.ts @@ -0,0 +1,17 @@ +/** + * Extract crumb-note:// references from markdown content. + * Mirrors extractTags' code-block/inline-code exclusion so a reference + * mentioned as an example inside a code sample isn't treated as a real link. + */ +export function extractNoteLinks(content: string): string[] { + if (!content) return []; + + const withoutCodeBlocks = content.replace(/```[\s\S]*?```/g, ''); + const withoutInlineCode = withoutCodeBlocks.replace(/`[^`]*`/g, ''); + + const matches = withoutInlineCode.match(/crumb-note:\/\/([\w-]+)/g); + if (!matches) return []; + + const ids = matches.map((m) => m.replace('crumb-note://', '')); + return [...new Set(ids)]; +}