Skip to content

Commit 76f9203

Browse files
authored
fix(utils): better handling of code blocks in link replacement (facebook#9046)
1 parent dcce8ff commit 76f9203

File tree

3 files changed

+73
-9
lines changed

3 files changed

+73
-9
lines changed

packages/docusaurus-utils/src/__tests__/__snapshots__/markdownLinks.test.ts.snap

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,22 @@ exports[`replaceMarkdownLinks handles stray spaces 1`] = `
4747
}
4848
`;
4949
50+
exports[`replaceMarkdownLinks handles unpaired fences 1`] = `
51+
{
52+
"brokenMarkdownLinks": [],
53+
"newContent": "
54+
\`\`\`foo
55+
hello
56+
57+
\`\`\`foo
58+
hello
59+
\`\`\`
60+
61+
A [link](/docs/file)
62+
",
63+
}
64+
`;
65+
5066
exports[`replaceMarkdownLinks ignores links in HTML comments 1`] = `
5167
{
5268
"brokenMarkdownLinks": [

packages/docusaurus-utils/src/__tests__/markdownLinks.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,32 @@ The following operations are defined for [URI]s:
371371
[URL](./file.md?foo=bar#baz)
372372
[URL](./file.md#a)
373373
[URL](./file.md?c)
374+
`,
375+
}),
376+
).toMatchSnapshot();
377+
});
378+
379+
it('handles unpaired fences', () => {
380+
expect(
381+
replaceMarkdownLinks({
382+
siteDir: '.',
383+
filePath: 'docs/file.md',
384+
contentPaths: {
385+
contentPath: 'docs',
386+
contentPathLocalized: 'i18n/docs-localized',
387+
},
388+
sourceToPermalink: {
389+
'@site/docs/file.md': '/docs/file',
390+
},
391+
fileString: `
392+
\`\`\`foo
393+
hello
394+
395+
\`\`\`foo
396+
hello
397+
\`\`\`
398+
399+
A [link](./file.md)
374400
`,
375401
}),
376402
).toMatchSnapshot();

packages/docusaurus-utils/src/markdownLinks.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,24 @@ export type BrokenMarkdownLink<T extends ContentPaths> = {
4040
link: string;
4141
};
4242

43+
type CodeFence = {
44+
type: '`' | '~';
45+
definitelyOpen: boolean;
46+
count: number;
47+
};
48+
49+
function parseCodeFence(line: string): CodeFence | null {
50+
const match = line.trim().match(/^(?<fence>`{3,}|~{3,})(?<rest>.*)/);
51+
if (!match) {
52+
return null;
53+
}
54+
return {
55+
type: match.groups!.fence![0]! as '`' | '~',
56+
definitelyOpen: !!match.groups!.rest!,
57+
count: match.groups!.fence!.length,
58+
};
59+
}
60+
4361
/**
4462
* Takes a Markdown file and replaces relative file references with their URL
4563
* counterparts, e.g. `[link](./intro.md)` => `[link](/docs/intro)`, preserving
@@ -82,19 +100,23 @@ export function replaceMarkdownLinks<T extends ContentPaths>({
82100
const brokenMarkdownLinks: BrokenMarkdownLink<T>[] = [];
83101

84102
// Replace internal markdown linking (except in fenced blocks).
85-
let lastCodeFence: string | null = null;
103+
let lastOpenCodeFence: CodeFence | null = null;
86104
const lines = fileString.split('\n').map((line) => {
87-
const codeFence = line.trimStart().match(/^`{3,}|^~{3,}/)?.[0];
105+
const codeFence = parseCodeFence(line);
88106
if (codeFence) {
89-
if (!lastCodeFence) {
90-
lastCodeFence = codeFence;
91-
// If we are in a ````-fenced block, all ``` would be plain text instead
92-
// of fences
93-
} else if (codeFence.startsWith(lastCodeFence)) {
94-
lastCodeFence = null;
107+
if (!lastOpenCodeFence) {
108+
lastOpenCodeFence = codeFence;
109+
} else if (
110+
!codeFence.definitelyOpen &&
111+
lastOpenCodeFence.type === codeFence.type &&
112+
lastOpenCodeFence.count <= codeFence.count
113+
) {
114+
// All three conditions must be met in order for this to be considered
115+
// a closing fence.
116+
lastOpenCodeFence = null;
95117
}
96118
}
97-
if (lastCodeFence) {
119+
if (lastOpenCodeFence) {
98120
return line;
99121
}
100122

0 commit comments

Comments
 (0)