mirror of
https://github.com/facebook/docusaurus.git
synced 2025-08-02 08:19:07 +02:00
fix(cli): write-heading-id should not generate colliding slugs when not overwriting (#6849)
This commit is contained in:
parent
027e8f506b
commit
a756ddb7e1
2 changed files with 55 additions and 103 deletions
|
@ -5,91 +5,63 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
transformMarkdownHeadingLine,
|
||||
transformMarkdownContent,
|
||||
} from '../writeHeadingIds';
|
||||
import {createSlugger} from '@docusaurus/utils';
|
||||
|
||||
describe('transformMarkdownHeadingLine', () => {
|
||||
test('throws when not a heading', () => {
|
||||
expect(() =>
|
||||
transformMarkdownHeadingLine('ABC', createSlugger()),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Line is not a Markdown heading: ABC."`,
|
||||
);
|
||||
});
|
||||
import {transformMarkdownContent} from '../writeHeadingIds';
|
||||
|
||||
describe('transformMarkdownContent', () => {
|
||||
test('works for simple level-2 heading', () => {
|
||||
expect(transformMarkdownHeadingLine('## ABC', createSlugger())).toEqual(
|
||||
'## ABC {#abc}',
|
||||
);
|
||||
expect(transformMarkdownContent('## ABC')).toEqual('## ABC {#abc}');
|
||||
});
|
||||
|
||||
test('works for simple level-3 heading', () => {
|
||||
expect(transformMarkdownHeadingLine('### ABC', createSlugger())).toEqual(
|
||||
'### ABC {#abc}',
|
||||
);
|
||||
expect(transformMarkdownContent('### ABC')).toEqual('### ABC {#abc}');
|
||||
});
|
||||
|
||||
test('works for simple level-4 heading', () => {
|
||||
expect(transformMarkdownHeadingLine('#### ABC', createSlugger())).toEqual(
|
||||
'#### ABC {#abc}',
|
||||
);
|
||||
expect(transformMarkdownContent('#### ABC')).toEqual('#### ABC {#abc}');
|
||||
});
|
||||
|
||||
test('unwraps markdown links', () => {
|
||||
const input = `## hello [facebook](https://facebook.com) [crowdin](https://crowdin.com/translate/docusaurus-v2/126/en-fr?filter=basic&value=0)`;
|
||||
expect(transformMarkdownHeadingLine(input, createSlugger())).toEqual(
|
||||
expect(transformMarkdownContent(input)).toEqual(
|
||||
`${input} {#hello-facebook-crowdin}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('can slugify complex headings', () => {
|
||||
const input = '## abc [Hello] How are you %Sébastien_-_$)( ## -56756';
|
||||
expect(transformMarkdownHeadingLine(input, createSlugger())).toEqual(
|
||||
expect(transformMarkdownContent(input)).toEqual(
|
||||
`${input} {#abc-hello-how-are-you-sébastien_-_---56756}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('does not duplicate duplicate id', () => {
|
||||
expect(
|
||||
transformMarkdownHeadingLine(
|
||||
'## hello world {#hello-world}',
|
||||
createSlugger(),
|
||||
),
|
||||
).toEqual('## hello world {#hello-world}');
|
||||
expect(transformMarkdownContent('## hello world {#hello-world}')).toEqual(
|
||||
'## hello world {#hello-world}',
|
||||
);
|
||||
});
|
||||
|
||||
test('respects existing heading', () => {
|
||||
expect(
|
||||
transformMarkdownHeadingLine(
|
||||
'## New heading {#old-heading}',
|
||||
createSlugger(),
|
||||
),
|
||||
).toEqual('## New heading {#old-heading}');
|
||||
expect(transformMarkdownContent('## New heading {#old-heading}')).toEqual(
|
||||
'## New heading {#old-heading}',
|
||||
);
|
||||
});
|
||||
|
||||
test('overwrites heading ID when asked to', () => {
|
||||
expect(
|
||||
transformMarkdownHeadingLine(
|
||||
'## New heading {#old-heading}',
|
||||
createSlugger(),
|
||||
{overwrite: true},
|
||||
),
|
||||
transformMarkdownContent('## New heading {#old-heading}', {
|
||||
overwrite: true,
|
||||
}),
|
||||
).toEqual('## New heading {#new-heading}');
|
||||
});
|
||||
|
||||
test('maintains casing when asked to', () => {
|
||||
expect(
|
||||
transformMarkdownHeadingLine('## getDataFromAPI()', createSlugger(), {
|
||||
transformMarkdownContent('## getDataFromAPI()', {
|
||||
maintainCase: true,
|
||||
}),
|
||||
).toEqual('## getDataFromAPI() {#getDataFromAPI}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformMarkdownContent', () => {
|
||||
test('transform the headings', () => {
|
||||
const input = `
|
||||
|
||||
|
@ -113,14 +85,11 @@ describe('transformMarkdownContent', () => {
|
|||
|
||||
`;
|
||||
|
||||
// TODO the first heading should probably rather be slugified to abc-1
|
||||
// otherwise we end up with 2 x "abc" anchors
|
||||
// not sure how to implement that atm
|
||||
const expected = `
|
||||
|
||||
# Ignored title
|
||||
|
||||
## abc {#abc}
|
||||
## abc {#abc-1}
|
||||
|
||||
### Hello world {#hello-world}
|
||||
|
||||
|
|
|
@ -38,66 +38,52 @@ function addHeadingId(
|
|||
|
||||
const headingText = line.slice(headingLevel).trimEnd();
|
||||
const headingHashes = line.slice(0, headingLevel);
|
||||
const slug = slugger
|
||||
.slug(unwrapMarkdownLinks(headingText).trim(), {maintainCase})
|
||||
.replace(/^-+/, '')
|
||||
.replace(/-+$/, '');
|
||||
const slug = slugger.slug(unwrapMarkdownLinks(headingText).trim(), {
|
||||
maintainCase,
|
||||
});
|
||||
|
||||
return `${headingHashes}${headingText} {#${slug}}`;
|
||||
}
|
||||
|
||||
export function transformMarkdownHeadingLine(
|
||||
line: string,
|
||||
slugger: Slugger,
|
||||
export function transformMarkdownContent(
|
||||
content: string,
|
||||
options: Options = {maintainCase: false, overwrite: false},
|
||||
): string {
|
||||
const {maintainCase = false, overwrite = false} = options;
|
||||
if (!line.startsWith('#')) {
|
||||
throw new Error(`Line is not a Markdown heading: ${line}.`);
|
||||
}
|
||||
|
||||
const parsedHeading = parseMarkdownHeadingId(line);
|
||||
|
||||
// Do not process if id is already there
|
||||
if (parsedHeading.id && !overwrite) {
|
||||
return line;
|
||||
}
|
||||
return addHeadingId(parsedHeading.text, slugger, maintainCase);
|
||||
}
|
||||
|
||||
function transformMarkdownLine(
|
||||
line: string,
|
||||
slugger: Slugger,
|
||||
options?: Options,
|
||||
): string {
|
||||
// Ignore h1 headings on purpose, as we don't create anchor links for those
|
||||
if (line.startsWith('##')) {
|
||||
return transformMarkdownHeadingLine(line, slugger, options);
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
function transformMarkdownLines(lines: string[], options?: Options): string[] {
|
||||
let inCode = false;
|
||||
const lines = content.split('\n');
|
||||
const slugger = createSlugger();
|
||||
|
||||
return lines.map((line) => {
|
||||
if (line.startsWith('```')) {
|
||||
inCode = !inCode;
|
||||
return line;
|
||||
}
|
||||
if (inCode) {
|
||||
return line;
|
||||
}
|
||||
return transformMarkdownLine(line, slugger, options);
|
||||
});
|
||||
}
|
||||
// If we can't overwrite existing slugs, make sure other headings don't
|
||||
// generate colliding slugs by first marking these slugs as occupied
|
||||
if (!overwrite) {
|
||||
lines.forEach((line) => {
|
||||
const parsedHeading = parseMarkdownHeadingId(line);
|
||||
if (parsedHeading.id) {
|
||||
slugger.slug(parsedHeading.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function transformMarkdownContent(
|
||||
content: string,
|
||||
options?: Options,
|
||||
): string {
|
||||
return transformMarkdownLines(content.split('\n'), options).join('\n');
|
||||
let inCode = false;
|
||||
return lines
|
||||
.map((line) => {
|
||||
if (line.startsWith('```')) {
|
||||
inCode = !inCode;
|
||||
return line;
|
||||
}
|
||||
// Ignore h1 headings, as we don't create anchor links for those
|
||||
if (inCode || !line.startsWith('##')) {
|
||||
return line;
|
||||
}
|
||||
const parsedHeading = parseMarkdownHeadingId(line);
|
||||
|
||||
// Do not process if id is already there
|
||||
if (parsedHeading.id && !overwrite) {
|
||||
return line;
|
||||
}
|
||||
return addHeadingId(parsedHeading.text, slugger, maintainCase);
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
async function transformMarkdownFile(
|
||||
|
@ -105,10 +91,7 @@ async function transformMarkdownFile(
|
|||
options?: Options,
|
||||
): Promise<string | undefined> {
|
||||
const content = await fs.readFile(filepath, 'utf8');
|
||||
const updatedContent = transformMarkdownLines(
|
||||
content.split('\n'),
|
||||
options,
|
||||
).join('\n');
|
||||
const updatedContent = transformMarkdownContent(content, options);
|
||||
if (content !== updatedContent) {
|
||||
await fs.writeFile(filepath, updatedContent);
|
||||
return filepath;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue