feat: siteConfig.markdown.parseFrontMatter hook (#9624)

This commit is contained in:
Sébastien Lorber 2023-12-16 02:50:26 +01:00 committed by GitHub
parent 28e7298211
commit affca7a9a2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 486 additions and 133 deletions

View file

@ -8,7 +8,7 @@
import fs from 'fs-extra'; import fs from 'fs-extra';
import logger from '@docusaurus/logger'; import logger from '@docusaurus/logger';
import { import {
parseFrontMatter, DEFAULT_PARSE_FRONT_MATTER,
escapePath, escapePath,
getFileLoaderUtils, getFileLoaderUtils,
getWebpackLoaderCompilerName, getWebpackLoaderCompilerName,
@ -133,7 +133,7 @@ function extractContentTitleData(data: {
export async function mdxLoader( export async function mdxLoader(
this: LoaderContext<Options>, this: LoaderContext<Options>,
fileString: string, fileContent: string,
): Promise<void> { ): Promise<void> {
const compilerName = getWebpackLoaderCompilerName(this); const compilerName = getWebpackLoaderCompilerName(this);
const callback = this.async(); const callback = this.async();
@ -143,11 +143,15 @@ export async function mdxLoader(
ensureMarkdownConfig(reqOptions); ensureMarkdownConfig(reqOptions);
const {frontMatter} = parseFrontMatter(fileString); const {frontMatter} = await reqOptions.markdownConfig.parseFrontMatter({
filePath,
fileContent,
defaultParseFrontMatter: DEFAULT_PARSE_FRONT_MATTER,
});
const mdxFrontMatter = validateMDXFrontMatter(frontMatter.mdx); const mdxFrontMatter = validateMDXFrontMatter(frontMatter.mdx);
const preprocessedContent = preprocessor({ const preprocessedContent = preprocessor({
fileContent: fileString, fileContent,
filePath, filePath,
admonitions: reqOptions.admonitions, admonitions: reqOptions.admonitions,
markdownConfig: reqOptions.markdownConfig, markdownConfig: reqOptions.markdownConfig,

View file

@ -8,6 +8,7 @@
import {jest} from '@jest/globals'; import {jest} from '@jest/globals';
import path from 'path'; import path from 'path';
import fs from 'fs-extra'; import fs from 'fs-extra';
import {DEFAULT_PARSE_FRONT_MATTER} from '@docusaurus/utils';
import {DEFAULT_OPTIONS} from '../options'; import {DEFAULT_OPTIONS} from '../options';
import {generateBlogPosts} from '../blogUtils'; import {generateBlogPosts} from '../blogUtils';
import {createBlogFeedFiles} from '../feed'; import {createBlogFeedFiles} from '../feed';
@ -31,6 +32,8 @@ const DefaultI18N: I18n = {
}, },
}; };
const markdown = {parseFrontMatter: DEFAULT_PARSE_FRONT_MATTER};
function getBlogContentPaths(siteDir: string): BlogContentPaths { function getBlogContentPaths(siteDir: string): BlogContentPaths {
return { return {
contentPath: path.resolve(siteDir, 'blog'), contentPath: path.resolve(siteDir, 'blog'),
@ -72,6 +75,7 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
baseUrl: '/', baseUrl: '/',
url: 'https://docusaurus.io', url: 'https://docusaurus.io',
favicon: 'image/favicon.ico', favicon: 'image/favicon.ico',
markdown,
}; };
const outDir = path.join(siteDir, 'build-snap'); const outDir = path.join(siteDir, 'build-snap');
@ -110,6 +114,7 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
baseUrl: '/myBaseUrl/', baseUrl: '/myBaseUrl/',
url: 'https://docusaurus.io', url: 'https://docusaurus.io',
favicon: 'image/favicon.ico', favicon: 'image/favicon.ico',
markdown,
}; };
// Build is quite difficult to mock, so we built the blog beforehand and // Build is quite difficult to mock, so we built the blog beforehand and
@ -152,6 +157,7 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
baseUrl: '/myBaseUrl/', baseUrl: '/myBaseUrl/',
url: 'https://docusaurus.io', url: 'https://docusaurus.io',
favicon: 'image/favicon.ico', favicon: 'image/favicon.ico',
markdown,
}; };
// Build is quite difficult to mock, so we built the blog beforehand and // Build is quite difficult to mock, so we built the blog beforehand and
@ -204,6 +210,7 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
baseUrl: '/myBaseUrl/', baseUrl: '/myBaseUrl/',
url: 'https://docusaurus.io', url: 'https://docusaurus.io',
favicon: 'image/favicon.ico', favicon: 'image/favicon.ico',
markdown,
}; };
// Build is quite difficult to mock, so we built the blog beforehand and // Build is quite difficult to mock, so we built the blog beforehand and

View file

@ -16,6 +16,7 @@ import type {
LoadContext, LoadContext,
I18n, I18n,
Validate, Validate,
MarkdownConfig,
} from '@docusaurus/types'; } from '@docusaurus/types';
import type { import type {
BlogPost, BlogPost,
@ -24,6 +25,24 @@ import type {
EditUrlFunction, EditUrlFunction,
} from '@docusaurus/plugin-content-blog'; } from '@docusaurus/plugin-content-blog';
const markdown: MarkdownConfig = {
format: 'mdx',
mermaid: true,
mdx1Compat: {
comments: true,
headingIds: true,
admonitions: true,
},
parseFrontMatter: async (params) => {
// Reuse the default parser
const result = await params.defaultParseFrontMatter(params);
if (result.frontMatter.title === 'Complex Slug') {
result.frontMatter.custom_frontMatter = 'added by parseFrontMatter';
}
return result;
},
};
function findByTitle( function findByTitle(
blogPosts: BlogPost[], blogPosts: BlogPost[],
title: string, title: string,
@ -81,6 +100,7 @@ const getPlugin = async (
title: 'Hello', title: 'Hello',
baseUrl: '/', baseUrl: '/',
url: 'https://docusaurus.io', url: 'https://docusaurus.io',
markdown,
} as DocusaurusConfig; } as DocusaurusConfig;
return pluginContentBlog( return pluginContentBlog(
{ {
@ -242,6 +262,7 @@ describe('blog plugin', () => {
slug: '/hey/my super path/héllô', slug: '/hey/my super path/héllô',
title: 'Complex Slug', title: 'Complex Slug',
tags: ['date', 'complex'], tags: ['date', 'complex'],
custom_frontMatter: 'added by parseFrontMatter',
}, },
tags: [ tags: [
{ {

View file

@ -11,7 +11,7 @@ import _ from 'lodash';
import logger from '@docusaurus/logger'; import logger from '@docusaurus/logger';
import readingTime from 'reading-time'; import readingTime from 'reading-time';
import { import {
parseMarkdownString, parseMarkdownFile,
normalizeUrl, normalizeUrl,
aliasedSitePath, aliasedSitePath,
getEditUrl, getEditUrl,
@ -29,7 +29,7 @@ import {
} from '@docusaurus/utils'; } from '@docusaurus/utils';
import {validateBlogPostFrontMatter} from './frontMatter'; import {validateBlogPostFrontMatter} from './frontMatter';
import {type AuthorsMap, getAuthorsMap, getBlogPostAuthors} from './authors'; import {type AuthorsMap, getAuthorsMap, getBlogPostAuthors} from './authors';
import type {LoadContext} from '@docusaurus/types'; import type {LoadContext, ParseFrontMatter} from '@docusaurus/types';
import type { import type {
PluginOptions, PluginOptions,
ReadingTimeFunction, ReadingTimeFunction,
@ -180,10 +180,19 @@ function formatBlogPostDate(
} }
} }
async function parseBlogPostMarkdownFile(blogSourceAbsolute: string) { async function parseBlogPostMarkdownFile({
const markdownString = await fs.readFile(blogSourceAbsolute, 'utf-8'); filePath,
parseFrontMatter,
}: {
filePath: string;
parseFrontMatter: ParseFrontMatter;
}) {
const fileContent = await fs.readFile(filePath, 'utf-8');
try { try {
const result = parseMarkdownString(markdownString, { const result = await parseMarkdownFile({
filePath,
fileContent,
parseFrontMatter,
removeContentTitle: true, removeContentTitle: true,
}); });
return { return {
@ -191,7 +200,7 @@ async function parseBlogPostMarkdownFile(blogSourceAbsolute: string) {
frontMatter: validateBlogPostFrontMatter(result.frontMatter), frontMatter: validateBlogPostFrontMatter(result.frontMatter),
}; };
} catch (err) { } catch (err) {
logger.error`Error while parsing blog post file path=${blogSourceAbsolute}.`; logger.error`Error while parsing blog post file path=${filePath}.`;
throw err; throw err;
} }
} }
@ -207,7 +216,10 @@ async function processBlogSourceFile(
authorsMap?: AuthorsMap, authorsMap?: AuthorsMap,
): Promise<BlogPost | undefined> { ): Promise<BlogPost | undefined> {
const { const {
siteConfig: {baseUrl}, siteConfig: {
baseUrl,
markdown: {parseFrontMatter},
},
siteDir, siteDir,
i18n, i18n,
} = context; } = context;
@ -228,7 +240,10 @@ async function processBlogSourceFile(
const blogSourceAbsolute = path.join(blogDirPath, blogSourceRelative); const blogSourceAbsolute = path.join(blogDirPath, blogSourceRelative);
const {frontMatter, content, contentTitle, excerpt} = const {frontMatter, content, contentTitle, excerpt} =
await parseBlogPostMarkdownFile(blogSourceAbsolute); await parseBlogPostMarkdownFile({
filePath: blogSourceAbsolute,
parseFrontMatter,
});
const aliasedSource = aliasedSitePath(blogSourceAbsolute, siteDir); const aliasedSource = aliasedSitePath(blogSourceAbsolute, siteDir);

View file

@ -11,4 +11,16 @@ module.exports = {
url: 'https://your-docusaurus-site.example.com', url: 'https://your-docusaurus-site.example.com',
baseUrl: '/', baseUrl: '/',
favicon: 'img/favicon.ico', favicon: 'img/favicon.ico',
markdown: {
parseFrontMatter: async (params) => {
// Reuse the default parser
const result = await params.defaultParseFrontMatter(params);
if (result.frontMatter.last_update?.author) {
result.frontMatter.last_update.author =
result.frontMatter.last_update.author +
' (processed by parseFrontMatter)';
}
return result;
},
},
}; };

View file

@ -463,7 +463,7 @@ exports[`simple website content: data 1`] = `
"frontMatter": { "frontMatter": {
"title": "Custom Last Update", "title": "Custom Last Update",
"last_update": { "last_update": {
"author": "Custom Author", "author": "Custom Author (processed by parseFrontMatter)",
"date": "1/1/2000" "date": "1/1/2000"
} }
} }
@ -686,7 +686,7 @@ exports[`simple website content: data 1`] = `
"frontMatter": { "frontMatter": {
"title": "Last Update Author Only", "title": "Last Update Author Only",
"last_update": { "last_update": {
"author": "Custom Author" "author": "Custom Author (processed by parseFrontMatter)"
} }
} }
}", }",

View file

@ -567,14 +567,14 @@ describe('simple site', () => {
description: 'Custom last update', description: 'Custom last update',
frontMatter: { frontMatter: {
last_update: { last_update: {
author: 'Custom Author', author: 'Custom Author (processed by parseFrontMatter)',
date: '1/1/2000', date: '1/1/2000',
}, },
title: 'Custom Last Update', title: 'Custom Last Update',
}, },
lastUpdatedAt: new Date('1/1/2000').getTime() / 1000, lastUpdatedAt: new Date('1/1/2000').getTime() / 1000,
formattedLastUpdatedAt: 'Jan 1, 2000', formattedLastUpdatedAt: 'Jan 1, 2000',
lastUpdatedBy: 'Custom Author', lastUpdatedBy: 'Custom Author (processed by parseFrontMatter)',
sidebarPosition: undefined, sidebarPosition: undefined,
tags: [], tags: [],
unlisted: false, unlisted: false,
@ -607,13 +607,13 @@ describe('simple site', () => {
description: 'Only custom author, so it will still use the date from Git', description: 'Only custom author, so it will still use the date from Git',
frontMatter: { frontMatter: {
last_update: { last_update: {
author: 'Custom Author', author: 'Custom Author (processed by parseFrontMatter)',
}, },
title: 'Last Update Author Only', title: 'Last Update Author Only',
}, },
lastUpdatedAt: 1539502055, lastUpdatedAt: 1539502055,
formattedLastUpdatedAt: 'Oct 14, 2018', formattedLastUpdatedAt: 'Oct 14, 2018',
lastUpdatedBy: 'Custom Author', lastUpdatedBy: 'Custom Author (processed by parseFrontMatter)',
sidebarPosition: undefined, sidebarPosition: undefined,
tags: [], tags: [],
unlisted: false, unlisted: false,
@ -685,7 +685,7 @@ describe('simple site', () => {
description: 'Custom last update', description: 'Custom last update',
frontMatter: { frontMatter: {
last_update: { last_update: {
author: 'Custom Author', author: 'Custom Author (processed by parseFrontMatter)',
date: '1/1/2000', date: '1/1/2000',
}, },
title: 'Custom Last Update', title: 'Custom Last Update',

View file

@ -15,7 +15,7 @@ import {
getFolderContainingFile, getFolderContainingFile,
getContentPathList, getContentPathList,
normalizeUrl, normalizeUrl,
parseMarkdownString, parseMarkdownFile,
posixPath, posixPath,
Globby, Globby,
normalizeFrontMatterTags, normalizeFrontMatterTags,
@ -140,13 +140,23 @@ async function doProcessDocMetadata({
env: DocEnv; env: DocEnv;
}): Promise<DocMetadataBase> { }): Promise<DocMetadataBase> {
const {source, content, contentPath, filePath} = docFile; const {source, content, contentPath, filePath} = docFile;
const {siteDir, i18n} = context; const {
siteDir,
i18n,
siteConfig: {
markdown: {parseFrontMatter},
},
} = context;
const { const {
frontMatter: unsafeFrontMatter, frontMatter: unsafeFrontMatter,
contentTitle, contentTitle,
excerpt, excerpt,
} = parseMarkdownString(content); } = await parseMarkdownFile({
filePath,
fileContent: content,
parseFrontMatter,
});
const frontMatter = validateDocFrontMatter(unsafeFrontMatter); const frontMatter = validateDocFrontMatter(unsafeFrontMatter);
const { const {

View file

@ -11,4 +11,11 @@ module.exports = {
url: 'https://your-docusaurus-site.example.com', url: 'https://your-docusaurus-site.example.com',
baseUrl: '/', baseUrl: '/',
favicon: 'img/favicon.ico', favicon: 'img/favicon.ico',
markdown: {
parseFrontMatter: async (params) => {
const result = await params.defaultParseFrontMatter(params);
result.frontMatter.custom_frontMatter = 'added by parseFrontMatter';
return result;
},
},
}; };

View file

@ -14,7 +14,9 @@ exports[`docusaurus-plugin-content-pages loads simple pages 1`] = `
}, },
{ {
"description": "Markdown index page", "description": "Markdown index page",
"frontMatter": {}, "frontMatter": {
"custom_frontMatter": "added by parseFrontMatter",
},
"permalink": "/hello/", "permalink": "/hello/",
"source": "@site/src/pages/hello/index.md", "source": "@site/src/pages/hello/index.md",
"title": "Index", "title": "Index",
@ -24,6 +26,7 @@ exports[`docusaurus-plugin-content-pages loads simple pages 1`] = `
{ {
"description": "my MDX page", "description": "my MDX page",
"frontMatter": { "frontMatter": {
"custom_frontMatter": "added by parseFrontMatter",
"description": "my MDX page", "description": "my MDX page",
"title": "MDX page", "title": "MDX page",
}, },
@ -40,7 +43,9 @@ exports[`docusaurus-plugin-content-pages loads simple pages 1`] = `
}, },
{ {
"description": "translated Markdown page", "description": "translated Markdown page",
"frontMatter": {}, "frontMatter": {
"custom_frontMatter": "added by parseFrontMatter",
},
"permalink": "/hello/translatedMd", "permalink": "/hello/translatedMd",
"source": "@site/src/pages/hello/translatedMd.md", "source": "@site/src/pages/hello/translatedMd.md",
"title": undefined, "title": undefined,
@ -69,7 +74,9 @@ exports[`docusaurus-plugin-content-pages loads simple pages with french translat
}, },
{ {
"description": "Markdown index page", "description": "Markdown index page",
"frontMatter": {}, "frontMatter": {
"custom_frontMatter": "added by parseFrontMatter",
},
"permalink": "/fr/hello/", "permalink": "/fr/hello/",
"source": "@site/src/pages/hello/index.md", "source": "@site/src/pages/hello/index.md",
"title": "Index", "title": "Index",
@ -79,6 +86,7 @@ exports[`docusaurus-plugin-content-pages loads simple pages with french translat
{ {
"description": "my MDX page", "description": "my MDX page",
"frontMatter": { "frontMatter": {
"custom_frontMatter": "added by parseFrontMatter",
"description": "my MDX page", "description": "my MDX page",
"title": "MDX page", "title": "MDX page",
}, },
@ -95,7 +103,9 @@ exports[`docusaurus-plugin-content-pages loads simple pages with french translat
}, },
{ {
"description": "translated Markdown page (fr)", "description": "translated Markdown page (fr)",
"frontMatter": {}, "frontMatter": {
"custom_frontMatter": "added by parseFrontMatter",
},
"permalink": "/fr/hello/translatedMd", "permalink": "/fr/hello/translatedMd",
"source": "@site/i18n/fr/docusaurus-plugin-content-pages/hello/translatedMd.md", "source": "@site/i18n/fr/docusaurus-plugin-content-pages/hello/translatedMd.md",
"title": undefined, "title": undefined,

View file

@ -19,7 +19,7 @@ import {
createAbsoluteFilePathMatcher, createAbsoluteFilePathMatcher,
normalizeUrl, normalizeUrl,
DEFAULT_PLUGIN_ID, DEFAULT_PLUGIN_ID,
parseMarkdownString, parseMarkdownFile,
isUnlisted, isUnlisted,
isDraft, isDraft,
} from '@docusaurus/utils'; } from '@docusaurus/utils';
@ -113,7 +113,11 @@ export default function pluginContentPages(
frontMatter: unsafeFrontMatter, frontMatter: unsafeFrontMatter,
contentTitle, contentTitle,
excerpt, excerpt,
} = parseMarkdownString(content); } = await parseMarkdownFile({
filePath: source,
fileContent: content,
parseFrontMatter: siteConfig.markdown.parseFrontMatter,
});
const frontMatter = validatePageFrontMatter(unsafeFrontMatter); const frontMatter = validatePageFrontMatter(unsafeFrontMatter);
if (isDraft({frontMatter})) { if (isDraft({frontMatter})) {

View file

@ -27,6 +27,20 @@ export type MDX1CompatOptions = {
headingIds: boolean; headingIds: boolean;
}; };
export type ParseFrontMatterParams = {filePath: string; fileContent: string};
export type ParseFrontMatterResult = {
frontMatter: {[key: string]: unknown};
content: string;
};
export type DefaultParseFrontMatter = (
params: ParseFrontMatterParams,
) => Promise<ParseFrontMatterResult>;
export type ParseFrontMatter = (
params: ParseFrontMatterParams & {
defaultParseFrontMatter: DefaultParseFrontMatter;
},
) => Promise<ParseFrontMatterResult>;
export type MarkdownConfig = { export type MarkdownConfig = {
/** /**
* The Markdown format to use by default. * The Markdown format to use by default.
@ -44,6 +58,14 @@ export type MarkdownConfig = {
*/ */
format: 'mdx' | 'md' | 'detect'; format: 'mdx' | 'md' | 'detect';
/**
* A function callback that lets users parse the front matter themselves.
* Gives the opportunity to read it from a different source, or process it.
*
* @see https://github.com/facebook/docusaurus/issues/5568
*/
parseFrontMatter: ParseFrontMatter;
/** /**
* Allow mermaid language code blocks to be rendered into Mermaid diagrams: * Allow mermaid language code blocks to be rendered into Mermaid diagrams:
* *

View file

@ -9,6 +9,8 @@ export {
ReportingSeverity, ReportingSeverity,
ThemeConfig, ThemeConfig,
MarkdownConfig, MarkdownConfig,
DefaultParseFrontMatter,
ParseFrontMatter,
DocusaurusConfig, DocusaurusConfig,
Config, Config,
} from './config'; } from './config';

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`parseMarkdownString deletes only first heading 1`] = ` exports[`parseMarkdownFile deletes only first heading 1`] = `
{ {
"content": "# Markdown Title "content": "# Markdown Title
@ -15,7 +15,7 @@ test test test # test bar
} }
`; `;
exports[`parseMarkdownString deletes only first heading 2 1`] = ` exports[`parseMarkdownFile deletes only first heading 2 1`] = `
{ {
"content": "# test "content": "# test
@ -30,7 +30,7 @@ test3",
} }
`; `;
exports[`parseMarkdownString does not warn for duplicate title if markdown title is not at the top 1`] = ` exports[`parseMarkdownFile does not warn for duplicate title if markdown title is not at the top 1`] = `
{ {
"content": "foo "content": "foo
@ -43,7 +43,7 @@ exports[`parseMarkdownString does not warn for duplicate title if markdown title
} }
`; `;
exports[`parseMarkdownString handles code blocks 1`] = ` exports[`parseMarkdownFile handles code blocks 1`] = `
{ {
"content": "\`\`\`js "content": "\`\`\`js
code code
@ -56,7 +56,7 @@ Content",
} }
`; `;
exports[`parseMarkdownString handles code blocks 2`] = ` exports[`parseMarkdownFile handles code blocks 2`] = `
{ {
"content": "\`\`\`\`js "content": "\`\`\`\`js
Foo Foo
@ -73,7 +73,7 @@ Content",
} }
`; `;
exports[`parseMarkdownString handles code blocks 3`] = ` exports[`parseMarkdownFile handles code blocks 3`] = `
{ {
"content": "\`\`\`\`js "content": "\`\`\`\`js
Foo Foo
@ -88,7 +88,7 @@ Content",
} }
`; `;
exports[`parseMarkdownString ignores markdown title if its not a first text 1`] = ` exports[`parseMarkdownFile ignores markdown title if its not a first text 1`] = `
{ {
"content": "foo "content": "foo
# test", # test",
@ -98,7 +98,21 @@ exports[`parseMarkdownString ignores markdown title if its not a first text 1`]
} }
`; `;
exports[`parseMarkdownString parse markdown with front matter 1`] = ` exports[`parseMarkdownFile parse markdown with custom front matter parser 1`] = `
{
"content": "Some text",
"contentTitle": undefined,
"excerpt": "Some text",
"frontMatter": {
"age": 84,
"extra": "value",
"great": true,
"title": "Frontmatter title",
},
}
`;
exports[`parseMarkdownFile parse markdown with front matter 1`] = `
{ {
"content": "Some text", "content": "Some text",
"contentTitle": undefined, "contentTitle": undefined,
@ -109,7 +123,7 @@ exports[`parseMarkdownString parse markdown with front matter 1`] = `
} }
`; `;
exports[`parseMarkdownString parses first heading as contentTitle 1`] = ` exports[`parseMarkdownFile parses first heading as contentTitle 1`] = `
{ {
"content": "# Markdown Title "content": "# Markdown Title
@ -120,7 +134,7 @@ Some text",
} }
`; `;
exports[`parseMarkdownString parses front-matter and ignore h2 1`] = ` exports[`parseMarkdownFile parses front-matter and ignore h2 1`] = `
{ {
"content": "## test", "content": "## test",
"contentTitle": undefined, "contentTitle": undefined,
@ -131,7 +145,7 @@ exports[`parseMarkdownString parses front-matter and ignore h2 1`] = `
} }
`; `;
exports[`parseMarkdownString parses title only 1`] = ` exports[`parseMarkdownFile parses title only 1`] = `
{ {
"content": "# test", "content": "# test",
"contentTitle": "test", "contentTitle": "test",
@ -140,7 +154,7 @@ exports[`parseMarkdownString parses title only 1`] = `
} }
`; `;
exports[`parseMarkdownString parses title only alternate 1`] = ` exports[`parseMarkdownFile parses title only alternate 1`] = `
{ {
"content": "test "content": "test
===", ===",
@ -150,7 +164,7 @@ exports[`parseMarkdownString parses title only alternate 1`] = `
} }
`; `;
exports[`parseMarkdownString reads front matter only 1`] = ` exports[`parseMarkdownFile reads front matter only 1`] = `
{ {
"content": "", "content": "",
"contentTitle": undefined, "contentTitle": undefined,
@ -161,7 +175,7 @@ exports[`parseMarkdownString reads front matter only 1`] = `
} }
`; `;
exports[`parseMarkdownString warns about duplicate titles (front matter + markdown alternate) 1`] = ` exports[`parseMarkdownFile warns about duplicate titles (front matter + markdown alternate) 1`] = `
{ {
"content": "Markdown Title alternate "content": "Markdown Title alternate
================ ================
@ -175,7 +189,7 @@ Some text",
} }
`; `;
exports[`parseMarkdownString warns about duplicate titles (front matter + markdown) 1`] = ` exports[`parseMarkdownFile warns about duplicate titles (front matter + markdown) 1`] = `
{ {
"content": "# Markdown Title "content": "# Markdown Title
@ -188,7 +202,7 @@ Some text",
} }
`; `;
exports[`parseMarkdownString warns about duplicate titles 1`] = ` exports[`parseMarkdownFile warns about duplicate titles 1`] = `
{ {
"content": "# test", "content": "# test",
"contentTitle": "test", "contentTitle": "test",

View file

@ -9,12 +9,14 @@ import dedent from 'dedent';
import { import {
createExcerpt, createExcerpt,
parseMarkdownContentTitle, parseMarkdownContentTitle,
parseMarkdownString,
parseMarkdownHeadingId, parseMarkdownHeadingId,
writeMarkdownHeadingId, writeMarkdownHeadingId,
escapeMarkdownHeadingIds, escapeMarkdownHeadingIds,
unwrapMdxCodeBlocks, unwrapMdxCodeBlocks,
admonitionTitleToDirectiveLabel, admonitionTitleToDirectiveLabel,
parseMarkdownFile,
DEFAULT_PARSE_FRONT_MATTER,
parseFileContentFrontMatter,
} from '../markdownUtils'; } from '../markdownUtils';
describe('createExcerpt', () => { describe('createExcerpt', () => {
@ -623,32 +625,110 @@ Lorem Ipsum
}); });
}); });
describe('parseMarkdownString', () => { describe('parseFileContentFrontMatter', () => {
it('parse markdown with front matter', () => { function test(fileContent: string) {
expect( return parseFileContentFrontMatter(fileContent);
parseMarkdownString(dedent` }
it('can parse front matter', () => {
const input = dedent`
---
title: Frontmatter title
author:
age: 42
birth: 2000-07-23
---
Some text
`;
const expectedResult = {
content: 'Some text',
frontMatter: {
title: 'Frontmatter title',
author: {age: 42, birth: new Date('2000-07-23')},
},
};
const result = test(input) as typeof expectedResult;
expect(result).toEqual(expectedResult);
expect(result.frontMatter.author.birth).toBeInstanceOf(Date);
// A regression test, ensure we don't return gray-matter cached objects
result.frontMatter.title = 'modified';
// @ts-expect-error: ok
result.frontMatter.author.age = 53;
expect(test(input)).toEqual(expectedResult);
});
});
describe('parseMarkdownFile', () => {
async function test(
fileContent: string,
options?: Partial<Parameters<typeof parseMarkdownFile>>[0],
) {
return parseMarkdownFile({
fileContent,
filePath: 'some-file-path.mdx',
parseFrontMatter: DEFAULT_PARSE_FRONT_MATTER,
...options,
});
}
it('parse markdown with front matter', async () => {
await expect(
test(dedent`
--- ---
title: Frontmatter title title: Frontmatter title
--- ---
Some text Some text
`), `),
).toMatchSnapshot(); ).resolves.toMatchSnapshot();
}); });
it('parses first heading as contentTitle', () => { it('parse markdown with custom front matter parser', async () => {
expect( await expect(
parseMarkdownString(dedent` test(
dedent`
---
title: Frontmatter title
age: 42
---
Some text
`,
{
parseFrontMatter: async (params) => {
const result = await params.defaultParseFrontMatter(params);
return {
...result,
frontMatter: {
...result.frontMatter,
age: result.frontMatter.age * 2,
extra: 'value',
great: true,
},
};
},
},
),
).resolves.toMatchSnapshot();
});
it('parses first heading as contentTitle', async () => {
await expect(
test(dedent`
# Markdown Title # Markdown Title
Some text Some text
`), `),
).toMatchSnapshot(); ).resolves.toMatchSnapshot();
}); });
it('warns about duplicate titles (front matter + markdown)', () => { it('warns about duplicate titles (front matter + markdown)', async () => {
expect( await expect(
parseMarkdownString(dedent` test(dedent`
--- ---
title: Frontmatter title title: Frontmatter title
--- ---
@ -657,12 +737,12 @@ describe('parseMarkdownString', () => {
Some text Some text
`), `),
).toMatchSnapshot(); ).resolves.toMatchSnapshot();
}); });
it('warns about duplicate titles (front matter + markdown alternate)', () => { it('warns about duplicate titles (front matter + markdown alternate)', async () => {
expect( await expect(
parseMarkdownString(dedent` test(dedent`
--- ---
title: Frontmatter title title: Frontmatter title
--- ---
@ -672,12 +752,12 @@ describe('parseMarkdownString', () => {
Some text Some text
`), `),
).toMatchSnapshot(); ).resolves.toMatchSnapshot();
}); });
it('does not warn for duplicate title if markdown title is not at the top', () => { it('does not warn for duplicate title if markdown title is not at the top', async () => {
expect( await expect(
parseMarkdownString(dedent` test(dedent`
--- ---
title: Frontmatter title title: Frontmatter title
--- ---
@ -686,12 +766,12 @@ describe('parseMarkdownString', () => {
# Markdown Title # Markdown Title
`), `),
).toMatchSnapshot(); ).resolves.toMatchSnapshot();
}); });
it('deletes only first heading', () => { it('deletes only first heading', async () => {
expect( await expect(
parseMarkdownString(dedent` test(dedent`
# Markdown Title # Markdown Title
test test test # test bar test test test # test bar
@ -700,12 +780,12 @@ describe('parseMarkdownString', () => {
### Markdown Title h3 ### Markdown Title h3
`), `),
).toMatchSnapshot(); ).resolves.toMatchSnapshot();
}); });
it('parses front-matter and ignore h2', () => { it('parses front-matter and ignore h2', async () => {
expect( await expect(
parseMarkdownString( test(
dedent` dedent`
--- ---
title: Frontmatter title title: Frontmatter title
@ -713,55 +793,55 @@ describe('parseMarkdownString', () => {
## test ## test
`, `,
), ),
).toMatchSnapshot(); ).resolves.toMatchSnapshot();
}); });
it('reads front matter only', () => { it('reads front matter only', async () => {
expect( await expect(
parseMarkdownString(dedent` test(dedent`
--- ---
title: test title: test
--- ---
`), `),
).toMatchSnapshot(); ).resolves.toMatchSnapshot();
}); });
it('parses title only', () => { it('parses title only', async () => {
expect(parseMarkdownString('# test')).toMatchSnapshot(); await expect(test('# test')).resolves.toMatchSnapshot();
}); });
it('parses title only alternate', () => { it('parses title only alternate', async () => {
expect( await expect(
parseMarkdownString(dedent` test(dedent`
test test
=== ===
`), `),
).toMatchSnapshot(); ).resolves.toMatchSnapshot();
}); });
it('warns about duplicate titles', () => { it('warns about duplicate titles', async () => {
expect( await expect(
parseMarkdownString(dedent` test(dedent`
--- ---
title: Frontmatter title title: Frontmatter title
--- ---
# test # test
`), `),
).toMatchSnapshot(); ).resolves.toMatchSnapshot();
}); });
it('ignores markdown title if its not a first text', () => { it('ignores markdown title if its not a first text', async () => {
expect( await expect(
parseMarkdownString(dedent` test(dedent`
foo foo
# test # test
`), `),
).toMatchSnapshot(); ).resolves.toMatchSnapshot();
}); });
it('deletes only first heading 2', () => { it('deletes only first heading 2', async () => {
expect( await expect(
parseMarkdownString(dedent` test(dedent`
# test # test
test test test test test test test test test test test test
@ -770,21 +850,21 @@ describe('parseMarkdownString', () => {
### test ### test
test3 test3
`), `),
).toMatchSnapshot(); ).resolves.toMatchSnapshot();
}); });
it('handles code blocks', () => { it('handles code blocks', async () => {
expect( await expect(
parseMarkdownString(dedent` test(dedent`
\`\`\`js \`\`\`js
code code
\`\`\` \`\`\`
Content Content
`), `),
).toMatchSnapshot(); ).resolves.toMatchSnapshot();
expect( await expect(
parseMarkdownString(dedent` test(dedent`
\`\`\`\`js \`\`\`\`js
Foo Foo
\`\`\`diff \`\`\`diff
@ -795,9 +875,9 @@ describe('parseMarkdownString', () => {
Content Content
`), `),
).toMatchSnapshot(); ).resolves.toMatchSnapshot();
expect( await expect(
parseMarkdownString(dedent` test(dedent`
\`\`\`\`js \`\`\`\`js
Foo Foo
\`\`\`diff \`\`\`diff
@ -806,17 +886,17 @@ describe('parseMarkdownString', () => {
Content Content
`), `),
).toMatchSnapshot(); ).resolves.toMatchSnapshot();
}); });
it('throws for invalid front matter', () => { it('throws for invalid front matter', async () => {
expect(() => await expect(
parseMarkdownString(dedent` test(dedent`
--- ---
foo: f: a foo: f: a
--- ---
`), `),
).toThrowErrorMatchingInlineSnapshot(` ).rejects.toThrowErrorMatchingInlineSnapshot(`
"incomplete explicit mapping pair; a key node is missed; or followed by a non-tabulated empty line at line 2, column 7: "incomplete explicit mapping pair; a key node is missed; or followed by a non-tabulated empty line at line 2, column 7:
foo: f: a foo: f: a
^" ^"

View file

@ -70,9 +70,9 @@ export {
unwrapMdxCodeBlocks, unwrapMdxCodeBlocks,
admonitionTitleToDirectiveLabel, admonitionTitleToDirectiveLabel,
createExcerpt, createExcerpt,
parseFrontMatter, DEFAULT_PARSE_FRONT_MATTER,
parseMarkdownContentTitle, parseMarkdownContentTitle,
parseMarkdownString, parseMarkdownFile,
writeMarkdownHeadingId, writeMarkdownHeadingId,
type WriteHeadingIDOptions, type WriteHeadingIDOptions,
} from './markdownUtils'; } from './markdownUtils';

View file

@ -8,6 +8,10 @@
import logger from '@docusaurus/logger'; import logger from '@docusaurus/logger';
import matter from 'gray-matter'; import matter from 'gray-matter';
import {createSlugger, type Slugger, type SluggerOptions} from './slugger'; import {createSlugger, type Slugger, type SluggerOptions} from './slugger';
import type {
ParseFrontMatter,
DefaultParseFrontMatter,
} from '@docusaurus/types';
// Some utilities for parsing Markdown content. These things are only used on // Some utilities for parsing Markdown content. These things are only used on
// server-side when we infer metadata like `title` and `description` from the // server-side when we infer metadata like `title` and `description` from the
@ -214,19 +218,40 @@ export function createExcerpt(fileString: string): string | undefined {
* --- * ---
* ``` * ```
*/ */
export function parseFrontMatter(markdownFileContent: string): { export function parseFileContentFrontMatter(fileContent: string): {
/** Front matter as parsed by gray-matter. */ /** Front matter as parsed by gray-matter. */
frontMatter: {[key: string]: unknown}; frontMatter: {[key: string]: unknown};
/** The remaining content, trimmed. */ /** The remaining content, trimmed. */
content: string; content: string;
} { } {
const {data, content} = matter(markdownFileContent); // TODO Docusaurus v4: replace gray-matter by a better lib
// gray-matter is unmaintained, not flexible, and the code doesn't look good
const {data, content} = matter(fileContent);
// gray-matter has an undocumented front matter caching behavior
// https://github.com/jonschlinkert/gray-matter/blob/ce67a86dba419381db0dd01cc84e2d30a1d1e6a5/index.js#L39
// Unfortunately, this becomes a problem when we mutate returned front matter
// We want to make it possible as part of the parseFrontMatter API
// So we make it safe to mutate by always providing a deep copy
const frontMatter =
// And of course structuredClone() doesn't work well with Date in Jest...
// See https://github.com/jestjs/jest/issues/2549
// So we parse again for tests with a {} option object
// This undocumented empty option object disables gray-matter caching..
process.env.JEST_WORKER_ID
? matter(fileContent, {}).data
: structuredClone(data);
return { return {
frontMatter: data, frontMatter,
content: content.trim(), content: content.trim(),
}; };
} }
export const DEFAULT_PARSE_FRONT_MATTER: DefaultParseFrontMatter = async (
params,
) => parseFileContentFrontMatter(params.fileContent);
function toTextContentTitle(contentTitle: string): string { function toTextContentTitle(contentTitle: string): string {
return contentTitle.replace(/`(?<text>[^`]*)`/g, '$<text>'); return contentTitle.replace(/`(?<text>[^`]*)`/g, '$<text>');
} }
@ -309,10 +334,16 @@ export function parseMarkdownContentTitle(
* @throws Throws when `parseFrontMatter` throws, usually because of invalid * @throws Throws when `parseFrontMatter` throws, usually because of invalid
* syntax. * syntax.
*/ */
export function parseMarkdownString( export async function parseMarkdownFile({
markdownFileContent: string, filePath,
options?: ParseMarkdownContentTitleOptions, fileContent,
): { parseFrontMatter,
removeContentTitle,
}: {
filePath: string;
fileContent: string;
parseFrontMatter: ParseFrontMatter;
} & ParseMarkdownContentTitleOptions): Promise<{
/** @see {@link parseFrontMatter} */ /** @see {@link parseFrontMatter} */
frontMatter: {[key: string]: unknown}; frontMatter: {[key: string]: unknown};
/** @see {@link parseMarkdownContentTitle} */ /** @see {@link parseMarkdownContentTitle} */
@ -324,14 +355,18 @@ export function parseMarkdownString(
* the `removeContentTitle` option. * the `removeContentTitle` option.
*/ */
content: string; content: string;
} { }> {
try { try {
const {frontMatter, content: contentWithoutFrontMatter} = const {frontMatter, content: contentWithoutFrontMatter} =
parseFrontMatter(markdownFileContent); await parseFrontMatter({
filePath,
fileContent,
defaultParseFrontMatter: DEFAULT_PARSE_FRONT_MATTER,
});
const {content, contentTitle} = parseMarkdownContentTitle( const {content, contentTitle} = parseMarkdownContentTitle(
contentWithoutFrontMatter, contentWithoutFrontMatter,
options, {removeContentTitle},
); );
const excerpt = createExcerpt(content); const excerpt = createExcerpt(content);

View file

@ -24,6 +24,7 @@ exports[`loadSiteConfig website with .cjs siteConfig 1`] = `
"headingIds": true, "headingIds": true,
}, },
"mermaid": false, "mermaid": false,
"parseFrontMatter": [Function],
"preprocessor": undefined, "preprocessor": undefined,
}, },
"noIndex": false, "noIndex": false,
@ -72,6 +73,7 @@ exports[`loadSiteConfig website with ts + js config 1`] = `
"headingIds": true, "headingIds": true,
}, },
"mermaid": false, "mermaid": false,
"parseFrontMatter": [Function],
"preprocessor": undefined, "preprocessor": undefined,
}, },
"noIndex": false, "noIndex": false,
@ -120,6 +122,7 @@ exports[`loadSiteConfig website with valid JS CJS config 1`] = `
"headingIds": true, "headingIds": true,
}, },
"mermaid": false, "mermaid": false,
"parseFrontMatter": [Function],
"preprocessor": undefined, "preprocessor": undefined,
}, },
"noIndex": false, "noIndex": false,
@ -168,6 +171,7 @@ exports[`loadSiteConfig website with valid JS ESM config 1`] = `
"headingIds": true, "headingIds": true,
}, },
"mermaid": false, "mermaid": false,
"parseFrontMatter": [Function],
"preprocessor": undefined, "preprocessor": undefined,
}, },
"noIndex": false, "noIndex": false,
@ -216,6 +220,7 @@ exports[`loadSiteConfig website with valid TypeScript CJS config 1`] = `
"headingIds": true, "headingIds": true,
}, },
"mermaid": false, "mermaid": false,
"parseFrontMatter": [Function],
"preprocessor": undefined, "preprocessor": undefined,
}, },
"noIndex": false, "noIndex": false,
@ -264,6 +269,7 @@ exports[`loadSiteConfig website with valid TypeScript ESM config 1`] = `
"headingIds": true, "headingIds": true,
}, },
"mermaid": false, "mermaid": false,
"parseFrontMatter": [Function],
"preprocessor": undefined, "preprocessor": undefined,
}, },
"noIndex": false, "noIndex": false,
@ -312,6 +318,7 @@ exports[`loadSiteConfig website with valid async config 1`] = `
"headingIds": true, "headingIds": true,
}, },
"mermaid": false, "mermaid": false,
"parseFrontMatter": [Function],
"preprocessor": undefined, "preprocessor": undefined,
}, },
"noIndex": false, "noIndex": false,
@ -362,6 +369,7 @@ exports[`loadSiteConfig website with valid async config creator function 1`] = `
"headingIds": true, "headingIds": true,
}, },
"mermaid": false, "mermaid": false,
"parseFrontMatter": [Function],
"preprocessor": undefined, "preprocessor": undefined,
}, },
"noIndex": false, "noIndex": false,
@ -412,6 +420,7 @@ exports[`loadSiteConfig website with valid config creator function 1`] = `
"headingIds": true, "headingIds": true,
}, },
"mermaid": false, "mermaid": false,
"parseFrontMatter": [Function],
"preprocessor": undefined, "preprocessor": undefined,
}, },
"noIndex": false, "noIndex": false,
@ -465,6 +474,7 @@ exports[`loadSiteConfig website with valid siteConfig 1`] = `
"headingIds": true, "headingIds": true,
}, },
"mermaid": false, "mermaid": false,
"parseFrontMatter": [Function],
"preprocessor": undefined, "preprocessor": undefined,
}, },
"noIndex": false, "noIndex": false,

View file

@ -98,6 +98,7 @@ exports[`load loads props for site with custom i18n path 1`] = `
"headingIds": true, "headingIds": true,
}, },
"mermaid": false, "mermaid": false,
"parseFrontMatter": [Function],
"preprocessor": undefined, "preprocessor": undefined,
}, },
"noIndex": false, "noIndex": false,

View file

@ -61,6 +61,8 @@ describe('normalizeConfig', () => {
markdown: { markdown: {
format: 'md', format: 'md',
mermaid: true, mermaid: true,
parseFrontMatter: async (params) =>
params.defaultParseFrontMatter(params),
preprocessor: ({fileContent}) => fileContent, preprocessor: ({fileContent}) => fileContent,
mdx1Compat: { mdx1Compat: {
comments: true, comments: true,
@ -504,6 +506,8 @@ describe('markdown', () => {
const markdown: DocusaurusConfig['markdown'] = { const markdown: DocusaurusConfig['markdown'] = {
format: 'md', format: 'md',
mermaid: true, mermaid: true,
parseFrontMatter: async (params) =>
params.defaultParseFrontMatter(params),
preprocessor: ({fileContent}) => fileContent, preprocessor: ({fileContent}) => fileContent,
mdx1Compat: { mdx1Compat: {
comments: false, comments: false,

View file

@ -6,6 +6,7 @@
*/ */
import { import {
DEFAULT_PARSE_FRONT_MATTER,
DEFAULT_STATIC_DIR_NAME, DEFAULT_STATIC_DIR_NAME,
DEFAULT_I18N_DIR_NAME, DEFAULT_I18N_DIR_NAME,
addLeadingSlash, addLeadingSlash,
@ -13,7 +14,11 @@ import {
removeTrailingSlash, removeTrailingSlash,
} from '@docusaurus/utils'; } from '@docusaurus/utils';
import {Joi, printWarning} from '@docusaurus/utils-validation'; import {Joi, printWarning} from '@docusaurus/utils-validation';
import type {DocusaurusConfig, I18nConfig} from '@docusaurus/types'; import type {
DocusaurusConfig,
I18nConfig,
MarkdownConfig,
} from '@docusaurus/types';
const DEFAULT_I18N_LOCALE = 'en'; const DEFAULT_I18N_LOCALE = 'en';
@ -24,6 +29,18 @@ export const DEFAULT_I18N_CONFIG: I18nConfig = {
localeConfigs: {}, localeConfigs: {},
}; };
export const DEFAULT_MARKDOWN_CONFIG: MarkdownConfig = {
format: 'mdx', // TODO change this to "detect" in Docusaurus v4?
mermaid: false,
preprocessor: undefined,
parseFrontMatter: DEFAULT_PARSE_FRONT_MATTER,
mdx1Compat: {
comments: true,
admonitions: true,
headingIds: true,
},
};
export const DEFAULT_CONFIG: Pick< export const DEFAULT_CONFIG: Pick<
DocusaurusConfig, DocusaurusConfig,
| 'i18n' | 'i18n'
@ -64,16 +81,7 @@ export const DEFAULT_CONFIG: Pick<
tagline: '', tagline: '',
baseUrlIssueBanner: true, baseUrlIssueBanner: true,
staticDirectories: [DEFAULT_STATIC_DIR_NAME], staticDirectories: [DEFAULT_STATIC_DIR_NAME],
markdown: { markdown: DEFAULT_MARKDOWN_CONFIG,
format: 'mdx', // TODO change this to "detect" in Docusaurus v4?
mermaid: false,
preprocessor: undefined,
mdx1Compat: {
comments: true,
admonitions: true,
headingIds: true,
},
},
}; };
function createPluginSchema(theme: boolean) { function createPluginSchema(theme: boolean) {
@ -280,6 +288,9 @@ export const ConfigSchema = Joi.object<DocusaurusConfig>({
format: Joi.string() format: Joi.string()
.equal('mdx', 'md', 'detect') .equal('mdx', 'md', 'detect')
.default(DEFAULT_CONFIG.markdown.format), .default(DEFAULT_CONFIG.markdown.format),
parseFrontMatter: Joi.function().default(
() => DEFAULT_CONFIG.markdown.parseFrontMatter,
),
mermaid: Joi.boolean().default(DEFAULT_CONFIG.markdown.mermaid), mermaid: Joi.boolean().default(DEFAULT_CONFIG.markdown.mermaid),
preprocessor: Joi.function() preprocessor: Joi.function()
.arity(1) .arity(1)

View file

@ -0,0 +1,10 @@
---
unlisted: false
force_unlisted_parseFrontMatter_test: true
---
# force_unlisted_parseFrontMatter_test
This doc is hidden despite `unlisted: false`
We use `parseFrontMatter` to force it to true thanks to `force_unlisted_parseFrontMatter_test: true`

View file

@ -24,6 +24,7 @@ In production, unlisted items should remain accessible, but be hidden in the sid
- [./some-unlisteds/unlisted1.md](./some-unlisteds/unlisted1.mdx) - [./some-unlisteds/unlisted1.md](./some-unlisteds/unlisted1.mdx)
- [./some-unlisteds/unlisted2.md](./some-unlisteds/unlisted2.mdx) - [./some-unlisteds/unlisted2.md](./some-unlisteds/unlisted2.mdx)
- [./some-unlisteds/unlisted-subcategory/unlisted3.md](./some-unlisteds/unlisted-subcategory/unlisted3.mdx) - [./some-unlisteds/unlisted-subcategory/unlisted3.md](./some-unlisteds/unlisted-subcategory/unlisted3.mdx)
- [./force-unlisted.mdx](./force-unlisted.mdx)
--- ---

View file

@ -10,6 +10,15 @@ import type {Options as DocsOptions} from '@docusaurus/plugin-content-docs';
import type {Options as BlogOptions} from '@docusaurus/plugin-content-blog'; import type {Options as BlogOptions} from '@docusaurus/plugin-content-blog';
import type {Options as PageOptions} from '@docusaurus/plugin-content-pages'; import type {Options as PageOptions} from '@docusaurus/plugin-content-pages';
export function dogfoodingTransformFrontMatter(frontMatter: {
[key: string]: unknown;
}): {[key: string]: unknown} {
if (frontMatter.force_unlisted_parseFrontMatter_test === true) {
return {...frontMatter, unlisted: true};
}
return frontMatter;
}
export const dogfoodingThemeInstances: PluginConfig[] = [ export const dogfoodingThemeInstances: PluginConfig[] = [
function swizzleThemeTests(): Plugin { function swizzleThemeTests(): Plugin {
return { return {

View file

@ -421,10 +421,20 @@ type MDX1CompatOptions =
headingIds: boolean; headingIds: boolean;
}; };
export type ParseFrontMatter = (params: {
filePath: string;
fileContent: string;
defaultParseFrontMatter: ParseFrontMatter;
}) => Promise<{
frontMatter: {[key: string]: unknown};
content: string;
}>;
type MarkdownConfig = { type MarkdownConfig = {
format: 'mdx' | 'md' | 'detect'; format: 'mdx' | 'md' | 'detect';
mermaid: boolean; mermaid: boolean;
preprocessor?: MarkdownPreprocessor; preprocessor?: MarkdownPreprocessor;
parseFrontMatter?: ParseFrontMatter;
mdx1Compat: MDX1CompatOptions; mdx1Compat: MDX1CompatOptions;
}; };
``` ```
@ -439,6 +449,12 @@ export default {
preprocessor: ({filePath, fileContent}) => { preprocessor: ({filePath, fileContent}) => {
return fileContent.replaceAll('{{MY_VAR}}', 'MY_VALUE'); return fileContent.replaceAll('{{MY_VAR}}', 'MY_VALUE');
}, },
parseFrontMatter: async (params) => {
const result = await params.defaultParseFrontMatter(params);
result.frontMatter.description =
result.frontMatter.description?.replaceAll('{{MY_VAR}}', 'MY_VALUE');
return result;
},
mdx1Compat: { mdx1Compat: {
comments: true, comments: true,
admonitions: true, admonitions: true,
@ -457,6 +473,7 @@ export default {
| `format` | `'mdx' \| 'md' \| 'detect'` | `'mdx'` | The default parser format to use for Markdown content. Using 'detect' will select the appropriate format automatically based on file extensions: `.md` vs `.mdx`. | | `format` | `'mdx' \| 'md' \| 'detect'` | `'mdx'` | The default parser format to use for Markdown content. Using 'detect' will select the appropriate format automatically based on file extensions: `.md` vs `.mdx`. |
| `mermaid` | `boolean` | `false` | When `true`, allows Docusaurus to render Markdown code blocks with `mermaid` language as Mermaid diagrams. | | `mermaid` | `boolean` | `false` | When `true`, allows Docusaurus to render Markdown code blocks with `mermaid` language as Mermaid diagrams. |
| `preprocessor` | `MarkdownPreprocessor` | `undefined` | Gives you the ability to alter the Markdown content string before parsing. Use it as a last-resort escape hatch or workaround: it is almost always better to implement a Remark/Rehype plugin. | | `preprocessor` | `MarkdownPreprocessor` | `undefined` | Gives you the ability to alter the Markdown content string before parsing. Use it as a last-resort escape hatch or workaround: it is almost always better to implement a Remark/Rehype plugin. |
| `parseFrontMatter` | `ParseFrontMatter` | `undefined` | Gives you the ability to provide your own front matter parser, or to enhance the default parser. Read our [front matter guide](../guides/markdown-features/markdown-features-intro.mdx#front-matter) for details. |
| `mdx1Compat` | `MDX1CompatOptions` | `{comments: true, admonitions: true, headingIds: true}` | Compatibility options to make it easier to upgrade to Docusaurus v3+. | | `mdx1Compat` | `MDX1CompatOptions` | `{comments: true, admonitions: true, headingIds: true}` | Compatibility options to make it easier to upgrade to Docusaurus v3+. |
```mdx-code-block ```mdx-code-block

View file

@ -120,6 +120,45 @@ The API documentation of each official plugin lists the supported attributes:
::: :::
:::tip enhance your front matter
Use the [Markdown config `parseFrontMatter` function](../../api/docusaurus.config.js.mdx#markdown) to provide your own front matter parser, or to enhance the default parser.
It is possible to reuse the default parser to wrap it with your own custom proprietary logic. This makes it possible to implement convenient front matter transformations, shortcuts, or to integrate with external systems using front matter that Docusaurus plugins do not support.
```js title="docusaurus.config.js"
export default {
markdown: {
// highlight-start
parseFrontMatter: async (params) => {
// Reuse the default parser
const result = await params.defaultParseFrontMatter(params);
// Process front matter description placeholders
result.frontMatter.description =
result.frontMatter.description?.replaceAll('{{MY_VAR}}', 'MY_VALUE');
// Create your own front matter shortcut
if (result.frontMatter.i_do_not_want_docs_pagination) {
result.frontMatter.pagination_prev = null;
result.frontMatter.pagination_next = null;
}
// Rename an unsupported front matter coming from another system
if (result.frontMatter.cms_seo_summary) {
result.frontMatter.description = result.frontMatter.cms_seo_summary;
delete result.frontMatter.cms_seo_summary;
}
return result;
},
// highlight-end
},
};
```
:::
## Quotes {#quotes} ## Quotes {#quotes}
Markdown quotes are beautifully styled: Markdown quotes are beautifully styled:

View file

@ -17,6 +17,7 @@ import {
dogfoodingPluginInstances, dogfoodingPluginInstances,
dogfoodingThemeInstances, dogfoodingThemeInstances,
dogfoodingRedirects, dogfoodingRedirects,
dogfoodingTransformFrontMatter,
} from './_dogfooding/dogfooding.config'; } from './_dogfooding/dogfooding.config';
import ConfigLocalized from './docusaurus.config.localized.json'; import ConfigLocalized from './docusaurus.config.localized.json';
@ -176,6 +177,13 @@ export default async function createConfigAsync() {
mdx1Compat: { mdx1Compat: {
// comments: false, // comments: false,
}, },
parseFrontMatter: async (params) => {
const result = await params.defaultParseFrontMatter(params);
return {
...result,
frontMatter: dogfoodingTransformFrontMatter(result.frontMatter),
};
},
preprocessor: ({filePath, fileContent}) => { preprocessor: ({filePath, fileContent}) => {
let result = fileContent; let result = fileContent;