fix(cli): write-heading-id should not generate colliding slugs when not overwriting (#6849)

This commit is contained in:
Joshua Chen 2022-03-05 17:25:47 +08:00 committed by GitHub
parent 027e8f506b
commit a756ddb7e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 55 additions and 103 deletions

View file

@ -5,91 +5,63 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import { import {transformMarkdownContent} from '../writeHeadingIds';
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."`,
);
});
describe('transformMarkdownContent', () => {
test('works for simple level-2 heading', () => { test('works for simple level-2 heading', () => {
expect(transformMarkdownHeadingLine('## ABC', createSlugger())).toEqual( expect(transformMarkdownContent('## ABC')).toEqual('## ABC {#abc}');
'## ABC {#abc}',
);
}); });
test('works for simple level-3 heading', () => { test('works for simple level-3 heading', () => {
expect(transformMarkdownHeadingLine('### ABC', createSlugger())).toEqual( expect(transformMarkdownContent('### ABC')).toEqual('### ABC {#abc}');
'### ABC {#abc}',
);
}); });
test('works for simple level-4 heading', () => { test('works for simple level-4 heading', () => {
expect(transformMarkdownHeadingLine('#### ABC', createSlugger())).toEqual( expect(transformMarkdownContent('#### ABC')).toEqual('#### ABC {#abc}');
'#### ABC {#abc}',
);
}); });
test('unwraps markdown links', () => { 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)`; 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}`, `${input} {#hello-facebook-crowdin}`,
); );
}); });
test('can slugify complex headings', () => { test('can slugify complex headings', () => {
const input = '## abc [Hello] How are you %Sébastien_-_$)( ## -56756'; 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}`, `${input} {#abc-hello-how-are-you-sébastien_-_---56756}`,
); );
}); });
test('does not duplicate duplicate id', () => { test('does not duplicate duplicate id', () => {
expect( expect(transformMarkdownContent('## hello world {#hello-world}')).toEqual(
transformMarkdownHeadingLine( '## hello world {#hello-world}',
'## hello world {#hello-world}', );
createSlugger(),
),
).toEqual('## hello world {#hello-world}');
}); });
test('respects existing heading', () => { test('respects existing heading', () => {
expect( expect(transformMarkdownContent('## New heading {#old-heading}')).toEqual(
transformMarkdownHeadingLine( '## New heading {#old-heading}',
'## New heading {#old-heading}', );
createSlugger(),
),
).toEqual('## New heading {#old-heading}');
}); });
test('overwrites heading ID when asked to', () => { test('overwrites heading ID when asked to', () => {
expect( expect(
transformMarkdownHeadingLine( transformMarkdownContent('## New heading {#old-heading}', {
'## New heading {#old-heading}', overwrite: true,
createSlugger(), }),
{overwrite: true},
),
).toEqual('## New heading {#new-heading}'); ).toEqual('## New heading {#new-heading}');
}); });
test('maintains casing when asked to', () => { test('maintains casing when asked to', () => {
expect( expect(
transformMarkdownHeadingLine('## getDataFromAPI()', createSlugger(), { transformMarkdownContent('## getDataFromAPI()', {
maintainCase: true, maintainCase: true,
}), }),
).toEqual('## getDataFromAPI() {#getDataFromAPI}'); ).toEqual('## getDataFromAPI() {#getDataFromAPI}');
}); });
});
describe('transformMarkdownContent', () => {
test('transform the headings', () => { test('transform the headings', () => {
const input = ` 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 = ` const expected = `
# Ignored title # Ignored title
## abc {#abc} ## abc {#abc-1}
### Hello world {#hello-world} ### Hello world {#hello-world}

View file

@ -38,66 +38,52 @@ function addHeadingId(
const headingText = line.slice(headingLevel).trimEnd(); const headingText = line.slice(headingLevel).trimEnd();
const headingHashes = line.slice(0, headingLevel); const headingHashes = line.slice(0, headingLevel);
const slug = slugger const slug = slugger.slug(unwrapMarkdownLinks(headingText).trim(), {
.slug(unwrapMarkdownLinks(headingText).trim(), {maintainCase}) maintainCase,
.replace(/^-+/, '') });
.replace(/-+$/, '');
return `${headingHashes}${headingText} {#${slug}}`; return `${headingHashes}${headingText} {#${slug}}`;
} }
export function transformMarkdownHeadingLine( export function transformMarkdownContent(
line: string, content: string,
slugger: Slugger,
options: Options = {maintainCase: false, overwrite: false}, options: Options = {maintainCase: false, overwrite: false},
): string { ): string {
const {maintainCase = false, overwrite = false} = options; const {maintainCase = false, overwrite = false} = options;
if (!line.startsWith('#')) { const lines = content.split('\n');
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 slugger = createSlugger(); const slugger = createSlugger();
return lines.map((line) => { // If we can't overwrite existing slugs, make sure other headings don't
if (line.startsWith('```')) { // generate colliding slugs by first marking these slugs as occupied
inCode = !inCode; if (!overwrite) {
return line; lines.forEach((line) => {
} const parsedHeading = parseMarkdownHeadingId(line);
if (inCode) { if (parsedHeading.id) {
return line; slugger.slug(parsedHeading.id);
} }
return transformMarkdownLine(line, slugger, options); });
}); }
}
export function transformMarkdownContent( let inCode = false;
content: string, return lines
options?: Options, .map((line) => {
): string { if (line.startsWith('```')) {
return transformMarkdownLines(content.split('\n'), options).join('\n'); 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( async function transformMarkdownFile(
@ -105,10 +91,7 @@ async function transformMarkdownFile(
options?: Options, options?: Options,
): Promise<string | undefined> { ): Promise<string | undefined> {
const content = await fs.readFile(filepath, 'utf8'); const content = await fs.readFile(filepath, 'utf8');
const updatedContent = transformMarkdownLines( const updatedContent = transformMarkdownContent(content, options);
content.split('\n'),
options,
).join('\n');
if (content !== updatedContent) { if (content !== updatedContent) {
await fs.writeFile(filepath, updatedContent); await fs.writeFile(filepath, updatedContent);
return filepath; return filepath;