mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-26 14:37:52 +02:00
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:
parent
85e87b560e
commit
57806798c5
20 changed files with 246 additions and 178 deletions
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue