MDX: Use custom util to parse frontmatter

This commit is contained in:
Fuma Nama
2025-06-14 10:05:09 +08:00
parent 3c3079d142
commit 14e267b58f
8 changed files with 85 additions and 20 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-mdx': patch
---
Use custom util to parse frontmatter
-1
View File
@@ -54,7 +54,6 @@
"chokidar": "^4.0.3",
"esbuild": "^0.25.5",
"estree-util-value-to-estree": "^3.4.0",
"gray-matter": "^4.0.3",
"js-yaml": "^4.1.0",
"lru-cache": "^11.1.0",
"picocolors": "^1.1.1",
+6 -5
View File
@@ -1,11 +1,11 @@
import * as path from 'node:path';
import { parse } from 'node:querystring';
import grayMatter from 'gray-matter';
import { type LoaderContext } from 'webpack';
import { getConfigHash, loadConfig, type LoadedConfig } from '@/utils/config';
import { buildMDX } from '@/utils/build-mdx';
import { getGitTimestamp } from './utils/git-timestamp';
import { validate, ValidationError } from '@/utils/schema';
import { fumaMatter } from '@/utils/fuma-matter';
export interface Options {
configPath: string;
@@ -26,7 +26,7 @@ export default async function loader(
const context = this.context;
const filePath = this.resourcePath;
const { configPath, outDir } = this.getOptions();
const matter = grayMatter(source);
const matter = fumaMatter(source);
// notice that `resourceQuery` can be missing (e.g. `page.mdx`)
const {
@@ -48,12 +48,13 @@ export default async function loader(
collection = undefined;
}
let data = matter.data;
const mdxOptions =
collection?.mdxOptions ?? (await loadDefaultOptions(config));
if (collection?.schema) {
try {
matter.data = (await validate(
data = await validate(
collection.schema,
matter.data,
{
@@ -61,7 +62,7 @@ export default async function loader(
path: filePath,
},
`invalid frontmatter in ${filePath}`,
)) as Record<string, unknown>;
);
} catch (e) {
if (e instanceof ValidationError) {
return callback(new Error(e.toStringFormatted()));
@@ -89,7 +90,7 @@ export default async function loader(
development: this.mode === 'development',
...mdxOptions,
filePath,
frontmatter: matter.data,
frontmatter: data as Record<string, unknown>,
data: {
lastModified: timestamp,
},
+6 -5
View File
@@ -7,10 +7,10 @@ import type { LoadedConfig } from '@/utils/config';
import type { DocCollection, MetaCollection } from '@/config';
import { validate } from '@/utils/schema';
import { fileCache } from '@/map/file-cache';
import matter from 'gray-matter';
import type { AsyncRuntimeFile } from '@/runtime/types';
import { load } from 'js-yaml';
import { getGitTimestamp } from '@/utils/git-timestamp';
import { fumaMatter } from '@/utils/fuma-matter';
async function readFileWithCache(file: string): Promise<string> {
const cached = fileCache.read<string>('read-file', file);
@@ -102,17 +102,18 @@ export async function generateJS(
}
const entries = files.map(async (file) => {
const parsed = matter(
const parsed = fumaMatter(
await readFileWithCache(file.absolutePath).catch(() => ''),
);
let data = parsed.data;
if (collection.schema) {
parsed.data = (await validate(
data = await validate(
collection.schema,
parsed.data,
{ path: file.absolutePath, source: parsed.content },
`invalid frontmatter in ${file.absolutePath}`,
)) as Record<string, unknown>;
);
}
let lastModified: Date | undefined;
@@ -123,7 +124,7 @@ export async function generateJS(
return JSON.stringify({
info: file,
lastModified,
data: parsed.data,
data: data as Record<string, unknown>,
content: parsed.content,
} satisfies AsyncRuntimeFile);
});
@@ -3,8 +3,8 @@ import { visit } from 'unist-util-visit';
import type { Code, Root, RootContent } from 'mdast';
import * as path from 'node:path';
import * as fs from 'node:fs/promises';
import matter from 'gray-matter';
import type { CompilerOptions } from '@/utils/build-mdx';
import { fumaMatter } from '@/utils/fuma-matter';
function flattenNode(node: RootContent): string {
if ('children' in node)
@@ -67,6 +67,7 @@ export function remarkInclude(this: Processor): Transformer<Root, Root> {
queue.push(
fs
.readFile(targetPath)
.then((buffer) => buffer.toString())
.then(async (content) => {
compiler?.addDependency(targetPath);
@@ -83,7 +84,7 @@ export function remarkInclude(this: Processor): Transformer<Root, Root> {
return;
}
const parsed = processor.parse(matter(content).content);
const parsed = processor.parse(fumaMatter(content).content);
await update(parsed as Root, targetPath, processor, compiler);
Object.assign(
+61
View File
@@ -0,0 +1,61 @@
/**
* Inspired by https://github.com/jonschlinkert/gray-matter
*/
import { LRUCache } from 'lru-cache';
import { load } from 'js-yaml';
const cache = new LRUCache<string, Output>({
max: 200,
});
interface Output {
matter: string;
content: string;
data: unknown;
}
/**
* parse frontmatter, it supports only yaml format
*/
export function fumaMatter(input: string): Output {
if (input === '') {
return { data: {}, content: input, matter: '' };
}
const cached = cache.get(input);
if (cached) return cached;
const result = parseMatter(input);
cache.set(input, result);
// avoid mutation
return structuredClone(result);
}
const delimiter = '---';
function parseMatter(str: string): Output {
const output: Output = { matter: '', data: {}, content: str };
const open = delimiter + '\n';
const close = '\n' + delimiter;
if (!str.startsWith(open)) {
return output;
}
str = str.slice(open.length);
const len = str.length;
let closeIdx = str.indexOf(close);
if (closeIdx === -1) {
closeIdx = len;
}
// get the raw front-matter block
output.matter = str.slice(0, closeIdx);
output.content = str.slice(closeIdx + close.length);
const loaded = load(output.matter);
output.data = loaded ?? {};
return output;
}
+1 -1
View File
@@ -153,7 +153,7 @@ export function SearchDialogFooter(props: ComponentProps<'div'>) {
<div
{...props}
className={cn(
'border-t backdrop-brightness-130 p-3 empty:hidden',
'border-t bg-fd-secondary/50 p-3 empty:hidden',
props.className,
)}
/>
+3 -6
View File
@@ -279,7 +279,7 @@ importers:
version: 0.2.2(@content-collections/[email protected]([email protected]))([email protected])([email protected]([email protected]))([email protected])
'@content-collections/next':
specifier: ^0.2.6
version: 0.2.6(@content-collections/[email protected]([email protected]))([email protected](@opentelemetry/[email protected])([email protected]([email protected]))([email protected]))
version: 0.2.6(@content-collections/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]))
'@types/mdx':
specifier: ^2.0.13
version: 2.0.13
@@ -926,7 +926,7 @@ importers:
version: 0.2.2(@content-collections/[email protected]([email protected]))([email protected])([email protected]([email protected]))([email protected])
'@content-collections/next':
specifier: ^0.2.6
version: 0.2.6(@content-collections/[email protected]([email protected]))([email protected](@opentelemetry/[email protected])([email protected]([email protected]))([email protected]))
version: 0.2.6(@content-collections/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]))
'@react-router/dev':
specifier: ^7.6.2
version: 7.6.2(@react-router/[email protected]([email protected]([email protected]([email protected]))([email protected]))([email protected]))(@types/[email protected])([email protected])([email protected])([email protected])([email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected])([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected]))([email protected])
@@ -1086,9 +1086,6 @@ importers:
estree-util-value-to-estree:
specifier: ^3.4.0
version: 3.4.0
gray-matter:
specifier: ^4.0.3
version: 4.0.3
js-yaml:
specifier: ^4.1.0
version: 4.1.0
@@ -10340,7 +10337,7 @@ snapshots:
- acorn
- supports-color
'@content-collections/[email protected](@content-collections/[email protected]([email protected]))([email protected](@opentelemetry/[email protected])([email protected]([email protected]))([email protected]))':
'@content-collections/[email protected](@content-collections/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]))':
dependencies:
'@content-collections/core': 0.9.0([email protected])
'@content-collections/integrations': 0.2.1(@content-collections/[email protected]([email protected]))