Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(v2): frontmatter-less: read first heading as title and use it in front-matter #4485

Merged
merged 9 commits into from
Mar 23, 2021
4 changes: 2 additions & 2 deletions packages/docusaurus-mdx-loader/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"access": "public"
},
"scripts": {
"build": "tsc"
"build": "tsc",
"watch": "tsc --watch"
},
"repository": {
"type": "git",
Expand All @@ -27,7 +28,6 @@
"file-loader": "^6.2.0",
"fs-extra": "^9.1.0",
"github-slugger": "^1.3.0",
"gray-matter": "^4.0.2",
"loader-utils": "^2.0.0",
"mdast-util-to-string": "^2.0.0",
"remark-emoji": "^2.1.0",
Expand Down
19 changes: 11 additions & 8 deletions packages/docusaurus-mdx-loader/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const {getOptions} = require('loader-utils');
const {readFile} = require('fs-extra');
const mdx = require('@mdx-js/mdx');
const emoji = require('remark-emoji');
const matter = require('gray-matter');
const {readFrontMatter} = require('@docusaurus/utils');
const stringifyObject = require('stringify-object');
const headings = require('./remark/headings');
const toc = require('./remark/toc');
Expand All @@ -24,9 +24,15 @@ const DEFAULT_OPTIONS = {

module.exports = async function docusaurusMdxLoader(fileString) {
const callback = this.async();

const {data, content} = matter(fileString);
const reqOptions = getOptions(this) || {};

const {frontMatter, content, hasFrontMatter} = readFrontMatter(
fileString,
this.resourcePath,
{},
reqOptions.removeTitleHeading,
);

const options = {
...reqOptions,
remarkPlugins: [
Expand Down Expand Up @@ -58,7 +64,7 @@ module.exports = async function docusaurusMdxLoader(fileString) {
return callback(err);
}

let exportStr = `export const frontMatter = ${stringifyObject(data)};`;
let exportStr = `export const frontMatter = ${stringifyObject(frontMatter)};`;

// Read metadata for this MDX and export it.
if (options.metadataPath && typeof options.metadataPath === 'function') {
Expand All @@ -77,10 +83,7 @@ module.exports = async function docusaurusMdxLoader(fileString) {
options.forbidFrontMatter &&
typeof options.forbidFrontMatter === 'function'
) {
if (
options.forbidFrontMatter(this.resourcePath) &&
Object.keys(data).length > 0
) {
if (options.forbidFrontMatter(this.resourcePath) && hasFrontMatter) {
return callback(new Error(`Front matter is forbidden in this file`));
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
date: 2019-01-02
---

# some heading
Original file line number Diff line number Diff line change
Expand Up @@ -164,12 +164,33 @@ describe('loadBlog', () => {
tags: [],
truncated: false,
});

expect({
...blogPosts.find((v) => v.metadata.title === 'some heading')!.metadata,
prevItem: undefined,
}).toEqual({
editUrl: `${BaseEditUrl}/blog/heading-as-title.md`,
permalink: '/blog/heading-as-title',
readingTime: 0,
source: path.posix.join('@site', PluginPath, 'heading-as-title.md'),
title: 'some heading',
description: '',
date: new Date('2019-01-02'),
formattedDate: 'January 2, 2019',
prevItem: undefined,
tags: [],
nextItem: {
permalink: '/blog/date-matter',
title: 'date-matter',
},
truncated: false,
});
});

test('simple website blog dates localized', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'website');
const blogPostsFrench = await getBlogPosts(siteDir, {}, getI18n('fr'));
expect(blogPostsFrench).toHaveLength(5);
expect(blogPostsFrench).toHaveLength(6);
expect(blogPostsFrench[0].metadata.formattedDate).toMatchInlineSnapshot(
`"16 août 2020"`,
);
Expand All @@ -180,9 +201,12 @@ describe('loadBlog', () => {
`"27 février 2020"`,
);
expect(blogPostsFrench[3].metadata.formattedDate).toMatchInlineSnapshot(
`"1 janvier 2019"`,
`"2 janvier 2019"`,
);
expect(blogPostsFrench[4].metadata.formattedDate).toMatchInlineSnapshot(
`"1 janvier 2019"`,
);
expect(blogPostsFrench[5].metadata.formattedDate).toMatchInlineSnapshot(
`"14 décembre 2018"`,
);
});
Expand Down Expand Up @@ -212,7 +236,7 @@ describe('loadBlog', () => {
expect(blogPost.metadata.editUrl).toEqual(hardcodedEditUrl);
});

expect(editUrlFunction).toHaveBeenCalledTimes(5);
expect(editUrlFunction).toHaveBeenCalledTimes(6);
expect(editUrlFunction).toHaveBeenCalledWith({
blogDirPath: 'blog',
blogPath: 'date-matter.md',
Expand Down Expand Up @@ -243,6 +267,12 @@ describe('loadBlog', () => {
permalink: '/blog/2018/12/14/Happy-First-Birthday-Slash',
locale: 'en',
});
expect(editUrlFunction).toHaveBeenCalledWith({
blogDirPath: 'blog',
blogPath: 'heading-as-title.md',
locale: 'en',
permalink: '/blog/heading-as-title',
});
});

test('draft blog post not exists in production build', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# My heading as title
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ Object {
"path": "/docs/foo/bazSlug.html",
"sidebar": "docs",
},
Object {
"id": "headingAsTitle",
"path": "/docs/headingAsTitle",
"sidebar": undefined,
},
Object {
"id": "hello",
"path": "/docs/",
Expand Down Expand Up @@ -172,6 +177,17 @@ Object {
\\"title\\": \\"Hello, World !\\",
\\"permalink\\": \\"/docs/\\"
}
}",
"site-docs-heading-as-title-md-c6d.json": "{
\\"unversionedId\\": \\"headingAsTitle\\",
\\"id\\": \\"headingAsTitle\\",
\\"isDocsHomePage\\": false,
\\"title\\": \\"My heading as title\\",
\\"description\\": \\"\\",
\\"source\\": \\"@site/docs/headingAsTitle.md\\",
\\"slug\\": \\"/headingAsTitle\\",
\\"permalink\\": \\"/docs/headingAsTitle\\",
\\"version\\": \\"current\\"
}",
"site-docs-hello-md-9df.json": "{
\\"unversionedId\\": \\"hello\\",
Expand Down Expand Up @@ -383,6 +399,11 @@ Object {
"path": "/docs/foo/bazSlug.html",
"sidebar": "docs",
},
Object {
"id": "headingAsTitle",
"path": "/docs/headingAsTitle",
"sidebar": undefined,
},
Object {
"id": "hello",
"path": "/docs/",
Expand Down Expand Up @@ -494,6 +515,14 @@ Array [
},
"path": "/docs/foo/bazSlug.html",
},
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/docs/headingAsTitle.md",
},
"path": "/docs/headingAsTitle",
},
Object {
"component": "@theme/DocItem",
"exact": true,
Expand Down Expand Up @@ -579,6 +608,7 @@ These sidebar document ids do not exist:
Available document ids=
- foo/bar
- foo/baz
- headingAsTitle
- hello
- ipsum
- lorem
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ describe('simple site', () => {
'rootRelativeSlug.md',
'rootResolvedSlug.md',
'rootTryToEscapeSlug.md',
'headingAsTitle.md',
'foo/bar.md',
'foo/baz.md',
'slugs/absoluteSlug.md',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ describe('simple website', () => {
expect(isMatch('docs/hello.js', matchPattern)).toEqual(false);
expect(isMatch('docs/super.mdl', matchPattern)).toEqual(false);
expect(isMatch('docs/mdx', matchPattern)).toEqual(false);
expect(isMatch('docs/headingAsTitle.md', matchPattern)).toEqual(true);
expect(isMatch('sidebars.json', matchPattern)).toEqual(true);
expect(isMatch('versioned_docs/hello.md', matchPattern)).toEqual(false);
expect(isMatch('hello.md', matchPattern)).toEqual(false);
Expand Down
2 changes: 1 addition & 1 deletion packages/docusaurus-plugin-content-docs/src/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export function processDocMetadata({
// ex: myDoc -> .
const docsFileDirName = path.dirname(source);

const {frontMatter = {}, excerpt} = parseMarkdownString(content);
const {frontMatter = {}, excerpt} = parseMarkdownString(content, source);
const {
sidebar_label: sidebarLabel,
custom_edit_url: customEditURL,
Expand Down
1 change: 1 addition & 0 deletions packages/docusaurus-plugin-content-pages/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ export default function pluginContentPages(
rehypePlugins,
beforeDefaultRehypePlugins,
beforeDefaultRemarkPlugins,
removeTitleHeading: false,
staticDir: path.join(siteDir, STATIC_DIR_NAME),
// Note that metadataPath must be the same/in-sync as
// the path from createData for each MDX.
Expand Down
74 changes: 54 additions & 20 deletions packages/docusaurus-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,43 +252,77 @@ export function createExcerpt(fileString: string): string | undefined {
}

type ParsedMarkdown = {
frontMatter: {
// Returned by gray-matter
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
};
// Returned by gray-matter
// eslint-disable-next-line @typescript-eslint/no-explicit-any
frontMatter: Record<string, any>;
content: string;
excerpt: string | undefined;
hasFrontMatter: boolean;
};
export function parseMarkdownString(markdownString: string): ParsedMarkdown {
const options: Record<string, unknown> = {
excerpt: (file: matter.GrayMatterFile<string>): void => {
// Hacky way of stripping out import statements from the excerpt
// TODO: Find a better way to do so, possibly by compiling the Markdown content,
// stripping out HTML tags and obtaining the first line.
file.excerpt = createExcerpt(file.content);
},
};

export function readFrontMatter(
markdownString: string,
source?: string,
options: Record<string, unknown> = {},
removeTitleHeading = true,
): ParsedMarkdown {
try {
const {data: frontMatter, content, excerpt} = matter(
markdownString,
options,
);
return {frontMatter, content, excerpt};
const result = matter(markdownString, options);
result.data = result.data || {};
result.content = result.content.trim();

const hasFrontMatter = Object.keys(result.data).length > 0;

const heading = /^# (.*)[\n\r]?/gi.exec(result.content);
if (heading) {
if (result.data.title) {
console.warn(
`Duplicate title detected in \`${source || 'this'}\` file`,
);
} else {
result.data.title = heading[1].trim();
if (removeTitleHeading) {
result.content = result.content.replace(heading[0], '');
if (result.excerpt) {
result.excerpt = result.excerpt.replace(heading[1], '');
}
Comment on lines +276 to +288
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wonder if we could extract this easily in a separate function and test this in isolation?

Copy link
Contributor Author

@armano2 armano2 Mar 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could actually go few steps further in separate pr

index should jsut be an entry point with bunch of reexports and we could create multiple files with specific things.
this file is starting to become really long and unreadable

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

totally agree :)

was like that before and want to refactor that for a long time already

}
}
}

return {
frontMatter: result.data,
content: result.content,
excerpt: result.excerpt,
hasFrontMatter,
};
} catch (e) {
throw new Error(`Error while parsing markdown front matter.
This can happen if you use special characters like : in frontmatter values (try using "" around that value)
${e.message}`);
}
}

export function parseMarkdownString(
markdownString: string,
source?: string,
): ParsedMarkdown {
return readFrontMatter(markdownString, source, {
excerpt: (file: matter.GrayMatterFile<string>): void => {
// Hacky way of stripping out import statements from the excerpt
// TODO: Find a better way to do so, possibly by compiling the Markdown content,
// stripping out HTML tags and obtaining the first line.
file.excerpt = createExcerpt(file.content);
},
});
}

export async function parseMarkdownFile(
source: string,
): Promise<ParsedMarkdown> {
const markdownString = await fs.readFile(source, 'utf-8');
try {
return parseMarkdownString(markdownString);
return parseMarkdownString(markdownString, source);
} catch (e) {
throw new Error(
`Error while parsing markdown file ${source}
Expand Down
3 changes: 2 additions & 1 deletion website/docs/cli.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
---
id: cli
title: CLI
---

# CLI
Comment on lines 3 to +5
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i can rollback this md file change as proper tests are now added

Suggested change
---
# CLI
title: CLI
---

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I think it's fine and we should dogfood this a bit too ;)


Docusaurus provides a set of scripts to help you generate, serve, and deploy your website.

Once your website is bootstrapped, the website source will contain the Docusaurus scripts that you can invoke with your package manager:
Expand Down