mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
Core: unify remark nodes parsing & improve types
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-core': patch
|
||||
---
|
||||
|
||||
unify remark nodes parsing & improve types
|
||||
@@ -28,8 +28,8 @@ export function rehypeToc(
|
||||
}[] = [];
|
||||
|
||||
visit(tree, ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], (element) => {
|
||||
const id = element.properties.id as string | undefined;
|
||||
if (!id) return 'skip';
|
||||
const id = element.properties.id;
|
||||
if (typeof id !== 'string') return 'skip';
|
||||
let isTocOnly = false;
|
||||
|
||||
const last = element.children.at(-1);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { Processor, Transformer } from 'unified';
|
||||
import type { Code, Root } from 'mdast';
|
||||
import type { BlockContent, Code, DefinitionContent, Root } from 'mdast';
|
||||
import { visit } from 'unist-util-visit';
|
||||
import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx';
|
||||
|
||||
const TabRegex = /tab="(.+?)"/;
|
||||
import {
|
||||
generateCodeBlockTabs,
|
||||
parseCodeBlockAttributes,
|
||||
} from '@/mdx-plugins/codeblock-utils';
|
||||
|
||||
type TabType = keyof typeof Types;
|
||||
export interface RemarkCodeTabOptions {
|
||||
@@ -17,6 +19,12 @@ export interface RemarkCodeTabOptions {
|
||||
parseMdx?: boolean;
|
||||
}
|
||||
|
||||
declare module 'mdast' {
|
||||
export interface CodeData {
|
||||
tab?: string;
|
||||
}
|
||||
}
|
||||
|
||||
const Tabs = {
|
||||
convert(
|
||||
processor: Processor,
|
||||
@@ -143,64 +151,28 @@ const CodeBlockTabs = {
|
||||
withParent = true,
|
||||
): MdxJsxFlowElement {
|
||||
const names = processTabValue(nodes);
|
||||
const children: MdxJsxFlowElement[] = [
|
||||
{
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: 'CodeBlockTabsList',
|
||||
attributes: [],
|
||||
children: names.map((name) => {
|
||||
return {
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: 'CodeBlockTabsTrigger',
|
||||
attributes: [
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'value',
|
||||
const node = generateCodeBlockTabs({
|
||||
defaultValue: names[0],
|
||||
triggers: names.map((name) => ({
|
||||
value: name,
|
||||
children: [
|
||||
withMdx
|
||||
? // eslint-disable-next-line @typescript-eslint/no-explicit-any -- needed
|
||||
(mdxToAst(processor, name) as any)
|
||||
: {
|
||||
type: 'text',
|
||||
value: name,
|
||||
},
|
||||
],
|
||||
children: [
|
||||
withMdx
|
||||
? // eslint-disable-next-line @typescript-eslint/no-explicit-any -- needed
|
||||
(mdxToAst(processor, name) as any)
|
||||
: {
|
||||
type: 'text',
|
||||
value: name,
|
||||
},
|
||||
],
|
||||
};
|
||||
}),
|
||||
},
|
||||
...nodes.map((node, i) => {
|
||||
return {
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: 'CodeBlockTab',
|
||||
attributes: [
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'value',
|
||||
value: names[i],
|
||||
},
|
||||
],
|
||||
children: [node],
|
||||
} as MdxJsxFlowElement;
|
||||
}),
|
||||
];
|
||||
],
|
||||
})),
|
||||
tabs: nodes.map((node, i) => ({
|
||||
value: names[i],
|
||||
children: [node],
|
||||
})),
|
||||
});
|
||||
|
||||
if (!withParent) return createFragment(children);
|
||||
|
||||
return {
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: 'CodeBlockTabs',
|
||||
attributes: [
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'defaultValue',
|
||||
value: names[0],
|
||||
},
|
||||
],
|
||||
children,
|
||||
};
|
||||
if (!withParent) return createFragment(node.children);
|
||||
return node;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -216,10 +188,10 @@ export function remarkCodeTab(
|
||||
const { parseMdx = false, Tabs = 'CodeBlockTabs' } = options;
|
||||
|
||||
return (tree) => {
|
||||
const ignored = new WeakSet();
|
||||
|
||||
visit(tree, (node) => {
|
||||
if (!('children' in node)) return;
|
||||
let start = -1;
|
||||
let i = 0;
|
||||
if (!('children' in node) || ignored.has(node)) return 'skip';
|
||||
let localTabs: TabType = Tabs;
|
||||
let localParseMdx = parseMdx;
|
||||
let withParent = true;
|
||||
@@ -233,7 +205,7 @@ export function remarkCodeTab(
|
||||
localTabs = node.name as TabType;
|
||||
|
||||
// for `Tabs` in simple mode, it doesn't support MDX tab names
|
||||
if (node.name === 'Tabs') {
|
||||
if (node.name === 'Tabs' && localParseMdx) {
|
||||
localParseMdx = node.attributes.every(
|
||||
(attribute) =>
|
||||
attribute.type !== 'mdxJsxAttribute' ||
|
||||
@@ -242,50 +214,49 @@ export function remarkCodeTab(
|
||||
}
|
||||
}
|
||||
|
||||
while (i < node.children.length) {
|
||||
const child = node.children[i];
|
||||
const isSwitcher =
|
||||
child.type === 'code' && child.meta && child.meta.match(TabRegex);
|
||||
let start = -1;
|
||||
let end = 0;
|
||||
const close = () => {
|
||||
if (start === -1 || start === end) return;
|
||||
const replacement = Types[localTabs].convert(
|
||||
this,
|
||||
node.children.slice(start, end) as Code[],
|
||||
localParseMdx,
|
||||
withParent,
|
||||
);
|
||||
|
||||
if (isSwitcher && start === -1) {
|
||||
start = i;
|
||||
ignored.add(replacement);
|
||||
node.children.splice(start, end - start, replacement);
|
||||
end = start;
|
||||
start = -1;
|
||||
};
|
||||
|
||||
for (; end < node.children.length; end++) {
|
||||
const child = node.children[end];
|
||||
if (child.type !== 'code' || !child.meta) {
|
||||
close();
|
||||
continue;
|
||||
}
|
||||
|
||||
// if switcher code blocks terminated, convert them to tabs
|
||||
const isLast = i === node.children.length - 1;
|
||||
if (start !== -1 && (isLast || !isSwitcher)) {
|
||||
const end = isSwitcher ? i + 1 : i;
|
||||
const targets = node.children.slice(start, end) as Code[];
|
||||
const replacement = Types[localTabs].convert(
|
||||
this,
|
||||
targets,
|
||||
localParseMdx,
|
||||
withParent,
|
||||
);
|
||||
|
||||
node.children.splice(start, end - start, replacement);
|
||||
if (isLast) break;
|
||||
i = start + 1;
|
||||
start = -1;
|
||||
} else {
|
||||
i++;
|
||||
const meta = parseCodeBlockAttributes(child.meta, ['tab']);
|
||||
if (!meta.attributes.tab) {
|
||||
close();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (start === -1) start = end;
|
||||
child.meta = meta.rest;
|
||||
child.data ??= {};
|
||||
child.data.tab = meta.attributes.tab;
|
||||
}
|
||||
|
||||
close();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function processTabValue(nodes: Code[]) {
|
||||
return nodes.map((node, i) => {
|
||||
let title = `Tab ${i + 1}`;
|
||||
|
||||
node.meta = node.meta?.replace(TabRegex, (_, value) => {
|
||||
title = value;
|
||||
return '';
|
||||
});
|
||||
|
||||
return title;
|
||||
});
|
||||
return nodes.map((node, i) => node.data?.tab ?? `Tab ${i + 1}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -305,7 +276,9 @@ function mdxToAst(processor: Processor, name: string) {
|
||||
return node;
|
||||
}
|
||||
|
||||
function createFragment(children: MdxJsxFlowElement[]): MdxJsxFlowElement {
|
||||
function createFragment(
|
||||
children: (BlockContent | DefinitionContent)[],
|
||||
): MdxJsxFlowElement {
|
||||
return {
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: null,
|
||||
|
||||
@@ -50,34 +50,31 @@ export function remarkHeading({
|
||||
visit(root, 'heading', (heading) => {
|
||||
heading.data ||= {};
|
||||
heading.data.hProperties ||= {};
|
||||
const props = heading.data.hProperties;
|
||||
|
||||
let id = heading.data.hProperties.id;
|
||||
const lastNode = heading.children.at(-1);
|
||||
|
||||
if (!id && lastNode?.type === 'text' && customId) {
|
||||
if (lastNode?.type === 'text' && customId) {
|
||||
const match = regex.exec(lastNode.value);
|
||||
|
||||
if (match?.[1]) {
|
||||
id = match[1];
|
||||
props.id = match[1];
|
||||
lastNode.value = lastNode.value.slice(0, match.index);
|
||||
}
|
||||
}
|
||||
|
||||
let flattened: string | null = null;
|
||||
if (!id) {
|
||||
if (!props.id) {
|
||||
flattened ??= flattenNode(heading);
|
||||
|
||||
id = defaultSlug
|
||||
props.id = defaultSlug
|
||||
? defaultSlug(root, heading, flattened)
|
||||
: slugger.slug(flattened);
|
||||
}
|
||||
|
||||
heading.data.hProperties.id = id;
|
||||
|
||||
if (generateToc) {
|
||||
toc.push({
|
||||
title: flattened ?? flattenNode(heading),
|
||||
url: `#${id}`,
|
||||
url: `#${props.id}`,
|
||||
depth: heading.depth,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -62,7 +62,11 @@ declare module 'mdast' {
|
||||
}
|
||||
}
|
||||
|
||||
const slugger = new Slugger();
|
||||
declare module 'vfile' {
|
||||
interface DataMap {
|
||||
structuredData: StructuredData;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach structured data to VFile, you can access via `vfile.data.structuredData`.
|
||||
@@ -81,6 +85,8 @@ export function remarkStructure({
|
||||
return ['TypeTable', 'Callout'].includes(node.name);
|
||||
},
|
||||
}: StructureOptions = {}): Transformer<Root, Root> {
|
||||
const slugger = new Slugger();
|
||||
|
||||
if (Array.isArray(allowedMdxAttributes)) {
|
||||
const arr = allowedMdxAttributes;
|
||||
allowedMdxAttributes = (_node, attribute) =>
|
||||
@@ -95,7 +101,7 @@ export function remarkStructure({
|
||||
return (node, file) => {
|
||||
slugger.reset();
|
||||
const data: StructuredData = { contents: [], headings: [] };
|
||||
let lastHeading: string | undefined = '';
|
||||
let lastHeading: string | undefined;
|
||||
|
||||
// Fumadocs OpenAPI Generated Structured Data
|
||||
if (file.data.frontmatter) {
|
||||
@@ -198,5 +204,5 @@ export function structure(
|
||||
.use(remarkStructure, options)
|
||||
.processSync(content);
|
||||
|
||||
return result.data.structuredData as StructuredData;
|
||||
return result.data.structuredData!;
|
||||
}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||

|
||||

|
||||
|
||||
@@ -1 +1 @@
|
||||
<img alt="External" src="https://picsum.photos/id/237/200/300" width="200" height="300" />
|
||||
<img alt="External" src="https://fumadocs.dev/banner.png" width="1200" height="630" />
|
||||
|
||||
@@ -106,7 +106,7 @@ test('Remark Image: `publicDir` with URL', async () => {
|
||||
);
|
||||
const result = await remark()
|
||||
.use(remarkImage, {
|
||||
publicDir: 'https://picsum.photos/id',
|
||||
publicDir: 'https://fumadocs.dev',
|
||||
useImport: false,
|
||||
})
|
||||
.use(remarkMdx)
|
||||
|
||||
Reference in New Issue
Block a user