mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
Core: Move fumadocs-core code highlighting utils to fumadocs-core/highlight and fumadocs-core/highlight/client
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'fumadocs-openapi': patch
|
||||
'fumadocs-core': patch
|
||||
'fumadocs-ui': patch
|
||||
---
|
||||
|
||||
Move `fumadocs-core` highlighting utils to `fumadocs-core/highlight` and `fumadocs-core/highlight/client`
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as Base from 'fumadocs-ui/components/codeblock';
|
||||
import { highlight } from 'fumadocs-core/server';
|
||||
import { highlight } from 'fumadocs-core/highlight';
|
||||
|
||||
export interface CodeBlockProps {
|
||||
code: string;
|
||||
|
||||
@@ -75,6 +75,14 @@
|
||||
"./i18n": {
|
||||
"import": "./dist/i18n/index.js",
|
||||
"types": "./dist/i18n/index.d.ts"
|
||||
},
|
||||
"./highlight": {
|
||||
"import": "./dist/highlight/index.js",
|
||||
"types": "./dist/highlight/index.d.ts"
|
||||
},
|
||||
"./highlight/client": {
|
||||
"import": "./dist/highlight/client.js",
|
||||
"types": "./dist/highlight/client.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
'use client';
|
||||
import {
|
||||
type DependencyList,
|
||||
type ReactNode,
|
||||
useId,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
_highlight,
|
||||
_renderHighlight,
|
||||
highlight,
|
||||
type HighlightOptions,
|
||||
} from './shiki';
|
||||
import type { RegexEngine } from 'shiki';
|
||||
import type { Root } from 'hast';
|
||||
|
||||
let jsEngine: Promise<RegexEngine> | undefined;
|
||||
|
||||
function getHighlightOptions(from: HighlightOptions): HighlightOptions {
|
||||
if (from.engine) return from;
|
||||
|
||||
if (!jsEngine) {
|
||||
jsEngine = import('shiki/engine/javascript').then((res) =>
|
||||
res.createJavaScriptRegexEngine(),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...from,
|
||||
engine: jsEngine,
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
_use_shiki?: Map<string, Root>;
|
||||
}
|
||||
}
|
||||
|
||||
interface Task {
|
||||
key: string;
|
||||
aborted: boolean;
|
||||
}
|
||||
|
||||
export function useShiki(
|
||||
code: string,
|
||||
{
|
||||
defaultValue,
|
||||
withPrerenderScript = false,
|
||||
...options
|
||||
}: HighlightOptions & {
|
||||
withPrerenderScript?: boolean;
|
||||
defaultValue?: ReactNode;
|
||||
},
|
||||
deps?: DependencyList,
|
||||
): ReactNode {
|
||||
const scriptKey = useId();
|
||||
const key = useMemo(
|
||||
() => (deps ? JSON.stringify(deps) : `${options.lang}:${code}`),
|
||||
[code, deps, options.lang],
|
||||
);
|
||||
const shikiOptions = getHighlightOptions(options);
|
||||
const currentTask = useRef<Task | undefined>({
|
||||
key,
|
||||
aborted: false,
|
||||
});
|
||||
|
||||
const [rendered, setRendered] = useState<ReactNode>(() => {
|
||||
if (defaultValue) return defaultValue;
|
||||
// @ts-expect-error -- use shiki is typed
|
||||
const hast = globalThis._use_shiki?.get(scriptKey);
|
||||
|
||||
if (hast && withPrerenderScript) {
|
||||
return (
|
||||
<>
|
||||
<PrerenderScript scriptKey={scriptKey} tree={hast} />
|
||||
{_renderHighlight(hast, shikiOptions)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
currentTask.current = undefined;
|
||||
const Pre = (options.components?.pre ?? 'pre') as 'pre';
|
||||
const Code = (options.components?.code ?? 'code') as 'code';
|
||||
return (
|
||||
<Pre>
|
||||
<Code>{code}</Code>
|
||||
</Pre>
|
||||
);
|
||||
});
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
// return promise on server
|
||||
return _highlight(code, shikiOptions).then((tree) => {
|
||||
return (
|
||||
<>
|
||||
{withPrerenderScript && (
|
||||
<PrerenderScript scriptKey={scriptKey} tree={tree} />
|
||||
)}
|
||||
{_renderHighlight(tree, shikiOptions)}
|
||||
</>
|
||||
);
|
||||
}) as ReactNode;
|
||||
}
|
||||
|
||||
// on change
|
||||
if (!currentTask.current || currentTask.current.key !== key) {
|
||||
if (currentTask.current) {
|
||||
currentTask.current.aborted = true;
|
||||
}
|
||||
|
||||
const task: Task = {
|
||||
key,
|
||||
aborted: false,
|
||||
};
|
||||
currentTask.current = task;
|
||||
|
||||
void highlight(code, shikiOptions).then((result) => {
|
||||
if (!task.aborted) setRendered(result);
|
||||
});
|
||||
}
|
||||
|
||||
return rendered;
|
||||
}
|
||||
|
||||
function PrerenderScript({
|
||||
scriptKey,
|
||||
tree,
|
||||
}: {
|
||||
tree: Root;
|
||||
scriptKey: string;
|
||||
}) {
|
||||
return (
|
||||
<script>{`if (typeof globalThis._use_shiki === "undefined") globalThis._use_shiki = new Map()
|
||||
globalThis._use_shiki.set(${JSON.stringify(scriptKey)}, ${JSON.stringify(tree)})`}</script>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
highlight,
|
||||
createStyleTransformer,
|
||||
type HighlightOptions,
|
||||
} from './shiki';
|
||||
@@ -18,7 +18,7 @@ import { createOnigurumaEngine } from 'shiki/engine/oniguruma';
|
||||
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
|
||||
import type { IconOptions, CodeBlockIcon } from './transformer-icon';
|
||||
import { transformerIcon } from './transformer-icon';
|
||||
import { createStyleTransformer, defaultThemes } from '@/utils/shiki';
|
||||
import { createStyleTransformer, defaultThemes } from '@/highlight/shiki';
|
||||
|
||||
interface MetaValue {
|
||||
name: string;
|
||||
|
||||
@@ -4,8 +4,9 @@ export * as PageTree from './page-tree';
|
||||
export * from './git-api';
|
||||
export * from './types';
|
||||
export * from './metadata';
|
||||
export {
|
||||
highlight,
|
||||
createStyleTransformer,
|
||||
type HighlightOptions,
|
||||
} from '../utils/shiki';
|
||||
|
||||
// TODO: Remove on next major
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export * from '../highlight';
|
||||
|
||||
@@ -1,141 +1,5 @@
|
||||
'use client';
|
||||
import {
|
||||
type DependencyList,
|
||||
type ReactNode,
|
||||
use,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
_highlight,
|
||||
_renderHighlight,
|
||||
highlight,
|
||||
type HighlightOptions,
|
||||
} from '@/utils/shiki';
|
||||
import type { RegexEngine } from 'shiki';
|
||||
import type { Root } from 'hast';
|
||||
|
||||
let jsEngine: Promise<RegexEngine> | undefined;
|
||||
|
||||
function getHighlightOptions(from: HighlightOptions): HighlightOptions {
|
||||
if (from.engine) return from;
|
||||
|
||||
if (!jsEngine) {
|
||||
jsEngine = import('shiki/engine/javascript').then((res) =>
|
||||
res.createJavaScriptRegexEngine(),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...from,
|
||||
engine: jsEngine,
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
_use_shiki?: Map<string, Root>;
|
||||
}
|
||||
}
|
||||
|
||||
interface Task {
|
||||
key: string;
|
||||
aborted: boolean;
|
||||
}
|
||||
|
||||
export function useShiki(
|
||||
code: string,
|
||||
{
|
||||
defaultValue,
|
||||
scriptKey,
|
||||
...options
|
||||
}: HighlightOptions & {
|
||||
defaultValue?: ReactNode;
|
||||
scriptKey?: string;
|
||||
},
|
||||
deps?: DependencyList,
|
||||
): ReactNode {
|
||||
const key = useMemo(
|
||||
() => (deps ? JSON.stringify(deps) : `${options.lang}:${code}`),
|
||||
[code, deps, options.lang],
|
||||
);
|
||||
const shikiOptions = getHighlightOptions(options);
|
||||
const currentTask = useRef<Task | undefined>({
|
||||
key,
|
||||
aborted: false,
|
||||
});
|
||||
|
||||
const [rendered, setRendered] = useState<ReactNode>(() => {
|
||||
if (defaultValue) return defaultValue;
|
||||
// @ts-expect-error -- use shiki is typed
|
||||
const hast = globalThis._use_shiki?.get(scriptKey);
|
||||
|
||||
if (hast) {
|
||||
return _renderHighlight(hast, shikiOptions);
|
||||
}
|
||||
|
||||
currentTask.current = undefined;
|
||||
const Pre = (options.components?.pre ?? 'pre') as 'pre';
|
||||
const Code = (options.components?.code ?? 'code') as 'code';
|
||||
return (
|
||||
<Pre>
|
||||
<Code>{code}</Code>
|
||||
</Pre>
|
||||
);
|
||||
});
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
// return promise on server
|
||||
return highlight(code, shikiOptions) as ReactNode;
|
||||
}
|
||||
|
||||
// on change
|
||||
if (!currentTask.current || currentTask.current.key !== key) {
|
||||
if (currentTask.current) {
|
||||
currentTask.current.aborted = true;
|
||||
}
|
||||
|
||||
const task: Task = {
|
||||
key,
|
||||
aborted: false,
|
||||
};
|
||||
currentTask.current = task;
|
||||
|
||||
highlight(code, shikiOptions).then((result) => {
|
||||
if (!task.aborted) setRendered(result);
|
||||
});
|
||||
}
|
||||
|
||||
return rendered;
|
||||
}
|
||||
|
||||
export function PrerenderScript({
|
||||
scriptKey,
|
||||
code,
|
||||
options,
|
||||
}: {
|
||||
scriptKey: string;
|
||||
code: string;
|
||||
options: HighlightOptions;
|
||||
}) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const tree =
|
||||
typeof window === 'undefined'
|
||||
? use(_highlight(code, getHighlightOptions(options)))
|
||||
: // @ts-expect-error -- typed
|
||||
globalThis._use_shiki?.get(scriptKey);
|
||||
|
||||
if (mounted || !tree) return null;
|
||||
|
||||
return (
|
||||
<script>{`if (typeof globalThis._use_shiki === "undefined") globalThis._use_shiki = new Map()
|
||||
globalThis._use_shiki.set(${JSON.stringify(scriptKey)}, ${JSON.stringify(tree)})`}</script>
|
||||
);
|
||||
}
|
||||
// TODO: Remove on next major
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export * from '../highlight/client';
|
||||
|
||||
@@ -23,5 +23,7 @@ export default defineConfig({
|
||||
'src/utils/use-shiki.tsx',
|
||||
'src/utils/use-effect-event.ts',
|
||||
'src/i18n/index.ts',
|
||||
'src/highlight/index.ts',
|
||||
'src/highlight/client.tsx',
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type HTMLAttributes } from 'react';
|
||||
import * as Base from 'fumadocs-ui/components/codeblock';
|
||||
import type { RenderContext } from '@/types';
|
||||
import { highlight } from 'fumadocs-core/server';
|
||||
import { highlight } from 'fumadocs-core/highlight';
|
||||
|
||||
export type CodeBlockProps = HTMLAttributes<HTMLPreElement> & {
|
||||
code: string;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
'use client';
|
||||
import { CodeBlock, Pre } from '@/components/codeblock';
|
||||
import type { HighlightOptions } from 'fumadocs-core/server';
|
||||
import { PrerenderScript, useShiki } from 'fumadocs-core/utils/use-shiki';
|
||||
import { useId } from 'react';
|
||||
import { useShiki } from 'fumadocs-core/highlight/client';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
const components = {
|
||||
@@ -24,26 +23,13 @@ export function DynamicCodeBlock({
|
||||
code: string;
|
||||
options?: Omit<HighlightOptions, 'lang'>;
|
||||
}) {
|
||||
const scriptKey = useId();
|
||||
const shikiOptions = {
|
||||
return useShiki(code, {
|
||||
lang,
|
||||
scriptKey,
|
||||
...options,
|
||||
components: {
|
||||
...components,
|
||||
...options?.components,
|
||||
},
|
||||
};
|
||||
const children = useShiki(code, shikiOptions);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PrerenderScript
|
||||
scriptKey={scriptKey}
|
||||
code={code}
|
||||
options={shikiOptions}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
withPrerenderScript: true,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user