fix(v2): fix contentTitle issues when markdown h1 title contains code blocks (#4882)

* attempt to fix contentTitle issues when markdown h1 title contains inline code blocks

* mention hide_title frontmatter only prevents frontmatter.title from being added in the dom (not a markdown # title in content)

* alwayss insert MainHeading under the div.markdown container for consistency

* ensure MainHeading has no useless id

* revert https://github.com/facebook/docusaurus/pull/4859 as it's now useless: docMeta.title contains the text/frontmatter title in priority over the contentTitle

* fix docs test after revert

* improve markdownParser and fix tests

* fix docs tests

* markdownParser: restore option to remove contentTitle (mostly for blog plugin)

* use removeContentTitle for blog
This commit is contained in:
Sébastien Lorber 2021-06-03 17:45:19 +02:00 committed by GitHub
parent 85e87b560e
commit 57806798c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 246 additions and 178 deletions

View file

@ -141,11 +141,71 @@ describe('parseMarkdownContentTitle', () => {
`;
expect(parseMarkdownContentTitle(markdown)).toEqual({
content: markdown,
contentTitle: 'Markdown Title',
});
});
test('Should parse markdown h1 title at the top and remove it', () => {
const markdown = dedent`
# Markdown Title
Lorem Ipsum
`;
expect(
parseMarkdownContentTitle(markdown, {removeContentTitle: true}),
).toEqual({
content: 'Lorem Ipsum',
contentTitle: 'Markdown Title',
});
});
test('Should parse markdown h1 title at the top and unwrap inline code block', () => {
const markdown = dedent`
# \`Markdown Title\`
Lorem Ipsum
`;
expect(parseMarkdownContentTitle(markdown)).toEqual({
content: markdown,
contentTitle: 'Markdown Title',
});
});
test('Should parse markdown h1 title and trim content', () => {
const markdown = `
# Markdown Title
Lorem Ipsum
`;
expect(parseMarkdownContentTitle(markdown)).toEqual({
content: markdown.trim(),
contentTitle: 'Markdown Title',
});
});
test('Should parse not parse markdown h1 title and trim content', () => {
const markdown = `
Lorem Ipsum
`;
expect(parseMarkdownContentTitle(markdown)).toEqual({
content: markdown.trim(),
contentTitle: undefined,
});
});
test('Should parse markdown h1 title with fixed anchor-id syntax', () => {
const markdown = dedent`
@ -155,7 +215,7 @@ describe('parseMarkdownContentTitle', () => {
`;
expect(parseMarkdownContentTitle(markdown)).toEqual({
content: 'Lorem Ipsum',
content: markdown,
contentTitle: 'Markdown Title',
});
});
@ -169,7 +229,7 @@ describe('parseMarkdownContentTitle', () => {
`;
expect(parseMarkdownContentTitle(markdown)).toEqual({
content: 'Lorem Ipsum',
content: markdown,
contentTitle: 'Markdown Title',
});
});
@ -185,12 +245,7 @@ describe('parseMarkdownContentTitle', () => {
`;
expect(parseMarkdownContentTitle(markdown)).toEqual({
content: dedent`
## Heading 2
Lorem Ipsum
`,
content: markdown,
contentTitle: 'Markdown Title',
});
});
@ -206,12 +261,7 @@ describe('parseMarkdownContentTitle', () => {
`;
expect(parseMarkdownContentTitle(markdown)).toEqual({
content: dedent`
# Markdown Title 2
Lorem Ipsum
`,
content: markdown,
contentTitle: 'Markdown Title',
});
});
@ -227,15 +277,7 @@ describe('parseMarkdownContentTitle', () => {
`;
expect(parseMarkdownContentTitle(markdown)).toEqual({
content: dedent`
Lorem Ipsum
# Markdown Title 2
Lorem Ipsum
`,
content: markdown,
contentTitle: undefined,
});
});
@ -250,6 +292,23 @@ describe('parseMarkdownContentTitle', () => {
`;
expect(parseMarkdownContentTitle(markdown)).toEqual({
content: markdown,
contentTitle: 'Markdown Title',
});
});
test('Should parse markdown h1 alternate title and remove it', () => {
const markdown = dedent`
Markdown Title
================
Lorem Ipsum
`;
expect(
parseMarkdownContentTitle(markdown, {removeContentTitle: true}),
).toEqual({
content: 'Lorem Ipsum',
contentTitle: 'Markdown Title',
});
@ -271,17 +330,7 @@ describe('parseMarkdownContentTitle', () => {
// remove the useless line breaks? Does not matter too much
expect(parseMarkdownContentTitle(markdown)).toEqual({
content: dedent`
import Component1 from '@site/src/components/Component1';
import Component2 from '@site/src/components/Component2'
import Component3 from '@site/src/components/Component3'
import './styles.css';
Lorem Ipsum
`,
content: markdown,
contentTitle: 'Markdown Title',
});
});
@ -306,10 +355,16 @@ import "module-name"
# Markdown Title
Lorem Ipsum
`;
`;
expect(parseMarkdownContentTitle(markdown)).toEqual({
content: `
content: markdown.trim(),
contentTitle: 'Markdown Title',
});
});
test('Should parse markdown h1 title placed after various import declarations and remove it', () => {
const markdown = `
import DefaultComponent from '@site/src/components/Component1';
import DefaultComponent2 from '../relative/path/Component2';
import * as EntireComponent from './relative/path/Component3';
@ -325,16 +380,22 @@ import './styles.css';
import _ from 'underscore';
import "module-name"
# Markdown Title
Lorem Ipsum
`.trim(),
`;
expect(
parseMarkdownContentTitle(markdown, {removeContentTitle: true}),
).toEqual({
content: markdown.trim().replace('# Markdown Title', ''),
contentTitle: 'Markdown Title',
});
});
test('Should parse markdown h1 alternate title placed after import declarations', () => {
const markdown = dedent`
import Component from '@site/src/components/Component';
import Component from '@site/src/components/Component'
import './styles.css';
@ -346,41 +407,40 @@ Lorem Ipsum
`;
expect(parseMarkdownContentTitle(markdown)).toEqual({
content: dedent`
import Component from '@site/src/components/Component';
import Component from '@site/src/components/Component'
import './styles.css';
Lorem Ipsum
`,
content: markdown,
contentTitle: 'Markdown Title',
});
});
test('Should parse title-only', () => {
const markdown = '# Document With Only A Title ';
expect(parseMarkdownContentTitle(markdown)).toEqual({
content: '',
contentTitle: 'Document With Only A Title',
});
});
test('Should parse markdown h1 title at the top but keep it in content', () => {
test('Should parse markdown h1 alternate title placed after import declarations and remove it', () => {
const markdown = dedent`
# Markdown Title
import Component from '@site/src/components/Component';
import Component from '@site/src/components/Component'
import './styles.css';
Markdown Title
==============
Lorem Ipsum
`;
expect(
parseMarkdownContentTitle(markdown, {keepContentTitle: true}),
parseMarkdownContentTitle(markdown, {removeContentTitle: true}),
).toEqual({
content: markdown.trim(),
content: markdown.replace('Markdown Title\n==============\n\n', ''),
contentTitle: 'Markdown Title',
});
});
test('Should parse title-only', () => {
const markdown = '# Document With Only A Title';
expect(parseMarkdownContentTitle(markdown)).toEqual({
content: markdown,
contentTitle: 'Document With Only A Title',
});
});
test('Should not parse markdown h1 title in the middle of a doc', () => {
const markdown = dedent`
@ -439,7 +499,13 @@ Lorem Ipsum
`;
expect(parseMarkdownContentTitle(markdown)).toEqual({
content: dedent`
content: markdown,
contentTitle: 'Markdown Title',
});
});
test('Should parse markdown h1 title placed after multiple import declarations and remove it', () => {
const markdown = dedent`
import Component1 from '@site/src/components/Component1';
import Component2 from '@site/src/components/Component2';
import Component3 from '@site/src/components/Component3';
@ -456,11 +522,16 @@ Lorem Ipsum
import Component14 from '@site/src/components/Component14';
import Component15 from '@site/src/components/Component15';
# Markdown Title
Lorem Ipsum
`,
`;
expect(
parseMarkdownContentTitle(markdown, {removeContentTitle: true}),
).toEqual({
content: markdown.replace('# Markdown Title', ''),
contentTitle: 'Markdown Title',
});
});
@ -497,7 +568,9 @@ describe('parseMarkdownString', () => {
`),
).toMatchInlineSnapshot(`
Object {
"content": "Some text",
"content": "# Markdown Title
Some text",
"contentTitle": "Markdown Title",
"excerpt": "Some text",
"frontMatter": Object {},
@ -518,7 +591,9 @@ describe('parseMarkdownString', () => {
`),
).toMatchInlineSnapshot(`
Object {
"content": "Some text",
"content": "# Markdown Title
Some text",
"contentTitle": "Markdown Title",
"excerpt": "Some text",
"frontMatter": Object {
@ -542,36 +617,11 @@ describe('parseMarkdownString', () => {
`),
).toMatchInlineSnapshot(`
Object {
"content": "Some text",
"contentTitle": "Markdown Title alternate",
"excerpt": "Some text",
"frontMatter": Object {
"title": "Frontmatter title",
},
}
`);
});
test('should not warn for duplicate title if keepContentTitle=true', () => {
expect(
parseMarkdownString(
dedent`
---
title: Frontmatter title
---
# Markdown Title
Some text
`,
{keepContentTitle: true},
),
).toMatchInlineSnapshot(`
Object {
"content": "# Markdown Title
"content": "Markdown Title alternate
================
Some text",
"contentTitle": "Markdown Title",
"contentTitle": "Markdown Title alternate",
"excerpt": "Some text",
"frontMatter": Object {
"title": "Frontmatter title",
@ -605,24 +655,6 @@ describe('parseMarkdownString', () => {
`);
});
test('should parse markdown title and keep it in content', () => {
expect(
parseMarkdownString(
dedent`
# Markdown Title
`,
{keepContentTitle: true},
),
).toMatchInlineSnapshot(`
Object {
"content": "# Markdown Title",
"contentTitle": "Markdown Title",
"excerpt": undefined,
"frontMatter": Object {},
}
`);
});
test('should delete only first heading', () => {
expect(
parseMarkdownString(dedent`
@ -636,7 +668,9 @@ describe('parseMarkdownString', () => {
`),
).toMatchInlineSnapshot(`
Object {
"content": "test test test # test bar
"content": "# Markdown Title
test test test # test bar
# Markdown Title 2
@ -692,7 +726,7 @@ describe('parseMarkdownString', () => {
test('should parse title only', () => {
expect(parseMarkdownString('# test')).toMatchInlineSnapshot(`
Object {
"content": "",
"content": "# test",
"contentTitle": "test",
"excerpt": undefined,
"frontMatter": Object {},
@ -708,7 +742,8 @@ describe('parseMarkdownString', () => {
`),
).toMatchInlineSnapshot(`
Object {
"content": "",
"content": "test
===",
"contentTitle": "test",
"excerpt": undefined,
"frontMatter": Object {},
@ -726,7 +761,7 @@ describe('parseMarkdownString', () => {
`),
).toMatchInlineSnapshot(`
Object {
"content": "",
"content": "# test",
"contentTitle": "test",
"excerpt": undefined,
"frontMatter": Object {
@ -766,7 +801,9 @@ describe('parseMarkdownString', () => {
`),
).toMatchInlineSnapshot(`
Object {
"content": "test test test test test test
"content": "# test
test test test test test test
test test test # test bar
# test2
### test

View file

@ -80,11 +80,21 @@ export function parseFrontMatter(
};
}
// Try to convert markdown heading as text
// Does not need to be perfect, it is only used as a fallback when frontMatter.title is not provided
// For now, we just unwrap possible inline code blocks (# `config.js`)
function toTextContentTitle(contentTitle: string): string {
if (contentTitle.startsWith('`') && contentTitle.endsWith('`')) {
return contentTitle.substring(1, contentTitle.length - 1);
}
return contentTitle;
}
export function parseMarkdownContentTitle(
contentUntrimmed: string,
options?: {keepContentTitle?: boolean},
options?: {removeContentTitle?: boolean},
): {content: string; contentTitle: string | undefined} {
const keepContentTitleOption = options?.keepContentTitle ?? false;
const removeContentTitleOption = options?.removeContentTitle ?? false;
const content = contentUntrimmed.trim();
@ -108,16 +118,15 @@ export function parseMarkdownContentTitle(
if (!pattern || !title) {
return {content, contentTitle: undefined};
} else {
const newContent = removeContentTitleOption
? content.replace(pattern, '')
: content;
return {
content: newContent.trim(),
contentTitle: toTextContentTitle(title.trim()).trim(),
};
}
const newContent = keepContentTitleOption
? content
: content.replace(pattern, '');
return {
content: newContent.trim(),
contentTitle: title.trim(),
};
}
type ParsedMarkdown = {
@ -129,22 +138,16 @@ type ParsedMarkdown = {
export function parseMarkdownString(
markdownFileContent: string,
options?: {
keepContentTitle?: boolean;
},
options?: {removeContentTitle?: boolean},
): ParsedMarkdown {
try {
const keepContentTitle = options?.keepContentTitle ?? false;
const {frontMatter, content: contentWithoutFrontMatter} = parseFrontMatter(
markdownFileContent,
);
const {content, contentTitle} = parseMarkdownContentTitle(
contentWithoutFrontMatter,
{
keepContentTitle,
},
options,
);
const excerpt = createExcerpt(content);
@ -166,10 +169,11 @@ This can happen if you use special characters like : in frontmatter values (try
export async function parseMarkdownFile(
source: string,
options?: {removeContentTitle?: boolean},
): Promise<ParsedMarkdown> {
const markdownString = await fs.readFile(source, 'utf-8');
try {
return parseMarkdownString(markdownString);
return parseMarkdownString(markdownString, options);
} catch (e) {
throw new Error(
`Error while parsing markdown file ${source}