mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-10 07:37:19 +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
|
@ -31,9 +31,8 @@ module.exports = async function docusaurusMdxLoader(fileString) {
|
|||
|
||||
const {frontMatter, content: contentWithTitle} = parseFrontMatter(fileString);
|
||||
|
||||
// By default, will remove the markdown title from the content
|
||||
const {content} = parseMarkdownContentTitle(contentWithTitle, {
|
||||
keepContentTitle: reqOptions.keepContentTitle,
|
||||
const {content, contentTitle} = parseMarkdownContentTitle(contentWithTitle, {
|
||||
removeContentTitle: reqOptions.removeContentTitle,
|
||||
});
|
||||
|
||||
const hasFrontMatter = Object.keys(frontMatter).length > 0;
|
||||
|
@ -69,7 +68,11 @@ module.exports = async function docusaurusMdxLoader(fileString) {
|
|||
return callback(err);
|
||||
}
|
||||
|
||||
let exportStr = `export const frontMatter = ${stringifyObject(frontMatter)};`;
|
||||
let exportStr = ``;
|
||||
exportStr += `\nexport const frontMatter = ${stringifyObject(frontMatter)};`;
|
||||
exportStr += `\nexport const contentTitle = ${stringifyObject(
|
||||
contentTitle,
|
||||
)};`;
|
||||
|
||||
// Read metadata for this MDX and export it.
|
||||
if (options.metadataPath && typeof options.metadataPath === 'function') {
|
||||
|
|
|
@ -146,7 +146,7 @@ export async function generateBlogPosts(
|
|||
content,
|
||||
contentTitle,
|
||||
excerpt,
|
||||
} = await parseMarkdownFile(source);
|
||||
} = await parseMarkdownFile(source, {removeContentTitle: true});
|
||||
const frontMatter = validateBlogPostFrontMatter(unsafeFrontMatter);
|
||||
|
||||
const aliasedSource = aliasedSitePath(source, siteDir);
|
||||
|
|
|
@ -459,6 +459,9 @@ export default function pluginContentBlog(
|
|||
`${docuHash(aliasedPath)}.json`,
|
||||
);
|
||||
},
|
||||
// For blog posts a title in markdown is always removed
|
||||
// Blog posts title are rendered separately
|
||||
removeContentTitle: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -168,7 +168,7 @@ Object {
|
|||
\\"unversionedId\\": \\"foo/bar\\",
|
||||
\\"id\\": \\"foo/bar\\",
|
||||
\\"isDocsHomePage\\": false,
|
||||
\\"title\\": \\"Remarkable\\",
|
||||
\\"title\\": \\"Bar\\",
|
||||
\\"description\\": \\"This is custom description\\",
|
||||
\\"source\\": \\"@site/docs/foo/bar.md\\",
|
||||
\\"sourceDirName\\": \\"foo\\",
|
||||
|
@ -190,7 +190,7 @@ Object {
|
|||
\\"unversionedId\\": \\"foo/baz\\",
|
||||
\\"id\\": \\"foo/baz\\",
|
||||
\\"isDocsHomePage\\": false,
|
||||
\\"title\\": \\"Baz markdown title\\",
|
||||
\\"title\\": \\"baz\\",
|
||||
\\"description\\": \\"Images\\",
|
||||
\\"source\\": \\"@site/docs/foo/baz.md\\",
|
||||
\\"sourceDirName\\": \\"foo\\",
|
||||
|
@ -418,12 +418,12 @@ Object {
|
|||
\\"items\\": [
|
||||
{
|
||||
\\"type\\": \\"link\\",
|
||||
\\"label\\": \\"Remarkable\\",
|
||||
\\"label\\": \\"Bar\\",
|
||||
\\"href\\": \\"/docs/foo/bar\\"
|
||||
},
|
||||
{
|
||||
\\"type\\": \\"link\\",
|
||||
\\"label\\": \\"Baz markdown title\\",
|
||||
\\"label\\": \\"baz\\",
|
||||
\\"href\\": \\"/docs/foo/bazSlug.html\\"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -180,7 +180,7 @@ describe('simple site', () => {
|
|||
isDocsHomePage: false,
|
||||
permalink: '/docs/foo/bar',
|
||||
slug: '/foo/bar',
|
||||
title: 'Remarkable',
|
||||
title: 'Bar',
|
||||
description: 'This is custom description',
|
||||
frontMatter: {
|
||||
description: 'This is custom description',
|
||||
|
@ -254,7 +254,7 @@ describe('simple site', () => {
|
|||
isDocsHomePage: true,
|
||||
permalink: '/docs/',
|
||||
slug: '/',
|
||||
title: 'Remarkable',
|
||||
title: 'Bar',
|
||||
description: 'This is custom description',
|
||||
frontMatter: {
|
||||
description: 'This is custom description',
|
||||
|
@ -286,7 +286,7 @@ describe('simple site', () => {
|
|||
isDocsHomePage: false,
|
||||
permalink: '/docs/foo/bazSlug.html',
|
||||
slug: '/foo/bazSlug.html',
|
||||
title: 'Baz markdown title',
|
||||
title: 'baz',
|
||||
editUrl:
|
||||
'https://github.com/facebook/docusaurus/edit/master/website/docs/foo/baz.md',
|
||||
description: 'Images',
|
||||
|
@ -345,7 +345,7 @@ describe('simple site', () => {
|
|||
isDocsHomePage: false,
|
||||
permalink: '/docs/foo/bazSlug.html',
|
||||
slug: '/foo/bazSlug.html',
|
||||
title: 'Baz markdown title',
|
||||
title: 'baz',
|
||||
editUrl: hardcodedEditUrl,
|
||||
description: 'Images',
|
||||
frontMatter: {
|
||||
|
|
|
@ -375,7 +375,7 @@ describe('simple website', () => {
|
|||
'foo',
|
||||
'bar.md',
|
||||
),
|
||||
title: 'Remarkable',
|
||||
title: 'Bar',
|
||||
description: 'This is custom description',
|
||||
frontMatter: {
|
||||
description: 'This is custom description',
|
||||
|
|
|
@ -202,11 +202,9 @@ export function processDocMetadata({
|
|||
numberPrefixParser: options.numberPrefixParser,
|
||||
});
|
||||
|
||||
// TODO expose both headingTitle+metaTitle to theme?
|
||||
// Different fallbacks order on purpose!
|
||||
// See https://github.com/facebook/docusaurus/issues/4665#issuecomment-825831367
|
||||
const headingTitle: string = contentTitle ?? frontMatter.title ?? baseID;
|
||||
// const metaTitle: string = frontMatter.title ?? contentTitle ?? baseID;
|
||||
// Note: the title is used by default for page title, sidebar label, pagination buttons...
|
||||
// frontMatter.title should be used in priority over contentTitle (because it can contain markdown/JSX syntax)
|
||||
const title: string = frontMatter.title ?? contentTitle ?? baseID;
|
||||
|
||||
const description: string = frontMatter.description ?? excerpt ?? '';
|
||||
|
||||
|
@ -245,7 +243,7 @@ export function processDocMetadata({
|
|||
unversionedId,
|
||||
id,
|
||||
isDocsHomePage,
|
||||
title: headingTitle,
|
||||
title,
|
||||
description,
|
||||
source: aliasedSitePath(filePath, siteDir),
|
||||
sourceDirName,
|
||||
|
|
|
@ -199,11 +199,7 @@ export default function pluginContentDocs(
|
|||
nextId,
|
||||
} = sidebarsUtils.getDocNavigation(doc.id);
|
||||
const toDocNavLink = (navDocId: string): DocNavLink => ({
|
||||
// Use frontMatter.title in priority over a potential # title found in markdown
|
||||
// See https://github.com/facebook/docusaurus/issues/4665#issuecomment-825831367
|
||||
title:
|
||||
docsBaseById[navDocId].frontMatter.title ||
|
||||
docsBaseById[navDocId].title,
|
||||
title: docsBaseById[navDocId].title,
|
||||
permalink: docsBaseById[navDocId].permalink,
|
||||
});
|
||||
return {
|
||||
|
|
|
@ -82,6 +82,7 @@ declare module '@theme/DocItem' {
|
|||
readonly frontMatter: FrontMatter;
|
||||
readonly metadata: Metadata;
|
||||
readonly toc: readonly TOCItem[];
|
||||
readonly contentTitle: string | undefined;
|
||||
(): JSX.Element;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -222,7 +222,6 @@ export default function pluginContentPages(
|
|||
rehypePlugins,
|
||||
beforeDefaultRehypePlugins,
|
||||
beforeDefaultRemarkPlugins,
|
||||
keepContentTitle: true,
|
||||
staticDir: path.join(siteDir, STATIC_DIR_NAME),
|
||||
// Note that metadataPath must be the same/in-sync as
|
||||
// the path from createData for each MDX.
|
||||
|
|
|
@ -47,8 +47,8 @@ function BlogPostItem(props: Props): JSX.Element {
|
|||
truncated,
|
||||
isBlogPostPage = false,
|
||||
} = props;
|
||||
const {date, formattedDate, permalink, tags, readingTime} = metadata;
|
||||
const {author, title, image, keywords} = frontMatter;
|
||||
const {date, formattedDate, permalink, tags, readingTime, title} = metadata;
|
||||
const {author, image, keywords} = frontMatter;
|
||||
|
||||
const authorURL = frontMatter.author_url || frontMatter.authorURL;
|
||||
const authorTitle = frontMatter.author_title || frontMatter.authorTitle;
|
||||
|
|
|
@ -13,6 +13,7 @@ import LastUpdated from '@theme/LastUpdated';
|
|||
import type {Props} from '@theme/DocItem';
|
||||
import TOC from '@theme/TOC';
|
||||
import EditThisPage from '@theme/EditThisPage';
|
||||
import {MainHeading} from '@theme/Heading';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import styles from './styles.module.css';
|
||||
|
@ -49,13 +50,15 @@ function DocItem(props: Props): JSX.Element {
|
|||
// See https://github.com/facebook/docusaurus/issues/3362
|
||||
const showVersionBadge = versions.length > 1;
|
||||
|
||||
// For meta title, using frontMatter.title in priority over a potential # title found in markdown
|
||||
// See https://github.com/facebook/docusaurus/issues/4665#issuecomment-825831367
|
||||
const metaTitle = frontMatter.title || title;
|
||||
// We only add a title if:
|
||||
// - user asks to hide it with frontmatter
|
||||
// - the markdown content does not already contain a top-level h1 heading
|
||||
const shouldAddTitle =
|
||||
!hideTitle && typeof DocContent.contentTitle === 'undefined';
|
||||
|
||||
return (
|
||||
<>
|
||||
<Seo {...{title: metaTitle, description, keywords, image}} />
|
||||
<Seo {...{title, description, keywords, image}} />
|
||||
|
||||
<div className="row">
|
||||
<div
|
||||
|
@ -72,12 +75,13 @@ function DocItem(props: Props): JSX.Element {
|
|||
</span>
|
||||
</div>
|
||||
)}
|
||||
{!hideTitle && (
|
||||
<header>
|
||||
<h1 className={styles.docTitle}>{title}</h1>
|
||||
</header>
|
||||
)}
|
||||
<div className="markdown">
|
||||
{/*
|
||||
Title can be declared inside md content or declared through frontmatter and added manually
|
||||
To make both cases consistent, the added title is added under the same div.markdown block
|
||||
See https://github.com/facebook/docusaurus/pull/4882#issuecomment-853021120
|
||||
*/}
|
||||
{shouldAddTitle && <MainHeading>{title}</MainHeading>}
|
||||
<DocContent />
|
||||
</div>
|
||||
</article>
|
||||
|
|
|
@ -5,11 +5,6 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
.docTitle {
|
||||
font-size: 3rem;
|
||||
margin-bottom: calc(var(--ifm-leading-desktop) * var(--ifm-leading));
|
||||
}
|
||||
|
||||
.docItemContainer {
|
||||
margin: 0 auto;
|
||||
padding: 0 0.5rem;
|
||||
|
|
|
@ -16,7 +16,24 @@ import {useThemeConfig} from '@docusaurus/theme-common';
|
|||
import './styles.css';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
const Heading = (Tag: HeadingType): ((props: Props) => JSX.Element) =>
|
||||
type HeadingComponent = (props: Props) => JSX.Element;
|
||||
|
||||
export const MainHeading: HeadingComponent = function MainHeading({...props}) {
|
||||
return (
|
||||
<header>
|
||||
<h1
|
||||
{...props}
|
||||
id={undefined} // h1 headings do not need an id because they don't appear in the TOC
|
||||
className={styles.h1Heading}>
|
||||
{props.children}
|
||||
</h1>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
const createAnchorHeading = (
|
||||
Tag: HeadingType,
|
||||
): ((props: Props) => JSX.Element) =>
|
||||
function TargetComponent({id, ...props}) {
|
||||
const {
|
||||
navbar: {hideOnScroll},
|
||||
|
@ -51,4 +68,8 @@ const Heading = (Tag: HeadingType): ((props: Props) => JSX.Element) =>
|
|||
);
|
||||
};
|
||||
|
||||
const Heading = (headingType: HeadingType): ((props: Props) => JSX.Element) => {
|
||||
return headingType === 'h1' ? MainHeading : createAnchorHeading(headingType);
|
||||
};
|
||||
|
||||
export default Heading;
|
||||
|
|
|
@ -8,3 +8,8 @@
|
|||
.enhancedAnchor {
|
||||
top: calc(var(--ifm-navbar-height) * -1 - 0.5rem);
|
||||
}
|
||||
|
||||
.h1Heading {
|
||||
font-size: 3rem;
|
||||
margin-bottom: calc(var(--ifm-leading-desktop) * var(--ifm-leading));
|
||||
}
|
||||
|
|
|
@ -114,6 +114,7 @@ declare module '@theme/Heading' {
|
|||
|
||||
const Heading: (Tag: HeadingType) => (props: Props) => JSX.Element;
|
||||
export default Heading;
|
||||
export const MainHeading: (props: Props) => JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/hooks/useAnnouncementBar' {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
---
|
||||
id: docusaurus.config.js
|
||||
title: docusaurus.config.js
|
||||
description: API reference for Docusaurus configuration file.
|
||||
slug: /docusaurus.config.js
|
||||
---
|
||||
|
||||
# `docusaurus.config.js`
|
||||
|
||||
## Overview {#overview}
|
||||
|
||||
`docusaurus.config.js` contains configurations for your site and is placed in the root directory of your site.
|
||||
|
|
|
@ -197,7 +197,7 @@ Markdown documents can use the following markdown frontmatter metadata fields, e
|
|||
|
||||
- `id`: A unique document id. If this field is not present, the document's `id` will default to its file name (without the extension)
|
||||
- `title`: The title of your document. If this field is not present, the document's `title` will default to its `id`
|
||||
- `hide_title`: Whether to hide the title at the top of the doc. By default, it is `false`
|
||||
- `hide_title`: Whether to hide the title at the top of the doc. It only hides a title declared through the frontmatter, and have no effect on a title at the top of your Markdown document. By default, it is `false`
|
||||
- `hide_table_of_contents`: Whether to hide the table of contents to the right. By default it is `false`
|
||||
- `sidebar_label`: The text shown in the document sidebar and in the next/previous button for this document. If this field is not present, the document's `sidebar_label` will default to its `title`
|
||||
- `sidebar_position`: Permits to control the position of a doc inside the generated sidebar slice, when using `autogenerated` sidebar items. Can be Int or Float.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue