mirror of
https://github.com/kdnk/obsidian-automatic-linker.git
synced 2026-07-22 05:37:46 +00:00
fix(replace-links): preserve unclosed fenced code blocks
Why: - Markdown treats an opening fenced code block without a closing fence as code through the end of the document. - Automatic linking was still processing that content as normal text, which could turn code block contents into wiki links. What: - Extract fenced code blocks before heading, callout, table, and link replacement processing. - Preserve unmatched fenced blocks through EOF and restore them unchanged after replacement. - Add regression coverage for unclosed code blocks, including when heading protection is enabled.
This commit is contained in:
parent
504e37a8d1
commit
846ebda9ce
2 changed files with 105 additions and 2 deletions
|
|
@ -44,4 +44,47 @@ describe("ignore code", () => {
|
|||
})
|
||||
expect(result).toBe("```typescript\nexample\n```")
|
||||
})
|
||||
|
||||
it("unclosed code block", () => {
|
||||
const settings = {
|
||||
scoped: false,
|
||||
baseDir: undefined,
|
||||
}
|
||||
const { candidateMap, trie } = buildCandidateTrieForTest({
|
||||
files: [{ path: "example" }, { path: "typescript" }],
|
||||
settings,
|
||||
})
|
||||
const result = replaceLinks({
|
||||
body: "```typescript\nexample",
|
||||
linkResolverContext: {
|
||||
filePath: "journals/2022-01-01",
|
||||
trie,
|
||||
candidateMap,
|
||||
},
|
||||
settings,
|
||||
})
|
||||
expect(result).toBe("```typescript\nexample")
|
||||
})
|
||||
|
||||
it("unclosed code block when headings are ignored", () => {
|
||||
const settings = {
|
||||
scoped: false,
|
||||
baseDir: undefined,
|
||||
ignoreHeadings: true,
|
||||
}
|
||||
const { candidateMap, trie } = buildCandidateTrieForTest({
|
||||
files: [{ path: "example" }, { path: "typescript" }],
|
||||
settings,
|
||||
})
|
||||
const result = replaceLinks({
|
||||
body: "```typescript\nexample",
|
||||
linkResolverContext: {
|
||||
filePath: "journals/2022-01-01",
|
||||
trie,
|
||||
candidateMap,
|
||||
},
|
||||
settings,
|
||||
})
|
||||
expect(result).toBe("```typescript\nexample")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -165,6 +165,58 @@ const extractLinkParts = (
|
|||
return { linkPath: canonicalPath, alias: "", hasAlias }
|
||||
}
|
||||
|
||||
const escapeRegExp = (text: string): string =>
|
||||
text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||
|
||||
const extractFencedCodeBlocks = (
|
||||
body: string,
|
||||
): { body: string, codeBlocks: Array<{ placeholder: string, content: string }> } => {
|
||||
if (!body.includes("```") && !body.includes("~~~")) {
|
||||
return { body, codeBlocks: [] }
|
||||
}
|
||||
|
||||
const codeBlocks: Array<{ placeholder: string, content: string }> = []
|
||||
let result = ""
|
||||
let cursor = 0
|
||||
let codeBlockIndex = 0
|
||||
|
||||
const openingFencePattern = /^ {0,3}(`{3,}|~{3,})[^\r\n]*(?:\r?\n|$)/gm
|
||||
let openingMatch: RegExpExecArray | null
|
||||
|
||||
while ((openingMatch = openingFencePattern.exec(body)) !== null) {
|
||||
const start = openingMatch.index
|
||||
if (start < cursor) {
|
||||
continue
|
||||
}
|
||||
|
||||
const openingFence = openingMatch[1]
|
||||
const fenceChar = openingFence[0]
|
||||
const fenceLength = openingFence.length
|
||||
const closingFencePattern = new RegExp(
|
||||
`^ {0,3}${escapeRegExp(fenceChar)}{${fenceLength},}[ \\t]*(?:\\r?\\n|$)`,
|
||||
"gm",
|
||||
)
|
||||
closingFencePattern.lastIndex = openingMatch.index + openingMatch[0].length
|
||||
const closingMatch = closingFencePattern.exec(body)
|
||||
const end = closingMatch
|
||||
? closingMatch.index + closingMatch[0].length
|
||||
: body.length
|
||||
|
||||
const placeholder = `__CODE_BLOCK_${codeBlockIndex}__`
|
||||
codeBlocks.push({
|
||||
placeholder,
|
||||
content: body.slice(start, end),
|
||||
})
|
||||
result += body.slice(cursor, start) + placeholder
|
||||
cursor = end
|
||||
codeBlockIndex++
|
||||
openingFencePattern.lastIndex = end
|
||||
}
|
||||
|
||||
result += body.slice(cursor)
|
||||
return { body: result, codeBlocks }
|
||||
}
|
||||
|
||||
// Self-linking Prevention
|
||||
const isSelfLink = (
|
||||
candidateData: CandidateData,
|
||||
|
|
@ -945,14 +997,17 @@ export const replaceLinks = ({
|
|||
})
|
||||
}
|
||||
|
||||
// Extract and protect fenced code blocks before any other block-level rules.
|
||||
const { body: bodyAfterCodeBlocks, codeBlocks } = extractFencedCodeBlocks(body)
|
||||
|
||||
// Extract and protect headings first
|
||||
const headingPattern = /^#{1,6}\s+.*$/gm
|
||||
const headings: Array<{ placeholder: string, content: string }> = []
|
||||
let headingIndex = 0
|
||||
|
||||
let bodyAfterHeadings = body
|
||||
let bodyAfterHeadings = bodyAfterCodeBlocks
|
||||
if (settings.ignoreHeadings) {
|
||||
bodyAfterHeadings = body.replace(headingPattern, (match) => {
|
||||
bodyAfterHeadings = bodyAfterCodeBlocks.replace(headingPattern, (match) => {
|
||||
const placeholder = `__HEADING_${headingIndex}__`
|
||||
headings.push({ placeholder, content: match })
|
||||
headingIndex++
|
||||
|
|
@ -1044,5 +1099,10 @@ export const replaceLinks = ({
|
|||
resultBody = resultBody.replace(placeholder, content)
|
||||
}
|
||||
|
||||
// Restore fenced code blocks
|
||||
for (const { placeholder, content } of codeBlocks) {
|
||||
resultBody = resultBody.replace(placeholder, content)
|
||||
}
|
||||
|
||||
return resultBody
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue