From f1729723dcfcaa99bd8c90279f720a0237b73f24 Mon Sep 17 00:00:00 2001 From: slorber Date: Tue, 1 Jun 2021 20:10:09 +0200 Subject: [PATCH 01/10] attempt to fix contentTitle issues when markdown h1 title contains inline code blocks --- packages/docusaurus-mdx-loader/src/index.js | 11 ++-- .../src/docs.ts | 9 +-- .../src/plugin-content-docs.d.ts | 1 + .../src/index.ts | 1 - .../src/theme/DocItem/index.tsx | 17 +++-- .../src/theme/DocItem/styles.module.css | 5 -- .../src/theme/Heading/index.tsx | 20 +++++- .../src/theme/Heading/styles.module.css | 5 ++ .../docusaurus-theme-classic/src/types.d.ts | 1 + .../src/__tests__/markdownParser.test.ts | 62 ------------------- .../docusaurus-utils/src/markdownParser.ts | 33 ++++------ website/docs/api/docusaurus.config.js.md | 3 +- 12 files changed, 57 insertions(+), 111 deletions(-) diff --git a/packages/docusaurus-mdx-loader/src/index.js b/packages/docusaurus-mdx-loader/src/index.js index a7f509a79c1c..d62157dfb770 100644 --- a/packages/docusaurus-mdx-loader/src/index.js +++ b/packages/docusaurus-mdx-loader/src/index.js @@ -31,10 +31,7 @@ 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); const hasFrontMatter = Object.keys(frontMatter).length > 0; @@ -69,7 +66,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') { diff --git a/packages/docusaurus-plugin-content-docs/src/docs.ts b/packages/docusaurus-plugin-content-docs/src/docs.ts index 7e515c6bc6fd..ee296d61c674 100644 --- a/packages/docusaurus-plugin-content-docs/src/docs.ts +++ b/packages/docusaurus-plugin-content-docs/src/docs.ts @@ -203,12 +203,7 @@ 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; - + const title: string = frontMatter.title ?? contentTitle ?? baseID; const description: string = frontMatter.description ?? excerpt ?? ''; const permalink = normalizeUrl([versionMetadata.versionPath, docSlug]); @@ -246,7 +241,7 @@ export function processDocMetadata({ unversionedId, id, isDocsHomePage, - title: headingTitle, + title, description, source: aliasedSitePath(filePath, siteDir), sourceDirName, diff --git a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts index 87b6c07d07e5..abef806debfb 100644 --- a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts +++ b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts @@ -82,6 +82,7 @@ declare module '@theme/DocItem' { readonly frontMatter: FrontMatter; readonly metadata: Metadata; readonly toc: readonly TOCItem[]; + readonly contentTitle: string | undefined; (): JSX.Element; }; }; diff --git a/packages/docusaurus-plugin-content-pages/src/index.ts b/packages/docusaurus-plugin-content-pages/src/index.ts index d94075e9f745..9e28197a934a 100644 --- a/packages/docusaurus-plugin-content-pages/src/index.ts +++ b/packages/docusaurus-plugin-content-pages/src/index.ts @@ -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. diff --git a/packages/docusaurus-theme-classic/src/theme/DocItem/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocItem/index.tsx index 12553384c874..8c3d7a673ba1 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocItem/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocItem/index.tsx @@ -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 ( <> - +
)} - {!hideTitle && ( -
-

{title}

-
- )} + {shouldAddTitle && {title}}
diff --git a/packages/docusaurus-theme-classic/src/theme/DocItem/styles.module.css b/packages/docusaurus-theme-classic/src/theme/DocItem/styles.module.css index 41c2d0c0e005..2a486bb45f2b 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocItem/styles.module.css +++ b/packages/docusaurus-theme-classic/src/theme/DocItem/styles.module.css @@ -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; diff --git a/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx b/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx index 6c0cc35b2747..b4d91ccad37c 100644 --- a/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx @@ -16,7 +16,21 @@ 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 ( +
+

+ {props.children} +

+
+ ); +}; + +const createAnchorHeading = ( + Tag: HeadingType, +): ((props: Props) => JSX.Element) => function TargetComponent({id, ...props}) { const { navbar: {hideOnScroll}, @@ -51,4 +65,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; diff --git a/packages/docusaurus-theme-classic/src/theme/Heading/styles.module.css b/packages/docusaurus-theme-classic/src/theme/Heading/styles.module.css index 4e24477449a5..3f94fe995476 100644 --- a/packages/docusaurus-theme-classic/src/theme/Heading/styles.module.css +++ b/packages/docusaurus-theme-classic/src/theme/Heading/styles.module.css @@ -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)); +} diff --git a/packages/docusaurus-theme-classic/src/types.d.ts b/packages/docusaurus-theme-classic/src/types.d.ts index 114e977f7705..3174b1f6b0d4 100644 --- a/packages/docusaurus-theme-classic/src/types.d.ts +++ b/packages/docusaurus-theme-classic/src/types.d.ts @@ -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' { diff --git a/packages/docusaurus-utils/src/__tests__/markdownParser.test.ts b/packages/docusaurus-utils/src/__tests__/markdownParser.test.ts index 221aaf018a97..ae48c4d4aaf8 100644 --- a/packages/docusaurus-utils/src/__tests__/markdownParser.test.ts +++ b/packages/docusaurus-utils/src/__tests__/markdownParser.test.ts @@ -357,22 +357,6 @@ Lorem Ipsum }); }); - test('Should parse markdown h1 title at the top but keep it in content', () => { - const markdown = dedent` - - # Markdown Title - - Lorem Ipsum - - `; - expect( - parseMarkdownContentTitle(markdown, {keepContentTitle: true}), - ).toEqual({ - content: markdown.trim(), - contentTitle: 'Markdown Title', - }); - }); - test('Should not parse markdown h1 title in the middle of a doc', () => { const markdown = dedent` @@ -544,34 +528,6 @@ describe('parseMarkdownString', () => { `); }); - 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 - - Some text", - "contentTitle": "Markdown Title", - "excerpt": "Some text", - "frontMatter": Object { - "title": "Frontmatter title", - }, - } - `); - }); - test('should not warn for duplicate title if markdown title is not at the top', () => { expect( parseMarkdownString(dedent` @@ -597,24 +553,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` diff --git a/packages/docusaurus-utils/src/markdownParser.ts b/packages/docusaurus-utils/src/markdownParser.ts index 0e86a714b766..08e8edafc0f3 100644 --- a/packages/docusaurus-utils/src/markdownParser.ts +++ b/packages/docusaurus-utils/src/markdownParser.ts @@ -78,12 +78,17 @@ export function parseFrontMatter( }; } +// Unwrap possible inline code blocks (# `config.js`) +function cleanContentTitle(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}, ): {content: string; contentTitle: string | undefined} { - const keepContentTitleOption = options?.keepContentTitle ?? false; - const content = contentUntrimmed.trim(); const IMPORT_STATEMENT = /import\s+(([\w*{}\s\n,]+)from\s+)?["'\s]([@\w/_.-]+)["'\s];?|\n/ @@ -106,16 +111,12 @@ export function parseMarkdownContentTitle( if (!pattern || !title) { return {content, contentTitle: undefined}; + } else { + return { + content, + contentTitle: cleanContentTitle(title.trim()).trim(), + }; } - - const newContent = keepContentTitleOption - ? content - : content.replace(pattern, ''); - - return { - content: newContent.trim(), - contentTitle: title.trim(), - }; } type ParsedMarkdown = { @@ -127,22 +128,14 @@ type ParsedMarkdown = { export function parseMarkdownString( markdownFileContent: string, - options?: { - keepContentTitle?: boolean; - }, ): ParsedMarkdown { try { - const keepContentTitle = options?.keepContentTitle ?? false; - const {frontMatter, content: contentWithoutFrontMatter} = parseFrontMatter( markdownFileContent, ); const {content, contentTitle} = parseMarkdownContentTitle( contentWithoutFrontMatter, - { - keepContentTitle, - }, ); const excerpt = createExcerpt(content); diff --git a/website/docs/api/docusaurus.config.js.md b/website/docs/api/docusaurus.config.js.md index 5af7569d277c..b44fb6a32c22 100644 --- a/website/docs/api/docusaurus.config.js.md +++ b/website/docs/api/docusaurus.config.js.md @@ -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. From 60edbadb00a2c4a8d1fd40a4f6a34d552a2dbb1c Mon Sep 17 00:00:00 2001 From: slorber Date: Thu, 3 Jun 2021 15:36:26 +0200 Subject: [PATCH 02/10] mention hide_title frontmatter only prevents frontmatter.title from being added in the dom (not a markdown # title in content) --- website/docs/api/plugins/plugin-content-docs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/api/plugins/plugin-content-docs.md b/website/docs/api/plugins/plugin-content-docs.md index 2861ce4f72c9..a51eaf5f7447 100644 --- a/website/docs/api/plugins/plugin-content-docs.md +++ b/website/docs/api/plugins/plugin-content-docs.md @@ -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. From 068bffe2a476b71e7fa25767dc5d560d4dcff243 Mon Sep 17 00:00:00 2001 From: slorber Date: Thu, 3 Jun 2021 15:36:52 +0200 Subject: [PATCH 03/10] alwayss insert MainHeading under the div.markdown container for consistency --- .../docusaurus-theme-classic/src/theme/DocItem/index.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/docusaurus-theme-classic/src/theme/DocItem/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocItem/index.tsx index 8c3d7a673ba1..1b63062047ab 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocItem/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocItem/index.tsx @@ -75,8 +75,13 @@ function DocItem(props: Props): JSX.Element {
)} - {shouldAddTitle && {title}}
+ {/* + 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 && {title}}
From 92f44ccb9b9e46c0323f8980da24f4a2fcd65bbb Mon Sep 17 00:00:00 2001 From: slorber Date: Thu, 3 Jun 2021 15:37:11 +0200 Subject: [PATCH 04/10] ensure MainHeading has no useless id --- .../docusaurus-theme-classic/src/theme/Heading/index.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx b/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx index b4d91ccad37c..4daaebf30c62 100644 --- a/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx @@ -21,7 +21,10 @@ type HeadingComponent = (props: Props) => JSX.Element; export const MainHeading: HeadingComponent = function MainHeading({...props}) { return (
-

+

{props.children}

From 6554dedbee8624a37be5e73a7fbf8cc9a2ae0f9d Mon Sep 17 00:00:00 2001 From: slorber Date: Thu, 3 Jun 2021 15:46:00 +0200 Subject: [PATCH 05/10] 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 --- packages/docusaurus-plugin-content-docs/src/docs.ts | 3 +++ packages/docusaurus-plugin-content-docs/src/index.ts | 6 +----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/docusaurus-plugin-content-docs/src/docs.ts b/packages/docusaurus-plugin-content-docs/src/docs.ts index 4ddb617934e2..b12e2df7c821 100644 --- a/packages/docusaurus-plugin-content-docs/src/docs.ts +++ b/packages/docusaurus-plugin-content-docs/src/docs.ts @@ -202,7 +202,10 @@ export function processDocMetadata({ numberPrefixParser: options.numberPrefixParser, }); + // 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 ?? ''; const permalink = normalizeUrl([versionMetadata.versionPath, docSlug]); diff --git a/packages/docusaurus-plugin-content-docs/src/index.ts b/packages/docusaurus-plugin-content-docs/src/index.ts index 319549a36ace..76eb8af6d843 100644 --- a/packages/docusaurus-plugin-content-docs/src/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/index.ts @@ -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 { From c0e3c3987f106a012749789088322878825c3c75 Mon Sep 17 00:00:00 2001 From: slorber Date: Thu, 3 Jun 2021 15:51:41 +0200 Subject: [PATCH 06/10] fix docs test after revert --- .../src/__tests__/docs.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts index 9d123cc6cc99..d23cc7385269 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts @@ -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: { From a78a1ae32084310d30cff2baff73888bba0fdb65 Mon Sep 17 00:00:00 2001 From: slorber Date: Thu, 3 Jun 2021 16:11:21 +0200 Subject: [PATCH 07/10] improve markdownParser and fix tests --- .../src/__tests__/markdownParser.test.ts | 175 ++++++++---------- .../docusaurus-utils/src/markdownParser.ts | 8 +- 2 files changed, 84 insertions(+), 99 deletions(-) diff --git a/packages/docusaurus-utils/src/__tests__/markdownParser.test.ts b/packages/docusaurus-utils/src/__tests__/markdownParser.test.ts index 03d08b7187c8..ee3706725fbc 100644 --- a/packages/docusaurus-utils/src/__tests__/markdownParser.test.ts +++ b/packages/docusaurus-utils/src/__tests__/markdownParser.test.ts @@ -141,11 +141,55 @@ describe('parseMarkdownContentTitle', () => { `; expect(parseMarkdownContentTitle(markdown)).toEqual({ - content: 'Lorem Ipsum', + content: markdown, + 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 +199,7 @@ describe('parseMarkdownContentTitle', () => { `; expect(parseMarkdownContentTitle(markdown)).toEqual({ - content: 'Lorem Ipsum', + content: markdown, contentTitle: 'Markdown Title', }); }); @@ -169,7 +213,7 @@ describe('parseMarkdownContentTitle', () => { `; expect(parseMarkdownContentTitle(markdown)).toEqual({ - content: 'Lorem Ipsum', + content: markdown, contentTitle: 'Markdown Title', }); }); @@ -185,12 +229,7 @@ describe('parseMarkdownContentTitle', () => { `; expect(parseMarkdownContentTitle(markdown)).toEqual({ - content: dedent` - ## Heading 2 - - Lorem Ipsum - - `, + content: markdown, contentTitle: 'Markdown Title', }); }); @@ -206,12 +245,7 @@ describe('parseMarkdownContentTitle', () => { `; expect(parseMarkdownContentTitle(markdown)).toEqual({ - content: dedent` - # Markdown Title 2 - - Lorem Ipsum - - `, + content: markdown, contentTitle: 'Markdown Title', }); }); @@ -227,15 +261,7 @@ describe('parseMarkdownContentTitle', () => { `; expect(parseMarkdownContentTitle(markdown)).toEqual({ - content: dedent` - - Lorem Ipsum - - # Markdown Title 2 - - Lorem Ipsum - - `, + content: markdown, contentTitle: undefined, }); }); @@ -250,7 +276,7 @@ describe('parseMarkdownContentTitle', () => { `; expect(parseMarkdownContentTitle(markdown)).toEqual({ - content: 'Lorem Ipsum', + content: markdown, contentTitle: 'Markdown Title', }); }); @@ -271,17 +297,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,35 +322,17 @@ import "module-name" # Markdown Title Lorem Ipsum - `; +`; expect(parseMarkdownContentTitle(markdown)).toEqual({ - content: ` -import DefaultComponent from '@site/src/components/Component1'; -import DefaultComponent2 from '../relative/path/Component2'; -import * as EntireComponent from './relative/path/Component3'; - -import { Component4 } from "double-quote-module-name"; -import { Component51, Component52, \n Component53, \n\t\t Component54 } from "double-quote-module-name"; -import { Component6 as AliasComponent6 } from "module-name"; -import DefaultComponent8, { DefaultComponent81 ,\nDefaultComponent82 } from "module-name"; -import DefaultComponent9, * as EntireComponent9 from "module-name"; -import {Component71,\nComponent72 as AliasComponent72,\nComponent73\n} \nfrom "module-name"; - -import './styles.css'; -import _ from 'underscore'; -import "module-name" - - - -Lorem Ipsum - `.trim(), + content: markdown.trim(), 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,21 +344,15 @@ 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 '; + const markdown = '# Document With Only A Title'; expect(parseMarkdownContentTitle(markdown)).toEqual({ - content: '', + content: markdown, contentTitle: 'Document With Only A Title', }); }); @@ -423,28 +415,7 @@ Lorem Ipsum `; 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 Component4 from '@site/src/components/Component4'; - import Component5 from '@site/src/components/Component5'; - import Component6 from '@site/src/components/Component6'; - import Component7 from '@site/src/components/Component7'; - import Component8 from '@site/src/components/Component8'; - import Component9 from '@site/src/components/Component9'; - import Component10 from '@site/src/components/Component10'; - import Component11 from '@site/src/components/Component11'; - import Component12 from '@site/src/components/Component12'; - import Component13 from '@site/src/components/Component13'; - import Component14 from '@site/src/components/Component14'; - import Component15 from '@site/src/components/Component15'; - - - - Lorem Ipsum - - `, + content: markdown, contentTitle: 'Markdown Title', }); }); @@ -481,7 +452,9 @@ describe('parseMarkdownString', () => { `), ).toMatchInlineSnapshot(` Object { - "content": "Some text", + "content": "# Markdown Title + + Some text", "contentTitle": "Markdown Title", "excerpt": "Some text", "frontMatter": Object {}, @@ -502,7 +475,9 @@ describe('parseMarkdownString', () => { `), ).toMatchInlineSnapshot(` Object { - "content": "Some text", + "content": "# Markdown Title + + Some text", "contentTitle": "Markdown Title", "excerpt": "Some text", "frontMatter": Object { @@ -526,7 +501,10 @@ describe('parseMarkdownString', () => { `), ).toMatchInlineSnapshot(` Object { - "content": "Some text", + "content": "Markdown Title alternate + ================ + + Some text", "contentTitle": "Markdown Title alternate", "excerpt": "Some text", "frontMatter": Object { @@ -574,7 +552,9 @@ describe('parseMarkdownString', () => { `), ).toMatchInlineSnapshot(` Object { - "content": "test test test # test bar + "content": "# Markdown Title + + test test test # test bar # Markdown Title 2 @@ -630,7 +610,7 @@ describe('parseMarkdownString', () => { test('should parse title only', () => { expect(parseMarkdownString('# test')).toMatchInlineSnapshot(` Object { - "content": "", + "content": "# test", "contentTitle": "test", "excerpt": undefined, "frontMatter": Object {}, @@ -646,7 +626,8 @@ describe('parseMarkdownString', () => { `), ).toMatchInlineSnapshot(` Object { - "content": "", + "content": "test + ===", "contentTitle": "test", "excerpt": undefined, "frontMatter": Object {}, @@ -664,7 +645,7 @@ describe('parseMarkdownString', () => { `), ).toMatchInlineSnapshot(` Object { - "content": "", + "content": "# test", "contentTitle": "test", "excerpt": undefined, "frontMatter": Object { @@ -704,7 +685,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 diff --git a/packages/docusaurus-utils/src/markdownParser.ts b/packages/docusaurus-utils/src/markdownParser.ts index 8e0253af2434..baa8150eff54 100644 --- a/packages/docusaurus-utils/src/markdownParser.ts +++ b/packages/docusaurus-utils/src/markdownParser.ts @@ -80,8 +80,10 @@ export function parseFrontMatter( }; } -// Unwrap possible inline code blocks (# `config.js`) -function cleanContentTitle(contentTitle: string): string { +// 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); } @@ -116,7 +118,7 @@ export function parseMarkdownContentTitle( } else { return { content, - contentTitle: cleanContentTitle(title.trim()).trim(), + contentTitle: toTextContentTitle(title.trim()).trim(), }; } } From 60ac3f4bb9e881820f48fac147f001daaaae754f Mon Sep 17 00:00:00 2001 From: slorber Date: Thu, 3 Jun 2021 16:13:39 +0200 Subject: [PATCH 08/10] fix docs tests --- .../src/__tests__/__snapshots__/index.test.ts.snap | 8 ++++---- .../src/__tests__/index.test.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap index 9572c7a04d3f..722dd3df6475 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap @@ -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\\" } ] diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts index 3bd8944ed6c7..77c8d3db4d33 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts @@ -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', From 52979e4dc2e34975a08f8c6e60141ac8303a7183 Mon Sep 17 00:00:00 2001 From: slorber Date: Thu, 3 Jun 2021 16:43:39 +0200 Subject: [PATCH 09/10] markdownParser: restore option to remove contentTitle (mostly for blog plugin) --- packages/docusaurus-mdx-loader/src/index.js | 4 +- .../src/index.ts | 3 + .../src/theme/BlogPostItem/index.tsx | 4 +- .../src/__tests__/markdownParser.test.ts | 116 ++++++++++++++++++ .../docusaurus-utils/src/markdownParser.ts | 10 +- 5 files changed, 133 insertions(+), 4 deletions(-) diff --git a/packages/docusaurus-mdx-loader/src/index.js b/packages/docusaurus-mdx-loader/src/index.js index d62157dfb770..ea9efc1240af 100644 --- a/packages/docusaurus-mdx-loader/src/index.js +++ b/packages/docusaurus-mdx-loader/src/index.js @@ -31,7 +31,9 @@ module.exports = async function docusaurusMdxLoader(fileString) { const {frontMatter, content: contentWithTitle} = parseFrontMatter(fileString); - const {content, contentTitle} = parseMarkdownContentTitle(contentWithTitle); + const {content, contentTitle} = parseMarkdownContentTitle(contentWithTitle, { + removeContentTitle: reqOptions.removeContentTitle, + }); const hasFrontMatter = Object.keys(frontMatter).length > 0; diff --git a/packages/docusaurus-plugin-content-blog/src/index.ts b/packages/docusaurus-plugin-content-blog/src/index.ts index d0228b6720f5..1d47eb628cb3 100644 --- a/packages/docusaurus-plugin-content-blog/src/index.ts +++ b/packages/docusaurus-plugin-content-blog/src/index.ts @@ -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, }, }, { diff --git a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/index.tsx index 2307b57da905..f4b76b68d659 100644 --- a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/index.tsx @@ -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; diff --git a/packages/docusaurus-utils/src/__tests__/markdownParser.test.ts b/packages/docusaurus-utils/src/__tests__/markdownParser.test.ts index ee3706725fbc..2f36481e4b41 100644 --- a/packages/docusaurus-utils/src/__tests__/markdownParser.test.ts +++ b/packages/docusaurus-utils/src/__tests__/markdownParser.test.ts @@ -146,6 +146,22 @@ describe('parseMarkdownContentTitle', () => { }); }); + 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` @@ -281,6 +297,23 @@ Lorem Ipsum }); }); + 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', + }); + }); + test('Should parse markdown h1 title placed after import declarations', () => { const markdown = dedent` import Component1 from '@site/src/components/Component1'; @@ -330,6 +363,36 @@ Lorem Ipsum }); }); + 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'; + +import { Component4 } from "double-quote-module-name"; +import { Component51, Component52, \n Component53, \n\t\t Component54 } from "double-quote-module-name"; +import { Component6 as AliasComponent6 } from "module-name"; +import DefaultComponent8, { DefaultComponent81 ,\nDefaultComponent82 } from "module-name"; +import DefaultComponent9, * as EntireComponent9 from "module-name"; +import {Component71,\nComponent72 as AliasComponent72,\nComponent73\n} \nfrom "module-name"; + +import './styles.css'; +import _ from 'underscore'; +import "module-name" + +# Markdown Title + +Lorem Ipsum +`; + + 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` @@ -349,6 +412,27 @@ Lorem Ipsum }); }); + test('Should parse markdown h1 alternate title placed after import declarations and remove it', () => { + const markdown = dedent` + + import Component from '@site/src/components/Component'; + import Component from '@site/src/components/Component' + import './styles.css'; + + Markdown Title + ============== + + Lorem Ipsum + + `; + expect( + parseMarkdownContentTitle(markdown, {removeContentTitle: true}), + ).toEqual({ + 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({ @@ -419,6 +503,38 @@ Lorem Ipsum 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'; + import Component4 from '@site/src/components/Component4'; + import Component5 from '@site/src/components/Component5'; + import Component6 from '@site/src/components/Component6'; + import Component7 from '@site/src/components/Component7'; + import Component8 from '@site/src/components/Component8'; + import Component9 from '@site/src/components/Component9'; + import Component10 from '@site/src/components/Component10'; + import Component11 from '@site/src/components/Component11'; + import Component12 from '@site/src/components/Component12'; + import Component13 from '@site/src/components/Component13'; + 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', + }); + }); }); describe('parseMarkdownString', () => { diff --git a/packages/docusaurus-utils/src/markdownParser.ts b/packages/docusaurus-utils/src/markdownParser.ts index baa8150eff54..01e696360ec1 100644 --- a/packages/docusaurus-utils/src/markdownParser.ts +++ b/packages/docusaurus-utils/src/markdownParser.ts @@ -92,7 +92,10 @@ function toTextContentTitle(contentTitle: string): string { export function parseMarkdownContentTitle( contentUntrimmed: string, + options?: {removeContentTitle?: boolean}, ): {content: string; contentTitle: string | undefined} { + const removeContentTitleOption = options?.removeContentTitle ?? false; + const content = contentUntrimmed.trim(); const IMPORT_STATEMENT = /import\s+(([\w*{}\s\n,]+)from\s+)?["'\s]([@\w/_.-]+)["'\s];?|\n/ @@ -116,8 +119,11 @@ export function parseMarkdownContentTitle( if (!pattern || !title) { return {content, contentTitle: undefined}; } else { + const newContent = removeContentTitleOption + ? content.replace(pattern, '') + : content; return { - content, + content: newContent.trim(), contentTitle: toTextContentTitle(title.trim()).trim(), }; } @@ -132,6 +138,7 @@ type ParsedMarkdown = { export function parseMarkdownString( markdownFileContent: string, + options?: {removeContentTitle?: boolean}, ): ParsedMarkdown { try { const {frontMatter, content: contentWithoutFrontMatter} = parseFrontMatter( @@ -140,6 +147,7 @@ export function parseMarkdownString( const {content, contentTitle} = parseMarkdownContentTitle( contentWithoutFrontMatter, + options, ); const excerpt = createExcerpt(content); From dbe52b17310c72dc74857e34c64a443eb6982b28 Mon Sep 17 00:00:00 2001 From: slorber Date: Thu, 3 Jun 2021 16:49:31 +0200 Subject: [PATCH 10/10] use removeContentTitle for blog --- packages/docusaurus-plugin-content-blog/src/blogUtils.ts | 2 +- packages/docusaurus-utils/src/markdownParser.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts index dbcb29eefd5d..84e173caaeb7 100644 --- a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts +++ b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts @@ -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); diff --git a/packages/docusaurus-utils/src/markdownParser.ts b/packages/docusaurus-utils/src/markdownParser.ts index 01e696360ec1..6270a791c133 100644 --- a/packages/docusaurus-utils/src/markdownParser.ts +++ b/packages/docusaurus-utils/src/markdownParser.ts @@ -169,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 { 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}