diff --git a/src/__tests__/formatting-run.test.ts b/src/__tests__/formatting-run.test.ts index bd89042..ed43131 100644 --- a/src/__tests__/formatting-run.test.ts +++ b/src/__tests__/formatting-run.test.ts @@ -175,4 +175,34 @@ describe("formatMarkdownSelection", () => { "[[notes/TypeScript|TypeScript]] https://github.com/openai/openai/issues/1", ) }) + + it("leaves linear URLs unchanged when a linear note exists", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "notes/linear" }, + { path: "notes/TypeScript" }, + ], + settings: { + scoped: false, + baseDir: undefined, + ignoreCase: true, + }, + }) + + const result = formatMarkdownSelection({ + body: "linear://workspace/issue/ACME-123", + filePath: "current-file.md", + settings: { + ...DEFAULT_SETTINGS, + formatGitHubURLs: true, + formatJiraURLs: true, + formatLinearURLs: true, + replaceUrlWithTitle: true, + ignoreCase: true, + }, + candidateIndex: { candidateMap, trie }, + }) + + expect(result).toBe("linear://workspace/issue/ACME-123") + }) }) diff --git a/src/__tests__/markdown-segments.test.ts b/src/__tests__/markdown-segments.test.ts index f64f4a0..2a16760 100644 --- a/src/__tests__/markdown-segments.test.ts +++ b/src/__tests__/markdown-segments.test.ts @@ -62,6 +62,23 @@ describe("segmentMarkdown", () => { ]) expect(segments.map(segment => segment.text).join("")).toBe(text) }) + + it("protects linear URLs when requested", () => { + const text = "linear://workspace/issue/ACME-123" + const segments = segmentMarkdown(text, { + protectUrls: true, + }) + + expect(segments).toEqual([ + { + kind: "protected", + protectedKind: "url", + start: 0, + end: text.length, + text, + }, + ]) + }) }) describe("mapMarkdownProse", () => { diff --git a/src/markdown-segments.ts b/src/markdown-segments.ts index c0a37c9..37e9a62 100644 --- a/src/markdown-segments.ts +++ b/src/markdown-segments.ts @@ -138,7 +138,7 @@ const buildProtectedPattern = (protectUrls: boolean): RegExp => { ] if (protectUrls) { - parts.push("https?:\\/\\/[^\\s]+") + parts.push("(?:https?:\\/\\/|linear:\\/\\/)[^\\s]+") } return new RegExp(`(${parts.join("|")})`, "g") diff --git a/src/replace-urls/__tests__/replace-urls.test.ts b/src/replace-urls/__tests__/replace-urls.test.ts index fbf68d6..e6684b5 100644 --- a/src/replace-urls/__tests__/replace-urls.test.ts +++ b/src/replace-urls/__tests__/replace-urls.test.ts @@ -22,6 +22,14 @@ describe("replace-urls", () => { ) }) + it("does not rewrite https URLs inside wikilinks", () => { + const input = "[[https://github.com/kdnk/obsidian-automatic-linker]]" + + expect( + replaceURLs(input, baseSettings, formatGitHubURL), + ).toBe(input) + }) + it("should replace linear protocol URLs", () => { const input = "linear://workspace/issue/ACME-123" const expected @@ -39,6 +47,21 @@ describe("replace-urls", () => { ).toBe(expected) }) + it("does not rewrite linear protocol URLs inside wikilinks", () => { + const input = "[[linear://workspace/issue/ACME-123]]" + + expect( + replaceURLs( + input, + { + ...baseSettings, + formatLinearURLs: true, + }, + formatLinearURL, + ), + ).toBe(input) + }) + it("keeps raw helper semantics inside Markdown links and angle autolinks", () => { const input = [ "[label](https://github.com/kdnk/obsidian-automatic-linker)", diff --git a/src/replace-urls/replace-urls.ts b/src/replace-urls/replace-urls.ts index 75f5c63..5587d41 100644 --- a/src/replace-urls/replace-urls.ts +++ b/src/replace-urls/replace-urls.ts @@ -1,9 +1,39 @@ import { AutomaticLinkerSettings } from "../settings/settings-info" const URL_PATTERN = /(?:https?:\/\/|linear:\/\/)[^\s<>\]]+/g +const WIKILINK_PATTERN = /\[\[[\s\S]*?\]\]/g + +const collectWikilinkRanges = (text: string): Array<{ start: number, end: number }> => { + const ranges: Array<{ start: number, end: number }> = [] + let match: RegExpExecArray | null + + while ((match = WIKILINK_PATTERN.exec(text)) !== null) { + ranges.push({ + start: match.index, + end: match.index + match[0].length, + }) + } + + return ranges +} + +const isInsideRange = ( + index: number, + ranges: Array<{ start: number, end: number }>, +): boolean => ranges.some(range => index >= range.start && index < range.end) export const replaceURLs = ( fileContent: string, settings: AutomaticLinkerSettings, formatter: (url: string, settings: AutomaticLinkerSettings) => string, -) => fileContent.replace(URL_PATTERN, match => formatter(match, settings)) +) => { + const wikilinkRanges = collectWikilinkRanges(fileContent) + + return fileContent.replace(URL_PATTERN, (match, offset) => { + if (isInsideRange(offset, wikilinkRanges)) { + return match + } + + return formatter(match, settings) + }) +}