mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-20 17:43:24 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 36877d92a6 | |||
| 23433d1287 | |||
| 5d9183e1b4 | |||
| a1d5326ffa | |||
| cc26fb34aa | |||
| b1abbf603b | |||
| a70e8cd7dc | |||
| b38eed850f | |||
| f4c50c7e3c | |||
| 5d308c764e | |||
| 08e780496b | |||
| e4cd67ebe2 | |||
| 939b160bfa | |||
| 689e78d8e7 | |||
| 37c1d4b8ea | |||
| 75855585f5 | |||
| dcf0f9aac5 | |||
| 1d7253307a |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Implement retry logic for the DO cache to prevent when revalidating content.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@gitbook/colors": patch
|
||||
---
|
||||
|
||||
Desaturate text colors by decreasing chroma for the last steps of the color scale
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Improve OpenAPI schema style
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Improve the error message returned by the revalidate endpoint.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Do not set cookie to identify visitor for insights when disabled.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"@gitbook/react-openapi": patch
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Improve OpenAPI schemas block ungrouped style. Classnames have changed, please refer to this PR to update GBX.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@gitbook/react-openapi": patch
|
||||
---
|
||||
|
||||
Hide deprecated properties in examples
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Better error handling in cache revalidation.
|
||||
+5
-2
@@ -7,12 +7,12 @@
|
||||
"turbo": "^2.4.4",
|
||||
"vercel": "^39.3.0"
|
||||
},
|
||||
"packageManager": "bun@1.2.8",
|
||||
"packageManager": "bun@1.2.5",
|
||||
"overrides": {
|
||||
"@codemirror/state": "6.4.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"@gitbook/api": "0.108.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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ export function colorScale(
|
||||
continue;
|
||||
}
|
||||
|
||||
const chromaRatio = index === 8 || index === 9 ? 1 : index * 0.05;
|
||||
const chromaRatio = index < 8 ? index * 0.05 : 1;
|
||||
|
||||
const shade = {
|
||||
L: targetL, // Blend lightness
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.2.3",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"next": "canary",
|
||||
"next": "^15.2.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"@gitbook/api": "*",
|
||||
|
||||
+11
-1
@@ -3,9 +3,10 @@ import {
|
||||
generateSitePageMetadata,
|
||||
generateSitePageViewport,
|
||||
} from '@/components/SitePage';
|
||||
import { getCacheTag } from '@gitbook/cache-tags';
|
||||
import { type RouteParams, getPagePathFromParams, getStaticSiteContext } from '@v2/app/utils';
|
||||
|
||||
import type { Metadata, Viewport } from 'next';
|
||||
import { unstable_cacheTag as cacheTag } from 'next/cache';
|
||||
|
||||
export const dynamic = 'force-static';
|
||||
|
||||
@@ -14,10 +15,19 @@ type PageProps = {
|
||||
};
|
||||
|
||||
export default async function Page(props: PageProps) {
|
||||
'use cache';
|
||||
|
||||
const params = await props.params;
|
||||
const { context } = await getStaticSiteContext(params);
|
||||
const pathname = getPagePathFromParams(params);
|
||||
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'site',
|
||||
site: context.site.id,
|
||||
})
|
||||
);
|
||||
|
||||
return <SitePage context={context} pageParams={{ pathname }} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ import {
|
||||
generateSiteLayoutMetadata,
|
||||
generateSiteLayoutViewport,
|
||||
} from '@/components/SiteLayout';
|
||||
import { getCacheTag } from '@gitbook/cache-tags';
|
||||
import { type RouteLayoutParams, getStaticSiteContext } from '@v2/app/utils';
|
||||
import { GITBOOK_DISABLE_TRACKING } from '@v2/lib/env';
|
||||
import { unstable_cacheTag as cacheTag } from 'next/cache';
|
||||
|
||||
interface SiteStaticLayoutProps {
|
||||
params: Promise<RouteLayoutParams>;
|
||||
@@ -15,8 +17,17 @@ export default async function SiteStaticLayout({
|
||||
params,
|
||||
children,
|
||||
}: React.PropsWithChildren<SiteStaticLayoutProps>) {
|
||||
'use cache';
|
||||
|
||||
const { context, visitorAuthClaims } = await getStaticSiteContext(await params);
|
||||
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'site',
|
||||
site: context.site.id,
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<CustomizationRootLayout customization={context.customization}>
|
||||
<SiteLayout
|
||||
|
||||
@@ -179,10 +179,6 @@ export function createDataFetcher(
|
||||
getUserById(userId) {
|
||||
return trace('getUserById', () => getUserById(input, { userId }));
|
||||
},
|
||||
|
||||
streamAIResponse(params) {
|
||||
return streamAIResponse(input, params);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -647,22 +643,6 @@ const renderIntegrationUi = memoize(async function renderIntegrationUi(
|
||||
});
|
||||
});
|
||||
|
||||
async function* streamAIResponse(
|
||||
input: DataFetcherInput,
|
||||
params: Parameters<GitBookDataFetcher['streamAIResponse']>[0]
|
||||
) {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.orgs.streamAiResponseInSite(params.organizationId, params.siteId, {
|
||||
input: params.input,
|
||||
output: params.output,
|
||||
model: params.model,
|
||||
});
|
||||
|
||||
for await (const event of res) {
|
||||
yield event;
|
||||
}
|
||||
}
|
||||
|
||||
let loggedServiceBinding = false;
|
||||
|
||||
/**
|
||||
|
||||
@@ -179,15 +179,4 @@ export interface GitBookDataFetcher {
|
||||
integrationName: string;
|
||||
request: api.RenderIntegrationUI;
|
||||
}): Promise<DataFetcherResponse<api.ContentKitRenderOutput>>;
|
||||
|
||||
/**
|
||||
* Stream an AI response.
|
||||
*/
|
||||
streamAIResponse(params: {
|
||||
organizationId: string;
|
||||
siteId: string;
|
||||
input: api.AIMessageInput[];
|
||||
output: api.AIOutputFormat;
|
||||
model: api.AIModel;
|
||||
}): AsyncGenerator<api.AIStreamResponse, void, unknown>;
|
||||
}
|
||||
|
||||
@@ -72,17 +72,6 @@ export function createLinker(
|
||||
|
||||
const siteBasePath = withTrailingSlash(withLeadingSlash(servedOn.siteBasePath));
|
||||
const spaceBasePath = withTrailingSlash(withLeadingSlash(servedOn.spaceBasePath));
|
||||
const protocol = (() => {
|
||||
if (servedOn.protocol) {
|
||||
return servedOn.protocol;
|
||||
}
|
||||
|
||||
if (servedOn.host) {
|
||||
return servedOn.host.startsWith('localhost') ? 'http:' : 'https:';
|
||||
}
|
||||
|
||||
return 'https:';
|
||||
})();
|
||||
|
||||
const linker: GitBookLinker = {
|
||||
toPathInSpace(relativePath: string): string {
|
||||
@@ -108,7 +97,7 @@ export function createLinker(
|
||||
return absolutePath;
|
||||
}
|
||||
|
||||
return `${protocol}//${joinPaths(servedOn.host, absolutePath)}`;
|
||||
return `${servedOn.protocol ?? 'https:'}//${joinPaths(servedOn.host, absolutePath)}`;
|
||||
},
|
||||
|
||||
toPathForPage({ pages, page, anchor }) {
|
||||
|
||||
@@ -276,9 +276,6 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
|
||||
internationalization: {
|
||||
locale: CustomizationLocale.En,
|
||||
},
|
||||
insights: {
|
||||
trackingCookie: true,
|
||||
},
|
||||
favicon: {},
|
||||
header: {
|
||||
preset: CustomizationHeaderPreset.Default,
|
||||
|
||||
@@ -29,11 +29,9 @@
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.3",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@sindresorhus/fnv1a": "^3.1.0",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@vercel/og": "0.6.8.",
|
||||
"ai": "^4.2.2",
|
||||
"assert-never": "^1.2.1",
|
||||
"bun-types": "^1.1.20",
|
||||
@@ -46,7 +44,7 @@
|
||||
"mathjax": "^3.2.2",
|
||||
"mdast-util-to-markdown": "^2.1.2",
|
||||
"memoizee": "^0.4.17",
|
||||
"next": "14.2.26",
|
||||
"next": "14.2.25",
|
||||
"next-themes": "^0.2.1",
|
||||
"nuqs": "^2.2.3",
|
||||
"object-hash": "^3.0.0",
|
||||
|
||||
@@ -14,15 +14,7 @@ interface JsonBody {
|
||||
* The body should be a JSON with { tags: string[] }
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
let json: JsonBody;
|
||||
|
||||
try {
|
||||
json = await req.json();
|
||||
} catch (err) {
|
||||
return NextResponse.json({
|
||||
error: `invalid json body: ${err}`,
|
||||
});
|
||||
}
|
||||
const json = (await req.json()) as JsonBody;
|
||||
|
||||
if (!json.tags || !Array.isArray(json.tags)) {
|
||||
return NextResponse.json(
|
||||
@@ -33,18 +25,10 @@ export async function POST(req: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await revalidateTags(json.tags);
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
stats: result.stats,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `${err}`,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
const result = await revalidateTags(json.tags);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
stats: result.stats,
|
||||
});
|
||||
}
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import { useScrollPage } from '@/components/hooks';
|
||||
export function PageClientLayout(props: { withSections?: boolean }) {
|
||||
// We use this hook in the page layout to ensure the elements for the blocks
|
||||
// are rendered before we scroll to a hash or to the top of the page
|
||||
useScrollPage({ scrollMarginTop: props.withSections ? 48 : undefined });
|
||||
useScrollPage({ scrollMarginTop: props.withSections ? 50 : undefined });
|
||||
|
||||
useStripFallbackQueryParam();
|
||||
return null;
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
'use client';
|
||||
import { useLanguage } from '@/intl/client';
|
||||
import { t } from '@/intl/translate';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useVisitedPages } from '../Insights';
|
||||
import { usePageContext } from '../PageContext';
|
||||
import { Loading } from '../primitives';
|
||||
import { streamLinkPageSummary } from './server-actions/streamLinkPageSummary';
|
||||
|
||||
@@ -12,18 +8,25 @@ 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 { targetSpaceId, targetPageId, linkPreview, linkTitle, showTrademark = true } = props;
|
||||
const {
|
||||
currentSpaceId,
|
||||
currentPageId,
|
||||
targetSpaceId,
|
||||
targetPageId,
|
||||
linkPreview,
|
||||
linkTitle,
|
||||
showTrademark = true,
|
||||
} = props;
|
||||
|
||||
const currentPage = usePageContext();
|
||||
|
||||
const language = useLanguage();
|
||||
const visitedPages = useVisitedPages((state) => state.pages);
|
||||
const [highlight, setHighlight] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -33,14 +36,13 @@ export function AIPageLinkSummary(props: {
|
||||
|
||||
(async () => {
|
||||
const stream = await streamLinkPageSummary({
|
||||
currentSpaceId: currentPage.spaceId,
|
||||
currentPageId: currentPage.pageId,
|
||||
currentPageTitle: currentPage.title,
|
||||
currentSpaceId,
|
||||
currentPageId,
|
||||
targetSpaceId,
|
||||
targetPageId,
|
||||
linkPreview,
|
||||
linkTitle,
|
||||
visitedPages,
|
||||
previousPageIds: [],
|
||||
});
|
||||
|
||||
for await (const highlight of stream) {
|
||||
@@ -52,25 +54,7 @@ export function AIPageLinkSummary(props: {
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [
|
||||
currentPage.pageId,
|
||||
currentPage.spaceId,
|
||||
currentPage.title,
|
||||
targetSpaceId,
|
||||
targetPageId,
|
||||
linkPreview,
|
||||
linkTitle,
|
||||
visitedPages,
|
||||
]);
|
||||
|
||||
const shimmerBlocks = [
|
||||
'w-[20%] [animation-delay:-1s]',
|
||||
'w-[35%] [animation-delay:-0.8s]',
|
||||
'w-[25%] [animation-delay:-0.6s]',
|
||||
'w-[10%] [animation-delay:-0.4s]',
|
||||
'w-[40%] [animation-delay:-0.2s]',
|
||||
'w-[30%] [animation-delay:0s]',
|
||||
];
|
||||
}, [currentSpaceId, currentPageId, targetSpaceId, targetPageId, linkPreview, linkTitle]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
@@ -80,23 +64,12 @@ export function AIPageLinkSummary(props: {
|
||||
) : (
|
||||
<Icon icon="sparkle" className="size-3" />
|
||||
)}
|
||||
<h6 className="text-tint">{t(language, 'link_tooltip_ai_summary')}</h6>
|
||||
<h6 className="text-tint">Page highlight</h6>
|
||||
</div>
|
||||
{highlight.length > 0 ? <p>{highlight}</p> : null}
|
||||
{highlight.length > 0 ? (
|
||||
<p className="animate-fadeIn">{highlight}</p>
|
||||
) : (
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{shimmerBlocks.map((block, index) => (
|
||||
<div
|
||||
key={`${index}-${block}`}
|
||||
className={`${block} h-4 animate-pulse rounded straight-corners:rounded-none bg-tint-active`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{highlight.length > 0 ? (
|
||||
<div className="animate-fadeIn text-tint-subtle text-xs">
|
||||
{t(language, 'link_tooltip_ai_summary_description')}
|
||||
<div className="text-tint-subtle text-xs">
|
||||
Based on your context. May contain mistakes.
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -54,9 +54,9 @@ export async function streamGenerateObject<T>(
|
||||
previousResponseId?: string;
|
||||
}
|
||||
) {
|
||||
const rawStream = context.dataFetcher.streamAIResponse({
|
||||
organizationId,
|
||||
siteId,
|
||||
const apiClient = await context.dataFetcher.api();
|
||||
|
||||
const rawStream = apiClient.orgs.streamAiResponseInSite(organizationId, siteId, {
|
||||
input: messages,
|
||||
output: {
|
||||
type: 'object',
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
'use server';
|
||||
import { filterOutNullable } from '@/lib/typescript';
|
||||
import { getV1BaseContext } from '@/lib/v1';
|
||||
import { isV2 } from '@/lib/v2';
|
||||
import { AIMessageRole } from '@gitbook/api';
|
||||
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
|
||||
import { getServerActionBaseContext } from '@v2/lib/server-actions';
|
||||
import { fetchServerActionSiteContext, getServerActionBaseContext } from '@v2/lib/server-actions';
|
||||
import { z } from 'zod';
|
||||
import { streamGenerateObject } from './api';
|
||||
|
||||
@@ -18,142 +17,104 @@ export async function* streamLinkPageSummary({
|
||||
targetPageId,
|
||||
linkPreview,
|
||||
linkTitle,
|
||||
visitedPages,
|
||||
}: {
|
||||
currentSpaceId: string;
|
||||
currentPageId: string;
|
||||
currentPageTitle: string;
|
||||
targetSpaceId: string;
|
||||
targetPageId: string;
|
||||
linkPreview?: string;
|
||||
linkTitle?: string;
|
||||
visitedPages?: Array<{ spaceId: string; pageId: string }>;
|
||||
previousPageIds?: string[];
|
||||
}) {
|
||||
const baseContext = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
|
||||
const siteURLData = await getSiteURLDataFromMiddleware();
|
||||
|
||||
const { stream } = await streamGenerateObject(
|
||||
baseContext,
|
||||
{
|
||||
organizationId: siteURLData.organization,
|
||||
siteId: siteURLData.site,
|
||||
},
|
||||
{
|
||||
schema: z.object({
|
||||
highlight: z
|
||||
.string()
|
||||
.describe('The reason why the user should read the target page.'),
|
||||
// questions: z.array(z.string().describe('The questions to sea')).max(3),
|
||||
}),
|
||||
messages: [
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# 1. Role
|
||||
You are a contextual fact extractor. Your job is to find the exact fact from the linked page that directly answers the implied question in the current paragraph.
|
||||
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.
|
||||
|
||||
# 2. Task
|
||||
Extract a contextually-relevant fact that:
|
||||
- Directly answers the specific need or question implied by the link's placement
|
||||
- States a capability, limitation, or specification from the target page
|
||||
- Connects precisely to the user's current paragraph or sentence
|
||||
- Completes the user's understanding based on what they're currently reading
|
||||
# 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”.
|
||||
|
||||
# 3. Instructions
|
||||
1. First, identify the exact need, question, or gap in the current paragraph where the link appears
|
||||
2. Find the specific fact in the target page that addresses this exact contextual need
|
||||
3. Ensure the fact relates directly to the context of the paragraph containing the link
|
||||
4. Avoid ALL instructional language including words like "use", "click", "select", "create"
|
||||
5. Keep it under 30 words, factual and declarative about what EXISTS or IS TRUE`,
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# 4. Current 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' as const,
|
||||
spaceId: currentSpaceId,
|
||||
pageId: currentPageId,
|
||||
},
|
||||
],
|
||||
},
|
||||
...(visitedPages
|
||||
? [
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: '# 5. Previous pages',
|
||||
},
|
||||
...visitedPages.map(({ spaceId, pageId }) => ({
|
||||
role: AIMessageRole.Developer,
|
||||
content: `## Page ${pageId}`,
|
||||
attachments: [
|
||||
{
|
||||
type: 'page' as const,
|
||||
spaceId,
|
||||
pageId,
|
||||
},
|
||||
],
|
||||
})),
|
||||
]
|
||||
: []),
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# 6. Target page
|
||||
attachments: [
|
||||
{
|
||||
type: 'page',
|
||||
spaceId: currentSpaceId,
|
||||
pageId: currentPageId,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `## Target page
|
||||
The content of the target page is:`,
|
||||
attachments: [
|
||||
{
|
||||
type: 'page' as const,
|
||||
spaceId: targetSpaceId,
|
||||
pageId: targetPageId,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# 7. Link preview
|
||||
attachments: [
|
||||
{
|
||||
type: 'page',
|
||||
spaceId: targetSpaceId,
|
||||
pageId: targetPageId,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `## Link preview
|
||||
The content of the link preview is:
|
||||
> ${linkPreview}
|
||||
> Page ID: ${targetPageId}`,
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# 8. Guidelines & Examples
|
||||
ALWAYS:
|
||||
- ALWAYS choose facts that directly fulfill the contextual need where the link appears
|
||||
- ALWAYS connect target page information specifically to the current paragraph context
|
||||
- ALWAYS focus on the gap in knowledge that the link is meant to fill
|
||||
- ALWAYS consider user's navigation history to ensure contextual continuity
|
||||
- ALWAYS use action verbs like "click", "select", "use", "create", "enable"
|
||||
|
||||
NEVER:
|
||||
- NEVER include ANY unspecifc language like "learn", "how to", "discover", etc. State the fact directly.
|
||||
- NEVER select general facts unrelated to the specific link context
|
||||
- NEVER ignore the specific context where the link appears
|
||||
- NEVER repeat the same fact in different words
|
||||
|
||||
## Examples
|
||||
Current paragraph: "When organizing content, headings are limited to 3 levels. For more advanced editing, you can use (multiple select)[/multiple-select] to move multiple blocks at once."
|
||||
Preview: "Multiple Select: Select multiple content blocks at once."
|
||||
✓ "Shift selects content between two points, useful for reorganizing your current heading structure."
|
||||
✗ "Shift and Ctrl/Cmd keys are the modifiers for selecting multiple blocks."
|
||||
|
||||
Current paragraph: "Most changes can be published directly, but for major revisions, if you want others to review changes before publishing, create a (change request)[/change-requests]."
|
||||
Preview: "Change Requests: Collaborative content editing workflow."
|
||||
✓ "Each reviewer's approval is tracked separately, with specific change highlighting for your major revisions."
|
||||
✗ "Each reviewer receives an email notification and can approve or request changes."
|
||||
|
||||
Current paragraph: "Your team mentioned issues with conflicting edits. Need to collaborate in real-time? You can use (live edit mode)[/live-edit]."
|
||||
Preview: "Live Edit: Real-time collaborative editing."
|
||||
✓ "Teams with GitHub repositories (like yours) cannot use this feature due to sync limitations."
|
||||
✗ "Incompatible with GitHub/GitLab sync and requires specific visibility settings."`,
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.User,
|
||||
content: `I'm considering reading the link titled "${linkTitle}" pointing to page ${targetPageId}. Why should I read it? Relate it to the paragraph I'm currently reading.`,
|
||||
},
|
||||
].filter(filterOutNullable),
|
||||
}
|
||||
);
|
||||
> ${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;
|
||||
|
||||
@@ -20,15 +20,7 @@ export function Heading(props: BlockProps<DocumentBlockHeading>) {
|
||||
return (
|
||||
<Tag
|
||||
id={id}
|
||||
className={tcls(
|
||||
textStyle.textSize,
|
||||
'heading',
|
||||
'group',
|
||||
'relative',
|
||||
'grid',
|
||||
'scroll-m-12',
|
||||
style
|
||||
)}
|
||||
className={tcls(textStyle.textSize, 'heading', 'group', 'relative', 'grid', style)}
|
||||
>
|
||||
<div
|
||||
className={tcls(
|
||||
|
||||
@@ -31,7 +31,12 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
|
||||
const isExternal = inline.data.ref.kind === 'url';
|
||||
|
||||
return (
|
||||
<InlineLinkTooltip inline={inline} context={context.contentContext} resolved={resolved}>
|
||||
<InlineLinkTooltip
|
||||
inline={inline}
|
||||
document={document}
|
||||
context={context}
|
||||
ancestorInlines={ancestorInlines}
|
||||
>
|
||||
<StyledLink
|
||||
href={resolved.href}
|
||||
insights={{
|
||||
|
||||
@@ -1,56 +1,60 @@
|
||||
import type { DocumentInlineLink } from '@gitbook/api';
|
||||
|
||||
import type { ResolvedContentRef } from '@/lib/references';
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
|
||||
import { getSpaceLanguage } from '@/intl/server';
|
||||
import { tString } from '@/intl/translate';
|
||||
import { languages } from '@/intl/translations';
|
||||
import { getNodeText } from '@/lib/document';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||
import type { GitBookAnyContext } from '@v2/lib/context';
|
||||
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: {
|
||||
inline: DocumentInlineLink;
|
||||
context: GitBookAnyContext;
|
||||
children: React.ReactNode;
|
||||
resolved: ResolvedContentRef;
|
||||
}) {
|
||||
const { inline, context, resolved, children } = props;
|
||||
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 language =
|
||||
'customization' in context ? getSpaceLanguage(context.customization) : languages.en;
|
||||
const isExternal = inline.data.ref.kind === 'url';
|
||||
const isSamePage = inline.data.ref.kind === 'anchor' && inline.data.ref.page === undefined;
|
||||
if (isExternal) {
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: tString(language, 'link_tooltip_external_link'),
|
||||
label: 'External link to',
|
||||
},
|
||||
];
|
||||
}
|
||||
if (isSamePage) {
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: tString(language, 'link_tooltip_page_anchor'),
|
||||
label: 'Jump to section',
|
||||
icon: <Icon icon="arrow-down-short-wide" className="size-3" />,
|
||||
},
|
||||
];
|
||||
resolved.subText = undefined;
|
||||
}
|
||||
|
||||
const hasAISummary =
|
||||
!isExternal &&
|
||||
!isSamePage &&
|
||||
'customization' in context &&
|
||||
context.customization.ai?.pageLinkSummaries.enabled &&
|
||||
(inline.data.ref.kind === 'page' || inline.data.ref.kind === 'anchor');
|
||||
|
||||
return (
|
||||
<Tooltip.Provider delayDuration={200}>
|
||||
<Tooltip.Root>
|
||||
@@ -59,7 +63,7 @@ export async function InlineLinkTooltip(props: {
|
||||
<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="flex items-start gap-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">
|
||||
@@ -113,13 +117,13 @@ export async function InlineLinkTooltip(props: {
|
||||
className={tcls(
|
||||
'-mx-2 -my-2 ml-auto',
|
||||
breadcrumbs?.length === 0
|
||||
? 'place-self-center'
|
||||
: null
|
||||
? null
|
||||
: 'place-self-start'
|
||||
)}
|
||||
variant="blank"
|
||||
href={resolved.href}
|
||||
target="_blank"
|
||||
label={tString(language, 'open_in_new_tab')}
|
||||
label="Open in new tab"
|
||||
size="small"
|
||||
icon="arrow-up-right-from-square"
|
||||
iconOnly={true}
|
||||
@@ -131,26 +135,41 @@ export async function InlineLinkTooltip(props: {
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{hasAISummary && 'page' in context && 'page' in inline.data.ref ? (
|
||||
{'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={
|
||||
resolved.page?.id ??
|
||||
inline.data.ref.page ??
|
||||
context.page.id
|
||||
inline.data.ref.page ?? context.contentContext.page.id
|
||||
}
|
||||
targetSpaceId={inline.data.ref.space ?? context.space.id}
|
||||
linkTitle={getNodeText(inline)}
|
||||
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={
|
||||
'customization' in context &&
|
||||
context.customization.trademark.enabled
|
||||
context.contentContext.customization.trademark.enabled
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Tooltip.Arrow className={hasAISummary ? 'fill-tint-3' : 'fill-tint-1'} />
|
||||
<Tooltip.Arrow className="fill-tint-1" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import type { JSONDocument } from '@gitbook/api';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { OpenAPIOperation as BaseOpenAPIOperation } from '@gitbook/react-openapi';
|
||||
|
||||
import { resolveOpenAPIOperationBlock } from '@/lib/openapi/resolveOpenAPIOperationBlock';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import type { BlockProps } from '../Block';
|
||||
import { PlainCodeBlock } from '../CodeBlock';
|
||||
import { DocumentView } from '../DocumentView';
|
||||
import { Heading } from '../Heading';
|
||||
|
||||
import './scalar.css';
|
||||
import './style.css';
|
||||
import type { AnyOpenAPIOperationsBlock } from '@/lib/openapi/types';
|
||||
import { getOpenAPIContext } from './context';
|
||||
|
||||
/**
|
||||
* Render an openapi block or an openapi-operation block.
|
||||
@@ -51,7 +55,56 @@ async function OpenAPIOperationBody(props: BlockProps<AnyOpenAPIOperationsBlock>
|
||||
return (
|
||||
<BaseOpenAPIOperation
|
||||
data={data}
|
||||
context={getOpenAPIContext({ props, specUrl })}
|
||||
context={{
|
||||
specUrl,
|
||||
icons: {
|
||||
chevronDown: <Icon icon="chevron-down" />,
|
||||
chevronRight: <Icon icon="chevron-right" />,
|
||||
plus: <Icon icon="plus" />,
|
||||
},
|
||||
renderCodeBlock: (codeProps) => <PlainCodeBlock {...codeProps} />,
|
||||
renderDocument: (documentProps) => (
|
||||
<DocumentView
|
||||
document={documentProps.document as JSONDocument}
|
||||
context={props.context}
|
||||
style="space-y-6"
|
||||
blockStyle="max-w-full"
|
||||
/>
|
||||
),
|
||||
renderHeading: (headingProps) => (
|
||||
<Heading
|
||||
document={props.document}
|
||||
ancestorBlocks={props.ancestorBlocks}
|
||||
isEstimatedOffscreen={props.isEstimatedOffscreen}
|
||||
context={props.context}
|
||||
style={tcls([
|
||||
headingProps.deprecated ? 'line-through' : undefined,
|
||||
headingProps.deprecated || !!headingProps.stability
|
||||
? '[&>div]:mt-0'
|
||||
: 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,
|
||||
}}
|
||||
className="openapi-block"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { resolveOpenAPISchemasBlock } from '@/lib/openapi/resolveOpenAPISchemasBlock';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { OpenAPISchemas as BaseOpenAPISchemas } from '@gitbook/react-openapi';
|
||||
|
||||
import type { BlockProps } from '../Block';
|
||||
@@ -7,7 +8,6 @@ import type { BlockProps } from '../Block';
|
||||
import './scalar.css';
|
||||
import './style.css';
|
||||
import type { OpenAPISchemasBlock } from '@/lib/openapi/types';
|
||||
import { getOpenAPIContext } from './context';
|
||||
|
||||
/**
|
||||
* Render an openapi-schemas block.
|
||||
@@ -49,9 +49,19 @@ async function OpenAPISchemasBody(props: BlockProps<OpenAPISchemasBlock>) {
|
||||
|
||||
return (
|
||||
<BaseOpenAPISchemas
|
||||
schemas={data.schemas}
|
||||
data={data}
|
||||
grouped={block.data.grouped}
|
||||
context={getOpenAPIContext({ props, specUrl })}
|
||||
context={{
|
||||
specUrl,
|
||||
icons: {
|
||||
chevronDown: <Icon icon="chevron-down" />,
|
||||
chevronRight: <Icon icon="chevron-right" />,
|
||||
plus: <Icon icon="plus" />,
|
||||
},
|
||||
defaultInteractiveOpened: context.mode === 'print',
|
||||
id: block.meta?.id,
|
||||
blockKey: block.key,
|
||||
}}
|
||||
className="openapi-block"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import type { JSONDocument } from '@gitbook/api';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import type { OpenAPIContext } from '@gitbook/react-openapi';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import type { BlockProps } from '../Block';
|
||||
import { PlainCodeBlock } from '../CodeBlock';
|
||||
import { DocumentView } from '../DocumentView';
|
||||
import { Heading } from '../Heading';
|
||||
|
||||
import './scalar.css';
|
||||
import './style.css';
|
||||
import type { AnyOpenAPIOperationsBlock, OpenAPISchemasBlock } from '@/lib/openapi/types';
|
||||
|
||||
/**
|
||||
* Get the OpenAPI context to render a block.
|
||||
*/
|
||||
export function getOpenAPIContext(args: {
|
||||
props: BlockProps<AnyOpenAPIOperationsBlock | OpenAPISchemasBlock>;
|
||||
specUrl: string;
|
||||
}): OpenAPIContext {
|
||||
const { props, specUrl } = args;
|
||||
const { block } = props;
|
||||
return {
|
||||
specUrl,
|
||||
icons: {
|
||||
chevronDown: <Icon icon="chevron-down" />,
|
||||
chevronRight: <Icon icon="chevron-right" />,
|
||||
plus: <Icon icon="plus" />,
|
||||
},
|
||||
renderCodeBlock: (codeProps) => <PlainCodeBlock {...codeProps} />,
|
||||
renderDocument: (documentProps) => (
|
||||
<DocumentView
|
||||
document={documentProps.document as JSONDocument}
|
||||
context={props.context}
|
||||
style="space-y-6"
|
||||
blockStyle="max-w-full"
|
||||
/>
|
||||
),
|
||||
renderHeading: (headingProps) => (
|
||||
<Heading
|
||||
document={props.document}
|
||||
ancestorBlocks={props.ancestorBlocks}
|
||||
isEstimatedOffscreen={props.isEstimatedOffscreen}
|
||||
context={props.context}
|
||||
style={tcls([
|
||||
headingProps.deprecated ? 'line-through' : undefined,
|
||||
headingProps.deprecated || !!headingProps.stability
|
||||
? '[&>div]:mt-0'
|
||||
: 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: props.context.mode === 'print',
|
||||
id: block.meta?.id,
|
||||
blockKey: block.key,
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
/* Layout Components */
|
||||
.openapi-operation,
|
||||
.openapi-schemas {
|
||||
.openapi-operation {
|
||||
@apply flex-1 flex flex-col gap-8 mb-14 min-w-0;
|
||||
}
|
||||
|
||||
@@ -18,7 +17,7 @@
|
||||
}
|
||||
|
||||
.openapi-summary {
|
||||
@apply flex flex-col items-start justify-start gap-3 scroll-m-12;
|
||||
@apply flex flex-col items-start justify-start gap-3;
|
||||
}
|
||||
|
||||
.openapi-summary-tags {
|
||||
@@ -152,10 +151,6 @@
|
||||
/* unstyled */
|
||||
}
|
||||
|
||||
.openapi-schema-root-description.openapi-markdown {
|
||||
@apply prose-sm text-balance mt-1.5 !text-[0.813rem] text-tint overflow-hidden !font-normal select-text prose-strong:font-semibold prose-strong:text-inherit;
|
||||
}
|
||||
|
||||
.openapi-schema-properties {
|
||||
@apply flex flex-col;
|
||||
}
|
||||
@@ -204,7 +199,7 @@
|
||||
.openapi-schema-name {
|
||||
/* To make double click on the property name select only the name,
|
||||
we disable selection on the parent and re-enable it on the children. */
|
||||
@apply select-none text-sm text-balance *:whitespace-nowrap flex flex-wrap gap-y-1.5 gap-x-2.5;
|
||||
@apply select-none flex gap-x-2.5 items-baseline text-sm flex-wrap;
|
||||
}
|
||||
|
||||
.openapi-schema-name .openapi-deprecated {
|
||||
@@ -283,7 +278,7 @@
|
||||
|
||||
/* Schema Description */
|
||||
.openapi-schema-description.openapi-markdown {
|
||||
@apply prose-sm text-tint overflow-hidden text-pretty !font-normal select-text prose-strong:font-semibold prose-strong:text-inherit;
|
||||
@apply prose-sm text-tint overflow-hidden !font-normal select-text prose-strong:font-semibold prose-strong:text-inherit;
|
||||
}
|
||||
|
||||
.openapi-schema-description.openapi-markdown pre:has(code) {
|
||||
@@ -303,15 +298,13 @@
|
||||
|
||||
/* Schema Examples */
|
||||
.openapi-schema-example,
|
||||
.openapi-schema-pattern,
|
||||
.openapi-schema-default {
|
||||
.openapi-schema-pattern {
|
||||
@apply prose-sm text-tint;
|
||||
}
|
||||
|
||||
.openapi-schema-example code,
|
||||
.openapi-schema-pattern code,
|
||||
.openapi-schema-enum-value code,
|
||||
.openapi-schema-default code {
|
||||
.openapi-schema-enum-value code {
|
||||
@apply py-px px-1 min-w-[1.625rem] text-tint-strong font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint bg-tint rounded text-xs leading-[calc(max(1.20em,1.25rem))] before:!content-none after:!content-none;
|
||||
}
|
||||
|
||||
@@ -325,7 +318,7 @@
|
||||
}
|
||||
|
||||
.openapi-securities-description.openapi-markdown {
|
||||
@apply prose-sm text-tint !font-normal select-text text-pretty prose-strong:font-semibold prose-strong:text-inherit;
|
||||
@apply prose-sm text-tint !font-normal select-text prose-strong:font-semibold prose-strong:text-inherit;
|
||||
}
|
||||
|
||||
.openapi-securities-label {
|
||||
@@ -351,7 +344,7 @@
|
||||
}
|
||||
|
||||
.openapi-requestbody-description.openapi-markdown {
|
||||
@apply prose-sm text-tint !font-normal text-pretty select-text prose-strong:font-semibold prose-strong:text-inherit;
|
||||
@apply prose-sm text-tint !font-normal select-text prose-strong:font-semibold prose-strong:text-inherit;
|
||||
}
|
||||
|
||||
/* Responses */
|
||||
@@ -368,7 +361,7 @@
|
||||
}
|
||||
|
||||
.openapi-response-description.openapi-markdown {
|
||||
@apply text-left prose-sm text-[0.813rem] text-pretty h-auto relative leading-[1.125rem] text-tint !font-normal truncate select-text prose-strong:font-semibold prose-strong:text-inherit;
|
||||
@apply text-left prose-sm text-[0.813rem] h-auto relative leading-[1.125rem] text-tint !font-normal truncate select-text prose-strong:font-semibold prose-strong:text-inherit;
|
||||
}
|
||||
|
||||
.openapi-response-description.openapi-markdown::-webkit-scrollbar {
|
||||
@@ -484,30 +477,12 @@
|
||||
@apply flex flex-row items-center py-2 px-3 justify-end border-t border-tint-subtle;
|
||||
}
|
||||
|
||||
/* Panel */
|
||||
.openapi-panel {
|
||||
/* Response Example */
|
||||
.openapi-response-example {
|
||||
@apply border rounded bg-tint border-tint-subtle;
|
||||
}
|
||||
|
||||
.openapi-panel-heading {
|
||||
@apply font-medium px-4 py-2 text-xs uppercase;
|
||||
}
|
||||
|
||||
.openapi-panel-body {
|
||||
@apply relative;
|
||||
@apply before:w-full before:h-px before:absolute before:bg-tint-6 before:-top-px before:z-10;
|
||||
}
|
||||
|
||||
.openapi-panel-footer {
|
||||
@apply px-3 py-2 pt-2.5 border-t border-tint-subtle text-[0.813rem] text-tint;
|
||||
}
|
||||
|
||||
.openapi-panel-footer .openapi-markdown {
|
||||
@apply text-[0.813rem] text-tint;
|
||||
}
|
||||
|
||||
/* Example */
|
||||
.openapi-example-empty {
|
||||
.openapi-response-example-empty {
|
||||
@apply relative text-tint bg-tint min-h-20 flex flex-col justify-center items-center;
|
||||
}
|
||||
|
||||
@@ -578,6 +553,15 @@
|
||||
|
||||
.openapi-tabs-panel {
|
||||
@apply flex-1 text-sm relative focus-visible:outline-none;
|
||||
@apply before:w-full before:h-px before:absolute before:bg-tint-6 before:-top-px before:z-10;
|
||||
}
|
||||
|
||||
.openapi-tabs-footer {
|
||||
@apply px-3 py-2 pt-2.5 border-t border-tint-subtle text-[0.813rem] text-tint;
|
||||
}
|
||||
|
||||
.openapi-tabs-footer .openapi-markdown {
|
||||
@apply text-[0.813rem] text-tint;
|
||||
}
|
||||
|
||||
/* Disclosure group */
|
||||
|
||||
@@ -8,7 +8,6 @@ import { useDebounceCallback, useEventCallback } from 'usehooks-ts';
|
||||
import type { VisitorAuthClaims } from '@/lib/adaptive';
|
||||
import { getAllBrowserCookiesMap } from '@/lib/browser-cookies';
|
||||
import { getSession } from './sessions';
|
||||
import { useVisitedPages } from './useVisitedPages';
|
||||
import { getVisitorId } from './visitorId';
|
||||
|
||||
export type InsightsEventName = api.SiteInsightsEvent['type'];
|
||||
@@ -64,19 +63,9 @@ type TrackEventCallback = <EventName extends InsightsEventName>(
|
||||
const InsightsContext = React.createContext<TrackEventCallback>(() => {});
|
||||
|
||||
interface InsightsProviderProps extends InsightsEventContext {
|
||||
/** If true, the events will be sent to the server. */
|
||||
enabled: boolean;
|
||||
|
||||
/** If true, the visitor cookie tracking will be used */
|
||||
visitorCookieTrackingEnabled: boolean;
|
||||
|
||||
/** The URL of the app. */
|
||||
appURL: string;
|
||||
|
||||
/** The host of the API. */
|
||||
apiHost: string;
|
||||
|
||||
/** The children of the provider. */
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -84,9 +73,8 @@ interface InsightsProviderProps extends InsightsEventContext {
|
||||
* Wrap the content of the app with the InsightsProvider to track events.
|
||||
*/
|
||||
export function InsightsProvider(props: InsightsProviderProps) {
|
||||
const { enabled, appURL, apiHost, children, visitorCookieTrackingEnabled, ...context } = props;
|
||||
const { enabled, appURL, apiHost, children, ...context } = props;
|
||||
|
||||
const addVisitedPage = useVisitedPages((state) => state.addPage);
|
||||
const visitorIdRef = React.useRef<string | null>(null);
|
||||
const eventsRef = React.useRef<{
|
||||
[pathname: string]:
|
||||
@@ -136,14 +124,6 @@ export function InsightsProvider(props: InsightsProviderProps) {
|
||||
...eventsForPathname,
|
||||
events: [],
|
||||
};
|
||||
|
||||
// Mark the page as visited in our local state
|
||||
if (eventsForPathname.pageContext.pageId) {
|
||||
addVisitedPage({
|
||||
spaceId: context.spaceId,
|
||||
pageId: eventsForPathname.pageContext.pageId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (allEvents.length > 0) {
|
||||
@@ -160,8 +140,7 @@ export function InsightsProvider(props: InsightsProviderProps) {
|
||||
});
|
||||
|
||||
const flushBatchedEvents = useDebounceCallback(async () => {
|
||||
const visitorId =
|
||||
visitorIdRef.current ?? (await getVisitorId(appURL, visitorCookieTrackingEnabled));
|
||||
const visitorId = visitorIdRef.current ?? (await getVisitorId(appURL));
|
||||
visitorIdRef.current = visitorId;
|
||||
|
||||
flushEventsSync();
|
||||
@@ -205,7 +184,7 @@ export function InsightsProvider(props: InsightsProviderProps) {
|
||||
* Get the visitor ID and store it in a ref.
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
getVisitorId(appURL, visitorCookieTrackingEnabled).then((visitorId) => {
|
||||
getVisitorId(appURL).then((visitorId) => {
|
||||
visitorIdRef.current = visitorId;
|
||||
// When the page is unloaded, flush all events, but only if the visitor ID is set
|
||||
window.addEventListener('beforeunload', flushEventsSync);
|
||||
@@ -213,7 +192,7 @@ export function InsightsProvider(props: InsightsProviderProps) {
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', flushEventsSync);
|
||||
};
|
||||
}, [flushEventsSync, appURL, visitorCookieTrackingEnabled]);
|
||||
}, [flushEventsSync, appURL]);
|
||||
|
||||
return (
|
||||
<InsightsContext.Provider value={trackEvent}>
|
||||
|
||||
@@ -2,4 +2,3 @@ export * from './InsightsProvider';
|
||||
export * from './visitorId';
|
||||
export * from './cookies';
|
||||
export * from './TrackPageViewEvent';
|
||||
export * from './useVisitedPages';
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
type VisitedPage = {
|
||||
spaceId: string;
|
||||
pageId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A store for the pages that have been visited in the current session.
|
||||
*/
|
||||
export const useVisitedPages = create<{
|
||||
pages: VisitedPage[];
|
||||
addPage: (page: VisitedPage) => void;
|
||||
}>((set) => ({
|
||||
pages: [],
|
||||
addPage: (page) =>
|
||||
set((state) => {
|
||||
const lastPage = state.pages[state.pages.length - 1];
|
||||
if (lastPage && lastPage.spaceId === page.spaceId && lastPage.pageId === page.pageId) {
|
||||
return { pages: state.pages };
|
||||
}
|
||||
|
||||
return { pages: [...state.pages, page] };
|
||||
}),
|
||||
}));
|
||||
@@ -13,13 +13,10 @@ let pendingVisitorId: Promise<string> | null = null;
|
||||
/**
|
||||
* Return the current visitor identifier.
|
||||
*/
|
||||
export async function getVisitorId(
|
||||
appURL: string,
|
||||
visitorCookieTrackingEnabled: boolean
|
||||
): Promise<string> {
|
||||
export async function getVisitorId(appURL: string): Promise<string> {
|
||||
if (!visitorId) {
|
||||
if (!pendingVisitorId) {
|
||||
pendingVisitorId = fetchVisitorID(appURL, visitorCookieTrackingEnabled).finally(() => {
|
||||
pendingVisitorId = fetchVisitorID(appURL).finally(() => {
|
||||
pendingVisitorId = null;
|
||||
});
|
||||
}
|
||||
@@ -33,13 +30,10 @@ export async function getVisitorId(
|
||||
/**
|
||||
* Propose a visitor identifier to the GitBook.com server and get the devideId back.
|
||||
*/
|
||||
async function fetchVisitorID(
|
||||
appURL: string,
|
||||
visitorCookieTrackingEnabled: boolean
|
||||
): Promise<string> {
|
||||
async function fetchVisitorID(appURL: string): Promise<string> {
|
||||
const withoutCookies = isCookiesTrackingDisabled();
|
||||
|
||||
if (withoutCookies || !visitorCookieTrackingEnabled) {
|
||||
if (withoutCookies) {
|
||||
return generateRandomId();
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import type { RevisionPageDocument, RevisionPageDocumentCover } from '@gitbook/api';
|
||||
import type { GitBookSiteContext } from '@v2/lib/context';
|
||||
import type { StaticImageData } from 'next/image';
|
||||
|
||||
import { Image, type ImageSize } from '@/components/utils';
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import defaultPageCoverSVG from './default-page-cover.svg';
|
||||
import defaultPageCover from './default-page-cover.svg';
|
||||
|
||||
const defaultPageCover = defaultPageCoverSVG as StaticImageData;
|
||||
const PAGE_COVER_SIZE: ImageSize = { width: 1990, height: 480 };
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,7 +27,7 @@ export async function PageHeader(props: {
|
||||
>
|
||||
{ancestors.length > 0 && (
|
||||
<nav>
|
||||
<ol className={tcls('flex', 'flex-wrap', 'items-center', 'gap-2', 'text-tint')}>
|
||||
<ol className={tcls('flex', 'flex-wrap', 'items-center', 'gap-2')}>
|
||||
{ancestors.map((breadcrumb, index) => {
|
||||
const href = linker.toPathForPage({ pages, page: breadcrumb });
|
||||
return (
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export type PageContextType = {
|
||||
pageId: string;
|
||||
spaceId: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export const PageContext = React.createContext<PageContextType | null>(null);
|
||||
|
||||
/**
|
||||
* Client side context provider to pass information about the current page.
|
||||
*/
|
||||
export function PageContextProvider(props: PageContextType & { children: React.ReactNode }) {
|
||||
const { pageId, spaceId, title, children } = props;
|
||||
|
||||
const value = React.useMemo(() => ({ pageId, spaceId, title }), [pageId, spaceId, title]);
|
||||
|
||||
return <PageContext.Provider value={value}>{children}</PageContext.Provider>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to use the page context.
|
||||
*/
|
||||
export function usePageContext() {
|
||||
const context = React.useContext(PageContext);
|
||||
if (!context) {
|
||||
throw new Error('usePageContext must be used within a PageContextProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './PageContext';
|
||||
@@ -11,7 +11,7 @@ import { useScrollPage } from '@/components/hooks';
|
||||
export function PageClientLayout(props: { withSections?: boolean }) {
|
||||
// We use this hook in the page layout to ensure the elements for the blocks
|
||||
// are rendered before we scroll to a hash or to the top of the page
|
||||
useScrollPage({ scrollMarginTop: props.withSections ? 48 : undefined });
|
||||
useScrollPage({ scrollMarginTop: props.withSections ? 50 : undefined });
|
||||
|
||||
useStripFallbackQueryParam();
|
||||
return null;
|
||||
|
||||
@@ -11,10 +11,12 @@ import { getPagePath } from '@/lib/pages';
|
||||
import { isPageIndexable, isSiteIndexable } from '@/lib/seo';
|
||||
|
||||
import { getResizedImageURL } from '@v2/lib/images';
|
||||
import { PageContextProvider } from '../PageContext';
|
||||
import { PageClientLayout } from './PageClientLayout';
|
||||
import { type PagePathParams, fetchPageData, getPathnameParam } from './fetch';
|
||||
|
||||
export const runtime = 'edge';
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export type SitePageProps = {
|
||||
context: GitBookSiteContext;
|
||||
pageParams: PagePathParams;
|
||||
@@ -65,7 +67,7 @@ export async function SitePage(props: SitePageProps) {
|
||||
const document = await getPageDocument(context.dataFetcher, context.space, page);
|
||||
|
||||
return (
|
||||
<PageContextProvider pageId={page.id} spaceId={context.space.id} title={page.title}>
|
||||
<>
|
||||
{withFullPageCover && page.cover ? (
|
||||
<PageCover as="full" page={page} cover={page.cover} context={context} />
|
||||
) : null}
|
||||
@@ -90,7 +92,7 @@ export async function SitePage(props: SitePageProps) {
|
||||
<React.Suspense fallback={null}>
|
||||
<PageClientLayout withSections={withSections} />
|
||||
</React.Suspense>
|
||||
</PageContextProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,6 @@ export function SpaceLayout(props: {
|
||||
revisionId={context.revisionId}
|
||||
spaceId={context.space.id}
|
||||
visitorAuthClaims={visitorAuthClaims}
|
||||
visitorCookieTrackingEnabled={context.customization.insights?.trackingCookie}
|
||||
>
|
||||
<Announcement context={context} />
|
||||
<Header withTopHeader={withTopHeader} context={context} />
|
||||
|
||||
@@ -256,7 +256,7 @@ function ZoomImageModal(props: {
|
||||
)}
|
||||
onClick={onClose}
|
||||
>
|
||||
<Icon icon="close" className={tcls('size-5')} />
|
||||
<Icon icon="compress-wide" className={tcls('size-5')} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -58,9 +58,4 @@ export const de = {
|
||||
'Das PDF konnte für ${1} Seiten nicht generiert werden, Generierung wurde bei ${2} gestoppt.',
|
||||
pdf_limit_reached_continue: 'Mit ${1} weiteren Seiten erweitern.',
|
||||
more: 'Mehr',
|
||||
link_tooltip_external_link: 'Externe Verlinkung zu',
|
||||
link_tooltip_page_anchor: 'Zum Abschnitt springen',
|
||||
link_tooltip_ai_summary: 'Seitenhighlight',
|
||||
link_tooltip_ai_summary_description: 'Basierend auf Ihrem Kontext. Kann Fehler enthalten.',
|
||||
open_in_new_tab: 'In neuem Tab öffnen',
|
||||
};
|
||||
|
||||
@@ -56,9 +56,4 @@ export const en = {
|
||||
pdf_limit_reached: "Couldn't generate the PDF for ${1} pages, generation stopped at ${2}.",
|
||||
pdf_limit_reached_continue: 'Extend with ${1} more pages.',
|
||||
more: 'More',
|
||||
link_tooltip_external_link: 'External link to',
|
||||
link_tooltip_page_anchor: 'Jump to section',
|
||||
link_tooltip_ai_summary: 'Page highlight',
|
||||
link_tooltip_ai_summary_description: 'Based on your context. May contain mistakes.',
|
||||
open_in_new_tab: 'Open in new tab',
|
||||
};
|
||||
|
||||
@@ -60,9 +60,4 @@ export const es: TranslationLanguage = {
|
||||
'No se pudo generar el PDF para ${1} páginas, la generación se detuvo en ${2}.',
|
||||
pdf_limit_reached_continue: 'Extender con ${1} páginas más.',
|
||||
more: 'Más',
|
||||
link_tooltip_external_link: 'Enlace externo a',
|
||||
link_tooltip_page_anchor: 'Saltar a la sección',
|
||||
link_tooltip_ai_summary: 'Resumen de la página',
|
||||
link_tooltip_ai_summary_description: 'Basado en tu contexto. Puede contener errores.',
|
||||
open_in_new_tab: 'Abrir en una nueva pestaña',
|
||||
};
|
||||
|
||||
@@ -58,9 +58,4 @@ export const fr: TranslationLanguage = {
|
||||
pdf_limit_reached: 'Impossible de générer le PDF pour ${1} pages, génération arrêtée à ${2}.',
|
||||
pdf_limit_reached_continue: 'Étendre avec ${1} pages supplémentaires.',
|
||||
more: 'Plus',
|
||||
link_tooltip_external_link: 'Lien externe à',
|
||||
link_tooltip_page_anchor: 'Sauter à la section',
|
||||
link_tooltip_ai_summary: 'Résumé de la page',
|
||||
link_tooltip_ai_summary_description: 'Basé sur votre contexte. Peut contenir des erreurs.',
|
||||
open_in_new_tab: 'Ouvrir dans un nouvel onglet',
|
||||
};
|
||||
|
||||
@@ -58,10 +58,4 @@ export const ja: TranslationLanguage = {
|
||||
pdf_limit_reached: '${1}ページのPDFを生成できませんでした、${2}で生成が停止しました。',
|
||||
pdf_limit_reached_continue: 'さらに${1}ページで拡張',
|
||||
more: '詳細',
|
||||
link_tooltip_external_link: '外部リンク先',
|
||||
link_tooltip_page_anchor: 'ページ内リンク先',
|
||||
link_tooltip_ai_summary: 'ページのハイライト',
|
||||
link_tooltip_ai_summary_description:
|
||||
'あなたのコンテキストに基づいています。間違いが含まれる可能性があります。',
|
||||
open_in_new_tab: '新しいタブで開く',
|
||||
};
|
||||
|
||||
@@ -58,9 +58,4 @@ export const nl: TranslationLanguage = {
|
||||
pdf_limit_reached: "Kon de PDF niet genereren voor ${1} pagina's, generatie gestopt bij ${2}.",
|
||||
pdf_limit_reached_continue: 'Verleng met ${1} extra pagina’s.',
|
||||
more: 'Meer',
|
||||
link_tooltip_external_link: 'Externe link naar',
|
||||
link_tooltip_page_anchor: 'Spring naar sectie',
|
||||
link_tooltip_ai_summary: 'Pagina-samenvatting',
|
||||
link_tooltip_ai_summary_description: 'Gebaseerd op je context. Kan fouten bevatten.',
|
||||
open_in_new_tab: 'Open in nieuw tabblad',
|
||||
};
|
||||
|
||||
@@ -58,9 +58,4 @@ export const no: TranslationLanguage = {
|
||||
pdf_limit_reached: 'Kunne ikke generere PDF for ${1} sider, generering stoppet ved ${2}.',
|
||||
pdf_limit_reached_continue: 'Utvid med ${1} flere sider.',
|
||||
more: 'Mer',
|
||||
link_tooltip_external_link: 'Ekstern lenke til',
|
||||
link_tooltip_page_anchor: 'Hopp til seksjon',
|
||||
link_tooltip_ai_summary: 'Sidesammendrag',
|
||||
link_tooltip_ai_summary_description: 'Basert på din kontekst. Kan inneholde feil.',
|
||||
open_in_new_tab: 'Åpne i ny fane',
|
||||
};
|
||||
|
||||
@@ -58,9 +58,4 @@ export const pt_br = {
|
||||
'Não foi possível gerar o PDF para ${1} páginas, generation stopped at ${2}.',
|
||||
pdf_limit_reached_continue: 'Extender com mais ${1} páginas.',
|
||||
more: 'Mais',
|
||||
link_tooltip_external_link: 'Link externo para',
|
||||
link_tooltip_page_anchor: 'Pular para a seção',
|
||||
link_tooltip_ai_summary: 'Resumo da página',
|
||||
link_tooltip_ai_summary_description: 'Baseado no seu contexto. Pode conter erros.',
|
||||
open_in_new_tab: 'Abrir em uma nova guia',
|
||||
};
|
||||
|
||||
@@ -56,9 +56,4 @@ export const zh: TranslationLanguage = {
|
||||
pdf_limit_reached: '无法为${1}页生成 PDF,生成在${2}页时停止。',
|
||||
pdf_limit_reached_continue: '使用${1}页进行扩展。',
|
||||
more: '更多',
|
||||
link_tooltip_external_link: '外部链接到',
|
||||
link_tooltip_page_anchor: '跳转到页面',
|
||||
link_tooltip_ai_summary: '页面要点',
|
||||
link_tooltip_ai_summary_description: '基于您的上下文。可能包含错误。',
|
||||
open_in_new_tab: '在新标签页中打开',
|
||||
};
|
||||
|
||||
+1
-51
@@ -64,10 +64,7 @@ export const cloudflareDOCache: CacheBackend = {
|
||||
return;
|
||||
}
|
||||
|
||||
const keys = await retryOnDurableObjectError(async () => {
|
||||
return await stub.purge();
|
||||
});
|
||||
|
||||
const keys = await stub.purge();
|
||||
keys.forEach((key) => {
|
||||
entries.push({ key, tag });
|
||||
});
|
||||
@@ -102,50 +99,3 @@ async function getStub(tag: string): Promise<CacheObjectStub | null> {
|
||||
|
||||
return stub;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry an operation on a Durable Object if it fails with a retriable error.
|
||||
* It will retry up to 4 times with an exponential backoff.
|
||||
*/
|
||||
export async function retryOnDurableObjectError<T>(
|
||||
operation: () => T | Promise<T>,
|
||||
attemptsLeft = 4,
|
||||
delay = 50
|
||||
): Promise<T> {
|
||||
if (attemptsLeft <= 0) {
|
||||
return operation();
|
||||
}
|
||||
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
if (!shouldRetryError(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (attemptsLeft > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
return retryOnDurableObjectError(operation, attemptsLeft - 1, delay * 2);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const RETRIABLE_ERROR_MESSAGES = new Set([
|
||||
'Cannot resolve Durable Object due to transient issue on remote node.',
|
||||
'internal error',
|
||||
`Durable Object's isolate exceeded its memory limit and was reset.`,
|
||||
'cannot access storage because object has moved to a different machine',
|
||||
'Durable Object reset because its code was updated.',
|
||||
"The Durable Object's code has been updated, this version can no longer access storage.",
|
||||
// https://developers.cloudflare.com/workers/observability/errors/#runtime-errors
|
||||
'Network connection lost.',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Check if an error should be retried based on its error message.
|
||||
*/
|
||||
function shouldRetryError(error: unknown): boolean {
|
||||
return error instanceof Error && RETRIABLE_ERROR_MESSAGES.has(error.message);
|
||||
}
|
||||
|
||||
+11
-19
@@ -36,25 +36,18 @@ export async function revalidateTags(tags: string[]): Promise<{
|
||||
|
||||
await Promise.all(
|
||||
cacheBackends.map(async (backend, backendIndex) => {
|
||||
try {
|
||||
const { entries: addedEntries } = await backend.revalidateTags(tags);
|
||||
const { entries: addedEntries } = await backend.revalidateTags(tags);
|
||||
|
||||
addedEntries.forEach(({ key, tag }) => {
|
||||
stats[key] = stats[key] ?? {
|
||||
tag,
|
||||
backends: {},
|
||||
};
|
||||
stats[key].backends[backend.name] = { set: true };
|
||||
addedEntries.forEach(({ key, tag }) => {
|
||||
stats[key] = stats[key] ?? {
|
||||
tag,
|
||||
backends: {},
|
||||
};
|
||||
stats[key].backends[backend.name] = { set: true };
|
||||
|
||||
entries.set(key, { tag, key });
|
||||
keysByBackend.set(backendIndex, [
|
||||
...(keysByBackend.get(backendIndex) ?? []),
|
||||
key,
|
||||
]);
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error(`error revalidating tags on backend ${backend.name}: ${err}`);
|
||||
}
|
||||
entries.set(key, { tag, key });
|
||||
keysByBackend.set(backendIndex, [...(keysByBackend.get(backendIndex) ?? []), key]);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
@@ -78,8 +71,7 @@ export async function revalidateTags(tags: string[]): Promise<{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`error deleting entries on backend ${backend.name}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { GitBookAnyContext } from '@v2/lib/context';
|
||||
|
||||
import { getNodeText } from './document';
|
||||
import { resolveOpenAPIOperationBlock } from './openapi/resolveOpenAPIOperationBlock';
|
||||
import { resolveOpenAPISchemasBlock } from './openapi/resolveOpenAPISchemasBlock';
|
||||
|
||||
export interface DocumentSection {
|
||||
id: string;
|
||||
@@ -53,26 +52,6 @@ export async function getDocumentSections(
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
block.type === 'openapi-schemas' &&
|
||||
!block.data.grouped &&
|
||||
block.meta?.id &&
|
||||
block.data.schemas.length === 1
|
||||
) {
|
||||
const { data } = await resolveOpenAPISchemasBlock({
|
||||
block,
|
||||
context,
|
||||
});
|
||||
const schema = data?.schemas[0];
|
||||
if (schema) {
|
||||
sections.push({
|
||||
id: block.meta.id,
|
||||
title: `The ${schema.name} object`,
|
||||
depth: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sections;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { OpenAPIParseError, type OpenAPISchema } from '@gitbook/openapi-parser';
|
||||
import { resolveOpenAPISchemas } from '@gitbook/react-openapi';
|
||||
import { OpenAPIParseError } from '@gitbook/openapi-parser';
|
||||
import { type OpenAPISchemasData, resolveOpenAPISchemas } from '@gitbook/react-openapi';
|
||||
import { fetchOpenAPIFilesystem } from './fetch';
|
||||
import type {
|
||||
OpenAPISchemasBlock,
|
||||
@@ -7,9 +7,7 @@ import type {
|
||||
ResolveOpenAPIBlockResult,
|
||||
} from './types';
|
||||
|
||||
type ResolveOpenAPISchemasBlockResult = ResolveOpenAPIBlockResult<{
|
||||
schemas: OpenAPISchema[];
|
||||
}>;
|
||||
type ResolveOpenAPISchemasBlockResult = ResolveOpenAPIBlockResult<OpenAPISchemasData>;
|
||||
|
||||
const weakmap = new WeakMap<OpenAPISchemasBlock, Promise<ResolveOpenAPISchemasBlockResult>>();
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type {
|
||||
ContentRef,
|
||||
RevisionFile,
|
||||
RevisionPageDocument,
|
||||
RevisionReusableContent,
|
||||
SiteSpace,
|
||||
Space,
|
||||
@@ -29,6 +28,8 @@ 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 */
|
||||
@@ -39,8 +40,6 @@ export interface ResolvedContentRef {
|
||||
active: boolean;
|
||||
/** File, if the reference is a file */
|
||||
file?: RevisionFile;
|
||||
/** Page document resolved from the content ref */
|
||||
page?: RevisionPageDocument;
|
||||
/** Resolved reusable content, if the ref points to reusable content on a revision. */
|
||||
reusableContent?: RevisionReusableContent;
|
||||
/** Resolve OpenAPI spec filesystem. */
|
||||
@@ -178,7 +177,7 @@ export async function resolveContentRef(
|
||||
ancestors: ancestors,
|
||||
emoji,
|
||||
icon,
|
||||
page,
|
||||
id: page.id,
|
||||
active: !anchor && page.id === activePage?.id,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,9 +26,6 @@ export function defaultCustomization(): api.SiteCustomizationSettings {
|
||||
internationalization: {
|
||||
locale: api.CustomizationLocale.En,
|
||||
},
|
||||
insights: {
|
||||
trackingCookie: true,
|
||||
},
|
||||
favicon: {},
|
||||
header: {
|
||||
preset: api.CustomizationHeaderPreset.Default,
|
||||
|
||||
@@ -270,10 +270,6 @@ async function getDataFetcherV1(): Promise<GitBookDataFetcher> {
|
||||
return result;
|
||||
});
|
||||
},
|
||||
|
||||
streamAIResponse() {
|
||||
throw new Error('Not implemented in v1');
|
||||
},
|
||||
};
|
||||
|
||||
return dataFetcher;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ImageResponse } from '@vercel/og';
|
||||
import { notFound, redirect } from 'next/navigation';
|
||||
import { ImageResponse } from 'next/og';
|
||||
|
||||
import { getEmojiForCode } from '@/lib/emojis';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
@@ -32,6 +32,7 @@ export async function serveIcon(context: GitBookSiteContext, req: Request) {
|
||||
|
||||
const { site, customization } = context;
|
||||
const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null;
|
||||
|
||||
// If the site has a custom icon, redirect to it
|
||||
if (customIcon) {
|
||||
const iconUrl = options.theme === 'light' ? customIcon.light : customIcon.dark;
|
||||
@@ -44,6 +45,7 @@ export async function serveIcon(context: GitBookSiteContext, req: Request) {
|
||||
}
|
||||
|
||||
const contentTitle = site.title;
|
||||
|
||||
return new ImageResponse(
|
||||
<div
|
||||
tw={tcls(options.theme === 'light' ? 'bg-white' : 'bg-black', size.boxStyle)}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { StaticSection } from './StaticSection';
|
||||
import { type CodeSampleGenerator, codeSampleGenerators } from './code-samples';
|
||||
import { generateMediaTypeExamples, generateSchemaExample } from './generateSchemaExample';
|
||||
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
||||
import type { OpenAPIContext, OpenAPIOperationData } from './types';
|
||||
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
|
||||
import { getDefaultServerURL } from './util/server';
|
||||
import { checkIsReference, createStateKey } from './utils';
|
||||
|
||||
@@ -21,7 +21,7 @@ const CUSTOM_CODE_SAMPLES_KEYS = ['x-custom-examples', 'x-code-samples', 'x-code
|
||||
*/
|
||||
export function OpenAPICodeSample(props: {
|
||||
data: OpenAPIOperationData;
|
||||
context: OpenAPIContext;
|
||||
context: OpenAPIContextProps;
|
||||
}) {
|
||||
const { data } = props;
|
||||
|
||||
@@ -58,7 +58,7 @@ export function OpenAPICodeSample(props: {
|
||||
*/
|
||||
function generateCodeSamples(props: {
|
||||
data: OpenAPIOperationData;
|
||||
context: OpenAPIContext;
|
||||
context: OpenAPIContextProps;
|
||||
}) {
|
||||
const { data, context } = props;
|
||||
|
||||
@@ -189,7 +189,7 @@ export interface MediaTypeRenderer {
|
||||
function OpenAPICodeSampleFooter(props: {
|
||||
data: OpenAPIOperationData;
|
||||
renderers: MediaTypeRenderer[];
|
||||
context: OpenAPIContext;
|
||||
context: OpenAPIContextProps;
|
||||
}) {
|
||||
const { data, context, renderers } = props;
|
||||
const { method, path } = data;
|
||||
@@ -227,7 +227,7 @@ function OpenAPICodeSampleFooter(props: {
|
||||
*/
|
||||
function getCustomCodeSamples(props: {
|
||||
data: OpenAPIOperationData;
|
||||
context: OpenAPIContext;
|
||||
context: OpenAPIContextProps;
|
||||
}) {
|
||||
const { data, context } = props;
|
||||
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||
import { generateSchemaExample } from './generateSchemaExample';
|
||||
import { json2xml } from './json2xml';
|
||||
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
||||
import type { OpenAPIContext } from './types';
|
||||
import { checkIsReference } from './utils';
|
||||
|
||||
/**
|
||||
* Display an example.
|
||||
*/
|
||||
export function OpenAPIExample(props: {
|
||||
example: OpenAPIV3.ExampleObject;
|
||||
context: OpenAPIContext;
|
||||
syntax: string;
|
||||
}) {
|
||||
const { example, context, syntax } = props;
|
||||
const code = stringifyExample({ example, xml: syntax === 'xml' });
|
||||
|
||||
if (code === null) {
|
||||
return <OpenAPIEmptyExample />;
|
||||
}
|
||||
|
||||
return context.renderCodeBlock({ code, syntax });
|
||||
}
|
||||
|
||||
function stringifyExample(args: { example: OpenAPIV3.ExampleObject; xml: boolean }): string | null {
|
||||
const { example, xml } = args;
|
||||
|
||||
if (!example.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof example.value === 'string') {
|
||||
return example.value;
|
||||
}
|
||||
|
||||
if (xml) {
|
||||
return json2xml(example.value);
|
||||
}
|
||||
|
||||
return stringifyOpenAPI(example.value, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty response example.
|
||||
*/
|
||||
export function OpenAPIEmptyExample() {
|
||||
return (
|
||||
<pre className="openapi-example-empty">
|
||||
<p>No Content</p>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an example from a reference object.
|
||||
*/
|
||||
export function getExampleFromReference(ref: OpenAPIV3.ReferenceObject): OpenAPIV3.ExampleObject {
|
||||
return { summary: 'Unresolved reference', value: { $ref: ref.$ref } };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get examples from a media type object.
|
||||
*/
|
||||
export function getExamplesFromMediaTypeObject(args: {
|
||||
mediaType: string;
|
||||
mediaTypeObject: OpenAPIV3.MediaTypeObject;
|
||||
}): { key: string; example: OpenAPIV3.ExampleObject }[] {
|
||||
const { mediaTypeObject, mediaType } = args;
|
||||
if (mediaTypeObject.examples) {
|
||||
return Object.entries(mediaTypeObject.examples).map(([key, example]) => {
|
||||
return {
|
||||
key,
|
||||
example: checkIsReference(example) ? getExampleFromReference(example) : example,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (mediaTypeObject.example) {
|
||||
return [{ key: 'default', example: { value: mediaTypeObject.example } }];
|
||||
}
|
||||
|
||||
if (mediaTypeObject.schema) {
|
||||
if (mediaType === 'application/xml') {
|
||||
// @TODO normally we should use the name of the schema but we don't have it
|
||||
// fix it when we got the reference name
|
||||
const root = mediaTypeObject.schema.xml?.name ?? 'object';
|
||||
return [
|
||||
{
|
||||
key: 'default',
|
||||
example: {
|
||||
value: {
|
||||
[root]: generateSchemaExample(mediaTypeObject.schema, {
|
||||
xml: mediaType === 'application/xml',
|
||||
mode: 'read',
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
key: 'default',
|
||||
example: {
|
||||
value: generateSchemaExample(mediaTypeObject.schema, {
|
||||
mode: 'read',
|
||||
}),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get example from a schema object.
|
||||
*/
|
||||
export function getExampleFromSchema(args: {
|
||||
schema: OpenAPIV3.SchemaObject;
|
||||
}): OpenAPIV3.ExampleObject {
|
||||
const { schema } = args;
|
||||
|
||||
if (schema.example) {
|
||||
return { value: schema.example };
|
||||
}
|
||||
|
||||
return { value: generateSchemaExample(schema, { mode: 'read' }) };
|
||||
}
|
||||
@@ -10,8 +10,7 @@ import { OpenAPICodeSample } from './OpenAPICodeSample';
|
||||
import { OpenAPIPath } from './OpenAPIPath';
|
||||
import { OpenAPIResponseExample } from './OpenAPIResponseExample';
|
||||
import { OpenAPISpec } from './OpenAPISpec';
|
||||
import { getOpenAPIClientContext } from './context';
|
||||
import type { OpenAPIContext, OpenAPIOperationData } from './types';
|
||||
import type { OpenAPIClientContext, OpenAPIContextProps, OpenAPIOperationData } from './types';
|
||||
import { resolveDescription } from './utils';
|
||||
|
||||
/**
|
||||
@@ -20,12 +19,16 @@ import { resolveDescription } from './utils';
|
||||
export function OpenAPIOperation(props: {
|
||||
className?: string;
|
||||
data: OpenAPIOperationData;
|
||||
context: OpenAPIContext;
|
||||
context: OpenAPIContextProps;
|
||||
}) {
|
||||
const { className, data, context } = props;
|
||||
const { operation } = data;
|
||||
|
||||
const clientContext = getOpenAPIClientContext(context);
|
||||
const clientContext: OpenAPIClientContext = {
|
||||
defaultInteractiveOpened: context.defaultInteractiveOpened,
|
||||
icons: context.icons,
|
||||
blockKey: context.blockKey,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx('openapi-operation', className)}>
|
||||
@@ -76,7 +79,7 @@ export function OpenAPIOperation(props: {
|
||||
|
||||
function OpenAPIOperationDescription(props: {
|
||||
operation: OpenAPIV3.OperationObject<OpenAPICustomOperationProperties>;
|
||||
context: OpenAPIContext;
|
||||
context: OpenAPIContextProps;
|
||||
}) {
|
||||
const { operation } = props;
|
||||
if (operation['x-gitbook-description-document']) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { OpenAPICopyButton } from './OpenAPICopyButton';
|
||||
import type { OpenAPIContext, OpenAPIOperationData } from './types';
|
||||
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
|
||||
import { getDefaultServerURL } from './util/server';
|
||||
|
||||
/**
|
||||
@@ -7,7 +7,7 @@ import { getDefaultServerURL } from './util/server';
|
||||
*/
|
||||
export function OpenAPIPath(props: {
|
||||
data: OpenAPIOperationData;
|
||||
context: OpenAPIContext;
|
||||
context: OpenAPIContextProps;
|
||||
}) {
|
||||
const { data } = props;
|
||||
const { method, path, operation } = data;
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||
import { Markdown } from './Markdown';
|
||||
import {
|
||||
OpenAPIEmptyExample,
|
||||
OpenAPIExample,
|
||||
getExampleFromReference,
|
||||
getExamplesFromMediaTypeObject,
|
||||
} from './OpenAPIExample';
|
||||
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
|
||||
import { StaticSection } from './StaticSection';
|
||||
import type { OpenAPIContext, OpenAPIOperationData } from './types';
|
||||
import { generateSchemaExample } from './generateSchemaExample';
|
||||
import { json2xml } from './json2xml';
|
||||
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
||||
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
|
||||
import { checkIsReference, createStateKey, resolveDescription } from './utils';
|
||||
|
||||
/**
|
||||
@@ -16,7 +13,7 @@ import { checkIsReference, createStateKey, resolveDescription } from './utils';
|
||||
*/
|
||||
export function OpenAPIResponseExample(props: {
|
||||
data: OpenAPIOperationData;
|
||||
context: OpenAPIContext;
|
||||
context: OpenAPIContextProps;
|
||||
}) {
|
||||
const { data, context } = props;
|
||||
|
||||
@@ -65,7 +62,7 @@ export function OpenAPIResponseExample(props: {
|
||||
return {
|
||||
key: key,
|
||||
label: key,
|
||||
body: <OpenAPIEmptyExample />,
|
||||
body: <OpenAPIEmptyResponseExample />,
|
||||
footer: description ? <Markdown source={description} /> : undefined,
|
||||
};
|
||||
}
|
||||
@@ -84,7 +81,7 @@ export function OpenAPIResponseExample(props: {
|
||||
|
||||
return (
|
||||
<OpenAPITabs stateKey={createStateKey('response-example')} items={tabs}>
|
||||
<StaticSection header={<OpenAPITabsList />} className="openapi-panel">
|
||||
<StaticSection header={<OpenAPITabsList />} className="openapi-response-example">
|
||||
<OpenAPITabsPanels />
|
||||
</StaticSection>
|
||||
</OpenAPITabs>
|
||||
@@ -92,7 +89,7 @@ export function OpenAPIResponseExample(props: {
|
||||
}
|
||||
|
||||
function OpenAPIResponse(props: {
|
||||
context: OpenAPIContext;
|
||||
context: OpenAPIContextProps;
|
||||
content: {
|
||||
[media: string]: OpenAPIV3.MediaTypeObject;
|
||||
};
|
||||
@@ -144,7 +141,7 @@ function OpenAPIResponse(props: {
|
||||
function OpenAPIResponseMediaType(props: {
|
||||
mediaTypeObject: OpenAPIV3.MediaTypeObject;
|
||||
mediaType: string;
|
||||
context: OpenAPIContext;
|
||||
context: OpenAPIContextProps;
|
||||
}) {
|
||||
const { mediaTypeObject, mediaType } = props;
|
||||
const examples = getExamplesFromMediaTypeObject({ mediaTypeObject, mediaType });
|
||||
@@ -152,7 +149,7 @@ function OpenAPIResponseMediaType(props: {
|
||||
const firstExample = examples[0];
|
||||
|
||||
if (!firstExample) {
|
||||
return <OpenAPIEmptyExample />;
|
||||
return <OpenAPIEmptyResponseExample />;
|
||||
}
|
||||
|
||||
if (examples.length === 1) {
|
||||
@@ -187,6 +184,42 @@ function OpenAPIResponseMediaType(props: {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display an example.
|
||||
*/
|
||||
function OpenAPIExample(props: {
|
||||
example: OpenAPIV3.ExampleObject;
|
||||
context: OpenAPIContextProps;
|
||||
syntax: string;
|
||||
}) {
|
||||
const { example, context, syntax } = props;
|
||||
const code = stringifyExample({ example, xml: syntax === 'xml' });
|
||||
|
||||
if (code === null) {
|
||||
return <OpenAPIEmptyResponseExample />;
|
||||
}
|
||||
|
||||
return context.renderCodeBlock({ code, syntax });
|
||||
}
|
||||
|
||||
function stringifyExample(args: { example: OpenAPIV3.ExampleObject; xml: boolean }): string | null {
|
||||
const { example, xml } = args;
|
||||
|
||||
if (!example.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof example.value === 'string') {
|
||||
return example.value;
|
||||
}
|
||||
|
||||
if (xml) {
|
||||
return json2xml(example.value);
|
||||
}
|
||||
|
||||
return stringifyOpenAPI(example.value, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the syntax from a media type.
|
||||
*/
|
||||
@@ -201,3 +234,75 @@ function getSyntaxFromMediaType(mediaType: string): string {
|
||||
|
||||
return 'text';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get examples from a media type object.
|
||||
*/
|
||||
function getExamplesFromMediaTypeObject(args: {
|
||||
mediaType: string;
|
||||
mediaTypeObject: OpenAPIV3.MediaTypeObject;
|
||||
}): { key: string; example: OpenAPIV3.ExampleObject }[] {
|
||||
const { mediaTypeObject, mediaType } = args;
|
||||
if (mediaTypeObject.examples) {
|
||||
return Object.entries(mediaTypeObject.examples).map(([key, example]) => {
|
||||
return {
|
||||
key,
|
||||
example: checkIsReference(example) ? getExampleFromReference(example) : example,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (mediaTypeObject.example) {
|
||||
return [{ key: 'default', example: { value: mediaTypeObject.example } }];
|
||||
}
|
||||
|
||||
if (mediaTypeObject.schema) {
|
||||
if (mediaType === 'application/xml') {
|
||||
// @TODO normally we should use the name of the schema but we don't have it
|
||||
// fix it when we got the reference name
|
||||
const root = mediaTypeObject.schema.xml?.name ?? 'object';
|
||||
return [
|
||||
{
|
||||
key: 'default',
|
||||
example: {
|
||||
value: {
|
||||
[root]: generateSchemaExample(mediaTypeObject.schema, {
|
||||
xml: mediaType === 'application/xml',
|
||||
mode: 'read',
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
key: 'default',
|
||||
example: {
|
||||
value: generateSchemaExample(mediaTypeObject.schema, {
|
||||
mode: 'read',
|
||||
}),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty response example.
|
||||
*/
|
||||
function OpenAPIEmptyResponseExample() {
|
||||
return (
|
||||
<pre className="openapi-response-example-empty">
|
||||
<p>No body</p>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an example from a reference object.
|
||||
*/
|
||||
function getExampleFromReference(ref: OpenAPIV3.ReferenceObject): OpenAPIV3.ExampleObject {
|
||||
return { summary: 'Unresolved reference', value: { $ref: ref.$ref } };
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import { OpenAPICopyButton } from './OpenAPICopyButton';
|
||||
import { OpenAPIDisclosure } from './OpenAPIDisclosure';
|
||||
import { OpenAPISchemaName } from './OpenAPISchemaName';
|
||||
import { retrocycle } from './decycle';
|
||||
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
||||
import type { OpenAPIClientContext } from './types';
|
||||
import { checkIsReference, resolveDescription, resolveFirstExample } from './utils';
|
||||
|
||||
@@ -146,23 +145,17 @@ function OpenAPIRootSchema(props: {
|
||||
|
||||
const id = useId();
|
||||
const properties = getSchemaProperties(schema);
|
||||
const description = resolveDescription(schema);
|
||||
|
||||
if (properties?.length) {
|
||||
const circularRefs = new Map(parentCircularRefs);
|
||||
circularRefs.set(schema, id);
|
||||
|
||||
return (
|
||||
<>
|
||||
{description ? (
|
||||
<Markdown source={description} className="openapi-schema-root-description" />
|
||||
) : null}
|
||||
<OpenAPISchemaProperties
|
||||
properties={properties}
|
||||
circularRefs={circularRefs}
|
||||
context={context}
|
||||
/>
|
||||
</>
|
||||
<OpenAPISchemaProperties
|
||||
properties={properties}
|
||||
circularRefs={circularRefs}
|
||||
context={context}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -329,25 +322,15 @@ function OpenAPISchemaPresentation(props: { property: OpenAPISchemaPropertyEntry
|
||||
{description ? (
|
||||
<Markdown source={description} className="openapi-schema-description" />
|
||||
) : null}
|
||||
{schema.default !== undefined ? (
|
||||
<span className="openapi-schema-default">
|
||||
Default:{' '}
|
||||
<code>
|
||||
{typeof schema.default === 'string' && schema.default
|
||||
? schema.default
|
||||
: stringifyOpenAPI(schema.default)}
|
||||
</code>
|
||||
</span>
|
||||
) : null}
|
||||
{typeof example === 'string' ? (
|
||||
<span className="openapi-schema-example">
|
||||
<div className="openapi-schema-example">
|
||||
Example: <code>{example}</code>
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
{schema.pattern ? (
|
||||
<span className="openapi-schema-pattern">
|
||||
<div className="openapi-schema-pattern">
|
||||
Pattern: <code>{schema.pattern}</code>
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
<OpenAPISchemaEnum schema={schema} />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||
import type React from 'react';
|
||||
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
||||
|
||||
interface OpenAPISchemaNameProps {
|
||||
schema?: OpenAPIV3.SchemaObject;
|
||||
@@ -18,7 +19,7 @@ export function OpenAPISchemaName(props: OpenAPISchemaNameProps) {
|
||||
const additionalItems = schema && getAdditionalItems(schema);
|
||||
|
||||
return (
|
||||
<span className="openapi-schema-name">
|
||||
<div className="openapi-schema-name">
|
||||
{propertyName ? (
|
||||
<span data-deprecated={schema?.deprecated} className="openapi-schema-propertyname">
|
||||
{propertyName}
|
||||
@@ -40,7 +41,7 @@ export function OpenAPISchemaName(props: OpenAPISchemaNameProps) {
|
||||
<span className="openapi-schema-optional">optional</span>
|
||||
)}
|
||||
{schema?.deprecated ? <span className="openapi-deprecated">Deprecated</span> : null}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,6 +56,11 @@ function getAdditionalItems(schema: OpenAPIV3.SchemaObject): string {
|
||||
additionalItems += ` · max: ${schema.maximum || schema.maxLength || schema.maxItems}`;
|
||||
}
|
||||
|
||||
// If the schema has a default value, we display it
|
||||
if (typeof schema.default !== 'undefined') {
|
||||
additionalItems += ` · default: ${stringifyOpenAPI(schema.default)}`;
|
||||
}
|
||||
|
||||
if (schema.nullable) {
|
||||
additionalItems = ' | nullable';
|
||||
}
|
||||
|
||||
@@ -138,9 +138,9 @@ export function OpenAPITabsPanels() {
|
||||
|
||||
return (
|
||||
<TabPanel id={key} className="openapi-tabs-panel">
|
||||
<div className="openapi-panel-body">{selectedTab.body}</div>
|
||||
<div className="openapi-tabs-body">{selectedTab.body}</div>
|
||||
{selectedTab.footer ? (
|
||||
<div className="openapi-panel-footer">{selectedTab.footer}</div>
|
||||
<div className="openapi-tabs-footer">{selectedTab.footer}</div>
|
||||
) : null}
|
||||
</TabPanel>
|
||||
);
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
export interface OpenAPIClientContext {
|
||||
/**
|
||||
* Icons used in the block.
|
||||
*/
|
||||
icons: {
|
||||
chevronDown: React.ReactNode;
|
||||
chevronRight: React.ReactNode;
|
||||
plus: React.ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Force all sections to be opened by default.
|
||||
* @default false
|
||||
*/
|
||||
defaultInteractiveOpened?: boolean;
|
||||
|
||||
/**
|
||||
* The key of the block
|
||||
*/
|
||||
blockKey?: string;
|
||||
|
||||
/**
|
||||
* Optional id attached to the heading and used as an anchor.
|
||||
*/
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface OpenAPIContext extends OpenAPIClientContext {
|
||||
/**
|
||||
* Render a code block.
|
||||
*/
|
||||
renderCodeBlock: (props: { code: string; syntax: string }) => React.ReactNode;
|
||||
|
||||
/**
|
||||
* Render the heading of the operation.
|
||||
*/
|
||||
renderHeading: (props: {
|
||||
deprecated: boolean;
|
||||
title: string;
|
||||
stability?: string;
|
||||
}) => React.ReactNode;
|
||||
|
||||
/**
|
||||
* Render the document of the operation.
|
||||
*/
|
||||
renderDocument: (props: { document: object }) => React.ReactNode;
|
||||
|
||||
/**
|
||||
* Specification URL.
|
||||
*/
|
||||
specUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the client context from the OpenAPI context.
|
||||
*/
|
||||
export function getOpenAPIClientContext(context: OpenAPIContext): OpenAPIClientContext {
|
||||
return {
|
||||
icons: context.icons,
|
||||
defaultInteractiveOpened: context.defaultInteractiveOpened,
|
||||
blockKey: context.blockKey,
|
||||
id: context.id,
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -166,11 +166,6 @@ const getExampleFromSchema = (
|
||||
// But if `emptyString` is set, we do want to see some values.
|
||||
const makeUpRandomData = !!options?.emptyString;
|
||||
|
||||
// If the property is deprecated we don't show it in examples.
|
||||
if (schema.deprecated) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Check if the property is read-only/write-only
|
||||
if (
|
||||
(options?.mode === 'write' && schema.readOnly) ||
|
||||
|
||||
@@ -2,4 +2,4 @@ export * from './schemas';
|
||||
export * from './OpenAPIOperation';
|
||||
export * from './OpenAPIOperationContext';
|
||||
export * from './resolveOpenAPIOperation';
|
||||
export type { OpenAPIOperationData, OpenAPIContext } from './types';
|
||||
export type { OpenAPISchemasData, OpenAPIOperationData } from './types';
|
||||
|
||||
@@ -1,104 +1,99 @@
|
||||
import type { OpenAPISchema } from '@gitbook/openapi-parser';
|
||||
import clsx from 'clsx';
|
||||
import { OpenAPIDisclosureGroup } from '../OpenAPIDisclosureGroup';
|
||||
import { OpenAPIExample, getExampleFromSchema } from '../OpenAPIExample';
|
||||
import { OpenAPIRootSchema } from '../OpenAPISchemaServer';
|
||||
import { Section, SectionBody, StaticSection } from '../StaticSection';
|
||||
import { getOpenAPIClientContext } from '../context';
|
||||
import type { OpenAPIContext } from '../types';
|
||||
import { Section, SectionBody } from '../StaticSection';
|
||||
import type { OpenAPIClientContext, OpenAPIContextProps, OpenAPISchemasData } from '../types';
|
||||
|
||||
type OpenAPISchemasContextProps = Omit<
|
||||
OpenAPIContextProps,
|
||||
'renderCodeBlock' | 'renderHeading' | 'renderDocument'
|
||||
>;
|
||||
|
||||
/**
|
||||
* OpenAPI Schemas component.
|
||||
* Display OpenAPI Schemas.
|
||||
*/
|
||||
export function OpenAPISchemas(props: {
|
||||
className?: string;
|
||||
schemas: OpenAPISchema[];
|
||||
context: OpenAPIContext;
|
||||
data: OpenAPISchemasData;
|
||||
context: OpenAPISchemasContextProps;
|
||||
/**
|
||||
* Whether to show the schema directly if there is only one.
|
||||
*/
|
||||
grouped?: boolean;
|
||||
}) {
|
||||
const { schemas, context, grouped, className } = props;
|
||||
const { className, data, context, grouped } = props;
|
||||
const { schemas } = data;
|
||||
|
||||
const firstSchema = schemas[0];
|
||||
const clientContext: OpenAPIClientContext = {
|
||||
defaultInteractiveOpened: context.defaultInteractiveOpened,
|
||||
icons: context.icons,
|
||||
blockKey: context.blockKey,
|
||||
};
|
||||
|
||||
if (!firstSchema) {
|
||||
if (!schemas.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const clientContext = getOpenAPIClientContext(context);
|
||||
return (
|
||||
<div className={clsx('openapi-schemas', className)}>
|
||||
<OpenAPIRootSchemasSchema grouped={grouped} schemas={schemas} context={clientContext} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Root schema for OpenAPI schemas.
|
||||
* It displays a single model or a disclosure group for multiple schemas.
|
||||
*/
|
||||
function OpenAPIRootSchemasSchema(props: {
|
||||
schemas: OpenAPISchemasData['schemas'];
|
||||
context: OpenAPIClientContext;
|
||||
grouped?: boolean;
|
||||
}) {
|
||||
const { schemas, context, grouped } = props;
|
||||
|
||||
// If there is only one model and we are not grouping, we show it directly.
|
||||
if (schemas.length === 1 && !grouped) {
|
||||
const title = `The ${firstSchema.name} object`;
|
||||
const schema = schemas?.[0]?.schema;
|
||||
|
||||
if (!schema) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx('openapi-schemas', className)}>
|
||||
<div className="openapi-summary" id={context.id}>
|
||||
{context.renderHeading({
|
||||
title,
|
||||
})}
|
||||
</div>
|
||||
<div className="openapi-columns">
|
||||
<div className="openapi-column-spec">
|
||||
<StaticSection className="openapi-parameters" header="Attributes">
|
||||
<OpenAPIRootSchema
|
||||
schema={firstSchema.schema}
|
||||
context={clientContext}
|
||||
/>
|
||||
</StaticSection>
|
||||
</div>
|
||||
<div className="openapi-column-preview">
|
||||
<div className="openapi-column-preview-body">
|
||||
<div className="openapi-panel">
|
||||
<h4 className="openapi-panel-heading">{title}</h4>
|
||||
<div className="openapi-panel-body">
|
||||
<OpenAPIExample
|
||||
example={getExampleFromSchema({
|
||||
schema: firstSchema.schema,
|
||||
})}
|
||||
context={context}
|
||||
syntax="json"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Section>
|
||||
<SectionBody>
|
||||
<OpenAPIRootSchema schema={schema} context={context} />
|
||||
</SectionBody>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
// If there are multiple schemas, we use a disclosure group to show them all.
|
||||
return (
|
||||
<div className={clsx('openapi-schemas', className)}>
|
||||
<OpenAPIDisclosureGroup
|
||||
allowsMultipleExpanded
|
||||
icon={context.icons.chevronRight}
|
||||
groups={schemas.map(({ name, schema }) => ({
|
||||
id: name,
|
||||
label: (
|
||||
<div className="openapi-response-tab-content" key={`model-${name}`}>
|
||||
<span className="openapi-response-statuscode">{name}</span>
|
||||
</div>
|
||||
),
|
||||
tabs: [
|
||||
{
|
||||
id: 'model',
|
||||
body: (
|
||||
<Section className="openapi-section-schemas">
|
||||
<SectionBody>
|
||||
<OpenAPIRootSchema
|
||||
schema={schema}
|
||||
context={clientContext}
|
||||
/>
|
||||
</SectionBody>
|
||||
</Section>
|
||||
),
|
||||
},
|
||||
],
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<OpenAPIDisclosureGroup
|
||||
allowsMultipleExpanded
|
||||
icon={context.icons.chevronRight}
|
||||
groups={schemas.map(({ name, schema }) => ({
|
||||
id: name,
|
||||
label: (
|
||||
<div className="openapi-response-tab-content" key={`model-${name}`}>
|
||||
<span className="openapi-response-statuscode">{name}</span>
|
||||
</div>
|
||||
),
|
||||
tabs: [
|
||||
{
|
||||
id: 'model',
|
||||
body: (
|
||||
<Section className="openapi-section-schemas">
|
||||
<SectionBody>
|
||||
<OpenAPIRootSchema schema={schema} context={context} />
|
||||
</SectionBody>
|
||||
</Section>
|
||||
),
|
||||
},
|
||||
],
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { Filesystem, OpenAPISchema, OpenAPIV3xDocument } from '@gitbook/openapi-parser';
|
||||
import type { Filesystem, OpenAPIV3xDocument } from '@gitbook/openapi-parser';
|
||||
import { filterSelectedOpenAPISchemas } from '@gitbook/openapi-parser';
|
||||
import { dereferenceFilesystem } from '../dereference';
|
||||
import type { OpenAPISchemasData } from '../types';
|
||||
|
||||
//!!TODO: We should return only the schemas that are used in the block. Still a WIP awaiting future work.
|
||||
|
||||
/**
|
||||
* Resolve an OpenAPI schemas from a file and compile it to a more usable format.
|
||||
@@ -11,9 +14,7 @@ export async function resolveOpenAPISchemas(
|
||||
options: {
|
||||
schemas: string[];
|
||||
}
|
||||
): Promise<{
|
||||
schemas: OpenAPISchema[];
|
||||
} | null> {
|
||||
): Promise<OpenAPISchemasData | null> {
|
||||
const { schemas: selectedSchemas } = options;
|
||||
|
||||
const schema = await dereferenceFilesystem(filesystem);
|
||||
|
||||
@@ -1,13 +1,33 @@
|
||||
import type {
|
||||
OpenAPICustomOperationProperties,
|
||||
OpenAPICustomSpecProperties,
|
||||
OpenAPISchema,
|
||||
OpenAPIV3,
|
||||
} from '@gitbook/openapi-parser';
|
||||
|
||||
export interface OpenAPIClientContext {
|
||||
export interface OpenAPIContextProps extends OpenAPIClientContext {
|
||||
/**
|
||||
* Icons used in the block.
|
||||
* Render a code block.
|
||||
*/
|
||||
renderCodeBlock: (props: { code: string; syntax: string }) => React.ReactNode;
|
||||
/**
|
||||
* Render the heading of the operation.
|
||||
*/
|
||||
renderHeading: (props: {
|
||||
deprecated: boolean;
|
||||
title: string;
|
||||
stability?: string;
|
||||
}) => React.ReactNode;
|
||||
/**
|
||||
* Render the document of the operation.
|
||||
*/
|
||||
renderDocument: (props: { document: object }) => React.ReactNode;
|
||||
|
||||
/** Spec url for the Scalar Api Client */
|
||||
specUrl: string;
|
||||
}
|
||||
|
||||
export interface OpenAPIClientContext {
|
||||
icons: {
|
||||
chevronDown: React.ReactNode;
|
||||
chevronRight: React.ReactNode;
|
||||
@@ -19,44 +39,14 @@ export interface OpenAPIClientContext {
|
||||
* @default false
|
||||
*/
|
||||
defaultInteractiveOpened?: boolean;
|
||||
|
||||
/**
|
||||
* The key of the block
|
||||
*/
|
||||
blockKey?: string;
|
||||
|
||||
/**
|
||||
* Optional id attached to the heading and used as an anchor.
|
||||
*/
|
||||
/** Optional id attached to the OpenAPI Operation heading and used as an anchor */
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface OpenAPIContext extends OpenAPIClientContext {
|
||||
/**
|
||||
* Render a code block.
|
||||
*/
|
||||
renderCodeBlock: (props: { code: string; syntax: string }) => React.ReactNode;
|
||||
|
||||
/**
|
||||
* Render the heading of the operation.
|
||||
*/
|
||||
renderHeading: (props: {
|
||||
deprecated?: boolean;
|
||||
title: string;
|
||||
stability?: string;
|
||||
}) => React.ReactNode;
|
||||
|
||||
/**
|
||||
* Render the document of the operation.
|
||||
*/
|
||||
renderDocument: (props: { document: object }) => React.ReactNode;
|
||||
|
||||
/**
|
||||
* Specification URL.
|
||||
*/
|
||||
specUrl: string;
|
||||
}
|
||||
|
||||
export interface OpenAPIOperationData extends OpenAPICustomSpecProperties {
|
||||
path: string;
|
||||
method: string;
|
||||
@@ -70,3 +60,8 @@ export interface OpenAPIOperationData extends OpenAPICustomSpecProperties {
|
||||
/** Securities that should be used for this operation */
|
||||
securities: [string, OpenAPIV3.SecuritySchemeObject][];
|
||||
}
|
||||
|
||||
export interface OpenAPISchemasData {
|
||||
/** Components schemas to be used for schemas */
|
||||
schemas: OpenAPISchema[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user