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 { OpenAPIOperation } from '@gitbook/react-openapi';
import React from 'react'; import React from 'react';
import { LoadingPane } from '@/components/primitives';
import { fetchOpenAPIBlock } from '@/lib/openapi/fetch'; import { fetchOpenAPIBlock } from '@/lib/openapi/fetch';
import { tcls } from '@/lib/tailwind'; import { tcls } from '@/lib/tailwind';
import { BlockProps } from '../Block'; import { BlockProps } from '../Block';
import { PlainCodeBlock } from '../CodeBlock'; import { PlainCodeBlock } from '../CodeBlock';
import { Heading } from '../Heading';
import './style.css'; import './style.css';
import './scalar.css'; import './scalar.css';
@@ -54,6 +54,31 @@ async function OpenAPIBody(props: BlockProps<DocumentBlockOpenAPI>) {
plus: <Icon icon="plus" />, plus: <Icon icon="plus" />,
}, },
CodeBlock: PlainCodeBlock, 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', defaultInteractiveOpened: context.mode === 'print',
id: block.meta?.id, id: block.meta?.id,
blockKey: block.key, 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 */ /* Layout Components */
.openapi-operation { .openapi-operation {
content-visibility: auto;
contain-intrinsic-height: 600px;
@apply flex-1 flex flex-col gap-4 mb-14; @apply flex-1 flex flex-col gap-4 mb-14;
} }
@@ -18,16 +16,8 @@
@apply flex flex-col items-start justify-start gap-2; @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 { .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 { .openapi-deprecated-sunset-date {
@@ -15,31 +15,29 @@ import { ClassValue, tcls } from '@/lib/tailwind';
export function Text(props: { text: DocumentText }) { export function Text(props: { text: DocumentText }) {
const { text } = props; const { text } = props;
return ( return text.leaves.map((leaf, index) => {
<> return (
{text.leaves.map((leaf, index) => { <React.Fragment key={index}>
return ( {leaf.marks
<React.Fragment key={index}> // Sort to have code marks at the end, so that they don't interfere with other marks
{leaf.marks .sort((a, b) => (a.type === 'code' ? 1 : 0) - (b.type === 'code' ? 1 : 0))
// Sort to have code marks at the end, so that they don't interfere with other marks .reduce<React.ReactNode>((children, mark, index) => {
.sort( const Mark = MARK_STYLES[mark.type];
(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) { if (!Mark) {
return children; return children;
} }
// @ts-ignore return (
return <Mark mark={mark}>{children}</Mark>; // @ts-ignore
}, leaf.text)} <Mark key="mark" mark={mark}>
</React.Fragment> {children}
); </Mark>
})} );
</> }, leaf.text)}
); </React.Fragment>
);
});
} }
const MARK_STYLES = { const MARK_STYLES = {
@@ -25,14 +25,17 @@ export function OpenAPIOperation(props: {
blockKey: context.blockKey, blockKey: context.blockKey,
}; };
const description = resolveDescription(operation)?.trim(); const description = resolveDescription(operation);
return ( return (
<div className={clsx('openapi-operation', className)}> <div className={clsx('openapi-operation', className)}>
<div className="openapi-summary" id={context.id}> <div className="openapi-summary">
<h2 className="openapi-summary-title" data-deprecated={operation.deprecated}> {operation.summary
{operation.summary} ? context.renderHeading({
</h2> deprecated: operation.deprecated ?? false,
title: operation.summary,
})
: null}
{operation.deprecated && <div className="openapi-deprecated">Deprecated</div>} {operation.deprecated && <div className="openapi-deprecated">Deprecated</div>}
</div> </div>
<div className="openapi-columns"> <div className="openapi-columns">
+1
View File
@@ -6,6 +6,7 @@ import type {
export interface OpenAPIContextProps extends OpenAPIClientContext { export interface OpenAPIContextProps extends OpenAPIClientContext {
CodeBlock: React.ComponentType<{ code: string; syntax: string }>; CodeBlock: React.ComponentType<{ code: string; syntax: string }>;
renderHeading: (props: { deprecated: boolean; title: string }) => React.ReactNode;
/** Spec url for the Scalar Api Client */ /** Spec url for the Scalar Api Client */
specUrl: string; specUrl: string;
+2 -2
View File
@@ -16,9 +16,9 @@ export function createStateKey(key: string, scope?: string) {
export function resolveDescription(object: AnyObject) { export function resolveDescription(object: AnyObject) {
return 'x-gitbook-description-html' in object && return 'x-gitbook-description-html' in object &&
typeof object['x-gitbook-description-html'] === 'string' typeof object['x-gitbook-description-html'] === 'string'
? object['x-gitbook-description-html'] ? object['x-gitbook-description-html'].trim()
: typeof object.description === 'string' : typeof object.description === 'string'
? object.description ? object.description.trim()
: undefined; : undefined;
} }