mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
Core: Improve remarkSteps() integration & support tag usage
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-core': patch
|
||||
---
|
||||
|
||||
Improve `remarkSteps()` integration & support tag usage
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Processor, Transformer } from 'unified';
|
||||
import type { Root, Element } from 'hast';
|
||||
import { toEstree } from 'hast-util-to-estree';
|
||||
import type { JSXElement } from 'estree-jsx';
|
||||
import type { JSXElement, ObjectExpression } from 'estree-jsx';
|
||||
import { visit } from 'unist-util-visit';
|
||||
import { toMdxExportRaw } from './mdast-utils';
|
||||
import { handleTag, toMdxExportRaw } from './utils';
|
||||
|
||||
export interface RehypeTocOptions {
|
||||
/**
|
||||
@@ -45,6 +45,7 @@ export interface RehypeTOCItemType {
|
||||
title: Element;
|
||||
url: string;
|
||||
depth: number;
|
||||
_step?: number;
|
||||
}
|
||||
|
||||
const TocOnlyTag = '[toc]';
|
||||
@@ -75,14 +76,17 @@ export function rehypeToc(
|
||||
|
||||
const last = element.children[element.children.length - 1];
|
||||
if (last.type === 'text') {
|
||||
if (last.value.endsWith(NoTocTag)) {
|
||||
last.value = last.value.substring(0, last.value.length - NoTocTag.length).trimEnd();
|
||||
const noToc = handleTag(last.value, NoTocTag);
|
||||
|
||||
if (noToc !== false) {
|
||||
last.value = noToc;
|
||||
return 'skip';
|
||||
}
|
||||
|
||||
if (last.value.endsWith(TocOnlyTag)) {
|
||||
const tocOnly = handleTag(last.value, TocOnlyTag);
|
||||
if (tocOnly !== false) {
|
||||
isTocOnly = true;
|
||||
last.value = last.value.substring(0, last.value.length - TocOnlyTag.length).trimEnd();
|
||||
last.value = tocOnly;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +94,10 @@ export function rehypeToc(
|
||||
title: element,
|
||||
depth: Number(element.tagName[1]),
|
||||
url: `#${id}`,
|
||||
_step:
|
||||
typeof element.properties['data-fd-step'] === 'number'
|
||||
? element.properties['data-fd-step']
|
||||
: undefined,
|
||||
});
|
||||
|
||||
if (isTocOnly && parent && typeof idx === 'number') {
|
||||
@@ -104,6 +112,7 @@ export function rehypeToc(
|
||||
title: JSXElement;
|
||||
url: string;
|
||||
depth: number;
|
||||
_step?: number;
|
||||
}[] = [];
|
||||
for (const item of items) {
|
||||
const root = toEstree(item.title, {
|
||||
@@ -122,58 +131,80 @@ export function rehypeToc(
|
||||
tree.children.push(
|
||||
toMdxExportRaw(exportToc.name, {
|
||||
type: 'ArrayExpression',
|
||||
elements: esmItems.map((item) => ({
|
||||
type: 'ObjectExpression',
|
||||
properties: [
|
||||
{
|
||||
elements: esmItems.map((item) => {
|
||||
const obj: ObjectExpression = {
|
||||
type: 'ObjectExpression',
|
||||
properties: [
|
||||
{
|
||||
type: 'Property',
|
||||
method: false,
|
||||
shorthand: false,
|
||||
computed: false,
|
||||
key: {
|
||||
type: 'Identifier',
|
||||
name: 'depth',
|
||||
},
|
||||
value: {
|
||||
type: 'Literal',
|
||||
value: item.depth,
|
||||
},
|
||||
kind: 'init',
|
||||
},
|
||||
{
|
||||
type: 'Property',
|
||||
method: false,
|
||||
shorthand: false,
|
||||
computed: false,
|
||||
key: {
|
||||
type: 'Identifier',
|
||||
name: 'url',
|
||||
},
|
||||
value: {
|
||||
type: 'Literal',
|
||||
value: item.url,
|
||||
},
|
||||
kind: 'init',
|
||||
},
|
||||
{
|
||||
type: 'Property',
|
||||
method: false,
|
||||
shorthand: false,
|
||||
computed: false,
|
||||
key: {
|
||||
type: 'Identifier',
|
||||
name: 'title',
|
||||
},
|
||||
value: {
|
||||
type: 'JSXFragment',
|
||||
openingFragment: { type: 'JSXOpeningFragment' },
|
||||
closingFragment: { type: 'JSXClosingFragment' },
|
||||
children: item.title.children,
|
||||
},
|
||||
kind: 'init',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (typeof item._step === 'number') {
|
||||
obj.properties.push({
|
||||
type: 'Property',
|
||||
method: false,
|
||||
shorthand: false,
|
||||
computed: false,
|
||||
key: {
|
||||
type: 'Identifier',
|
||||
name: 'depth',
|
||||
name: '_step',
|
||||
},
|
||||
value: {
|
||||
type: 'Literal',
|
||||
value: item.depth,
|
||||
value: item._step,
|
||||
},
|
||||
kind: 'init',
|
||||
},
|
||||
{
|
||||
type: 'Property',
|
||||
method: false,
|
||||
shorthand: false,
|
||||
computed: false,
|
||||
key: {
|
||||
type: 'Identifier',
|
||||
name: 'url',
|
||||
},
|
||||
value: {
|
||||
type: 'Literal',
|
||||
value: item.url,
|
||||
},
|
||||
kind: 'init',
|
||||
},
|
||||
{
|
||||
type: 'Property',
|
||||
method: false,
|
||||
shorthand: false,
|
||||
computed: false,
|
||||
key: {
|
||||
type: 'Identifier',
|
||||
name: 'title',
|
||||
},
|
||||
value: {
|
||||
type: 'JSXFragment',
|
||||
openingFragment: { type: 'JSXOpeningFragment' },
|
||||
closingFragment: { type: 'JSXClosingFragment' },
|
||||
children: item.title.children,
|
||||
},
|
||||
kind: 'init',
|
||||
},
|
||||
],
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
return obj;
|
||||
}),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type Transformer } from 'unified';
|
||||
import { visit } from 'unist-util-visit';
|
||||
import { type Root, type RootContent } from 'mdast';
|
||||
import { flattenNode } from '@/mdx-plugins/mdast-utils';
|
||||
import { flattenNode } from '@/mdx-plugins/utils';
|
||||
|
||||
export interface RemarkAdmonitionOptions {
|
||||
tag?: string;
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { BlockContent, Root, RootContent } from 'mdast';
|
||||
import type { MdxJsxFlowElement } from 'mdast-util-mdx';
|
||||
import { visit } from 'unist-util-visit';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { flattenNode } from './mdast-utils';
|
||||
import { flattenNode } from './utils';
|
||||
|
||||
export interface RemarkFeedbackBlockOptions {
|
||||
/**
|
||||
|
||||
@@ -3,13 +3,13 @@ import type { Heading, Root } from 'mdast';
|
||||
import type { Transformer } from 'unified';
|
||||
import { visit } from 'unist-util-visit';
|
||||
import type { TOCItemType } from '@/toc';
|
||||
import { flattenNode } from '@/mdx-plugins/mdast-utils';
|
||||
import { flattenNode } from '@/mdx-plugins/utils';
|
||||
|
||||
declare module 'mdast' {
|
||||
export interface HeadingData extends Data {
|
||||
hProperties?: {
|
||||
id?: string;
|
||||
};
|
||||
} & Record<string, unknown>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Processor, Transformer } from 'unified';
|
||||
import { toMdxExport } from './mdast-utils';
|
||||
import { toMdxExport } from './utils';
|
||||
import type { Heading, Parents, Root } from 'mdast';
|
||||
import { defaultStringifier, type StringifyOptions } from './stringifier';
|
||||
import type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx';
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Transformer } from 'unified';
|
||||
import type { BlockContent, Heading, Root, RootContent } from 'mdast';
|
||||
import { visit } from 'unist-util-visit';
|
||||
import type { MdxJsxFlowElement } from 'mdast-util-mdx';
|
||||
import { handleTag } from './utils';
|
||||
|
||||
export interface RemarkStepsOptions {
|
||||
/**
|
||||
@@ -20,6 +21,7 @@ export interface RemarkStepsOptions {
|
||||
}
|
||||
|
||||
const StepRegex = /^(\d+)\.\s(.+)$/;
|
||||
const StepTag = '[step]';
|
||||
|
||||
/**
|
||||
* Convert headings in the format of `1. Hello World` into steps.
|
||||
@@ -68,6 +70,28 @@ export function remarkSteps({
|
||||
};
|
||||
}
|
||||
|
||||
function handleHeadingStep(node: Heading): boolean {
|
||||
const head = node.children[0];
|
||||
if (head && head.type === 'text') {
|
||||
const match = StepRegex.exec(head.value);
|
||||
if (match) {
|
||||
head.value = match[2];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const tail = node.children[node.children.length - 1];
|
||||
if (tail && tail.type === 'text') {
|
||||
const step = handleTag(tail.value, StepTag);
|
||||
if (step !== false) {
|
||||
tail.value = step;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return (tree) => {
|
||||
visit(tree, (parent) => {
|
||||
if (!('children' in parent) || parent.type === 'heading') return;
|
||||
@@ -75,15 +99,16 @@ export function remarkSteps({
|
||||
|
||||
let startIdx = -1;
|
||||
let i = 0;
|
||||
let currentStep = 1;
|
||||
|
||||
const onEnd = () => {
|
||||
if (startIdx === -1) return;
|
||||
// range: start index to i - 1
|
||||
const item = {};
|
||||
const nodes = parent.children.splice(startIdx, i - startIdx, item as RootContent);
|
||||
Object.assign(item, convertToSteps(nodes));
|
||||
i = startIdx + 1;
|
||||
startIdx = -1;
|
||||
currentStep = 1;
|
||||
};
|
||||
|
||||
for (; i < parent.children.length; i++) {
|
||||
@@ -97,19 +122,14 @@ export function remarkSteps({
|
||||
else if (node.depth < startDepth) onEnd();
|
||||
}
|
||||
|
||||
const head = node.children.filter((c) => c.type === 'text').at(0);
|
||||
if (!head) {
|
||||
if (!handleHeadingStep(node)) {
|
||||
onEnd();
|
||||
continue;
|
||||
}
|
||||
|
||||
const match = StepRegex.exec(head.value);
|
||||
if (!match) {
|
||||
onEnd();
|
||||
continue;
|
||||
}
|
||||
|
||||
head.value = match[2];
|
||||
node.data ??= {};
|
||||
node.data.hProperties ??= {};
|
||||
node.data.hProperties['data-fd-step'] = currentStep++;
|
||||
if (startIdx === -1) startIdx = i;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 { toMdxExport } from './utils';
|
||||
import {
|
||||
type MdxJsxAttribute,
|
||||
type MdxJsxExpressionAttribute,
|
||||
|
||||
@@ -48,3 +48,12 @@ export function toMdxExportRaw(name: string, expression: Expression): MdxjsEsm {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function handleTag(value: string, tag: string): string | false {
|
||||
const idx = value.indexOf(tag);
|
||||
if (idx !== -1) {
|
||||
return value.slice(0, idx).trimEnd() + value.slice(idx + tag.length);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -18,6 +18,8 @@ export interface TOCItemType {
|
||||
title: ReactNode;
|
||||
url: string;
|
||||
depth: number;
|
||||
/** [remark-steps] the step number */
|
||||
_step?: number;
|
||||
}
|
||||
|
||||
export type TableOfContents = TOCItemType[];
|
||||
@@ -96,11 +98,11 @@ export function TOCItem({ ref, onActiveChange = () => null, ...props }: TOCItemP
|
||||
const [active, setActive] = useState(
|
||||
() => id && items.some((item) => item.active && item.id === id),
|
||||
);
|
||||
const initialShouldScroll = useMemo(() => {
|
||||
const initialShouldScrollRef = useRef<boolean>(null);
|
||||
if (initialShouldScrollRef.current === null) {
|
||||
const lastActive = items.findLast((item) => item.active);
|
||||
return lastActive && lastActive.id === id;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
initialShouldScrollRef.current = lastActive !== undefined && lastActive.id === id;
|
||||
}
|
||||
|
||||
useOnChange(items, () => {
|
||||
if (id === null) return;
|
||||
@@ -129,7 +131,7 @@ export function TOCItem({ ref, onActiveChange = () => null, ...props }: TOCItemP
|
||||
const anchor = anchorRef.current;
|
||||
const container = containerRef?.current;
|
||||
|
||||
if (initialShouldScroll && container && anchor) {
|
||||
if (initialShouldScrollRef.current && container && anchor) {
|
||||
scrollIntoView(anchor, {
|
||||
behavior: 'instant',
|
||||
block: 'center',
|
||||
@@ -138,7 +140,7 @@ export function TOCItem({ ref, onActiveChange = () => null, ...props }: TOCItemP
|
||||
boundary: container,
|
||||
});
|
||||
}
|
||||
}, [containerRef, initialShouldScroll]);
|
||||
}, [containerRef]);
|
||||
|
||||
return <a ref={mergeRefs(anchorRef, ref)} data-active={active} {...props} />;
|
||||
}
|
||||
|
||||
+1
-1
@@ -24,6 +24,6 @@ content
|
||||
|
||||
content
|
||||
|
||||
# 2. Big: Second
|
||||
# Big: Second [step]
|
||||
|
||||
content
|
||||
|
||||
Reference in New Issue
Block a user