fix: restore wikilink protection for URL replacement

Why:
Linear URLs were being rewritten inside existing wikilinks, and protected markdown segments were not treating linear:// URLs as URLs. That regressed helper compatibility and selection safety.

What:
- Skip raw URL replacement matches that fall inside wikilinks while keeping the helper direct and linear-aware.
- Extend protected markdown URL segmentation to include linear://.
- Add regression tests for raw replacement, segmentation, and selection formatting.
This commit is contained in:
Kodai Nakamura 2026-06-22 03:41:26 +09:00
parent 3ddcf08538
commit cf5562f0cb
5 changed files with 102 additions and 2 deletions

View file

@ -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")
})
})

View file

@ -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", () => {

View file

@ -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")

View file

@ -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)",

View file

@ -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)
})
}