mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-11 16:17:25 +02:00
feat(v2): read first heading as title and use it in front-matter (#4485)
* feat(v2): read first heading as title and pass it to front-matter * fix(v2): always trim content after extracting front-matter * fix(v2): remove heading from rss and keep duplicate heading * fix(v2): rollback some unnecessary comment changes * test(v2): add unit tests to blog * test(v2): add unit tests to docs * test(v2): correct issue on windows * test(v2): add additional test cases
This commit is contained in:
parent
fb372c574d
commit
ea13c94cc2
16 changed files with 448 additions and 35 deletions
|
@ -0,0 +1,137 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`load utils: parseMarkdown parseMarkdownString should delete only first heading 1`] = `
|
||||
Object {
|
||||
"content": "
|
||||
test test test test test test
|
||||
test test test # test bar
|
||||
# test
|
||||
### test",
|
||||
"excerpt": "",
|
||||
"frontMatter": Object {
|
||||
"title": "test",
|
||||
},
|
||||
"hasFrontMatter": false,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`load utils: parseMarkdown parseMarkdownString should ignore heading if its not a first text 1`] = `
|
||||
Object {
|
||||
"content": "foo
|
||||
# test",
|
||||
"excerpt": "foo",
|
||||
"frontMatter": Object {},
|
||||
"hasFrontMatter": false,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`load utils: parseMarkdown parseMarkdownString should parse first heading as title 1`] = `
|
||||
Object {
|
||||
"content": "",
|
||||
"excerpt": "",
|
||||
"frontMatter": Object {
|
||||
"title": "test",
|
||||
},
|
||||
"hasFrontMatter": false,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`load utils: parseMarkdown parseMarkdownString should preserve front-matter title and warn about duplication 1`] = `
|
||||
Object {
|
||||
"content": "# test",
|
||||
"excerpt": "test",
|
||||
"frontMatter": Object {
|
||||
"title": "title",
|
||||
},
|
||||
"hasFrontMatter": true,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`load utils: parseMarkdown parseMarkdownString should read front matter 1`] = `
|
||||
Object {
|
||||
"content": "",
|
||||
"excerpt": undefined,
|
||||
"frontMatter": Object {
|
||||
"title": "test",
|
||||
},
|
||||
"hasFrontMatter": true,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`load utils: parseMarkdown readFrontMatter should delete only first heading 1`] = `
|
||||
Object {
|
||||
"content": "test test test # test bar
|
||||
# test
|
||||
### test",
|
||||
"excerpt": "",
|
||||
"frontMatter": Object {
|
||||
"title": "test",
|
||||
},
|
||||
"hasFrontMatter": false,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`load utils: parseMarkdown readFrontMatter should ignore heading if its not a first text 1`] = `
|
||||
Object {
|
||||
"content": "foo
|
||||
# test",
|
||||
"excerpt": "",
|
||||
"frontMatter": Object {},
|
||||
"hasFrontMatter": false,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`load utils: parseMarkdown readFrontMatter should parse first heading as title 1`] = `
|
||||
Object {
|
||||
"content": "",
|
||||
"excerpt": "",
|
||||
"frontMatter": Object {
|
||||
"title": "test",
|
||||
},
|
||||
"hasFrontMatter": false,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`load utils: parseMarkdown readFrontMatter should parse first heading as title and keep it in content 1`] = `
|
||||
Object {
|
||||
"content": "# test",
|
||||
"excerpt": "",
|
||||
"frontMatter": Object {
|
||||
"title": "test",
|
||||
},
|
||||
"hasFrontMatter": false,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`load utils: parseMarkdown readFrontMatter should parse front-matter and ignore h2 1`] = `
|
||||
Object {
|
||||
"content": "## test",
|
||||
"excerpt": "",
|
||||
"frontMatter": Object {
|
||||
"title": "title",
|
||||
},
|
||||
"hasFrontMatter": true,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`load utils: parseMarkdown readFrontMatter should preserve front-matter title and warn about duplication 1`] = `
|
||||
Object {
|
||||
"content": "# test",
|
||||
"excerpt": "",
|
||||
"frontMatter": Object {
|
||||
"title": "title",
|
||||
},
|
||||
"hasFrontMatter": true,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`load utils: parseMarkdown readFrontMatter should read front matter 1`] = `
|
||||
Object {
|
||||
"content": "",
|
||||
"excerpt": "",
|
||||
"frontMatter": Object {
|
||||
"title": "test",
|
||||
},
|
||||
"hasFrontMatter": true,
|
||||
}
|
||||
`;
|
160
packages/docusaurus-utils/src/__tests__/parseMarkdown.test.ts
Normal file
160
packages/docusaurus-utils/src/__tests__/parseMarkdown.test.ts
Normal file
|
@ -0,0 +1,160 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {parseMarkdownString, readFrontMatter} from '../index';
|
||||
import dedent from 'dedent';
|
||||
|
||||
describe('load utils: parseMarkdown', () => {
|
||||
describe('readFrontMatter', () => {
|
||||
test('should read front matter', () => {
|
||||
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
expect(
|
||||
readFrontMatter(dedent`
|
||||
---
|
||||
title: test
|
||||
---
|
||||
`),
|
||||
).toMatchSnapshot();
|
||||
expect(warn).not.toBeCalled();
|
||||
});
|
||||
test('should parse first heading as title', () => {
|
||||
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
expect(
|
||||
readFrontMatter(dedent`
|
||||
# test
|
||||
`),
|
||||
).toMatchSnapshot();
|
||||
expect(warn).not.toBeCalled();
|
||||
});
|
||||
test('should preserve front-matter title and warn about duplication', () => {
|
||||
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
expect(
|
||||
readFrontMatter(dedent`
|
||||
---
|
||||
title: title
|
||||
---
|
||||
# test
|
||||
`),
|
||||
).toMatchSnapshot();
|
||||
expect(warn).toBeCalledWith('Duplicate title detected in `this` file');
|
||||
warn.mockReset();
|
||||
});
|
||||
test('should ignore heading if its not a first text', () => {
|
||||
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
expect(
|
||||
readFrontMatter(dedent`
|
||||
foo
|
||||
# test
|
||||
`),
|
||||
).toMatchSnapshot();
|
||||
expect(warn).not.toBeCalled();
|
||||
});
|
||||
test('should parse first heading as title and keep it in content', () => {
|
||||
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
expect(
|
||||
readFrontMatter(
|
||||
dedent`
|
||||
# test
|
||||
`,
|
||||
undefined,
|
||||
{},
|
||||
false,
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
expect(warn).not.toBeCalled();
|
||||
});
|
||||
test('should delete only first heading', () => {
|
||||
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
expect(
|
||||
readFrontMatter(dedent`
|
||||
# test
|
||||
test test test # test bar
|
||||
# test
|
||||
### test
|
||||
`),
|
||||
).toMatchSnapshot();
|
||||
expect(warn).not.toBeCalled();
|
||||
});
|
||||
test('should parse front-matter and ignore h2', () => {
|
||||
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
expect(
|
||||
readFrontMatter(
|
||||
dedent`
|
||||
---
|
||||
title: title
|
||||
---
|
||||
## test
|
||||
`,
|
||||
undefined,
|
||||
{},
|
||||
false,
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
expect(warn).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseMarkdownString', () => {
|
||||
test('should read front matter', () => {
|
||||
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
expect(
|
||||
parseMarkdownString(dedent`
|
||||
---
|
||||
title: test
|
||||
---
|
||||
`),
|
||||
).toMatchSnapshot();
|
||||
expect(warn).not.toBeCalled();
|
||||
});
|
||||
test('should parse first heading as title', () => {
|
||||
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
expect(
|
||||
parseMarkdownString(dedent`
|
||||
# test
|
||||
`),
|
||||
).toMatchSnapshot();
|
||||
expect(warn).not.toBeCalled();
|
||||
});
|
||||
test('should preserve front-matter title and warn about duplication', () => {
|
||||
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
expect(
|
||||
parseMarkdownString(dedent`
|
||||
---
|
||||
title: title
|
||||
---
|
||||
# test
|
||||
`),
|
||||
).toMatchSnapshot();
|
||||
expect(warn).toBeCalledWith('Duplicate title detected in `this` file');
|
||||
warn.mockReset();
|
||||
});
|
||||
test('should ignore heading if its not a first text', () => {
|
||||
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
expect(
|
||||
parseMarkdownString(dedent`
|
||||
foo
|
||||
# test
|
||||
`),
|
||||
).toMatchSnapshot();
|
||||
expect(warn).not.toBeCalled();
|
||||
});
|
||||
test('should delete only first heading', () => {
|
||||
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
expect(
|
||||
parseMarkdownString(dedent`
|
||||
# test
|
||||
|
||||
test test test test test test
|
||||
test test test # test bar
|
||||
# test
|
||||
### test
|
||||
`),
|
||||
).toMatchSnapshot();
|
||||
expect(warn).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -252,30 +252,50 @@ 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], '');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
@ -283,12 +303,26 @@ ${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}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue