Add support for anchor link in OpenAPI blocks (#2858)

This commit is contained in:
Greg Bergé
2025-02-19 23:38:20 +01:00
committed by GitHub
parent a3f1fea27b
commit 82cd9f2979
7 changed files with 65 additions and 71 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@gitbook/react-openapi': patch
'gitbook': patch
---
Add support for anchor links in OpenAPI blocks
@@ -3,12 +3,12 @@ import { Icon } from '@gitbook/icons';
import { OpenAPIOperation } from '@gitbook/react-openapi';
import React from 'react';
import { LoadingPane } from '@/components/primitives';
import { fetchOpenAPIBlock } from '@/lib/openapi/fetch';
import { tcls } from '@/lib/tailwind';
import { BlockProps } from '../Block';
import { PlainCodeBlock } from '../CodeBlock';
import { Heading } from '../Heading';
import './style.css';
import './scalar.css';
@@ -54,6 +54,31 @@ async function OpenAPIBody(props: BlockProps<DocumentBlockOpenAPI>) {
plus: <Icon icon="plus" />,
},
CodeBlock: PlainCodeBlock,
renderHeading: (headingProps) => (
<Heading
document={props.document}
ancestorBlocks={props.ancestorBlocks}
isEstimatedOffscreen={props.isEstimatedOffscreen}
context={props.context}
style={headingProps.deprecated ? 'line-through' : undefined}
block={{
object: 'block',
key: `${block.key}-heading`,
meta: block.meta,
data: {},
type: 'heading-2',
nodes: [
{
key: `${block.key}-heading-text`,
object: 'text',
leaves: [
{ text: headingProps.title, object: 'leaf', marks: [] },
],
},
],
}}
/>
),
defaultInteractiveOpened: context.mode === 'print',
id: block.meta?.id,
blockKey: block.key,
@@ -62,32 +87,3 @@ async function OpenAPIBody(props: BlockProps<DocumentBlockOpenAPI>) {
/>
);
}
function OpenAPIFallback() {
return (
<div
role="status"
aria-busy
className={'openapi-block ' + tcls('flex', 'flex-1', 'flex-col', 'gap-3')}
>
<LoadingPane
tile={12}
style={['rounded-md', 'h-[47px]', '[max-width:calc(48rem-1px)]']}
/>
<LoadingPane
tile={12}
style={['rounded-md', 'h-[35px]', '[max-width:calc(48rem-1px)]']}
/>
<div className={tcls('flex', 'gap-[25px]')}>
<div className={tcls('flex', 'flex-1', 'flex-col', 'gap-3')}>
<LoadingPane tile={24} style={['rounded-md', 'aspect-[2.5/1]', 'w-full']} />
<LoadingPane tile={24} style={['rounded-md', 'aspect-[2.5/1]', 'w-full']} />
</div>
<div className={tcls('flex', 'flex-1', 'flex-col', 'gap-3')}>
<LoadingPane tile={24} style={['rounded-md', 'aspect-[4/1]', 'w-full']} />
<LoadingPane tile={24} style={['rounded-md', 'aspect-[4/1]', 'w-full']} />
</div>
</div>
</div>
);
}
@@ -1,7 +1,5 @@
/* Layout Components */
.openapi-operation {
content-visibility: auto;
contain-intrinsic-height: 600px;
@apply flex-1 flex flex-col gap-4 mb-14;
}
@@ -18,16 +16,8 @@
@apply flex flex-col items-start justify-start gap-2;
}
.openapi-summary-title {
@apply font-semibold text-xl;
}
.openapi-summary-title[data-deprecated='true'] {
@apply line-through;
}
.openapi-deprecated {
@apply py-0.5 px-1.5 min-w-[1.625rem] font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint bg-tint rounded-full text-sm leading-[calc(max(1.20em,1.25rem))] before:!content-none after:!content-none;
@apply py-0.5 px-1.5 min-w-[1.625rem] font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint bg-tint rounded text-sm leading-[calc(max(1.20em,1.25rem))] before:!content-none after:!content-none;
}
.openapi-deprecated-sunset-date {
@@ -15,31 +15,29 @@ import { ClassValue, tcls } from '@/lib/tailwind';
export function Text(props: { text: DocumentText }) {
const { text } = props;
return (
<>
{text.leaves.map((leaf, index) => {
return (
<React.Fragment key={index}>
{leaf.marks
// Sort to have code marks at the end, so that they don't interfere with other marks
.sort(
(a, b) => (a.type === 'code' ? 1 : 0) - (b.type === 'code' ? 1 : 0),
)
.reduce<React.ReactNode>((children, mark, index) => {
const Mark = MARK_STYLES[mark.type];
return text.leaves.map((leaf, index) => {
return (
<React.Fragment key={index}>
{leaf.marks
// Sort to have code marks at the end, so that they don't interfere with other marks
.sort((a, b) => (a.type === 'code' ? 1 : 0) - (b.type === 'code' ? 1 : 0))
.reduce<React.ReactNode>((children, mark, index) => {
const Mark = MARK_STYLES[mark.type];
if (!Mark) {
return children;
}
if (!Mark) {
return children;
}
// @ts-ignore
return <Mark mark={mark}>{children}</Mark>;
}, leaf.text)}
</React.Fragment>
);
})}
</>
);
return (
// @ts-ignore
<Mark key="mark" mark={mark}>
{children}
</Mark>
);
}, leaf.text)}
</React.Fragment>
);
});
}
const MARK_STYLES = {
@@ -25,14 +25,17 @@ export function OpenAPIOperation(props: {
blockKey: context.blockKey,
};
const description = resolveDescription(operation)?.trim();
const description = resolveDescription(operation);
return (
<div className={clsx('openapi-operation', className)}>
<div className="openapi-summary" id={context.id}>
<h2 className="openapi-summary-title" data-deprecated={operation.deprecated}>
{operation.summary}
</h2>
<div className="openapi-summary">
{operation.summary
? context.renderHeading({
deprecated: operation.deprecated ?? false,
title: operation.summary,
})
: null}
{operation.deprecated && <div className="openapi-deprecated">Deprecated</div>}
</div>
<div className="openapi-columns">
+1
View File
@@ -6,6 +6,7 @@ import type {
export interface OpenAPIContextProps extends OpenAPIClientContext {
CodeBlock: React.ComponentType<{ code: string; syntax: string }>;
renderHeading: (props: { deprecated: boolean; title: string }) => React.ReactNode;
/** Spec url for the Scalar Api Client */
specUrl: string;
+2 -2
View File
@@ -16,9 +16,9 @@ export function createStateKey(key: string, scope?: string) {
export function resolveDescription(object: AnyObject) {
return 'x-gitbook-description-html' in object &&
typeof object['x-gitbook-description-html'] === 'string'
? object['x-gitbook-description-html']
? object['x-gitbook-description-html'].trim()
: typeof object.description === 'string'
? object.description
? object.description.trim()
: undefined;
}