mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
OpenAPI: Support custom shiki options
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-openapi': minor
|
||||
---
|
||||
|
||||
Support custom `shiki` options
|
||||
@@ -26,7 +26,14 @@ export const blog = loader({
|
||||
source: createMDXSource(blogPosts, []),
|
||||
});
|
||||
|
||||
export const openapi = createOpenAPI();
|
||||
export const openapi = createOpenAPI({
|
||||
shikiOptions: {
|
||||
themes: {
|
||||
dark: 'vesper',
|
||||
light: 'vitesse-light',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export type Page = InferPageType<typeof source>;
|
||||
export type Meta = InferMetaType<typeof source>;
|
||||
|
||||
@@ -31,7 +31,7 @@ export async function updateOramaAi(): Promise<void> {
|
||||
const files = await fg([
|
||||
'./content/docs/**/*.mdx',
|
||||
'!*.model.mdx',
|
||||
'!./content/docs/ui/museum/**/*',
|
||||
'!./content/docs/openapi/**/*',
|
||||
]);
|
||||
const records: unknown[] = [];
|
||||
|
||||
|
||||
@@ -4,22 +4,26 @@ import { codeToHast } from 'shiki';
|
||||
import { type Jsx, toJsxRuntime } from 'hast-util-to-jsx-runtime';
|
||||
import { jsx, jsxs } from 'react/jsx-runtime';
|
||||
import { sharedTransformers } from '@/utils/shiki';
|
||||
import type { RenderContext } from '@/types';
|
||||
|
||||
export type CodeBlockProps = HTMLAttributes<HTMLPreElement> & {
|
||||
code: string;
|
||||
lang: string;
|
||||
options?: RenderContext['shikiOptions'];
|
||||
};
|
||||
|
||||
export async function CodeBlock({
|
||||
code,
|
||||
lang,
|
||||
...options
|
||||
options,
|
||||
...rest
|
||||
}: CodeBlockProps): Promise<React.ReactElement> {
|
||||
const html = await codeToHast(code, {
|
||||
lang,
|
||||
defaultColor: false,
|
||||
themes: { light: 'github-light', dark: 'github-dark' },
|
||||
transformers: sharedTransformers,
|
||||
...options,
|
||||
});
|
||||
|
||||
const codeblock = toJsxRuntime(html, {
|
||||
@@ -28,7 +32,7 @@ export async function CodeBlock({
|
||||
jsxs: jsxs as Jsx,
|
||||
Fragment,
|
||||
components: {
|
||||
pre: (props) => <Base.Pre {...props} {...options} />,
|
||||
pre: (props) => <Base.Pre {...props} {...rest} />,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
ObjectCollapsible,
|
||||
APIPlayground,
|
||||
} from '@/ui';
|
||||
import type { RenderContext } from '@/types';
|
||||
|
||||
export interface ResponsesProps {
|
||||
items: string[];
|
||||
@@ -87,34 +88,48 @@ export type {
|
||||
ReferenceSchema,
|
||||
} from '@/render/playground';
|
||||
|
||||
export const defaultRenderer: Renderer = {
|
||||
Root,
|
||||
API,
|
||||
APIInfo,
|
||||
APIExample,
|
||||
Responses: Tabs,
|
||||
Response: Tab,
|
||||
ResponseTypes: (props) => (
|
||||
<Accordions
|
||||
type="single"
|
||||
className="!-m-4 border-none pt-2"
|
||||
defaultValue="Response"
|
||||
>
|
||||
{props.children}
|
||||
</Accordions>
|
||||
),
|
||||
ResponseType: (props) => (
|
||||
<Accordion title={props.label}>
|
||||
<CodeBlock code={props.code} lang={props.lang} />
|
||||
</Accordion>
|
||||
),
|
||||
Property,
|
||||
ObjectCollapsible,
|
||||
Requests: (props) => <Tabs groupId="fumadocs_openapi_requests" {...props} />,
|
||||
Request: (props) => (
|
||||
<Tab value={props.name}>
|
||||
<CodeBlock lang={props.language} code={props.code} />
|
||||
</Tab>
|
||||
),
|
||||
APIPlayground,
|
||||
};
|
||||
export function createRenders(
|
||||
shikiOptions: RenderContext['shikiOptions'],
|
||||
): Renderer {
|
||||
return {
|
||||
Root: (props) => (
|
||||
<Root shikiOptions={shikiOptions} {...props}>
|
||||
{props.children}
|
||||
</Root>
|
||||
),
|
||||
API,
|
||||
APIInfo,
|
||||
APIExample,
|
||||
Responses: Tabs,
|
||||
Response: Tab,
|
||||
ResponseTypes: (props) => (
|
||||
<Accordions
|
||||
type="single"
|
||||
className="!-m-4 border-none pt-2"
|
||||
defaultValue="Response"
|
||||
>
|
||||
{props.children}
|
||||
</Accordions>
|
||||
),
|
||||
ResponseType: (props) => (
|
||||
<Accordion title={props.label}>
|
||||
<CodeBlock code={props.code} lang={props.lang} options={shikiOptions} />
|
||||
</Accordion>
|
||||
),
|
||||
Property,
|
||||
ObjectCollapsible,
|
||||
Requests: (props) => (
|
||||
<Tabs groupId="fumadocs_openapi_requests" {...props} />
|
||||
),
|
||||
Request: (props) => (
|
||||
<Tab value={props.name}>
|
||||
<CodeBlock
|
||||
lang={props.language}
|
||||
code={props.code}
|
||||
options={shikiOptions}
|
||||
/>
|
||||
</Tab>
|
||||
),
|
||||
APIPlayground,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ import Parser from '@apidevtools/json-schema-ref-parser';
|
||||
import { Operation } from '@/render/operation';
|
||||
import type { RenderContext } from '@/types';
|
||||
import { createMethod } from '@/schema/method';
|
||||
import { defaultRenderer, type Renderer } from '@/render/renderer';
|
||||
import { createRenders, type Renderer } from '@/render/renderer';
|
||||
|
||||
export interface ApiPageProps
|
||||
extends Pick<
|
||||
RenderContext,
|
||||
'generateCodeSamples' | 'generateTypeScriptSchema'
|
||||
'generateCodeSamples' | 'generateTypeScriptSchema' | 'shikiOptions'
|
||||
> {
|
||||
document: string | OpenAPI.Document;
|
||||
|
||||
@@ -77,9 +77,10 @@ function getContext(
|
||||
return {
|
||||
document,
|
||||
renderer: {
|
||||
...defaultRenderer,
|
||||
...createRenders(options.shikiOptions),
|
||||
...options.renderer,
|
||||
},
|
||||
shikiOptions: options.shikiOptions,
|
||||
generateTypeScriptSchema: options.generateTypeScriptSchema,
|
||||
generateCodeSamples: options.generateCodeSamples,
|
||||
baseUrl: document.servers?.[0].url ?? 'https://example.com',
|
||||
|
||||
@@ -3,6 +3,11 @@ import type { default as Slugger } from 'github-slugger';
|
||||
import { type Renderer } from '@/render/renderer';
|
||||
import type { EndpointSample } from '@/schema/sample';
|
||||
import type { CodeSample } from '@/render/operation';
|
||||
import type {
|
||||
BuiltinTheme,
|
||||
CodeOptionsThemes,
|
||||
CodeToHastOptionsCommon,
|
||||
} from 'shiki';
|
||||
|
||||
export interface RouteInformation {
|
||||
path: string;
|
||||
@@ -40,4 +45,7 @@ export interface RenderContext {
|
||||
* Generate code samples for endpoint.
|
||||
*/
|
||||
generateCodeSamples?: (endpoint: EndpointSample) => Awaitable<CodeSample[]>;
|
||||
|
||||
shikiOptions?: Omit<CodeToHastOptionsCommon, 'lang'> &
|
||||
CodeOptionsThemes<BuiltinTheme>;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { cn, useCopyButton, buttonVariants } from 'fumadocs-ui/components/api';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { ApiProvider, useApiContext } from '@/ui/contexts/api';
|
||||
import { type RootProps } from '@/render/renderer';
|
||||
import { RenderContext } from '@/types';
|
||||
|
||||
export const APIPlayground = dynamic(() =>
|
||||
import('./playground').then((mod) => mod.APIPlayground),
|
||||
@@ -18,8 +19,11 @@ export function Root({
|
||||
children,
|
||||
baseUrl,
|
||||
className,
|
||||
shikiOptions,
|
||||
...props
|
||||
}: RootProps & HTMLAttributes<HTMLDivElement>): ReactNode {
|
||||
}: RootProps & {
|
||||
shikiOptions: RenderContext['shikiOptions'];
|
||||
} & HTMLAttributes<HTMLDivElement>): ReactNode {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -28,7 +32,9 @@ export function Root({
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ApiProvider defaultBaseUrl={baseUrl}>{children}</ApiProvider>
|
||||
<ApiProvider shikiOptions={shikiOptions} defaultBaseUrl={baseUrl}>
|
||||
{children}
|
||||
</ApiProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,37 +1,39 @@
|
||||
import { type HTMLAttributes, useLayoutEffect, useState } from 'react';
|
||||
import { Fragment, type ReactNode, useEffect, useState } from 'react';
|
||||
import * as Base from 'fumadocs-ui/components/codeblock';
|
||||
import { useApiContext } from '@/ui/contexts/api';
|
||||
import { sharedTransformers } from '@/utils/shiki';
|
||||
import { toJsxRuntime } from 'hast-util-to-jsx-runtime';
|
||||
import { jsx, jsxs } from 'react/jsx-runtime';
|
||||
|
||||
export type CodeBlockProps = HTMLAttributes<HTMLPreElement> & {
|
||||
export type CodeBlockProps = {
|
||||
code: string;
|
||||
lang?: string;
|
||||
};
|
||||
|
||||
export function CodeBlock({
|
||||
code,
|
||||
lang = 'json',
|
||||
...props
|
||||
}: CodeBlockProps): React.ReactElement {
|
||||
const { highlighter } = useApiContext();
|
||||
const [html, setHtml] = useState('');
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!highlighter) return;
|
||||
|
||||
const themedHtml = highlighter.codeToHtml(code, {
|
||||
lang,
|
||||
defaultColor: false,
|
||||
themes: { light: 'github-light', dark: 'github-dark' },
|
||||
transformers: sharedTransformers,
|
||||
});
|
||||
|
||||
setHtml(themedHtml);
|
||||
}, [code, lang, highlighter]);
|
||||
|
||||
return (
|
||||
<Base.CodeBlock className="my-0">
|
||||
<Base.Pre {...props} dangerouslySetInnerHTML={{ __html: html }} />
|
||||
</Base.CodeBlock>
|
||||
export function CodeBlock({ code, lang = 'json' }: CodeBlockProps): ReactNode {
|
||||
const { highlight } = useApiContext();
|
||||
const [rendered, setRendered] = useState<ReactNode>(
|
||||
<Base.Pre className="max-h-[288px]">{code}</Base.Pre>,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
void highlight(lang, code).then((res) => {
|
||||
const output = toJsxRuntime(res, {
|
||||
jsx,
|
||||
jsxs,
|
||||
development: false,
|
||||
Fragment,
|
||||
components: {
|
||||
pre: (props) => (
|
||||
<Base.Pre className="max-h-[288px]" {...props}>
|
||||
{props.children}
|
||||
</Base.Pre>
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
setRendered(output);
|
||||
});
|
||||
}, [code, highlight, lang]);
|
||||
|
||||
return <Base.CodeBlock className="my-0">{rendered}</Base.CodeBlock>;
|
||||
}
|
||||
|
||||
@@ -1,67 +1,79 @@
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import type { HighlighterCore } from 'shiki/core';
|
||||
import {
|
||||
createContext,
|
||||
type ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useOnChange } from 'fumadocs-core/utils/use-on-change';
|
||||
import type { RenderContext } from '@/types';
|
||||
import { sharedTransformers } from '@/utils/shiki';
|
||||
|
||||
export interface ApiProviderProps {
|
||||
/**
|
||||
* Base URL for API requests
|
||||
*/
|
||||
defaultBaseUrl?: string;
|
||||
shikiOptions: RenderContext['shikiOptions'];
|
||||
|
||||
children?: React.ReactNode;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
interface ApiContextType {
|
||||
baseUrl?: string;
|
||||
setBaseUrl: (value: string) => void;
|
||||
highlighter: HighlighterCore | null;
|
||||
highlight: (
|
||||
lang: string,
|
||||
code: string,
|
||||
) => ReturnType<(typeof import('shiki/bundle/web'))['codeToHast']>;
|
||||
}
|
||||
|
||||
const ApiContext = createContext<ApiContextType>({
|
||||
baseUrl: undefined,
|
||||
setBaseUrl: () => undefined,
|
||||
highlighter: null,
|
||||
});
|
||||
const ApiContext = createContext<ApiContextType | undefined>(undefined);
|
||||
|
||||
export function useApiContext(): ApiContextType {
|
||||
return useContext(ApiContext);
|
||||
const ctx = useContext(ApiContext);
|
||||
if (!ctx) throw new Error('Component must be used under <ApiProvider />');
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
async function initHighlighter(): Promise<HighlighterCore> {
|
||||
const { getSingletonHighlighter } = await import('shiki/bundle/web');
|
||||
|
||||
return getSingletonHighlighter({
|
||||
themes: ['github-light', 'github-dark'],
|
||||
langs: ['json'],
|
||||
});
|
||||
}
|
||||
|
||||
let highlighterInstance: HighlighterCore | undefined;
|
||||
|
||||
export function ApiProvider({
|
||||
defaultBaseUrl,
|
||||
shikiOptions,
|
||||
children,
|
||||
}: ApiProviderProps): React.ReactElement {
|
||||
const [highlighter, setHighlighter] = useState<HighlighterCore | null>(null);
|
||||
const [baseUrl, setBaseUrl] = useState(defaultBaseUrl);
|
||||
|
||||
useEffect(() => {
|
||||
setBaseUrl((prev) => localStorage.getItem('apiBaseUrl') ?? prev);
|
||||
if (highlighterInstance) {
|
||||
setHighlighter(highlighterInstance);
|
||||
} else {
|
||||
void initHighlighter().then((res) => {
|
||||
highlighterInstance = res;
|
||||
setHighlighter(res);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
useOnChange(baseUrl, () => {
|
||||
if (baseUrl) localStorage.setItem('apiBaseUrl', baseUrl);
|
||||
}, [baseUrl]);
|
||||
});
|
||||
|
||||
return (
|
||||
<ApiContext.Provider value={{ baseUrl, setBaseUrl, highlighter }}>
|
||||
<ApiContext.Provider
|
||||
value={useMemo(
|
||||
() => ({
|
||||
baseUrl,
|
||||
setBaseUrl,
|
||||
highlight: async (lang, code) => {
|
||||
const { codeToHast } = await import('shiki/bundle/web');
|
||||
|
||||
return codeToHast(code, {
|
||||
lang,
|
||||
themes: { light: 'github-light', dark: 'github-dark' },
|
||||
transformers: sharedTransformers,
|
||||
defaultColor: false,
|
||||
...shikiOptions,
|
||||
});
|
||||
},
|
||||
}),
|
||||
[baseUrl, shikiOptions],
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</ApiContext.Provider>
|
||||
);
|
||||
|
||||
@@ -330,10 +330,7 @@ function ResultDisplay({
|
||||
</div>
|
||||
<p className="text-sm text-fd-muted-foreground">{data.status}</p>
|
||||
{data.data ? (
|
||||
<CodeBlock
|
||||
code={JSON.stringify(data.data, null, 2)}
|
||||
className="max-h-[288px]"
|
||||
/>
|
||||
<CodeBlock code={JSON.stringify(data.data, null, 2)} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -93,7 +93,7 @@ export function DocsPage({
|
||||
<AnchorProvider toc={toc} single={tocOptions.single}>
|
||||
<div
|
||||
id="nd-page"
|
||||
className="h-fit w-full min-w-0 max-w-[var(--fd-page-width)] md:transition-[max-width]"
|
||||
className="flex w-full min-w-0 max-w-[var(--fd-page-width)] flex-col md:transition-[max-width]"
|
||||
style={
|
||||
{
|
||||
'--fd-page-width':
|
||||
@@ -119,7 +119,7 @@ export function DocsPage({
|
||||
)}
|
||||
<article
|
||||
className={cn(
|
||||
'mx-auto flex size-full flex-col gap-6 px-4 pt-10 md:px-6 md:pt-12',
|
||||
'mx-auto flex w-full flex-1 flex-col gap-6 px-4 pt-10 md:px-6 md:pt-12',
|
||||
tocOptions.enabled ? 'max-w-[860px]' : 'max-w-[1120px]',
|
||||
)}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user