Compare commits

..

23 Commits

Author SHA1 Message Date
Zeno Kapitein 36877d92a6 Format 2025-04-03 13:57:22 +02:00
Zeno Kapitein 23433d1287 Change type 2025-04-03 13:56:16 +02:00
Zeno Kapitein 5d9183e1b4 Format 2025-04-03 13:53:41 +02:00
Zeno Kapitein a1d5326ffa Remove adaptive pane (for now) 2025-04-03 13:52:23 +02:00
Zeno Kapitein cc26fb34aa Remove other AI experiments (for now) 2025-04-03 13:52:23 +02:00
Zeno Kapitein b1abbf603b Use new customisation option 2025-04-03 13:52:23 +02:00
Zeno Kapitein a70e8cd7dc Refactor to split components into Link and LinkTooltip 2025-04-03 13:51:53 +02:00
Zeno Kapitein b38eed850f Improve prompt & layout 2025-04-03 13:51:53 +02:00
Zeno Kapitein f4c50c7e3c Add page paths 2025-04-03 13:51:53 +02:00
Zeno Kapitein 5d308c764e Update AIPageLinkSummary.tsx 2025-04-03 13:51:53 +02:00
Zeno Kapitein 08e780496b Tweak link summaries 2025-04-03 13:51:53 +02:00
Zeno Kapitein e4cd67ebe2 Tweak prompt 2025-04-03 13:51:53 +02:00
Zeno Kapitein 939b160bfa Make link summaries work 2025-04-03 13:51:53 +02:00
Zeno Kapitein 689e78d8e7 Fix typing 2025-04-03 13:51:53 +02:00
Zeno Kapitein 37c1d4b8ea Initial UI for tooltip experiment 2025-04-03 13:51:53 +02:00
Samy Pessé 75855585f5 Start panel 2025-04-03 13:51:53 +02:00
Samy Pessé dcf0f9aac5 New experiment 2025-04-03 13:51:53 +02:00
Samy Pessé 1d7253307a Show list of recommended questions at the bottom 2025-04-03 13:51:53 +02:00
Nolann B. 2b6c593289 Remove stable from x-stability (#3083) 2025-04-03 11:33:39 +02:00
Samy Pessé c0b339e406 Version Packages (#3075)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-02 13:36:04 +02:00
Nolann B. da485f5144 Fix read-only in generateSchemaExample (#3069) 2025-04-02 13:03:51 +02:00
Nolann B. 139a8050a5 Fix OpenAPI enum display (#3077) 2025-04-02 10:15:19 +02:00
Nolann B. da7b369ffd Fix missing headers in OpenAPIResponses (#3074) 2025-04-01 21:57:36 +02:00
36 changed files with 1844 additions and 1620 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@gitbook/openapi-parser': patch
'@gitbook/react-openapi': patch
'gitbook': patch
---
Remove stable from x-stability
+876 -1316
View File
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -12,7 +12,7 @@
"@codemirror/state": "6.4.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"@gitbook/api": "0.106.0"
"@gitbook/api": "0.107.0"
},
"private": true,
"scripts": {
@@ -38,5 +38,8 @@
"patchedDependencies": {
"decode-named-character-reference@1.0.2": "patches/decode-named-character-reference@1.0.2.patch",
"@vercel/next@4.4.2": "patches/@vercel%2Fnext@4.4.2.patch"
},
"dependencies": {
"@radix-ui/react-tooltip": "^1.1.8"
}
}
+11
View File
@@ -1,5 +1,16 @@
# gitbook
## 0.9.2
### Patch Changes
- da7b369: Fix missing headers in OpenAPIResponses
- 139a805: Fix OpenAPI enum display
- Updated dependencies [da7b369]
- Updated dependencies [da485f5]
- Updated dependencies [139a805]
- @gitbook/react-openapi@1.1.9
## 0.9.1
### Patch Changes
+8 -5
View File
@@ -1,6 +1,6 @@
{
"name": "gitbook",
"version": "0.9.1",
"version": "0.9.2",
"private": true,
"scripts": {
"dev": "env-cmd --silent -f ../../.env.local next dev",
@@ -32,9 +32,7 @@
"@sindresorhus/fnv1a": "^3.1.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.16",
"@upstash/redis": "^1.27.1",
"ai": "^4.1.46",
"ajv": "^8.12.0",
"ai": "^4.2.2",
"assert-never": "^1.2.1",
"bun-types": "^1.1.20",
"classnames": "^2.5.1",
@@ -68,7 +66,12 @@
"tailwind-shades": "^1.1.2",
"unified": "^11.0.5",
"url-join": "^5.0.0",
"usehooks-ts": "^3.1.0"
"usehooks-ts": "^3.1.0",
"zod": "^3.24.2",
"zod-to-json-schema": "^3.24.5",
"event-iterator": "^2.0.0",
"partial-json": "^0.1.7",
"zustand": "^5.0.3"
},
"devDependencies": {
"@argos-ci/playwright": "^4.3.0",
@@ -1,7 +1,7 @@
'use client';
import { Button } from '@/components/primitives/Button';
import { t, useLanguage } from '@/intl/client';
import { t, tString, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
export default function ErrorPage(props: {
@@ -35,9 +35,8 @@ export default function ErrorPage(props: {
}}
variant="secondary"
size="small"
>
{t(language, 'unexpected_error_retry')}
</Button>
label={tString(language, 'unexpected_error_retry')}
/>
</div>
</div>
</div>
@@ -0,0 +1,77 @@
'use client';
import { Icon } from '@gitbook/icons';
import { useEffect, useState } from 'react';
import { Loading } from '../primitives';
import { streamLinkPageSummary } from './server-actions/streamLinkPageSummary';
/**
* Summarise a page's content for use in a link preview
*/
export function AIPageLinkSummary(props: {
currentSpaceId: string;
currentPageId: string;
currentPageTitle: string;
targetSpaceId: string;
targetPageId: string;
linkPreview?: string;
linkTitle?: string;
showTrademark: boolean;
}) {
const {
currentSpaceId,
currentPageId,
targetSpaceId,
targetPageId,
linkPreview,
linkTitle,
showTrademark = true,
} = props;
const [highlight, setHighlight] = useState('');
useEffect(() => {
let canceled = false;
setHighlight('');
(async () => {
const stream = await streamLinkPageSummary({
currentSpaceId,
currentPageId,
targetSpaceId,
targetPageId,
linkPreview,
linkTitle,
previousPageIds: [],
});
for await (const highlight of stream) {
if (canceled) return;
setHighlight(highlight ?? '');
}
})();
return () => {
canceled = true;
};
}, [currentSpaceId, currentPageId, targetSpaceId, targetPageId, linkPreview, linkTitle]);
return (
<div className="flex flex-col gap-1">
<div className="flex w-screen items-center gap-1 font-semibold text-tint text-xs uppercase leading-tight tracking-wide">
{showTrademark ? (
<Loading className="size-4" busy={!highlight || highlight.length === 0} />
) : (
<Icon icon="sparkle" className="size-3" />
)}
<h6 className="text-tint">Page highlight</h6>
</div>
{highlight.length > 0 ? <p>{highlight}</p> : null}
{highlight.length > 0 ? (
<div className="text-tint-subtle text-xs">
Based on your context. May contain mistakes.
</div>
) : null}
</div>
);
}
@@ -0,0 +1 @@
export * from './AIPageLinkSummary';
@@ -0,0 +1,124 @@
'use server';
import { type AIMessageInput, AIModel, type AIStreamResponse } from '@gitbook/api';
import type { GitBookBaseContext } from '@v2/lib/context';
import { EventIterator } from 'event-iterator';
import type { MaybePromise } from 'p-map';
import * as partialJson from 'partial-json';
import type { DeepPartial } from 'ts-essentials';
import type { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
/**
* Get the latest value from a stream and the response id.
*/
export async function generate<T>(
promise: MaybePromise<{
stream: EventIterator<T>;
response: Promise<{ responseId: string }>;
}>
) {
const input = await promise;
let value: T | undefined;
for await (const event of input.stream) {
value = event;
}
const { responseId } = await input.response;
return {
responseId,
value,
};
}
/**
* Stream the generation of an object using the AI.
*/
export async function streamGenerateObject<T>(
context: GitBookBaseContext,
{
organizationId,
siteId,
}: {
organizationId: string;
siteId: string;
},
{
schema,
messages,
model = AIModel.Fast,
}: {
schema: z.ZodSchema<T>;
messages: AIMessageInput[];
model?: AIModel;
previousResponseId?: string;
}
) {
const apiClient = await context.dataFetcher.api();
const rawStream = apiClient.orgs.streamAiResponseInSite(organizationId, siteId, {
input: messages,
output: {
type: 'object',
schema: zodToJsonSchema(schema),
},
model,
});
let json = '';
return parseResponse<DeepPartial<T>>(rawStream, (event) => {
if (event.type === 'response_object') {
json += event.jsonChunk;
const parsed = partialJson.parse(json, partialJson.ALL);
return parsed;
}
});
}
/**
* Parse a stream from the API to extract the responseId.
*/
function parseResponse<T>(
responseStream: EventIterator<AIStreamResponse>,
parse: (response: AIStreamResponse) => T | undefined
): {
stream: EventIterator<T>;
response: Promise<{ responseId: string }>;
} {
let resolveResponse: (value: { responseId: string }) => void;
const response = new Promise<{ responseId: string }>((resolve) => {
resolveResponse = resolve;
});
const stream = new EventIterator<T>((queue) => {
(async () => {
let foundResponse = false;
for await (const event of responseStream) {
if (event.type === 'response_finish') {
foundResponse = true;
resolveResponse({ responseId: event.responseId });
} else {
const parsed = parse(event);
if (parsed !== undefined) {
queue.push(parsed);
}
}
}
if (!foundResponse) {
throw new Error('No response found');
}
})().then(
() => {
queue.stop();
},
(error) => {
queue.fail(error);
}
);
});
return { stream, response };
}
@@ -0,0 +1 @@
export * from './streamLinkPageSummary';
@@ -0,0 +1,127 @@
'use server';
import { getV1BaseContext } from '@/lib/v1';
import { isV2 } from '@/lib/v2';
import { AIMessageRole } from '@gitbook/api';
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
import { fetchServerActionSiteContext, getServerActionBaseContext } from '@v2/lib/server-actions';
import { z } from 'zod';
import { streamGenerateObject } from './api';
/**
* Get a summary of a page, in the context of another page
*/
export async function* streamLinkPageSummary({
currentSpaceId,
currentPageId,
targetSpaceId,
targetPageId,
linkPreview,
linkTitle,
}: {
currentSpaceId: string;
currentPageId: string;
targetSpaceId: string;
targetPageId: string;
linkPreview?: string;
linkTitle?: string;
previousPageIds?: string[];
}) {
const baseContext = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
const siteURLData = await getSiteURLDataFromMiddleware();
const [{ stream }] = await Promise.all([
streamGenerateObject(
baseContext,
{
organizationId: siteURLData.organization,
siteId: siteURLData.site,
},
{
schema: z.object({
highlight: z.string().describe('The most important content of the target page'),
// questions: z.array(z.string().describe('The questions to sea')).max(3),
}),
messages: [
{
role: AIMessageRole.Developer,
content: `# Role
You are a documentation navigator. Your job is to help the user read documentation more efficiently. Your aim is to prevent the user from having to read the target page by giving them all the information they need to know.
# Task
Using both the current page context and the target page content, produce a page highlight that:
- Highlights the key facts from the target page.
- Relates strongly to the topic the user is currently reading about.
- Is very succinct and direct, using only one or two short sentences (each sentence using no more than one comma).
- Remains strictly factual, without referring to “the page”.
# Instructions
1. Identify the key paragraph surrounding the link's text (e.g., “change request”) from the current page.
2. Extract and combine relevant information from the target page to address the link's context.
3. Combine in one or two short sentences that are direct and brief.
# Examples
## Example 1
- Link context: “This feature is only available on the Ultimate plan.”
- Link preview: “Pricing: Learn about our different pricing tiers.”
- Response: “The Ultimate plan costs $25 per month. A Pro plan is available too.”
## Example 2
- Link context: “You can use keyboard shortcuts to get to the Search menu faster.”
- Link preview: “Keyboard shortcuts: A quick reference guide to all the keyboard shortcuts available.”
- Response: “To open the Search menu, use the keyboard shortcut ⌘K or Ctrl+K.”
## Example 3
- Link context: “This feature can only be enabled by an admin.”
- Link preview: “Roles: An overview of the different roles on the platform.”
- Response: “The admin role is reserved for the creator of the organisation.”`,
},
{
role: AIMessageRole.Developer,
content: `# Context
## Current page
The content of the current page is:`,
attachments: [
{
type: 'page',
spaceId: currentSpaceId,
pageId: currentPageId,
},
],
},
{
role: AIMessageRole.Developer,
content: `## Target page
The content of the target page is:`,
attachments: [
{
type: 'page',
spaceId: targetSpaceId,
pageId: targetPageId,
},
],
},
{
role: AIMessageRole.Developer,
content: `## Link preview
The content of the link preview is:
> ${linkPreview}`,
},
{
role: AIMessageRole.User,
content: `I'm considering reading the link titled "${linkTitle}" to page ID ${targetPageId}. Give the most relevant information from this page. Relate it to my current page and in particular the paragraph I'm currently reading. Be very concise.`,
},
],
}
),
fetchServerActionSiteContext(baseContext),
]);
for await (const value of stream) {
const highlight = value.highlight;
if (!highlight) {
continue;
}
yield highlight;
}
}
@@ -97,9 +97,8 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
onClick={() => {
onUpdateState(true);
}}
>
{t(language, 'cookies_accept')}
</Button>
label={tString(language, 'cookies_accept')}
/>
<Button
variant="secondary"
size="small"
@@ -107,9 +106,8 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
onClick={() => {
onUpdateState(false);
}}
>
{t(language, 'cookies_reject')}
</Button>
label={tString(language, 'cookies_reject')}
/>
</div>
</div>
);
@@ -1,19 +1,22 @@
import { type DocumentInlineLink, SiteInsightsLinkPosition } from '@gitbook/api';
import { resolveContentRef } from '@/lib/references';
import { Icon } from '@gitbook/icons';
import { StyledLink } from '../primitives';
import type { InlineProps } from './Inline';
import { InlineLinkTooltip } from './InlineLinkTooltip';
import { Inlines } from './Inlines';
export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
const { inline, document, context, ancestorInlines } = props;
const resolved = context.contentContext
? await resolveContentRef(inline.data.ref, context.contentContext)
? await resolveContentRef(inline.data.ref, context.contentContext, {
resolveAnchorText: true,
})
: null;
if (!resolved) {
if (!context.contentContext || !resolved) {
return (
<span title="Broken link" className="underline">
<Inlines
@@ -25,24 +28,38 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
</span>
);
}
const isExternal = inline.data.ref.kind === 'url';
return (
<StyledLink
href={resolved.href}
insights={{
type: 'link_click',
link: {
target: inline.data.ref,
position: SiteInsightsLinkPosition.Content,
},
}}
<InlineLinkTooltip
inline={inline}
document={document}
context={context}
ancestorInlines={ancestorInlines}
>
<Inlines
context={context}
document={document}
nodes={inline.nodes}
ancestorInlines={[...ancestorInlines, inline]}
/>
</StyledLink>
<StyledLink
href={resolved.href}
insights={{
type: 'link_click',
link: {
target: inline.data.ref,
position: SiteInsightsLinkPosition.Content,
},
}}
>
<Inlines
context={context}
document={document}
nodes={inline.nodes}
ancestorInlines={[...ancestorInlines, inline]}
/>
{isExternal ? (
<Icon
icon="arrow-up-right"
className="ml-0.5 inline size-3 links-accent:text-tint-subtle"
/>
) : null}
</StyledLink>
</InlineLinkTooltip>
);
}
@@ -0,0 +1,178 @@
import type { DocumentInlineLink } from '@gitbook/api';
import { resolveContentRef } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
import { Icon } from '@gitbook/icons';
import * as Tooltip from '@radix-ui/react-tooltip';
import { Fragment } from 'react';
import { AIPageLinkSummary } from '../Adaptive/AIPageLinkSummary';
import { Button, StyledLink } from '../primitives';
import type { InlineProps } from './Inline';
import { Inlines } from './Inlines';
export async function InlineLinkTooltip(
props: InlineProps<DocumentInlineLink> & { children: React.ReactNode }
) {
const { inline, document, context, ancestorInlines, children } = props;
const resolved = context.contentContext
? await resolveContentRef(inline.data.ref, context.contentContext, {
resolveAnchorText: true,
})
: null;
if (!context.contentContext || !resolved) {
return (
<span title="Broken link" className="underline">
<Inlines
context={context}
document={document}
nodes={inline.nodes}
ancestorInlines={[...ancestorInlines, inline]}
/>
</span>
);
}
let breadcrumbs = resolved.ancestors;
const isExternal = inline.data.ref.kind === 'url';
const isSamePage = inline.data.ref.kind === 'anchor' && inline.data.ref.page === undefined;
if (isExternal) {
breadcrumbs = [
{
label: 'External link to',
},
];
}
if (isSamePage) {
breadcrumbs = [
{
label: 'Jump to section',
icon: <Icon icon="arrow-down-short-wide" className="size-3" />,
},
];
resolved.subText = undefined;
}
return (
<Tooltip.Provider delayDuration={200}>
<Tooltip.Root>
<Tooltip.Trigger asChild>{children}</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="z-40 w-screen max-w-md animate-present px-4 sm:w-auto">
<div className="overflow-hidden rounded-md straight-corners:rounded-none shadow-lg shadow-tint-12/4 ring-1 ring-tint-subtle dark:shadow-tint-1 ">
<div className="bg-tint-base p-4">
<div className={tcls('flex gap-4')}>
<div className="flex flex-col">
{breadcrumbs && breadcrumbs.length > 0 ? (
<div className="mb-1 flex grow flex-wrap items-center gap-x-2 gap-y-0.5 font-semibold text-tint text-xs uppercase leading-tight tracking-wide">
{breadcrumbs.map((crumb, index) => {
const Tag = crumb.href ? StyledLink : 'div';
return (
<Fragment key={crumb.label}>
{index !== 0 ? (
<Icon
icon="chevron-right"
className="size-3 text-tint-subtle"
/>
) : null}
<Tag
className={tcls(
'flex gap-1',
crumb.href &&
'links-default:text-tint no-underline hover:underline contrast-more:underline contrast-more:decoration-current'
)}
href={crumb.href ?? '#'}
>
{crumb.icon ? (
<span className="mt-0.5 text-tint-subtle empty:hidden">
{crumb.icon}
</span>
) : null}
{crumb.label}
</Tag>
</Fragment>
);
})}
</div>
) : null}
<div
className={tcls(
'flex gap-2 leading-snug',
isExternal && 'text-sm [overflow-wrap:anywhere]'
)}
>
{resolved.icon ? (
<div className="mt-1 text-tint-subtle empty:hidden">
{resolved.icon}
</div>
) : null}
<h5 className="font-semibold">{resolved.text}</h5>
</div>
</div>
{!isSamePage && resolved.href ? (
<Button
className={tcls(
'-mx-2 -my-2 ml-auto',
breadcrumbs?.length === 0
? null
: 'place-self-start'
)}
variant="blank"
href={resolved.href}
target="_blank"
label="Open in new tab"
size="small"
icon="arrow-up-right-from-square"
iconOnly={true}
/>
) : null}
</div>
{resolved.subText ? (
<p className="mt-1 text-sm text-tint">{resolved.subText}</p>
) : null}
</div>
{'customization' in context.contentContext &&
context.contentContext.customization.ai?.pageLinkSummaries.enabled &&
!isExternal &&
'page' in context.contentContext &&
inline.data.ref.kind === 'page' ? (
<div className="border-tint-subtle border-t bg-tint p-4">
<AIPageLinkSummary
currentPageId={context.contentContext.page.id}
currentSpaceId={context.contentContext.space.id}
currentPageTitle={context.contentContext.page.title}
targetPageId={
inline.data.ref.page ?? context.contentContext.page.id
}
targetSpaceId={
inline.data.ref.space ?? context.contentContext.space.id
}
linkTitle={inline.nodes
.map((node) => {
if (node.object === 'text') {
return node.leaves
.map((leaf) => leaf.text)
.join('');
}
return '';
})
.join('')}
linkPreview={`**${resolved.text}**: ${resolved.subText}`}
showTrademark={
context.contentContext.customization.trademark.enabled
}
/>
</div>
) : null}
</div>
<Tooltip.Arrow className="fill-tint-1" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
);
}
@@ -29,10 +29,6 @@
@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-stability-stable {
@apply text-green-600 dark:text-green-300 bg-green-50 dark:bg-green-900/6 ring-green-500/5;
}
.openapi-stability-alpha {
@apply text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-900/6 ring-amber-500/5;
}
@@ -265,15 +261,11 @@
/* Schema Enum */
.openapi-schema-enum {
@apply flex flex-row text-sm leading-relaxed gap-2 flex-wrap text-tint;
}
.openapi-schema-enum-list {
@apply flex flex-row gap-1.5 items-center;
@apply text-sm leading-relaxed max-w-full text-tint;
}
.openapi-schema-enum-value {
@apply text-sm;
@apply text-sm mr-1.5;
}
.openapi-schema-enum-value:first-child {
@@ -168,7 +168,7 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
key={index}
href={ref.href}
target="_blank"
style={['flex', 'flex-row', 'items-center', 'gap-2']}
className="flex flex-row items-center gap-2"
insights={
ref.file
? {
@@ -142,10 +142,9 @@ function HeaderItemButton(
position: SiteInsightsLinkPosition.Header,
},
}}
label={title}
{...rest}
>
{title}
</Button>
/>
);
}
@@ -5,7 +5,7 @@ import React from 'react';
import { useScrollActiveId } from '@/components/hooks';
import { Button } from '@/components/primitives';
import { t, useLanguage } from '@/intl/client';
import { t, tString, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
import { type PDFSearchParams, getPDFURLSearchParams } from './urls';
@@ -58,9 +58,8 @@ export function PageControlButtons(props: {
only: true,
}).toString()}`}
variant="secondary"
>
{t(language, 'pdf_mode_only_page')}
</Button>
label={tString(language, 'pdf_mode_only_page')}
/>
)}
<Button
href={`?${getPDFURLSearchParams({
@@ -69,9 +68,8 @@ export function PageControlButtons(props: {
only: false,
}).toString()}`}
variant="secondary"
>
{t(language, 'pdf_mode_all')}
</Button>
label={tString(language, 'pdf_mode_all')}
/>
{trademark ? <div className={tcls('mt-5')}>{trademark}</div> : null}
</div>
@@ -13,7 +13,6 @@ import urlJoin from 'url-join';
import { getSpaceLanguage, t } from '@/intl/server';
import { getDocumentSections } from '@/lib/document-sections';
import { tcls } from '@/lib/tailwind';
import { Ad } from '../Ads';
import { getPDFURLSearchParams } from '../PDF';
import { PageFeedbackForm } from '../PageFeedback';
@@ -42,12 +41,14 @@ export function PageAside(props: {
limit: 100,
}).toString()}`
);
return (
<aside
className={tcls(
'group/aside',
'hidden',
'xl:flex',
'text-sm',
// 'page-no-toc:lg:flex',
'flex-col',
'basis-56',
@@ -92,139 +93,109 @@ export function PageAside(props: {
)}
>
{page.layout.outline ? (
<>
<div
className={tcls(
'overflow-y-auto',
'overflow-x-visible',
'flex',
'flex-col',
'shrink',
'pb-12',
'sticky',
'lg:top:0',
'site-header:lg:top-16',
'site-header-sections:lg:top-[6.75rem]',
'gap-2',
'pt-8',
'page-api-block:xl:max-2xl:py-0',
// Hide it for api page, until hovered
'page-api-block:xl:max-2xl:hidden',
'page-api-block:xl:max-2xl:group-hover/aside:flex'
)}
>
{document ? (
<React.Suspense fallback={null}>
<PageAsideSections document={document} context={context} />
</React.Suspense>
) : null}
<div
className={tcls(
'hidden',
'page-api-block:xl:max-2xl:flex',
'text-xs',
'tracking-wide',
'font-semibold',
'uppercase',
'flex-row',
'items-center',
'gap-2'
)}
>
<Icon icon="block-quote" className={tcls('size-3')} />
{t(language, 'on_this_page')}
<Icon
icon="chevron-down"
className={tcls(
'size-3',
'opacity-6',
'ml-auto',
'page-api-block:xl:max-2xl:group-hover/aside:hidden'
)}
/>
</div>
<div
className={tcls(
'overflow-y-auto',
'overflow-x-visible',
'flex',
'flex-col',
'shrink',
'pb-12',
'sticky',
'lg:top:0',
'site-header:lg:top-16',
'site-header-sections:lg:top-[6.75rem]',
'gap-6',
'pt-8',
'page-api-block:xl:max-2xl:py-0',
// Hide it for api page, until hovered
'page-api-block:xl:max-2xl:hidden',
'page-api-block:xl:max-2xl:group-hover/aside:flex'
'gap-3',
'sidebar-list-default:px-3',
'border-t',
'first:border-none',
'border-tint-subtle',
'py-4',
'first:pt-0',
'page-api-block:xl:max-2xl:px-3',
'empty:hidden'
)}
>
{document ? (
{withPageFeedback ? (
<React.Suspense fallback={null}>
<PageAsideSections document={document} context={context} />
<PageFeedbackForm pageId={page.id} className={tcls('mt-2')} />
</React.Suspense>
) : null}
<div
className={tcls(
'flex',
'flex-col',
'gap-3',
'sidebar-list-default:px-3',
'border-t',
'first:border-none',
'border-tint-subtle',
'py-4',
'first:pt-0',
'page-api-block:xl:max-2xl:px-3',
'empty:hidden'
)}
>
{withPageFeedback ? (
<React.Suspense fallback={null}>
<PageFeedbackForm pageId={page.id} className={tcls('mt-2')} />
</React.Suspense>
) : null}
{customization.git.showEditLink && space.gitSync?.url && page.git ? (
<div>
<a
href={urlJoin(space.gitSync.url, page.git.path)}
className={tcls(
'flex',
'flex-row',
'items-center',
'text-sm',
'hover:text-tint-strong',
'links-accent:hover:underline',
'links-accent:hover:underline-offset-4',
'links-accent:hover:decoration-[3px]',
'links-accent:hover:decoration-primary-subtle',
'py-2'
)}
>
<Icon
icon={
space.gitSync.installationProvider === 'gitlab'
? 'gitlab'
: 'github'
}
className={tcls('size-4', 'mr-1.5')}
/>
{t(language, 'edit_on_git', getGitSyncName(space))}
</a>
</div>
) : null}
{customization.pdf.enabled ? (
<div>
<a
href={pdfHref}
className={tcls(
'flex',
'flex-row',
'items-center',
'text-sm',
'hover:text-tint-strong',
'links-accent:hover:underline',
'links-accent:hover:underline-offset-4',
'links-accent:hover:decoration-[3px]',
'links-accent:hover:decoration-primary-subtle',
'py-2'
)}
>
<Icon
icon="file-pdf"
className={tcls('size-4', 'mr-1.5')}
/>
{t(language, 'pdf_download')}
</a>
</div>
) : null}
</div>
{customization.git.showEditLink && space.gitSync?.url && page.git ? (
<div>
<a
href={urlJoin(space.gitSync.url, page.git.path)}
className={tcls(
'flex',
'flex-row',
'items-center',
'text-sm',
'hover:text-tint-strong',
'links-accent:hover:underline',
'links-accent:hover:underline-offset-4',
'links-accent:hover:decoration-[3px]',
'links-accent:hover:decoration-primary-subtle',
'py-2'
)}
>
<Icon
icon={
space.gitSync.installationProvider === 'gitlab'
? 'gitlab'
: 'github'
}
className={tcls('size-4', 'mr-1.5')}
/>
{t(language, 'edit_on_git', getGitSyncName(space))}
</a>
</div>
) : null}
{customization.pdf.enabled ? (
<div>
<a
href={pdfHref}
className={tcls(
'flex',
'flex-row',
'items-center',
'text-sm',
'hover:text-tint-strong',
'links-accent:hover:underline',
'links-accent:hover:underline-offset-4',
'links-accent:hover:decoration-[3px]',
'links-accent:hover:decoration-primary-subtle',
'py-2'
)}
>
<Icon icon="file-pdf" className={tcls('size-4', 'mr-1.5')} />
{t(language, 'pdf_download')}
</a>
</div>
) : null}
</div>
</>
</div>
) : null}
<div
className={tcls(
@@ -7,7 +7,6 @@ import { t } from '@/intl/translate';
import { hasFullWidthBlock, isNodeEmpty } from '@/lib/document';
import type { AncestorRevisionPage } from '@/lib/pages';
import { tcls } from '@/lib/tailwind';
import { DocumentView, DocumentViewSkeleton } from '../DocumentView';
import { TrackPageViewEvent } from '../Insights';
import { PageFeedbackForm } from '../PageFeedback';
@@ -35,7 +35,7 @@ export async function PageHeader(props: {
<li key={breadcrumb.id}>
<StyledLink
href={href}
style={tcls(
className={tcls(
'no-underline',
'hover:underline',
'text-xs',
@@ -106,9 +106,8 @@ export function PageFeedbackForm(props: {
<Button
size="small"
onClick={() => onSubmitComment(rating, comment)}
>
{t(languages, 'submit')}
</Button>
label={tString(languages, 'submit')}
/>
{comment.length > MAX_COMMENT_LENGTH * 0.8 ? (
<span
className={
@@ -1,62 +1,80 @@
'use client';
import type { HTMLAttributes } from 'react';
import type { HTMLAttributeAnchorTarget, HTMLAttributes } from 'react';
import { type ClassValue, tcls } from '@/lib/tailwind';
import { Icon, type IconName } from '@gitbook/icons';
import { Link, type LinkInsightsProps } from './Link';
type ButtonProps = {
href?: string;
variant?: 'primary' | 'secondary';
variant?: 'primary' | 'secondary' | 'blank';
icon?: IconName;
iconOnly?: boolean;
size?: 'default' | 'medium' | 'small';
className?: ClassValue;
label?: string;
} & LinkInsightsProps &
HTMLAttributes<HTMLElement>;
const variantClasses = {
primary: [
'bg-primary-solid',
'text-contrast-primary-solid',
'hover:bg-primary-solid-hover',
'hover:text-contrast-primary-solid-hover',
'ring-0',
'contrast-more:ring-1',
],
blank: [
'bg-transparent',
'text-tint',
'ring-0',
'shadow-none',
'hover:bg-primary-hover',
'hover:text-primary',
'hover:scale-1',
'hover:shadow-none',
'contrast-more:bg-tint-subtle',
],
secondary: [
'bg-tint',
'text-tint',
'hover:bg-tint-hover',
'hover:text-primary',
'contrast-more:bg-tint-subtle',
],
};
export function Button({
href,
children,
variant = 'primary',
size = 'default',
className,
insights,
target,
label,
icon,
iconOnly = false,
...rest
}: ButtonProps) {
const variantClasses =
variant === 'primary'
? //PRIMARY
[
'bg-primary-solid',
'text-contrast-primary-solid',
'hover:bg-primary-solid-hover',
'hover:text-contrast-primary-solid-hover',
'ring-0',
'contrast-more:ring-1',
]
: // SECONDARY
[
'bg-tint',
'text-tint',
'hover:bg-tint-hover',
'hover:text-primary',
'contrast-more:bg-tint-subtle',
];
}: ButtonProps & { target?: HTMLAttributeAnchorTarget }) {
const sizes = {
default: ['text-base', 'px-4', 'py-2'],
medium: ['text-sm', 'px-3', 'py-1.5'],
small: ['text-xs', 'px-3 py-2'],
small: ['text-xs', 'py-2', iconOnly ? 'px-2' : 'px-3'],
};
const sizeClasses = sizes[size] || sizes.default;
const domClassName = tcls(
'button',
'inline-block',
'inline-flex',
'items-center',
'gap-2',
'rounded-md',
'straight-corners:rounded-none',
'place-self-start',
// 'place-self-start',
'ring-1',
'ring-tint',
@@ -79,22 +97,31 @@ export function Button({
'grow-0',
'shrink-0',
'truncate',
variantClasses,
variantClasses[variant],
sizeClasses,
className
);
if (href) {
return (
<Link href={href} className={domClassName} insights={insights} {...rest}>
{children}
<Link
href={href}
className={domClassName}
insights={insights}
aria-label={label}
target={target}
{...rest}
>
{icon ? <Icon icon={icon} className={tcls('size-[1em]')} /> : null}
{iconOnly ? null : label}
</Link>
);
}
return (
<button type="button" className={domClassName} {...rest}>
{children}
<button type="button" className={domClassName} aria-label={label} {...rest}>
{icon ? <Icon icon={icon} className={tcls('size-[1em]')} /> : null}
{iconOnly ? null : label}
</button>
);
}
@@ -2,7 +2,10 @@ import type { SVGProps } from 'react';
import { tcls } from '@/lib/tailwind';
export const Loading = (props: Partial<SVGProps<SVGSVGElement>>) => {
export const Loading = ({
busy = true,
...props
}: { busy?: boolean } & SVGProps<SVGSVGElement>) => {
return (
<svg
width="100%"
@@ -14,7 +17,11 @@ export const Loading = (props: Partial<SVGProps<SVGSVGElement>>) => {
{...props}
>
<path
className={tcls('animate-[pathLoading_2s_ease_infinite_forwards]')}
className={tcls(
busy
? 'animate-[pathLoading_2s_ease_infinite_forwards]'
: 'animate-[pathLoading_2s_ease_forwards]'
)}
d="M6 59.5V56.291C6 45.8865 11.5194 36.263 20.5 31.0091V31.0091L60.9857 7.32407C63.4452 5.88525 66.4843 5.86317 68.9643 7.26611L116 33.8734L70.4183 60.2148C67.9468 61.6431 64.9014 61.6462 62.4269 60.223L29.9772 41.5592C19.3106 35.4242 6 43.1236 6 55.4288V64.8776C6 73.4486 10.5708 81.3691 17.9918 85.6575L54.59 106.807C62.0198 111.1 71.1766 111.1 78.6064 106.807L116.364 84.9874C120.074 82.8432 122.36 78.883 122.36 74.5975V59.2647C122.36 57.7248 120.692 56.7626 119.359 57.5331L72.6023 84.5529C68.8874 86.6996 64.309 86.6996 60.5941 84.5529L26 64.5617"
stroke="currentColor"
pathLength="100"
@@ -27,7 +34,8 @@ export const Loading = (props: Partial<SVGProps<SVGSVGElement>>) => {
d="M6 59.5V56.291C6 45.8865 11.5194 36.263 20.5 31.0091V31.0091L60.9857 7.32407C63.4452 5.88525 66.4843 5.86317 68.9643 7.26611L116 33.8734L70.4183 60.2148C67.9468 61.6431 64.9014 61.6462 62.4269 60.223L29.9772 41.5592C19.3106 35.4242 6 43.1236 6 55.4288V64.8776C6 73.4486 10.5708 81.3691 17.9918 85.6575L54.59 106.807C62.0198 111.1 71.1766 111.1 78.6064 106.807L116.364 84.9874C120.074 82.8432 122.36 78.883 122.36 74.5975V59.2647C122.36 57.7248 120.692 56.7626 119.359 57.5331L72.6023 84.5529C68.8874 86.6996 64.309 86.6996 60.5941 84.5529L26 64.5617"
stroke="currentColor"
pathLength="100"
strokeOpacity="0.24"
strokeOpacity={busy ? 0.24 : 1}
className="transition-opacity duration-1000"
fill="none"
strokeWidth="11"
strokeLinecap="round"
@@ -25,11 +25,11 @@ export const linkStyles = [
/**
* Styled version of Link component.
*/
export function StyledLink(props: Omit<LinkProps, 'style'> & { style?: ClassValue }) {
const { style, ...rest } = props;
export function StyledLink(props: Omit<LinkProps, 'style'> & { className?: ClassValue }) {
const { className, ...rest } = props;
return (
<Link {...rest} className={tcls(linkStyles, style)}>
<Link {...rest} className={tcls(linkStyles, className)}>
{props.children}
</Link>
);
+29 -3
View File
@@ -19,6 +19,7 @@ import { getGitbookAppHref } from './links';
import { resolvePageId } from './pages';
import { findSiteSpaceById } from './sites';
import type { ClassValue } from './tailwind';
import { filterOutNullable } from './typescript';
export interface ResolvedContentRef {
/** Text to render in the content ref */
@@ -27,8 +28,12 @@ export interface ResolvedContentRef {
subText?: string;
/** Icon associated with it */
icon?: React.ReactNode;
/** ID of the content ref */
id?: string;
/** Emoji associated with the reference */
emoji?: string;
/** The content ref's ancestors */
ancestors?: { icon?: React.ReactNode; label: string; href?: string }[];
/** URL to open for the content ref */
href: string;
/** True if the content ref is active */
@@ -115,6 +120,14 @@ export async function resolveContentRef(
: resolvePageId(pages, contentRef.page);
const page = resolvePageResult?.page;
const ancestors =
resolvePageResult?.ancestors.map((ancestor) => ({
label: ancestor.title,
icon: <PageIcon page={ancestor} style={iconStyle} />,
href: resolveAsAbsoluteURL
? linker.toAbsoluteURL(linker.toPathForPage({ page: ancestor, pages }))
: linker.toPathForPage({ page: ancestor, pages }),
})) ?? [];
if (!page) {
return null;
}
@@ -125,10 +138,16 @@ export async function resolveContentRef(
let text = '';
let icon: React.ReactNode | undefined = undefined;
let emoji: string | undefined = undefined;
const href = linker.toPathForPage({ page, pages, anchor });
// Compute the text to display for the link
if (anchor) {
text = `#${anchor}`;
ancestors.push({
label: page.title,
icon: <PageIcon page={page} style={iconStyle} />,
href: resolveAsAbsoluteURL ? linker.toAbsoluteURL(href) : href,
});
if (resolveAnchorText) {
const document = await getPageDocument(dataFetcher, space, page);
@@ -151,13 +170,14 @@ export async function resolveContentRef(
icon = <PageIcon page={page} style={iconStyle} />;
}
const href = linker.toPathForPage({ page, pages, anchor });
return {
href: resolveAsAbsoluteURL ? linker.toAbsoluteURL(href) : href,
text,
subText: page.description,
ancestors: ancestors,
emoji,
icon,
id: page.id,
active: !anchor && page.id === activePage?.id,
};
}
@@ -346,6 +366,12 @@ async function resolveContentRefInSpace(
return {
...resolved,
subText: space.title,
ancestors: [
{
label: space.title,
href: baseURL.toString(),
},
...(resolved.ancestors ?? []),
].filter(filterOutNullable),
};
}
+2 -2
View File
@@ -295,7 +295,7 @@ const config: Config = {
),
},
animation: {
present: 'present .5s ease-out both',
present: 'present 200ms cubic-bezier(0.25, 1, 0.5, 1) both',
scaleIn: 'scaleIn 200ms ease',
scaleOut: 'scaleOut 200ms ease',
fadeIn: 'fadeIn 200ms ease forwards',
@@ -330,7 +330,7 @@ const config: Config = {
present: {
from: {
opacity: '0',
transform: 'translateY(2rem) scale(0.9)',
transform: 'translateY(1rem) scale(90%)',
},
to: {
opacity: '1',
+2 -2
View File
@@ -65,12 +65,12 @@ export interface OpenAPICustomOperationProperties {
/**
* Stability of the operation.
* @enum 'experimental' | 'alpha' | 'beta' | 'stable'
* @enum 'experimental' | 'alpha' | 'beta'
*/
'x-stability'?: OpenAPIStability;
}
export type OpenAPIStability = 'experimental' | 'alpha' | 'beta' | 'stable';
export type OpenAPIStability = 'experimental' | 'alpha' | 'beta';
/**
* Custom code samples that can be defined at the operation level.
+8
View File
@@ -1,5 +1,13 @@
# @gitbook/react-openapi
## 1.1.9
### Patch Changes
- da7b369: Fix missing headers in OpenAPIResponses
- da485f5: Fix read-only in generateSchemaExample
- 139a805: Fix OpenAPI enum display
## 1.1.8
### Patch Changes
+1 -1
View File
@@ -8,7 +8,7 @@
"default": "./dist/index.js"
}
},
"version": "1.1.8",
"version": "1.1.9",
"sideEffects": false,
"dependencies": {
"@gitbook/openapi-parser": "workspace:*",
@@ -108,7 +108,6 @@ const stabilityEnum = {
experimental: 'Experimental',
alpha: 'Alpha',
beta: 'Beta',
stable: 'Stable',
} as const;
function OpenAPIOperationStability(props: { stability: OpenAPIStability }) {
@@ -36,13 +36,15 @@ export function OpenAPIResponse(props: {
/>
</OpenAPIDisclosure>
) : null}
<div className="openapi-responsebody">
<OpenAPISchemaProperties
id={`response-${context.blockKey}`}
properties={mediaType.schema ? [{ schema: mediaType.schema }] : []}
context={context}
/>
</div>
{mediaType.schema && (
<div className="openapi-responsebody">
<OpenAPISchemaProperties
id={`response-${context.blockKey}`}
properties={[{ schema: mediaType.schema }]}
context={context}
/>
</div>
)}
</div>
);
}
@@ -268,6 +268,7 @@ function getExamplesFromMediaTypeObject(args: {
value: {
[root]: generateSchemaExample(mediaTypeObject.schema, {
xml: mediaType === 'application/xml',
mode: 'read',
}),
},
},
@@ -277,7 +278,11 @@ function getExamplesFromMediaTypeObject(args: {
return [
{
key: 'default',
example: { value: generateSchemaExample(mediaTypeObject.schema) },
example: {
value: generateSchemaExample(mediaTypeObject.schema, {
mode: 'read',
}),
},
},
];
}
+37 -12
View File
@@ -21,7 +21,42 @@ export function OpenAPIResponses(props: {
icon={context.icons.chevronRight}
groups={Object.entries(responses).map(
([statusCode, response]: [string, OpenAPIV3.ResponseObject]) => {
const content = Object.entries(response.content ?? {});
const tabs = (() => {
// If there is no content, but there are headers, we need to show the headers
if (
(!response.content || !Object.keys(response.content).length) &&
response.headers &&
Object.keys(response.headers).length
) {
return [
{
id: 'default',
body: (
<OpenAPIResponse
response={response}
mediaType={{}}
context={context}
/>
),
},
];
}
return Object.entries(response.content ?? {}).map(
([contentType, mediaType]) => ({
id: contentType,
label: contentType,
body: (
<OpenAPIResponse
response={response}
mediaType={mediaType}
context={context}
/>
),
})
);
})();
const description = response.description;
return {
@@ -39,17 +74,7 @@ export function OpenAPIResponses(props: {
) : null}
</div>
),
tabs: content.map(([contentType, mediaType]) => ({
id: contentType,
label: contentType,
body: (
<OpenAPIResponse
response={response}
mediaType={mediaType}
context={context}
/>
),
})),
tabs,
};
}
)}
+14 -16
View File
@@ -275,22 +275,20 @@ function OpenAPISchemaEnum(props: {
}
return (
<div className="openapi-schema-enum">
<span>Available options:</span>
<div className="openapi-schema-enum-list">
{enumValues.map((item, index) => (
<span key={index} className="openapi-schema-enum-value">
<OpenAPICopyButton
value={item.value}
label={item.description}
withTooltip={!!item.description}
>
<code>{`${item.value}`}</code>
</OpenAPICopyButton>
</span>
))}
</div>
</div>
<span className="openapi-schema-enum">
Available options:{' '}
{enumValues.map((item, index) => (
<span key={index} className="openapi-schema-enum-value">
<OpenAPICopyButton
value={item.value}
label={item.description}
withTooltip={!!item.description}
>
<code>{`${item.value}`}</code>
</OpenAPICopyButton>
</span>
))}
</span>
);
}
@@ -16,14 +16,10 @@ export function generateSchemaExample(
schema: OpenAPIV3.SchemaObject,
options?: GenerateSchemaExampleOptions
): JSONValue | undefined {
return getExampleFromSchema(
schema,
{
emptyString: 'text',
...options,
},
3 // Max depth for circular references
);
return getExampleFromSchema(schema, {
emptyString: 'text',
...options,
});
}
/**
@@ -103,21 +99,6 @@ function guessFromFormat(schema: Record<string, any>, fallback = '') {
return genericExampleValues[schema.format] ?? fallback;
}
/** Map of all the results */
const resultCache = new WeakMap<Record<string, any>, any>();
/** Store result in the cache, and return the result */
function cache(schema: Record<string, any>, result: unknown) {
// Avoid unnecessary WeakMap operations for primitive values
if (typeof result !== 'object' || result === null) {
return result;
}
resultCache.set(schema, result);
return result;
}
/**
* This function takes an OpenAPI schema and generates an example from it
* Forked from : https://github.com/scalar/scalar/blob/main/packages/oas-utils/src/spec-getters/getExampleFromSchema.ts
@@ -152,8 +133,20 @@ const getExampleFromSchema = (
},
level = 0,
parentSchema?: Record<string, any>,
name?: string
name?: string,
resultCache = new WeakMap<Record<string, any>, any>()
): any => {
// Store result in the cache, and return the result
function cache(schema: Record<string, any>, result: unknown) {
// Avoid unnecessary WeakMap operations for primitive values
if (typeof result !== 'object' || result === null) {
return result;
}
resultCache.set(schema, result);
return result;
}
// Check if the result is already cached
if (resultCache.has(schema)) {
return resultCache.get(schema);
@@ -245,7 +238,8 @@ const getExampleFromSchema = (
options,
level + 1,
schema,
propertyName
propertyName,
resultCache
);
if (typeof response[propertyXmlTagName ?? propertyName] === 'undefined') {
@@ -269,7 +263,8 @@ const getExampleFromSchema = (
options,
level + 1,
schema,
exampleKey
exampleKey,
resultCache
);
}
}
@@ -290,21 +285,51 @@ const getExampleFromSchema = (
response.ANY_ADDITIONAL_PROPERTY = getExampleFromSchema(
schema.additionalProperties,
options,
level + 1
level + 1,
undefined,
undefined,
resultCache
);
}
}
if (schema.anyOf !== undefined) {
Object.assign(response, getExampleFromSchema(schema.anyOf[0], options, level + 1));
Object.assign(
response,
getExampleFromSchema(
schema.anyOf[0],
options,
level + 1,
undefined,
undefined,
resultCache
)
);
} else if (schema.oneOf !== undefined) {
Object.assign(response, getExampleFromSchema(schema.oneOf[0], options, level + 1));
Object.assign(
response,
getExampleFromSchema(
schema.oneOf[0],
options,
level + 1,
undefined,
undefined,
resultCache
)
);
} else if (schema.allOf !== undefined) {
Object.assign(
response,
...schema.allOf
.map((item: Record<string, any>) =>
getExampleFromSchema(item, options, level + 1, schema)
getExampleFromSchema(
item,
options,
level + 1,
schema,
undefined,
resultCache
)
)
.filter((item: any) => item !== undefined)
);
@@ -335,7 +360,9 @@ const getExampleFromSchema = (
{ type: 'object', allOf: schema.items.allOf },
options,
level + 1,
schema
schema,
undefined,
resultCache
);
return cache(
@@ -346,7 +373,14 @@ const getExampleFromSchema = (
// For non-objects (like strings), collect all examples
const examples = schema.items.allOf
.map((item: Record<string, any>) =>
getExampleFromSchema(item, options, level + 1, schema)
getExampleFromSchema(
item,
options,
level + 1,
schema,
undefined,
resultCache
)
)
.filter((item: any) => item !== undefined);
@@ -368,7 +402,14 @@ const getExampleFromSchema = (
const schemas = schema.items[rule].slice(0, 1);
const exampleFromRule = schemas
.map((item: Record<string, any>) =>
getExampleFromSchema(item, options, level + 1, schema)
getExampleFromSchema(
item,
options,
level + 1,
schema,
undefined,
resultCache
)
)
.filter((item: any) => item !== undefined);
@@ -380,7 +421,14 @@ const getExampleFromSchema = (
}
if (schema.items?.type) {
const exampleFromSchema = getExampleFromSchema(schema.items, options, level + 1);
const exampleFromSchema = getExampleFromSchema(
schema.items,
options,
level + 1,
undefined,
undefined,
resultCache
);
return wrapItems ? [{ [itemsXmlTagName]: exampleFromSchema }] : [exampleFromSchema];
}
@@ -407,7 +455,14 @@ const getExampleFromSchema = (
const firstOneOfItem = discriminateSchema[0];
// Return an example for the first item
return getExampleFromSchema(firstOneOfItem, options, level + 1);
return getExampleFromSchema(
firstOneOfItem,
options,
level + 1,
undefined,
undefined,
resultCache
);
}
// Check if schema has the `allOf` key
@@ -417,7 +472,14 @@ const getExampleFromSchema = (
// Loop through all `allOf` schemas
schema.allOf.forEach((allOfItem: Record<string, any>) => {
// Return an example from the schema
const newExample = getExampleFromSchema(allOfItem, options, level + 1);
const newExample = getExampleFromSchema(
allOfItem,
options,
level + 1,
undefined,
undefined,
resultCache
);
// Merge or overwrite the example
example =