Support include in lint

This commit is contained in:
Fuma Nama
2024-12-22 22:41:16 +08:00
parent 1810868252
commit b71064aa42
13 changed files with 147 additions and 75 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-core': patch
---
Support remark plugins & vfile input on `getTableOfContents`
+37 -15
View File
@@ -5,6 +5,7 @@ import { getTableOfContents } from 'fumadocs-core/server';
import fs from 'node:fs/promises';
import path from 'node:path';
import matter from 'gray-matter';
import { remarkInclude } from 'fumadocs-mdx/config';
async function readFromPath(file: string) {
const content = await fs
@@ -28,23 +29,44 @@ async function checkLinks() {
await fg('content/blog/**/*.mdx').then((files) => files.map(readFromPath)),
);
const docs = docsFiles.map(async (file) => {
const info = parseFilePath(path.relative('content/docs', file.path));
return {
value: getSlugs(info),
hashes: (
await getTableOfContents(
{
path: file.path,
value: file.content,
},
[remarkInclude],
)
).map((item) => item.url.slice(1)),
};
});
const blogs = blogFiles.map(async (file) => {
const info = parseFilePath(path.relative('content/blog', file.path));
return {
value: getSlugs(info)[0],
hashes: (
await getTableOfContents(
{
path: file.path,
value: file.content,
},
[remarkInclude],
)
).map((item) => item.url.slice(1)),
};
});
const scanned = await scanURLs({
populate: {
'(home)/blog/[slug]': blogFiles.map((file) => {
const info = parseFilePath(path.relative('content/blog', file.path));
return { value: getSlugs(info)[0] };
}),
'docs/[...slug]': docsFiles.map((file) => {
const info = parseFilePath(path.relative('content/docs', file.path));
return {
value: getSlugs(info),
hashes: file.data?._mdx?.mirror
? undefined
: getTableOfContents(file.content).map((item) => item.url.slice(1)),
};
}),
'(home)/blog/[slug]': await Promise.all(blogs),
'docs/[...slug]': await Promise.all(docs),
},
});
+2 -1
View File
@@ -116,7 +116,8 @@
"shiki-transformers": "^1.0.1",
"tsconfig": "workspace:*",
"typescript": "^5.7.2",
"unified": "^11.0.5"
"unified": "^11.0.5",
"vfile": "^6.0.3"
},
"peerDependencies": {
"@oramacloud/client": "1.x.x || 2.x.x",
+33 -2
View File
@@ -1,6 +1,8 @@
import { remark } from 'remark';
import type { ReactNode } from 'react';
import { remarkHeading } from '@/mdx-plugins/remark-heading';
import type { PluggableList } from 'unified';
import type { Compatible } from 'vfile';
export interface TOCItemType {
title: ReactNode;
@@ -13,9 +15,38 @@ export type TableOfContents = TOCItemType[];
/**
* Get Table of Contents from markdown/mdx document (using remark)
*
* @param content - Markdown content
* @param content - Markdown content or file
*/
export function getTableOfContents(content: string): TableOfContents {
export function getTableOfContents(content: Compatible): TableOfContents;
/**
* Get Table of Contents from markdown/mdx document (using remark)
*
* @param content - Markdown content or file
* @param remarkPlugins - remark plugins to be applied first
*/
export function getTableOfContents(
content: Compatible,
remarkPlugins: PluggableList,
): Promise<TableOfContents>;
export function getTableOfContents(
content: Compatible,
remarkPlugins?: PluggableList,
): TableOfContents | Promise<TableOfContents> {
if (remarkPlugins) {
return remark()
.use(remarkPlugins)
.use(remarkHeading)
.process(content)
.then((result) => {
if ('toc' in result.data) return result.data.toc as TableOfContents;
return [];
});
}
// compatible with previous versions
const result = remark().use(remarkHeading).processSync(content);
if ('toc' in result.data) return result.data.toc as TableOfContents;
+1 -1
View File
@@ -1,5 +1,5 @@
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import * as fs from 'node:fs';
import { loadConfig, type LoadedConfig } from '@/config/load';
const cache = new Map<
+1
View File
@@ -2,3 +2,4 @@ export * from './types';
export * from './define';
export * from '../utils/mdx-options';
export { frontmatterSchema, metaSchema } from '../utils/schema';
export * from '../mdx-plugins/remark-include';
+2 -2
View File
@@ -1,5 +1,5 @@
import path from 'node:path';
import fs from 'node:fs';
import * as path from 'node:path';
import * as fs from 'node:fs';
import { readFile, writeFile } from 'node:fs/promises';
import grayMatter from 'gray-matter';
import { getConfigHash, loadConfigCached } from '@/config/cached';
+2 -2
View File
@@ -1,5 +1,5 @@
import fs from 'node:fs';
import path from 'node:path';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { type LoadedConfig } from '@/config/load';
import type { MetaFile } from '@/loader-mdx';
import { getTypeFromPath } from '@/utils/get-type-from-path';
@@ -0,0 +1,56 @@
import type { Processor, Transformer } from 'unified';
import { visit } from 'unist-util-visit';
import type { Literal, Root } from 'mdast';
import * as path from 'node:path';
import * as fs from 'node:fs/promises';
import matter from 'gray-matter';
export function remarkInclude(this: Processor): Transformer<Root, Root> {
return async (tree, file) => {
const queue: Promise<void>[] = [];
visit(tree, ['mdxJsxFlowElement', 'paragraph'] as const, (node) => {
let specifier: string | undefined;
// without MDX, parse from paragraph nodes
if (node.type === 'paragraph' && node.children.length === 3) {
const [open, content, closure] = node.children;
if (
open.type === 'html' &&
open.value === '<include>' &&
content.type === 'text' &&
closure.type === 'html' &&
closure.value === '</include>'
) {
specifier = content.value.trim();
}
}
if (node.type === 'mdxJsxFlowElement' && node.name === 'include') {
const child = node.children.at(0) as Literal | undefined;
if (!child || child.type !== 'text') return;
specifier = child.value;
}
if (!specifier) return 'skip';
const targetPath = path.resolve(path.dirname(file.path), specifier);
queue.push(
fs.readFile(targetPath).then((content) => {
const parsed = this.parse(matter(content).content);
if (file.data._compiler) {
file.data._compiler.addDependency(targetPath);
}
Object.assign(node, parsed);
}),
);
return 'skip';
});
await Promise.all(queue);
};
}
+2 -2
View File
@@ -1,5 +1,5 @@
import path from 'node:path';
import fs from 'node:fs';
import * as path from 'node:path';
import * as fs from 'node:fs';
import { findConfigFile, loadConfig } from '@/config/load';
import { generateTypes } from '@/map/generate';
+2 -44
View File
@@ -1,12 +1,6 @@
import { createProcessor, type ProcessorOptions } from '@mdx-js/mdx';
import type { VFile } from 'vfile';
import type { Transformer } from 'unified';
import { visit } from 'unist-util-visit';
import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx';
import type { Literal } from 'mdast';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import matter from 'gray-matter';
import { remarkInclude } from '@/mdx-plugins/remark-include';
type Processor = ReturnType<typeof createProcessor>;
@@ -50,38 +44,6 @@ declare module 'vfile' {
}
}
function remarkInclude(this: Processor): Transformer {
return async (tree, file) => {
const queue: Promise<void>[] = [];
visit(tree, 'mdxJsxFlowElement', (node: MdxJsxFlowElement) => {
if (node.name === 'include') {
const child = node.children.at(0) as Literal | undefined;
if (!child || child.type !== 'text') return;
const specifier = child.value;
const targetPath = path.resolve(path.dirname(file.path), specifier);
queue.push(
fs.readFile(targetPath).then((content) => {
const parsed = this.parse(matter(content).content);
if (file.data._compiler) {
file.data._compiler.addDependency(targetPath);
}
Object.assign(node, parsed);
}),
);
}
return 'skip';
});
await Promise.all(queue);
};
}
/**
* @param group - The cache group of MDX content, usually the collection name
* @param configHash - config hash
@@ -111,11 +73,7 @@ export function buildMDX(
outputFormat: 'program',
development: process.env.NODE_ENV === 'development',
...rest,
remarkPlugins: [
// @ts-expect-error -- processor
remarkInclude,
...(rest.remarkPlugins ?? []),
],
remarkPlugins: [remarkInclude, ...(rest.remarkPlugins ?? [])],
format,
}),
+1 -6
View File
@@ -91,7 +91,7 @@ const neutral: Preset = {
foreground: '0 0% 3.9%',
muted: '0 0% 96.1%',
'muted-foreground': '0 0% 45.1%',
popover: '0 0% 100%',
popover: '0 0% 98%',
'popover-foreground': '0 0% 15.1%',
card: '0 0% 94.7%',
'card-foreground': '0 0% 3.9%',
@@ -123,11 +123,6 @@ const neutral: Preset = {
ring: '0 0% 14.9%',
},
css: {
'#nd-sidebar': {
'--muted': '0deg 0% 89%',
'--secondary': '0deg 0% 99%',
'--muted-foreground': '0 0% 30%',
},
'.dark #nd-sidebar': {
'--muted': '0deg 0% 16%',
'--secondary': '0deg 0% 18%',
+3
View File
@@ -622,6 +622,9 @@ importers:
unified:
specifier: ^11.0.5
version: 11.0.5
vfile:
specifier: ^6.0.3
version: 6.0.3
packages/create-app:
dependencies: