mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 10:55:57 +00:00
Core: Expose Markdown stringifier
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-core': patch
|
||||
---
|
||||
|
||||
Expose Markdown stringifier
|
||||
@@ -1,18 +1,21 @@
|
||||
import type { Heading, Link, Nodes, Root } from 'mdast';
|
||||
import type { Nodes, Root } from 'mdast';
|
||||
import { remark } from 'remark';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import type { PluggableList, Processor, Transformer } from 'unified';
|
||||
import { visit } from 'unist-util-visit';
|
||||
import { toMdxExport } from './mdast-utils';
|
||||
import {
|
||||
mdxToMarkdown,
|
||||
type MdxJsxAttribute,
|
||||
type MdxJsxExpressionAttribute,
|
||||
type MdxJsxFlowElement,
|
||||
type MdxJsxTextElement,
|
||||
} from 'mdast-util-mdx';
|
||||
import { type Handle, type Options, toMarkdown } from 'mdast-util-to-markdown';
|
||||
import { remarkHeading } from './remark-heading';
|
||||
import {
|
||||
type Stringifier as BaseStringifier,
|
||||
type StringifyOptions as BaseStringifyOptions,
|
||||
defaultStringifier as defaultBaseStringifier,
|
||||
} from './stringifier';
|
||||
|
||||
interface StructuredDataHeading {
|
||||
id: string;
|
||||
@@ -32,52 +35,6 @@ export interface StructuredData {
|
||||
contents: StructuredDataContent[];
|
||||
}
|
||||
|
||||
type Stringifier = (this: Processor, node: Nodes, ctx: StringifierContext) => string;
|
||||
|
||||
interface StringifierContext {
|
||||
addContent: (...content: StructuredDataContent[]) => void;
|
||||
}
|
||||
|
||||
export interface StringifyOptions {
|
||||
/**
|
||||
* Filter the elements to be included in the output:
|
||||
*
|
||||
* - `true`: include element & its children.
|
||||
* - `children-only`: exclude element but keep its children.
|
||||
* - `false`: exclude element & its children.
|
||||
*
|
||||
* Default:
|
||||
*
|
||||
* ```ts
|
||||
* filterElement = (node) => {
|
||||
* switch (node.type) {
|
||||
* case 'mdxJsxFlowElement':
|
||||
* case 'mdxJsxTextElement':
|
||||
* switch (node.name) {
|
||||
* case 'File':
|
||||
* case 'TypeTable':
|
||||
* case 'Callout':
|
||||
* case 'Card':
|
||||
* return true;
|
||||
* }
|
||||
* return 'children-only';
|
||||
* }
|
||||
*
|
||||
* return true;
|
||||
* },
|
||||
* ```
|
||||
*/
|
||||
filterElement?: (node: Nodes) => boolean | 'children-only';
|
||||
|
||||
/**
|
||||
* Filter the attributes to stringify.
|
||||
*/
|
||||
filterMdxAttributes?: (
|
||||
node: MdxJsxFlowElement | MdxJsxTextElement,
|
||||
attribute: MdxJsxAttribute | MdxJsxExpressionAttribute,
|
||||
) => boolean;
|
||||
}
|
||||
|
||||
export interface StructureOptions {
|
||||
/**
|
||||
* MDAST node types to be scanned as a content block.
|
||||
@@ -116,11 +73,6 @@ export interface StructureOptions {
|
||||
|
||||
declare module 'mdast' {
|
||||
interface Data {
|
||||
/**
|
||||
* [Fumadocs: remark-structure] The stringified form of node for generating search index.
|
||||
*/
|
||||
_string?: string | (() => string);
|
||||
|
||||
/**
|
||||
* [Fumadocs: remark-structure] Items to add to the structured data.
|
||||
*/
|
||||
@@ -276,113 +228,18 @@ export function structure(
|
||||
return result.data.structuredData!;
|
||||
}
|
||||
|
||||
interface CustomRootNode {
|
||||
type: '_custom';
|
||||
root: Nodes;
|
||||
ctx: StringifierContext;
|
||||
interface StringifierContext {
|
||||
addContent: (...content: StructuredDataContent[]) => void;
|
||||
}
|
||||
export type Stringifier = BaseStringifier<StringifierContext>;
|
||||
export type StringifyOptions = BaseStringifyOptions<StringifierContext>;
|
||||
|
||||
export function defaultStringifier(config: Options & StringifyOptions = {}): Stringifier {
|
||||
const {
|
||||
filterMdxAttributes,
|
||||
filterElement = (node) => {
|
||||
switch (node.type) {
|
||||
case 'mdxJsxFlowElement':
|
||||
case 'mdxJsxTextElement':
|
||||
switch (node.name) {
|
||||
case 'File':
|
||||
case 'TypeTable':
|
||||
case 'Callout':
|
||||
case 'Card':
|
||||
return true;
|
||||
}
|
||||
return 'children-only';
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
...customExtension
|
||||
} = config;
|
||||
|
||||
function modHandler(handler: Handle, ctx: StringifierContext): Handle {
|
||||
return function (node: Nodes, parent, state, info) {
|
||||
export function defaultStringifier(config: StringifyOptions): Stringifier {
|
||||
return defaultBaseStringifier<StringifierContext>({
|
||||
...config,
|
||||
onStringify(node, ctx) {
|
||||
if (node.data?.structuredData) ctx.addContent(...node.data.structuredData.contents);
|
||||
if (node.data?._string)
|
||||
return typeof node.data._string === 'function' ? node.data._string() : node.data._string;
|
||||
const visibility = filterElement(node);
|
||||
|
||||
if (visibility === false) return '';
|
||||
|
||||
switch (node.type) {
|
||||
case 'mdxJsxFlowElement':
|
||||
case 'mdxJsxTextElement': {
|
||||
if (visibility === 'children-only') {
|
||||
return node.type === 'mdxJsxTextElement'
|
||||
? state.containerPhrasing(node, info)
|
||||
: state.containerFlow(node, info);
|
||||
}
|
||||
|
||||
const stringifiedAttributes: MdxJsxAttribute[] = [];
|
||||
for (const attr of node.attributes) {
|
||||
if (attr.type === 'mdxJsxExpressionAttribute') continue;
|
||||
if (filterMdxAttributes && !filterMdxAttributes(node, attr)) continue;
|
||||
const str = typeof attr.value === 'string' ? attr.value : attr.value?.value;
|
||||
if (!str) continue;
|
||||
|
||||
stringifiedAttributes.push({
|
||||
...attr,
|
||||
value: str,
|
||||
});
|
||||
}
|
||||
|
||||
const temp = node.attributes;
|
||||
node.attributes = stringifiedAttributes;
|
||||
const s = handler(node, parent, state, info);
|
||||
node.attributes = temp;
|
||||
return s;
|
||||
}
|
||||
default:
|
||||
if (visibility === 'children-only')
|
||||
return 'children' in node
|
||||
? state.containerFlow({ type: 'root', children: node.children }, info)
|
||||
: '';
|
||||
|
||||
return handler(node, parent, state, info);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const customToMarkdown: Options = {
|
||||
handlers: {
|
||||
link(node: Link, _, state, info) {
|
||||
return state.containerPhrasing(node, info);
|
||||
},
|
||||
heading(node: Heading, _, state, info) {
|
||||
return state.containerPhrasing(node, info);
|
||||
},
|
||||
image() {
|
||||
return '';
|
||||
},
|
||||
_custom(node: CustomRootNode, _, state, info) {
|
||||
const handlers: Record<string, Handle> = state.handlers;
|
||||
for (const k in handlers) {
|
||||
handlers[k] = modHandler(handlers[k], node.ctx);
|
||||
}
|
||||
|
||||
return state.handle(node.root, undefined, state, info);
|
||||
},
|
||||
} as Record<string, Handle>,
|
||||
};
|
||||
|
||||
return function (root, ctx) {
|
||||
return toMarkdown({ type: '_custom', root, ctx } satisfies CustomRootNode as never, {
|
||||
...this.data('settings'),
|
||||
extensions: [
|
||||
mdxToMarkdown(),
|
||||
...(this.data('toMarkdownExtensions') ?? []),
|
||||
customToMarkdown,
|
||||
customExtension,
|
||||
],
|
||||
});
|
||||
};
|
||||
config.onStringify?.(node, ctx);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import type { Nodes, Link, Heading } from 'mdast';
|
||||
import {
|
||||
type MdxJsxFlowElement,
|
||||
type MdxJsxTextElement,
|
||||
type MdxJsxAttribute,
|
||||
type MdxJsxExpressionAttribute,
|
||||
mdxToMarkdown,
|
||||
} from 'mdast-util-mdx';
|
||||
import { type Handle, type Options, toMarkdown } from 'mdast-util-to-markdown';
|
||||
import type { Processor } from 'unified';
|
||||
|
||||
declare module 'mdast' {
|
||||
interface Data {
|
||||
/**
|
||||
* [Fumadocs: stringify] The stringified form of node, used for generating search index, `llms.txt`, etc.
|
||||
*/
|
||||
_string?: string | (() => string);
|
||||
}
|
||||
}
|
||||
|
||||
export type Stringifier<Context> =
|
||||
| ((this: Processor, node: Nodes, ctx: Context) => string)
|
||||
| ((node: Nodes, ctx: Context) => string);
|
||||
|
||||
export interface StringifyOptions<Context = undefined> extends Options {
|
||||
/**
|
||||
* Filter the elements to be included in the output:
|
||||
*
|
||||
* - `true`: include element & its children.
|
||||
* - `children-only`: exclude element but keep its children.
|
||||
* - `false`: exclude element & its children.
|
||||
*
|
||||
* Default:
|
||||
*
|
||||
* ```ts
|
||||
* filterElement = (node) => {
|
||||
* switch (node.type) {
|
||||
* case 'mdxJsxFlowElement':
|
||||
* case 'mdxJsxTextElement':
|
||||
* switch (node.name) {
|
||||
* case 'File':
|
||||
* case 'TypeTable':
|
||||
* case 'Callout':
|
||||
* case 'Card':
|
||||
* return true;
|
||||
* }
|
||||
* return 'children-only';
|
||||
* }
|
||||
*
|
||||
* return true;
|
||||
* },
|
||||
* ```
|
||||
*/
|
||||
filterElement?: (node: Nodes) => boolean | 'children-only';
|
||||
|
||||
/**
|
||||
* Filter the attributes to stringify.
|
||||
*/
|
||||
filterMdxAttributes?: (
|
||||
node: MdxJsxFlowElement | MdxJsxTextElement,
|
||||
attribute: MdxJsxAttribute | MdxJsxExpressionAttribute,
|
||||
) => boolean;
|
||||
|
||||
onStringify?: (node: Nodes, ctx: Context) => void;
|
||||
}
|
||||
|
||||
interface CustomRootNode<Context> {
|
||||
type: '_custom';
|
||||
root: Nodes;
|
||||
ctx: Context;
|
||||
}
|
||||
|
||||
export function defaultStringifier<Context>(
|
||||
config: StringifyOptions<Context> = {},
|
||||
): Stringifier<Context> {
|
||||
const {
|
||||
filterMdxAttributes,
|
||||
filterElement = (node) => {
|
||||
switch (node.type) {
|
||||
case 'mdxJsxFlowElement':
|
||||
case 'mdxJsxTextElement':
|
||||
switch (node.name) {
|
||||
case 'File':
|
||||
case 'TypeTable':
|
||||
case 'Callout':
|
||||
case 'Card':
|
||||
return true;
|
||||
}
|
||||
return 'children-only';
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
onStringify,
|
||||
...customExtension
|
||||
} = config;
|
||||
|
||||
function modHandler(handler: Handle, ctx: Context): Handle {
|
||||
return function (node: Nodes, parent, state, info) {
|
||||
if (onStringify) onStringify(node, ctx);
|
||||
if (node.data?._string)
|
||||
return typeof node.data._string === 'function' ? node.data._string() : node.data._string;
|
||||
const visibility = filterElement(node);
|
||||
|
||||
if (visibility === false) return '';
|
||||
|
||||
switch (node.type) {
|
||||
case 'mdxJsxFlowElement':
|
||||
case 'mdxJsxTextElement': {
|
||||
if (visibility === 'children-only') {
|
||||
return node.type === 'mdxJsxTextElement'
|
||||
? state.containerPhrasing(node, info)
|
||||
: state.containerFlow(node, info);
|
||||
}
|
||||
|
||||
const stringifiedAttributes: MdxJsxAttribute[] = [];
|
||||
for (const attr of node.attributes) {
|
||||
if (attr.type === 'mdxJsxExpressionAttribute') continue;
|
||||
if (filterMdxAttributes && !filterMdxAttributes(node, attr)) continue;
|
||||
const str = typeof attr.value === 'string' ? attr.value : attr.value?.value;
|
||||
if (!str) continue;
|
||||
|
||||
stringifiedAttributes.push({
|
||||
...attr,
|
||||
value: str,
|
||||
});
|
||||
}
|
||||
|
||||
const temp = node.attributes;
|
||||
node.attributes = stringifiedAttributes;
|
||||
const s = handler(node, parent, state, info);
|
||||
node.attributes = temp;
|
||||
return s;
|
||||
}
|
||||
default:
|
||||
if (visibility === 'children-only')
|
||||
return 'children' in node
|
||||
? state.containerFlow({ type: 'root', children: node.children }, info)
|
||||
: '';
|
||||
|
||||
return handler(node, parent, state, info);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const customToMarkdown: Options = {
|
||||
handlers: {
|
||||
link(node: Link, _, state, info) {
|
||||
return state.containerPhrasing(node, info);
|
||||
},
|
||||
heading(node: Heading, _, state, info) {
|
||||
return state.containerPhrasing(node, info);
|
||||
},
|
||||
image() {
|
||||
return '';
|
||||
},
|
||||
_custom(node: CustomRootNode<Context>, _, state, info) {
|
||||
const handlers: Record<string, Handle> = state.handlers;
|
||||
for (const k in handlers) {
|
||||
handlers[k] = modHandler(handlers[k], node.ctx);
|
||||
}
|
||||
|
||||
return state.handle(node.root, undefined, state, info);
|
||||
},
|
||||
} as Record<string, Handle>,
|
||||
};
|
||||
|
||||
return function (this: Processor | undefined, root, ctx) {
|
||||
return toMarkdown({ type: '_custom', root, ctx } satisfies CustomRootNode<Context> as never, {
|
||||
...this?.data('settings'),
|
||||
extensions: [
|
||||
mdxToMarkdown(),
|
||||
...(this?.data('toMarkdownExtensions') ?? []),
|
||||
customToMarkdown,
|
||||
customExtension,
|
||||
],
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -25,6 +25,7 @@ export default defineConfig({
|
||||
'src/mdx-plugins/{index,codeblock-utils}.ts',
|
||||
'src/mdx-plugins/remark-*.ts',
|
||||
'src/mdx-plugins/rehype-*.ts',
|
||||
'src/mdx-plugins/stringifier.ts',
|
||||
'src/framework/*',
|
||||
],
|
||||
deps: {
|
||||
|
||||
Reference in New Issue
Block a user