mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-22 18:43:29 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cf803631c9 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix OpenAPI tabs indicator overflow
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Fix XML in code sample
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
'@gitbook/openapi-parser': patch
|
||||
'@gitbook/react-openapi': patch
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Remove stable from x-stability
|
||||
Move filterSelectedOpenAPISchemas to @gitbook/openapi-parser
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Track event when visitor is opening a search result.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix favicon not being displayed in Google because `robots.txt` was preventing the indexation of the image route
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Disallow crawling by web-robots of search/ask URLs
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Safe parse OpenAPI JSON schema
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix clicking search results when the site is embedded in an iframe.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix multiple request examples selector not showing
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Add OpenAPI write-only indicator
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Support site announcement banner
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Track event when clicking announcement banner link.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
Use stable site URL data for route rewrite in the middleware
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix a crash when a page contains a block of an integration that is no longer installed
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix flash when loading sites with dark mode as default theme
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@gitbook/react-openapi": patch
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix spec properties rendering and missing keys
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Replace $ref with $reference in json-decycle
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Trim the search query to avoid showing a loading state when typing
|
||||
+1
-4
@@ -12,7 +12,7 @@
|
||||
"@codemirror/state": "6.4.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"@gitbook/api": "0.107.0"
|
||||
"@gitbook/api": "0.105.0"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -38,8 +38,5 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ export function getCacheTag(
|
||||
| {
|
||||
tag: 'computed-document';
|
||||
space: string;
|
||||
sourceType: string;
|
||||
integration: string;
|
||||
}
|
||||
/**
|
||||
* All data related to the URL of a content
|
||||
@@ -84,14 +84,6 @@ export function getCacheTag(
|
||||
organization: string;
|
||||
openAPISpec: string;
|
||||
}
|
||||
/**
|
||||
* All data related to a translation
|
||||
*/
|
||||
| {
|
||||
tag: 'translation';
|
||||
organization: string;
|
||||
translationSettings: string;
|
||||
}
|
||||
): string {
|
||||
switch (spec.tag) {
|
||||
case 'user':
|
||||
@@ -107,15 +99,13 @@ export function getCacheTag(
|
||||
case 'document':
|
||||
return `space:${spec.space}:document:${spec.document}`;
|
||||
case 'computed-document':
|
||||
return `space:${spec.space}:computed-document:${spec.sourceType}`;
|
||||
return `space:${spec.space}:computed-document:${spec.integration}`;
|
||||
case 'site':
|
||||
return `site:${spec.site}`;
|
||||
case 'integration':
|
||||
return `integration:${spec.integration}`;
|
||||
case 'openapi':
|
||||
return `organization:${spec.organization}:openapi:${spec.openAPISpec}`;
|
||||
case 'translation':
|
||||
return `organization:${spec.organization}:translation:${spec.translationSettings}`;
|
||||
default:
|
||||
assertNever(spec);
|
||||
}
|
||||
@@ -167,15 +157,6 @@ export function getComputedContentSourceCacheTags(
|
||||
})
|
||||
);
|
||||
break;
|
||||
case 'translation-language':
|
||||
tags.push(
|
||||
getCacheTag({
|
||||
tag: 'translation',
|
||||
organization: inContext.organizationId,
|
||||
translationSettings: dependency.ref.translationSettings,
|
||||
})
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// Do not throw for unknown dependency types
|
||||
// as it might mean we are lacking behind the API version
|
||||
@@ -188,22 +169,18 @@ export function getComputedContentSourceCacheTags(
|
||||
getCacheTag({
|
||||
tag: 'computed-document',
|
||||
space: inContext.spaceId,
|
||||
sourceType: source.type,
|
||||
integration: source.integration,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// We invalidate the computed content when a new version of the integration is deployed.
|
||||
|
||||
if (source.type.startsWith('integration:')) {
|
||||
const integration = source.type.split(':')[1];
|
||||
tags.push(
|
||||
getCacheTag({
|
||||
tag: 'integration',
|
||||
integration,
|
||||
})
|
||||
);
|
||||
}
|
||||
tags.push(
|
||||
getCacheTag({
|
||||
tag: 'integration',
|
||||
integration: source.integration,
|
||||
})
|
||||
);
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
# @gitbook/colors
|
||||
|
||||
## 0.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fb90eb0: Reduce chroma of first color scale step
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "0.3.1",
|
||||
"version": "0.3.0",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
|
||||
@@ -220,7 +220,7 @@ export function colorScale(
|
||||
continue;
|
||||
}
|
||||
|
||||
const chromaRatio = index < 8 ? index * 0.05 : 1;
|
||||
const chromaRatio = index < 8 ? (index + 1) * 0.05 : 1;
|
||||
|
||||
const shade = {
|
||||
L: targetL, // Blend lightness
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
# gitbook-v2
|
||||
|
||||
## 0.2.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5b2bf82: Use stable site URL data for route rewrite in the middleware
|
||||
|
||||
## 0.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gitbook-v2",
|
||||
"version": "0.2.3",
|
||||
"version": "0.2.2",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"next": "^15.2.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getVisitorAuthClaims, getVisitorAuthClaimsFromToken } from '@/lib/adaptive';
|
||||
import type { SiteAPIToken } from '@gitbook/api';
|
||||
import { type SiteURLData, fetchSiteContextByURLLookup, getBaseContext } from '@v2/lib/context';
|
||||
import type { PublishedSiteContent, SiteAPIToken } from '@gitbook/api';
|
||||
import { fetchSiteContextByURLLookup, getBaseContext } from '@v2/lib/context';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
import { forbidden } from 'next/navigation';
|
||||
import rison from 'rison';
|
||||
@@ -98,7 +98,7 @@ function getModeFromParams(mode: string): RouteParamMode {
|
||||
/**
|
||||
* Get the decoded site data from the params.
|
||||
*/
|
||||
function getSiteURLDataFromParams(params: RouteLayoutParams): SiteURLData {
|
||||
function getSiteURLDataFromParams(params: RouteLayoutParams): PublishedSiteContent {
|
||||
const decoded = decodeURIComponent(params.siteData);
|
||||
return rison.decode(decoded);
|
||||
}
|
||||
|
||||
@@ -25,30 +25,6 @@ import { GITBOOK_URL } from './env';
|
||||
import { type ImageResizer, createImageResizer } from './images';
|
||||
import { type GitBookLinker, createLinker } from './links';
|
||||
|
||||
/**
|
||||
* Data about the site URL. Provided by the middleware.
|
||||
* These data are stable between pages in the same site space.
|
||||
*/
|
||||
export type SiteURLData = Pick<
|
||||
PublishedSiteContent,
|
||||
| 'organization'
|
||||
| 'apiToken'
|
||||
| 'site'
|
||||
| 'siteSpace'
|
||||
| 'space'
|
||||
| 'revision'
|
||||
| 'changeRequest'
|
||||
| 'shareKey'
|
||||
| 'siteSection'
|
||||
| 'siteBasePath'
|
||||
| 'basePath'
|
||||
> & {
|
||||
/**
|
||||
* Identifier used for image resizing.
|
||||
*/
|
||||
imagesContextId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic context when rendering content.
|
||||
*/
|
||||
@@ -135,7 +111,7 @@ export type GitBookPageContext = (GitBookSpaceContext | GitBookSiteContext) & {
|
||||
*/
|
||||
export function getBaseContext(input: {
|
||||
siteURL: URL | string;
|
||||
siteURLData: SiteURLData;
|
||||
siteURLData: PublishedSiteContent;
|
||||
urlMode: 'url' | 'url-host';
|
||||
}) {
|
||||
const { urlMode, siteURLData } = input;
|
||||
@@ -169,7 +145,7 @@ export function getBaseContext(input: {
|
||||
}
|
||||
|
||||
const imageResizer = createImageResizer({
|
||||
imagesContextId: siteURLData.imagesContextId,
|
||||
host: siteURL.host,
|
||||
// To ensure image resizing work for proxied sites,
|
||||
// we serve images from the root of the site.
|
||||
linker: linker,
|
||||
@@ -187,7 +163,7 @@ export function getBaseContext(input: {
|
||||
*/
|
||||
export async function fetchSiteContextByURLLookup(
|
||||
baseContext: GitBookBaseContext,
|
||||
data: SiteURLData
|
||||
data: PublishedSiteContent
|
||||
): Promise<GitBookSiteContext> {
|
||||
return await fetchSiteContextByIds(baseContext, {
|
||||
organization: data.organization,
|
||||
|
||||
@@ -33,13 +33,13 @@ export interface CloudflareImageOptions {
|
||||
* Create an image resizer for a rendering context.
|
||||
*/
|
||||
export function createImageResizer({
|
||||
imagesContextId,
|
||||
host,
|
||||
linker,
|
||||
}: {
|
||||
/** The linker to use to create URLs. */
|
||||
linker: GitBookLinker;
|
||||
/** The site identifier to use for verifying the image signature. */
|
||||
imagesContextId: string;
|
||||
/** The host name of the current site. */
|
||||
host: string;
|
||||
}): ImageResizer {
|
||||
if (!GITBOOK_IMAGE_RESIZE_URL || !GITBOOK_IMAGE_RESIZE_SIGNING_KEY) {
|
||||
return createNoopImageResizer();
|
||||
@@ -58,7 +58,7 @@ export function createImageResizer({
|
||||
|
||||
return async (options) => {
|
||||
cachedSignature ??= await generateImageSignature({
|
||||
imagesContextId,
|
||||
host,
|
||||
url: urlInput,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* Get the site identifier to use for image resizing for an incoming request.
|
||||
* This identifier can be obtained before resolving the request URL.
|
||||
*/
|
||||
export function getImageResizingContextId(url: URL): string {
|
||||
if (url.host === 'proxy.gitbook.site' || url.host === 'proxy.gitbook-staging.site') {
|
||||
// For proxy requests, we extract the site ID from the pathname
|
||||
// e.g. https://proxy.gitbook.site/site/siteId/...
|
||||
const pathname = url.pathname.slice(1).split('/');
|
||||
return pathname.slice(0, 2).join('/');
|
||||
}
|
||||
|
||||
return url.host;
|
||||
}
|
||||
@@ -2,4 +2,3 @@ export * from './types';
|
||||
export * from './createImageResizer';
|
||||
export * from './signatures';
|
||||
export * from './utils';
|
||||
export * from './getImageResizingContextId';
|
||||
|
||||
@@ -18,7 +18,7 @@ export const CURRENT_SIGNATURE_VERSION: SignatureVersion = '2';
|
||||
|
||||
type SignFnInput = {
|
||||
url: string;
|
||||
imagesContextId: string;
|
||||
host: string;
|
||||
};
|
||||
|
||||
type SignFn = (input: SignFnInput) => MaybePromise<string>;
|
||||
@@ -60,7 +60,7 @@ const generateSignatureV2: SignFn = async (input) => {
|
||||
assert(GITBOOK_IMAGE_RESIZE_SIGNING_KEY, 'GITBOOK_IMAGE_RESIZE_SIGNING_KEY is not set');
|
||||
const all = [
|
||||
input.url,
|
||||
input.imagesContextId, // The hostname is used to avoid serving images from other sites on the same domain
|
||||
input.host, // The hostname is used to avoid serving images from other sites on the same domain
|
||||
GITBOOK_IMAGE_RESIZE_SIGNING_KEY,
|
||||
]
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { CustomizationThemeMode } from '@gitbook/api';
|
||||
import { CustomizationThemeMode, type PublishedSiteContent } from '@gitbook/api';
|
||||
import { headers } from 'next/headers';
|
||||
import type { SiteURLData } from './context';
|
||||
|
||||
export enum MiddlewareHeaders {
|
||||
/**
|
||||
@@ -58,7 +57,7 @@ export async function getURLModeFromMiddleware(): Promise<'url' | 'url-host'> {
|
||||
* Get the site URL data from the middleware headers.
|
||||
* This function should only be called in a server action or a dynamic route.
|
||||
*/
|
||||
export async function getSiteURLDataFromMiddleware(): Promise<SiteURLData> {
|
||||
export async function getSiteURLDataFromMiddleware(): Promise<PublishedSiteContent> {
|
||||
const headersList = await headers();
|
||||
const siteURLData = headersList.get(MiddlewareHeaders.SiteURLData);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CustomizationThemeMode } from '@gitbook/api';
|
||||
import { CustomizationThemeMode, type PublishedSiteContentLookup } from '@gitbook/api';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
import rison from 'rison';
|
||||
@@ -8,6 +8,7 @@ import { validateSerializedCustomization } from '@/lib/customization';
|
||||
import { removeLeadingSlash, removeTrailingSlash } from '@/lib/paths';
|
||||
import {
|
||||
type ResponseCookies,
|
||||
type VisitorTokenLookup,
|
||||
getPathScopedCookieName,
|
||||
getResponseCookiesForVisitorAuth,
|
||||
getVisitorToken,
|
||||
@@ -21,9 +22,7 @@ import {
|
||||
throwIfDataError,
|
||||
} from '@v2/lib/data';
|
||||
import { isGitBookAssetsHostURL, isGitBookHostURL } from '@v2/lib/env';
|
||||
import { getImageResizingContextId } from '@v2/lib/images';
|
||||
import { MiddlewareHeaders } from '@v2/lib/middleware';
|
||||
import type { SiteURLData } from './lib/context';
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
@@ -66,21 +65,6 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
}
|
||||
|
||||
const { url: siteRequestURL, mode } = match;
|
||||
const imagesContextId = getImageResizingContextId(siteRequestURL);
|
||||
|
||||
/**
|
||||
* Serve image resizing requests (all requests containing `/~gitbook/image`).
|
||||
* All URLs containing `/~gitbook/image` are rewritten to `/~gitbook/image`
|
||||
* and serve from a single route handler.
|
||||
*
|
||||
* In GitBook v1: image resizing was done at the root of the hostname (docs.company.com/~gitbook/image)
|
||||
* In GitBook v2: image resizing is done at the content level (docs.company.com/section/variant/~gitbook/image)
|
||||
*/
|
||||
if (siteRequestURL.pathname.endsWith('/~gitbook/image')) {
|
||||
return await serveResizedImage(request, {
|
||||
imagesContextId: imagesContextId,
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Detect and extract the visitor authentication token from the request
|
||||
@@ -92,20 +76,7 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
});
|
||||
|
||||
const withAPIToken = async (apiToken: string | null) => {
|
||||
const siteURLData = await throwIfDataError(
|
||||
getPublishedContentByURL({
|
||||
url: siteRequestURL.toString(),
|
||||
visitorAuthToken: visitorToken?.token ?? null,
|
||||
// When the visitor auth token is pulled from the cookie, set redirectOnError when calling getPublishedContentByUrl to allow
|
||||
// redirecting when the token is invalid as we could be dealing with stale token stored in the cookie.
|
||||
// For example when the VA backend signature has changed but the token stored in the cookie is not yet expired.
|
||||
redirectOnError: visitorToken?.source === 'visitor-auth-cookie',
|
||||
|
||||
// Use the API token passed in the request, if any
|
||||
// as it could be used for .preview hostnames
|
||||
apiToken,
|
||||
})
|
||||
);
|
||||
const siteURLData = await getSiteURLData(siteRequestURL, visitorToken, apiToken);
|
||||
const cookies: ResponseCookies = [];
|
||||
|
||||
//
|
||||
@@ -206,7 +177,7 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
|
||||
// We pick only stable data from the siteURL data to prevent re-rendering of
|
||||
// the root layout when changing pages..
|
||||
const stableSiteURLData: SiteURLData = {
|
||||
const stableSiteURLData: Omit<typeof siteURLData, 'pathname' | 'canonicalUrl'> = {
|
||||
site: siteURLData.site,
|
||||
siteSection: siteURLData.siteSection,
|
||||
siteSpace: siteURLData.siteSpace,
|
||||
@@ -218,7 +189,8 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
revision: siteURLData.revision,
|
||||
shareKey: siteURLData.shareKey,
|
||||
apiToken: siteURLData.apiToken,
|
||||
imagesContextId: imagesContextId,
|
||||
complete: siteURLData.complete,
|
||||
contextId: siteURLData.contextId,
|
||||
};
|
||||
|
||||
const route = [
|
||||
@@ -261,6 +233,26 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
return writeResponseCookies(response, cookies);
|
||||
};
|
||||
|
||||
/**
|
||||
* Serve image resizing requests (all requests containing `/~gitbook/image`).
|
||||
* All URLs containing `/~gitbook/image` are rewritten to `/~gitbook/image`
|
||||
* and serve from a single route handler.
|
||||
*
|
||||
* In GitBook v1: image resizing was done at the root of the hostname (docs.company.com/~gitbook/image)
|
||||
* In GitBook v2: image resizing is done at the content level (docs.company.com/section/variant/~gitbook/image)
|
||||
*/
|
||||
if (siteRequestURL.pathname.endsWith('/~gitbook/image')) {
|
||||
const siteURLData = await getSiteURLData(siteRequestURL, visitorToken, null);
|
||||
if ('redirect' in siteURLData) {
|
||||
// This should not happen because the redirect would've been taken care of earlier.
|
||||
throw new Error(`Unexpected redirect while serving image at ${siteRequestURL}`);
|
||||
}
|
||||
|
||||
return await serveResizedImage(request, {
|
||||
host: new URL(siteURLData.canonicalUrl).host,
|
||||
});
|
||||
}
|
||||
|
||||
// For https://preview/<siteURL> requests,
|
||||
if (siteRequestURL.hostname === 'preview') {
|
||||
return serveWithQueryAPIToken(
|
||||
@@ -274,6 +266,29 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
return withAPIToken(null);
|
||||
}
|
||||
|
||||
async function getSiteURLData(
|
||||
siteRequestURL: URL,
|
||||
visitorToken: VisitorTokenLookup,
|
||||
apiToken: string | null
|
||||
): Promise<PublishedSiteContentLookup> {
|
||||
const siteURLData = await throwIfDataError(
|
||||
getPublishedContentByURL({
|
||||
url: siteRequestURL.toString(),
|
||||
visitorAuthToken: visitorToken?.token ?? null,
|
||||
// When the visitor auth token is pulled from the cookie, set redirectOnError when calling getPublishedContentByUrl to allow
|
||||
// redirecting when the token is invalid as we could be dealing with stale token stored in the cookie.
|
||||
// For example when the VA backend signature has changed but the token stored in the cookie is not yet expired.
|
||||
redirectOnError: visitorToken?.source === 'visitor-auth-cookie',
|
||||
|
||||
// Use the API token passed in the request, if any
|
||||
// as it could be used for .preview hostnames
|
||||
apiToken,
|
||||
})
|
||||
);
|
||||
|
||||
return siteURLData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve routes for PDF export for a space: /~space/:spaceId/~gitbook/pdf
|
||||
*/
|
||||
|
||||
@@ -1,66 +1,5 @@
|
||||
# gitbook
|
||||
|
||||
## 0.9.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- da7b369: Fix missing headers in OpenAPIResponses
|
||||
- 139a805: Fix OpenAPI enum display
|
||||
- Updated dependencies [da7b369]
|
||||
- Updated dependencies [da485f5]
|
||||
- Updated dependencies [139a805]
|
||||
- @gitbook/react-openapi@1.1.9
|
||||
|
||||
## 0.9.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fb90eb0: Limit tinted background on bold theme to sites with filled sidebar
|
||||
- 7d0b422: Handle grouped OpenAPISchemas
|
||||
- Updated dependencies [7d0b422]
|
||||
- Updated dependencies [fb90eb0]
|
||||
- @gitbook/react-openapi@1.1.8
|
||||
- @gitbook/colors@0.3.1
|
||||
|
||||
## 0.9.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 77fd393: Track event when visitor is opening a search result.
|
||||
- d70d566: Support site announcement banner
|
||||
- 77fd393: Track event when clicking announcement banner link.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e84a46a: Fix OpenAPI tabs indicator overflow
|
||||
- bc90adb: Fix favicon not being displayed in Google because `robots.txt` was preventing the indexation of the image route
|
||||
- 434af90: Fix image resizing when using the proxy feature in a site.
|
||||
- c756761: Add breadcrumbs to search results
|
||||
- 40e8e69: Disallow crawling by web-robots of search/ask URLs
|
||||
- 77fd393: Fix clicking search results when the site is embedded in an iframe.
|
||||
- 1505ddb: Fix multiple request examples selector not showing
|
||||
- 61db166: Add OpenAPI write-only indicator
|
||||
- 6f71da8: Fix padding in schemas
|
||||
- fa91eb7: Fix PDF generation when user has dark mode configured.
|
||||
- 5b1e01c: Support for x-stability property
|
||||
- 57ca4e0: Fix a crash when a page contains a block of an integration that is no longer installed
|
||||
- d236bf0: Fix flash when loading sites with dark mode as default theme
|
||||
- cd99ed5: Fix spec properties rendering and missing keys
|
||||
- 813b2af: Support for x-enumDescriptions and x-gitbook-enum
|
||||
- e9fa50d: Trim the search query to avoid showing a loading state when typing
|
||||
- Updated dependencies [bd35348]
|
||||
- Updated dependencies [ae78fc5]
|
||||
- Updated dependencies [7bb37c7]
|
||||
- Updated dependencies [373183a]
|
||||
- Updated dependencies [1505ddb]
|
||||
- Updated dependencies [61db166]
|
||||
- Updated dependencies [5b1e01c]
|
||||
- Updated dependencies [cd99ed5]
|
||||
- Updated dependencies [813b2af]
|
||||
- Updated dependencies [a25fded]
|
||||
- @gitbook/react-openapi@1.1.7
|
||||
- @gitbook/openapi-parser@2.1.2
|
||||
|
||||
## 0.8.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gitbook",
|
||||
"version": "0.9.2",
|
||||
"version": "0.8.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "env-cmd --silent -f ../../.env.local next dev",
|
||||
@@ -32,7 +32,9 @@
|
||||
"@sindresorhus/fnv1a": "^3.1.0",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"ai": "^4.2.2",
|
||||
"@upstash/redis": "^1.27.1",
|
||||
"ai": "^4.1.46",
|
||||
"ajv": "^8.12.0",
|
||||
"assert-never": "^1.2.1",
|
||||
"bun-types": "^1.1.20",
|
||||
"classnames": "^2.5.1",
|
||||
@@ -66,12 +68,7 @@
|
||||
"tailwind-shades": "^1.1.2",
|
||||
"unified": "^11.0.5",
|
||||
"url-join": "^5.0.0",
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"zod": "^3.24.2",
|
||||
"zod-to-json-schema": "^3.24.5",
|
||||
"event-iterator": "^2.0.0",
|
||||
"partial-json": "^0.1.7",
|
||||
"zustand": "^5.0.3"
|
||||
"usehooks-ts": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@argos-ci/playwright": "^4.3.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@/components/primitives/Button';
|
||||
import { t, tString, useLanguage } from '@/intl/client';
|
||||
import { t, useLanguage } from '@/intl/client';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
export default function ErrorPage(props: {
|
||||
@@ -35,8 +35,9 @@ export default function ErrorPage(props: {
|
||||
}}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
label={tString(language, 'unexpected_error_retry')}
|
||||
/>
|
||||
>
|
||||
{t(language, 'unexpected_error_retry')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
'use client';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Loading } from '../primitives';
|
||||
import { streamLinkPageSummary } from './server-actions/streamLinkPageSummary';
|
||||
|
||||
/**
|
||||
* Summarise a page's content for use in a link preview
|
||||
*/
|
||||
export function AIPageLinkSummary(props: {
|
||||
currentSpaceId: string;
|
||||
currentPageId: string;
|
||||
currentPageTitle: string;
|
||||
targetSpaceId: string;
|
||||
targetPageId: string;
|
||||
linkPreview?: string;
|
||||
linkTitle?: string;
|
||||
showTrademark: boolean;
|
||||
}) {
|
||||
const {
|
||||
currentSpaceId,
|
||||
currentPageId,
|
||||
targetSpaceId,
|
||||
targetPageId,
|
||||
linkPreview,
|
||||
linkTitle,
|
||||
showTrademark = true,
|
||||
} = props;
|
||||
|
||||
const [highlight, setHighlight] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
let canceled = false;
|
||||
|
||||
setHighlight('');
|
||||
|
||||
(async () => {
|
||||
const stream = await streamLinkPageSummary({
|
||||
currentSpaceId,
|
||||
currentPageId,
|
||||
targetSpaceId,
|
||||
targetPageId,
|
||||
linkPreview,
|
||||
linkTitle,
|
||||
previousPageIds: [],
|
||||
});
|
||||
|
||||
for await (const highlight of stream) {
|
||||
if (canceled) return;
|
||||
setHighlight(highlight ?? '');
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [currentSpaceId, currentPageId, targetSpaceId, targetPageId, linkPreview, linkTitle]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex w-screen items-center gap-1 font-semibold text-tint text-xs uppercase leading-tight tracking-wide">
|
||||
{showTrademark ? (
|
||||
<Loading className="size-4" busy={!highlight || highlight.length === 0} />
|
||||
) : (
|
||||
<Icon icon="sparkle" className="size-3" />
|
||||
)}
|
||||
<h6 className="text-tint">Page highlight</h6>
|
||||
</div>
|
||||
{highlight.length > 0 ? <p>{highlight}</p> : null}
|
||||
{highlight.length > 0 ? (
|
||||
<div className="text-tint-subtle text-xs">
|
||||
Based on your context. May contain mistakes.
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './AIPageLinkSummary';
|
||||
@@ -1,124 +0,0 @@
|
||||
'use server';
|
||||
import { type AIMessageInput, AIModel, type AIStreamResponse } from '@gitbook/api';
|
||||
import type { GitBookBaseContext } from '@v2/lib/context';
|
||||
import { EventIterator } from 'event-iterator';
|
||||
import type { MaybePromise } from 'p-map';
|
||||
import * as partialJson from 'partial-json';
|
||||
import type { DeepPartial } from 'ts-essentials';
|
||||
import type { z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
|
||||
/**
|
||||
* Get the latest value from a stream and the response id.
|
||||
*/
|
||||
export async function generate<T>(
|
||||
promise: MaybePromise<{
|
||||
stream: EventIterator<T>;
|
||||
response: Promise<{ responseId: string }>;
|
||||
}>
|
||||
) {
|
||||
const input = await promise;
|
||||
let value: T | undefined;
|
||||
|
||||
for await (const event of input.stream) {
|
||||
value = event;
|
||||
}
|
||||
|
||||
const { responseId } = await input.response;
|
||||
return {
|
||||
responseId,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream the generation of an object using the AI.
|
||||
*/
|
||||
export async function streamGenerateObject<T>(
|
||||
context: GitBookBaseContext,
|
||||
{
|
||||
organizationId,
|
||||
siteId,
|
||||
}: {
|
||||
organizationId: string;
|
||||
siteId: string;
|
||||
},
|
||||
{
|
||||
schema,
|
||||
messages,
|
||||
model = AIModel.Fast,
|
||||
}: {
|
||||
schema: z.ZodSchema<T>;
|
||||
messages: AIMessageInput[];
|
||||
model?: AIModel;
|
||||
previousResponseId?: string;
|
||||
}
|
||||
) {
|
||||
const apiClient = await context.dataFetcher.api();
|
||||
|
||||
const rawStream = apiClient.orgs.streamAiResponseInSite(organizationId, siteId, {
|
||||
input: messages,
|
||||
output: {
|
||||
type: 'object',
|
||||
schema: zodToJsonSchema(schema),
|
||||
},
|
||||
model,
|
||||
});
|
||||
|
||||
let json = '';
|
||||
return parseResponse<DeepPartial<T>>(rawStream, (event) => {
|
||||
if (event.type === 'response_object') {
|
||||
json += event.jsonChunk;
|
||||
|
||||
const parsed = partialJson.parse(json, partialJson.ALL);
|
||||
return parsed;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a stream from the API to extract the responseId.
|
||||
*/
|
||||
function parseResponse<T>(
|
||||
responseStream: EventIterator<AIStreamResponse>,
|
||||
parse: (response: AIStreamResponse) => T | undefined
|
||||
): {
|
||||
stream: EventIterator<T>;
|
||||
response: Promise<{ responseId: string }>;
|
||||
} {
|
||||
let resolveResponse: (value: { responseId: string }) => void;
|
||||
const response = new Promise<{ responseId: string }>((resolve) => {
|
||||
resolveResponse = resolve;
|
||||
});
|
||||
|
||||
const stream = new EventIterator<T>((queue) => {
|
||||
(async () => {
|
||||
let foundResponse = false;
|
||||
|
||||
for await (const event of responseStream) {
|
||||
if (event.type === 'response_finish') {
|
||||
foundResponse = true;
|
||||
resolveResponse({ responseId: event.responseId });
|
||||
} else {
|
||||
const parsed = parse(event);
|
||||
if (parsed !== undefined) {
|
||||
queue.push(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundResponse) {
|
||||
throw new Error('No response found');
|
||||
}
|
||||
})().then(
|
||||
() => {
|
||||
queue.stop();
|
||||
},
|
||||
(error) => {
|
||||
queue.fail(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return { stream, response };
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './streamLinkPageSummary';
|
||||
@@ -1,127 +0,0 @@
|
||||
'use server';
|
||||
import { getV1BaseContext } from '@/lib/v1';
|
||||
import { isV2 } from '@/lib/v2';
|
||||
import { AIMessageRole } from '@gitbook/api';
|
||||
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
|
||||
import { fetchServerActionSiteContext, getServerActionBaseContext } from '@v2/lib/server-actions';
|
||||
import { z } from 'zod';
|
||||
import { streamGenerateObject } from './api';
|
||||
|
||||
/**
|
||||
* Get a summary of a page, in the context of another page
|
||||
*/
|
||||
export async function* streamLinkPageSummary({
|
||||
currentSpaceId,
|
||||
currentPageId,
|
||||
targetSpaceId,
|
||||
targetPageId,
|
||||
linkPreview,
|
||||
linkTitle,
|
||||
}: {
|
||||
currentSpaceId: string;
|
||||
currentPageId: string;
|
||||
targetSpaceId: string;
|
||||
targetPageId: string;
|
||||
linkPreview?: string;
|
||||
linkTitle?: string;
|
||||
previousPageIds?: string[];
|
||||
}) {
|
||||
const baseContext = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
|
||||
const siteURLData = await getSiteURLDataFromMiddleware();
|
||||
|
||||
const [{ stream }] = await Promise.all([
|
||||
streamGenerateObject(
|
||||
baseContext,
|
||||
{
|
||||
organizationId: siteURLData.organization,
|
||||
siteId: siteURLData.site,
|
||||
},
|
||||
{
|
||||
schema: z.object({
|
||||
highlight: z.string().describe('The most important content of the target page'),
|
||||
// questions: z.array(z.string().describe('The questions to sea')).max(3),
|
||||
}),
|
||||
messages: [
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# Role
|
||||
You are a documentation navigator. Your job is to help the user read documentation more efficiently. Your aim is to prevent the user from having to read the target page by giving them all the information they need to know.
|
||||
|
||||
# Task
|
||||
Using both the current page context and the target page content, produce a page highlight that:
|
||||
- Highlights the key facts from the target page.
|
||||
- Relates strongly to the topic the user is currently reading about.
|
||||
- Is very succinct and direct, using only one or two short sentences (each sentence using no more than one comma).
|
||||
- Remains strictly factual, without referring to “the page”.
|
||||
|
||||
# Instructions
|
||||
1. Identify the key paragraph surrounding the link's text (e.g., “change request”) from the current page.
|
||||
2. Extract and combine relevant information from the target page to address the link's context.
|
||||
3. Combine in one or two short sentences that are direct and brief.
|
||||
|
||||
# Examples
|
||||
## Example 1
|
||||
- Link context: “This feature is only available on the Ultimate plan.”
|
||||
- Link preview: “Pricing: Learn about our different pricing tiers.”
|
||||
- Response: “The Ultimate plan costs $25 per month. A Pro plan is available too.”
|
||||
|
||||
## Example 2
|
||||
- Link context: “You can use keyboard shortcuts to get to the Search menu faster.”
|
||||
- Link preview: “Keyboard shortcuts: A quick reference guide to all the keyboard shortcuts available.”
|
||||
- Response: “To open the Search menu, use the keyboard shortcut ⌘K or Ctrl+K.”
|
||||
|
||||
## Example 3
|
||||
- Link context: “This feature can only be enabled by an admin.”
|
||||
- Link preview: “Roles: An overview of the different roles on the platform.”
|
||||
- Response: “The admin role is reserved for the creator of the organisation.”`,
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# Context
|
||||
## Current page
|
||||
The content of the current page is:`,
|
||||
attachments: [
|
||||
{
|
||||
type: 'page',
|
||||
spaceId: currentSpaceId,
|
||||
pageId: currentPageId,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `## Target page
|
||||
The content of the target page is:`,
|
||||
attachments: [
|
||||
{
|
||||
type: 'page',
|
||||
spaceId: targetSpaceId,
|
||||
pageId: targetPageId,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `## Link preview
|
||||
The content of the link preview is:
|
||||
> ${linkPreview}`,
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.User,
|
||||
content: `I'm considering reading the link titled "${linkTitle}" to page ID ${targetPageId}. Give the most relevant information from this page. Relate it to my current page and in particular the paragraph I'm currently reading. Be very concise.`,
|
||||
},
|
||||
],
|
||||
}
|
||||
),
|
||||
fetchServerActionSiteContext(baseContext),
|
||||
]);
|
||||
|
||||
for await (const value of stream) {
|
||||
const highlight = value.highlight;
|
||||
if (!highlight) {
|
||||
continue;
|
||||
}
|
||||
|
||||
yield highlight;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import {
|
||||
ANNOUNCEMENT_CSS_CLASS,
|
||||
ANNOUNCEMENT_DAYS_TILL_RESET,
|
||||
@@ -19,15 +18,6 @@ export function AnnouncementDismissedScript() {
|
||||
ANNOUNCEMENT_CSS_CLASS,
|
||||
]).slice(1, -1);
|
||||
|
||||
// makes sure the banner dismissed state is kept when navigating between sites
|
||||
React.useEffect(() => {
|
||||
checkStorageForDismissedScript(
|
||||
ANNOUNCEMENT_STORAGE_KEY,
|
||||
ANNOUNCEMENT_DAYS_TILL_RESET,
|
||||
ANNOUNCEMENT_CSS_CLASS
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<script
|
||||
suppressHydrationWarning
|
||||
|
||||
@@ -97,8 +97,9 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
|
||||
onClick={() => {
|
||||
onUpdateState(true);
|
||||
}}
|
||||
label={tString(language, 'cookies_accept')}
|
||||
/>
|
||||
>
|
||||
{t(language, 'cookies_accept')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
@@ -106,8 +107,9 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
|
||||
onClick={() => {
|
||||
onUpdateState(false);
|
||||
}}
|
||||
label={tString(language, 'cookies_reject')}
|
||||
/>
|
||||
>
|
||||
{t(language, 'cookies_reject')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -104,7 +104,7 @@ const HINT_STYLES: {
|
||||
'[&_.can-override-text]:text-neutral-strong',
|
||||
],
|
||||
container:
|
||||
'bg-info border-info theme-muted-tint:bg-info-solid/2 [html.sidebar-filled.theme-bold.tint_&]:bg-info-solid/2',
|
||||
'bg-info border-info theme-muted-tint:bg-info-solid/2 theme-bold-tint:bg-info-solid/2',
|
||||
containerWithHeader: 'border-info-solid bg-info-subtle',
|
||||
},
|
||||
warning: {
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import { type DocumentInlineLink, SiteInsightsLinkPosition } from '@gitbook/api';
|
||||
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
|
||||
import { StyledLink } from '../primitives';
|
||||
import type { InlineProps } from './Inline';
|
||||
import { InlineLinkTooltip } from './InlineLinkTooltip';
|
||||
import { Inlines } from './Inlines';
|
||||
|
||||
export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
|
||||
const { inline, document, context, ancestorInlines } = props;
|
||||
|
||||
const resolved = context.contentContext
|
||||
? await resolveContentRef(inline.data.ref, context.contentContext, {
|
||||
resolveAnchorText: true,
|
||||
})
|
||||
? await resolveContentRef(inline.data.ref, context.contentContext)
|
||||
: null;
|
||||
|
||||
if (!context.contentContext || !resolved) {
|
||||
if (!resolved) {
|
||||
return (
|
||||
<span title="Broken link" className="underline">
|
||||
<Inlines
|
||||
@@ -28,38 +25,24 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const isExternal = inline.data.ref.kind === 'url';
|
||||
|
||||
return (
|
||||
<InlineLinkTooltip
|
||||
inline={inline}
|
||||
document={document}
|
||||
context={context}
|
||||
ancestorInlines={ancestorInlines}
|
||||
<StyledLink
|
||||
href={resolved.href}
|
||||
insights={{
|
||||
type: 'link_click',
|
||||
link: {
|
||||
target: inline.data.ref,
|
||||
position: SiteInsightsLinkPosition.Content,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<StyledLink
|
||||
href={resolved.href}
|
||||
insights={{
|
||||
type: 'link_click',
|
||||
link: {
|
||||
target: inline.data.ref,
|
||||
position: SiteInsightsLinkPosition.Content,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Inlines
|
||||
context={context}
|
||||
document={document}
|
||||
nodes={inline.nodes}
|
||||
ancestorInlines={[...ancestorInlines, inline]}
|
||||
/>
|
||||
{isExternal ? (
|
||||
<Icon
|
||||
icon="arrow-up-right"
|
||||
className="ml-0.5 inline size-3 links-accent:text-tint-subtle"
|
||||
/>
|
||||
) : null}
|
||||
</StyledLink>
|
||||
</InlineLinkTooltip>
|
||||
<Inlines
|
||||
context={context}
|
||||
document={document}
|
||||
nodes={inline.nodes}
|
||||
ancestorInlines={[...ancestorInlines, inline]}
|
||||
/>
|
||||
</StyledLink>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
import type { DocumentInlineLink } from '@gitbook/api';
|
||||
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||
import { Fragment } from 'react';
|
||||
import { AIPageLinkSummary } from '../Adaptive/AIPageLinkSummary';
|
||||
import { Button, StyledLink } from '../primitives';
|
||||
import type { InlineProps } from './Inline';
|
||||
import { Inlines } from './Inlines';
|
||||
|
||||
export async function InlineLinkTooltip(
|
||||
props: InlineProps<DocumentInlineLink> & { children: React.ReactNode }
|
||||
) {
|
||||
const { inline, document, context, ancestorInlines, children } = props;
|
||||
|
||||
const resolved = context.contentContext
|
||||
? await resolveContentRef(inline.data.ref, context.contentContext, {
|
||||
resolveAnchorText: true,
|
||||
})
|
||||
: null;
|
||||
|
||||
if (!context.contentContext || !resolved) {
|
||||
return (
|
||||
<span title="Broken link" className="underline">
|
||||
<Inlines
|
||||
context={context}
|
||||
document={document}
|
||||
nodes={inline.nodes}
|
||||
ancestorInlines={[...ancestorInlines, inline]}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
let breadcrumbs = resolved.ancestors;
|
||||
const isExternal = inline.data.ref.kind === 'url';
|
||||
const isSamePage = inline.data.ref.kind === 'anchor' && inline.data.ref.page === undefined;
|
||||
if (isExternal) {
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: 'External link to',
|
||||
},
|
||||
];
|
||||
}
|
||||
if (isSamePage) {
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: 'Jump to section',
|
||||
icon: <Icon icon="arrow-down-short-wide" className="size-3" />,
|
||||
},
|
||||
];
|
||||
resolved.subText = undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip.Provider delayDuration={200}>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>{children}</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content className="z-40 w-screen max-w-md animate-present px-4 sm:w-auto">
|
||||
<div className="overflow-hidden rounded-md straight-corners:rounded-none shadow-lg shadow-tint-12/4 ring-1 ring-tint-subtle dark:shadow-tint-1 ">
|
||||
<div className="bg-tint-base p-4">
|
||||
<div className={tcls('flex gap-4')}>
|
||||
<div className="flex flex-col">
|
||||
{breadcrumbs && breadcrumbs.length > 0 ? (
|
||||
<div className="mb-1 flex grow flex-wrap items-center gap-x-2 gap-y-0.5 font-semibold text-tint text-xs uppercase leading-tight tracking-wide">
|
||||
{breadcrumbs.map((crumb, index) => {
|
||||
const Tag = crumb.href ? StyledLink : 'div';
|
||||
|
||||
return (
|
||||
<Fragment key={crumb.label}>
|
||||
{index !== 0 ? (
|
||||
<Icon
|
||||
icon="chevron-right"
|
||||
className="size-3 text-tint-subtle"
|
||||
/>
|
||||
) : null}
|
||||
<Tag
|
||||
className={tcls(
|
||||
'flex gap-1',
|
||||
crumb.href &&
|
||||
'links-default:text-tint no-underline hover:underline contrast-more:underline contrast-more:decoration-current'
|
||||
)}
|
||||
href={crumb.href ?? '#'}
|
||||
>
|
||||
{crumb.icon ? (
|
||||
<span className="mt-0.5 text-tint-subtle empty:hidden">
|
||||
{crumb.icon}
|
||||
</span>
|
||||
) : null}
|
||||
{crumb.label}
|
||||
</Tag>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
className={tcls(
|
||||
'flex gap-2 leading-snug',
|
||||
isExternal && 'text-sm [overflow-wrap:anywhere]'
|
||||
)}
|
||||
>
|
||||
{resolved.icon ? (
|
||||
<div className="mt-1 text-tint-subtle empty:hidden">
|
||||
{resolved.icon}
|
||||
</div>
|
||||
) : null}
|
||||
<h5 className="font-semibold">{resolved.text}</h5>
|
||||
</div>
|
||||
</div>
|
||||
{!isSamePage && resolved.href ? (
|
||||
<Button
|
||||
className={tcls(
|
||||
'-mx-2 -my-2 ml-auto',
|
||||
breadcrumbs?.length === 0
|
||||
? null
|
||||
: 'place-self-start'
|
||||
)}
|
||||
variant="blank"
|
||||
href={resolved.href}
|
||||
target="_blank"
|
||||
label="Open in new tab"
|
||||
size="small"
|
||||
icon="arrow-up-right-from-square"
|
||||
iconOnly={true}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{resolved.subText ? (
|
||||
<p className="mt-1 text-sm text-tint">{resolved.subText}</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{'customization' in context.contentContext &&
|
||||
context.contentContext.customization.ai?.pageLinkSummaries.enabled &&
|
||||
!isExternal &&
|
||||
'page' in context.contentContext &&
|
||||
inline.data.ref.kind === 'page' ? (
|
||||
<div className="border-tint-subtle border-t bg-tint p-4">
|
||||
<AIPageLinkSummary
|
||||
currentPageId={context.contentContext.page.id}
|
||||
currentSpaceId={context.contentContext.space.id}
|
||||
currentPageTitle={context.contentContext.page.title}
|
||||
targetPageId={
|
||||
inline.data.ref.page ?? context.contentContext.page.id
|
||||
}
|
||||
targetSpaceId={
|
||||
inline.data.ref.space ?? context.contentContext.space.id
|
||||
}
|
||||
linkTitle={inline.nodes
|
||||
.map((node) => {
|
||||
if (node.object === 'text') {
|
||||
return node.leaves
|
||||
.map((leaf) => leaf.text)
|
||||
.join('');
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.join('')}
|
||||
linkPreview={`**${resolved.text}**: ${resolved.subText}`}
|
||||
showTrademark={
|
||||
context.contentContext.customization.trademark.enabled
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Tooltip.Arrow className="fill-tint-1" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
);
|
||||
}
|
||||
@@ -77,12 +77,7 @@ async function OpenAPIOperationBody(props: BlockProps<AnyOpenAPIOperationsBlock>
|
||||
ancestorBlocks={props.ancestorBlocks}
|
||||
isEstimatedOffscreen={props.isEstimatedOffscreen}
|
||||
context={props.context}
|
||||
style={tcls([
|
||||
headingProps.deprecated ? 'line-through' : undefined,
|
||||
headingProps.deprecated || !!headingProps.stability
|
||||
? '[&>div]:mt-0'
|
||||
: undefined,
|
||||
])}
|
||||
style={headingProps.deprecated ? 'line-through' : undefined}
|
||||
block={{
|
||||
object: 'block',
|
||||
key: `${block.key}-heading`,
|
||||
|
||||
@@ -50,7 +50,6 @@ async function OpenAPISchemasBody(props: BlockProps<OpenAPISchemasBlock>) {
|
||||
return (
|
||||
<BaseOpenAPISchemas
|
||||
data={data}
|
||||
grouped={block.data.grouped}
|
||||
context={{
|
||||
specUrl,
|
||||
icons: {
|
||||
|
||||
@@ -20,27 +20,10 @@
|
||||
@apply flex flex-col items-start justify-start gap-3;
|
||||
}
|
||||
|
||||
.openapi-summary-tags {
|
||||
@apply flex flex-row gap-2 mt-[0.75em];
|
||||
}
|
||||
|
||||
.openapi-deprecated,
|
||||
.openapi-stability {
|
||||
.openapi-deprecated {
|
||||
@apply py-0.5 px-1.5 min-w-[1.625rem] font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint bg-tint rounded text-sm leading-[calc(max(1.20em,1.25rem))] before:!content-none after:!content-none;
|
||||
}
|
||||
|
||||
.openapi-stability-alpha {
|
||||
@apply text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-900/6 ring-amber-500/5;
|
||||
}
|
||||
|
||||
.openapi-stability-beta {
|
||||
@apply text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-900/6 ring-blue-500/5;
|
||||
}
|
||||
|
||||
.openapi-stability-experimental {
|
||||
@apply text-violet-700 dark:text-violet-300 bg-violet-50 dark:bg-violet-900/6 ring-violet-500/5;
|
||||
}
|
||||
|
||||
.openapi-deprecated-sunset-date {
|
||||
@apply font-semibold font-mono truncate;
|
||||
}
|
||||
@@ -148,7 +131,7 @@
|
||||
|
||||
/* Schema Structure */
|
||||
.openapi-schema-root {
|
||||
/* unstyled */
|
||||
@apply mt-2;
|
||||
}
|
||||
|
||||
.openapi-schema-properties {
|
||||
@@ -261,11 +244,11 @@
|
||||
|
||||
/* Schema Enum */
|
||||
.openapi-schema-enum {
|
||||
@apply text-sm leading-relaxed max-w-full text-tint;
|
||||
@apply flex flex-row text-sm leading-relaxed flex-wrap text-tint;
|
||||
}
|
||||
|
||||
.openapi-schema-enum-value {
|
||||
@apply text-sm mr-1.5;
|
||||
@apply text-sm;
|
||||
}
|
||||
|
||||
.openapi-schema-enum-value:first-child {
|
||||
@@ -672,8 +655,7 @@
|
||||
@apply border border-tint-subtle rounded-lg;
|
||||
}
|
||||
|
||||
.openapi-section-schemas > .openapi-section-body > .openapi-schema-properties > .openapi-schema,
|
||||
.openapi-section-schemas > .openapi-section-body > .openapi-schema-root {
|
||||
.openapi-section-schemas > .openapi-section-body > .openapi-schema-properties > .openapi-schema {
|
||||
@apply p-2.5;
|
||||
}
|
||||
|
||||
@@ -710,7 +692,3 @@
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.openapi-copy-button {
|
||||
@apply hover:brightness-95;
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
key={index}
|
||||
href={ref.href}
|
||||
target="_blank"
|
||||
className="flex flex-row items-center gap-2"
|
||||
style={['flex', 'flex-row', 'items-center', 'gap-2']}
|
||||
insights={
|
||||
ref.file
|
||||
? {
|
||||
|
||||
@@ -12,7 +12,7 @@ export function Tabs(props: BlockProps<DocumentBlockTabs>) {
|
||||
const tabs: TabsItem[] = [];
|
||||
const tabsBody: React.ReactNode[] = [];
|
||||
|
||||
block.nodes.forEach((tab, index) => {
|
||||
block.nodes.forEach((tab) => {
|
||||
tabs.push({
|
||||
id: tab.meta?.id ?? tab.key!,
|
||||
title: tab.data.title ?? '',
|
||||
@@ -20,7 +20,6 @@ export function Tabs(props: BlockProps<DocumentBlockTabs>) {
|
||||
|
||||
tabsBody.push(
|
||||
<Blocks
|
||||
key={tab.key ?? index}
|
||||
nodes={tab.nodes}
|
||||
document={document}
|
||||
ancestorBlocks={[...ancestorBlocks, block, tab]}
|
||||
|
||||
@@ -38,7 +38,7 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo
|
||||
|
||||
'bg-tint-base/9',
|
||||
'theme-muted:bg-tint-subtle/9',
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle/9',
|
||||
'theme-bold-tint:bg-tint-subtle/9',
|
||||
'theme-gradient:bg-gradient-primary',
|
||||
'theme-gradient-tint:bg-gradient-tint',
|
||||
'contrast-more:bg-tint-base',
|
||||
|
||||
@@ -142,9 +142,10 @@ function HeaderItemButton(
|
||||
position: SiteInsightsLinkPosition.Header,
|
||||
},
|
||||
}}
|
||||
label={title}
|
||||
{...rest}
|
||||
/>
|
||||
>
|
||||
{title}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { CustomizationRootLayout } from '@/components/RootLayout';
|
||||
import { defaultCustomization } from '@/lib/utils';
|
||||
import { CustomizationThemeMode } from '@gitbook/api';
|
||||
import type { GitBookSiteContext, GitBookSpaceContext } from '@v2/lib/context';
|
||||
|
||||
/**
|
||||
@@ -17,7 +16,6 @@ export async function PDFRootLayout(props: {
|
||||
customization={
|
||||
'customization' in context ? context.customization : defaultCustomization()
|
||||
}
|
||||
forcedTheme={CustomizationThemeMode.Light}
|
||||
>
|
||||
{children}
|
||||
</CustomizationRootLayout>
|
||||
|
||||
@@ -5,7 +5,7 @@ import React from 'react';
|
||||
|
||||
import { useScrollActiveId } from '@/components/hooks';
|
||||
import { Button } from '@/components/primitives';
|
||||
import { t, tString, useLanguage } from '@/intl/client';
|
||||
import { t, useLanguage } from '@/intl/client';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { type PDFSearchParams, getPDFURLSearchParams } from './urls';
|
||||
|
||||
@@ -58,8 +58,9 @@ export function PageControlButtons(props: {
|
||||
only: true,
|
||||
}).toString()}`}
|
||||
variant="secondary"
|
||||
label={tString(language, 'pdf_mode_only_page')}
|
||||
/>
|
||||
>
|
||||
{t(language, 'pdf_mode_only_page')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
href={`?${getPDFURLSearchParams({
|
||||
@@ -68,8 +69,9 @@ export function PageControlButtons(props: {
|
||||
only: false,
|
||||
}).toString()}`}
|
||||
variant="secondary"
|
||||
label={tString(language, 'pdf_mode_all')}
|
||||
/>
|
||||
>
|
||||
{t(language, 'pdf_mode_all')}
|
||||
</Button>
|
||||
|
||||
{trademark ? <div className={tcls('mt-5')}>{trademark}</div> : null}
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@ 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';
|
||||
@@ -41,14 +42,12 @@ 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',
|
||||
@@ -93,113 +92,143 @@ 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(
|
||||
'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'
|
||||
'hidden',
|
||||
'page-api-block:xl:max-2xl:flex',
|
||||
'text-xs',
|
||||
'tracking-wide',
|
||||
'font-semibold',
|
||||
'uppercase',
|
||||
|
||||
'flex-row',
|
||||
'items-center',
|
||||
'gap-2'
|
||||
)}
|
||||
>
|
||||
{withPageFeedback ? (
|
||||
<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'
|
||||
)}
|
||||
>
|
||||
{document ? (
|
||||
<React.Suspense fallback={null}>
|
||||
<PageFeedbackForm pageId={page.id} className={tcls('mt-2')} />
|
||||
<PageAsideSections document={document} context={context} />
|
||||
</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
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
<div
|
||||
className={tcls(
|
||||
'sticky bottom-0 z-10 mt-auto flex flex-col bg-tint-base theme-gradient-tint:bg-gradient-tint theme-gradient:bg-gradient-primary theme-muted:bg-tint-subtle pb-4 page-api-block:xl:max-2xl:hidden page-api-block:xl:max-2xl:pb-0 page-api-block:xl:max-2xl:group-hover/aside:flex [html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
|
||||
'sticky bottom-0 z-10 mt-auto flex flex-col bg-tint-base theme-bold-tint:bg-tint-subtle theme-gradient-tint:bg-gradient-tint theme-gradient:bg-gradient-primary theme-muted:bg-tint-subtle pb-4 page-api-block:xl:max-2xl:hidden page-api-block:xl:max-2xl:pb-0 page-api-block:xl:max-2xl:group-hover/aside:flex',
|
||||
'page-api-block:xl:max-2xl:bg-transparent'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -111,7 +111,7 @@ export function ScrollSectionsList(props: { sections: DocumentSection[] }) {
|
||||
|
||||
'hover:bg-primary-hover',
|
||||
'theme-muted:hover:bg-primary-active',
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:hover:bg-primary-active',
|
||||
'theme-bold-tint:hover:bg-primary-active',
|
||||
'theme-gradient:hover:bg-primary-active',
|
||||
|
||||
'tint:font-semibold',
|
||||
|
||||
@@ -7,6 +7,7 @@ import { t } from '@/intl/translate';
|
||||
import { hasFullWidthBlock, isNodeEmpty } from '@/lib/document';
|
||||
import type { AncestorRevisionPage } from '@/lib/pages';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { DocumentView, DocumentViewSkeleton } from '../DocumentView';
|
||||
import { TrackPageViewEvent } from '../Insights';
|
||||
import { PageFeedbackForm } from '../PageFeedback';
|
||||
|
||||
@@ -35,7 +35,7 @@ export async function PageHeader(props: {
|
||||
<li key={breadcrumb.id}>
|
||||
<StyledLink
|
||||
href={href}
|
||||
className={tcls(
|
||||
style={tcls(
|
||||
'no-underline',
|
||||
'hover:underline',
|
||||
'text-xs',
|
||||
|
||||
@@ -106,8 +106,9 @@ export function PageFeedbackForm(props: {
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => onSubmitComment(rating, comment)}
|
||||
label={tString(languages, 'submit')}
|
||||
/>
|
||||
>
|
||||
{t(languages, 'submit')}
|
||||
</Button>
|
||||
{comment.length > MAX_COMMENT_LENGTH * 0.8 ? (
|
||||
<span
|
||||
className={
|
||||
|
||||
@@ -153,9 +153,9 @@ export async function CustomizationRootLayout(props: {
|
||||
</head>
|
||||
<body
|
||||
className={tcls(
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
|
||||
'bg-tint-base',
|
||||
'theme-muted:bg-tint-subtle',
|
||||
'theme-bold-tint:bg-tint-subtle',
|
||||
|
||||
'theme-gradient:bg-gradient-primary',
|
||||
'theme-gradient-tint:bg-gradient-tint'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { Icon, type IconName } from '@gitbook/icons';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import React from 'react';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { Link } from '../primitives';
|
||||
import { HighlightQuery } from './HighlightQuery';
|
||||
import type { ComputedPageResult } from './server-actions';
|
||||
@@ -16,14 +16,6 @@ export const SearchPageResultItem = React.forwardRef(function SearchPageResultIt
|
||||
) {
|
||||
const { query, item, active } = props;
|
||||
|
||||
const breadcrumbs =
|
||||
item.breadcrumbs?.map((crumb) => (
|
||||
<span key={crumb.label} className="flex items-center gap-1">
|
||||
{crumb.icon ? <Icon className="size-3" icon={crumb.icon as IconName} /> : null}
|
||||
{crumb.label}
|
||||
</span>
|
||||
)) ?? [];
|
||||
|
||||
return (
|
||||
<Link
|
||||
ref={ref}
|
||||
@@ -61,44 +53,18 @@ export const SearchPageResultItem = React.forwardRef(function SearchPageResultIt
|
||||
/>
|
||||
</div>
|
||||
<div className={tcls('flex', 'flex-col', 'w-full')}>
|
||||
{breadcrumbs.length > 0 ? (
|
||||
{item.spaceTitle ? (
|
||||
<div
|
||||
className={tcls(
|
||||
'text-xs',
|
||||
'opacity-6',
|
||||
'contrast-more:opacity-11',
|
||||
'font-normal',
|
||||
'uppercase',
|
||||
'tracking-wider',
|
||||
'mb-1',
|
||||
'flex',
|
||||
'flex-wrap',
|
||||
'gap-x-2',
|
||||
'gap-y-1',
|
||||
'items-center'
|
||||
'mb-1'
|
||||
)}
|
||||
>
|
||||
{(breadcrumbs.length > 3
|
||||
? [
|
||||
...breadcrumbs.slice(0, 2),
|
||||
<Icon key="ellipsis" icon="ellipsis-h" className="size-3" />,
|
||||
...breadcrumbs.slice(-1),
|
||||
]
|
||||
: breadcrumbs
|
||||
).map((crumb, index) => (
|
||||
<>
|
||||
{index !== 0 ? (
|
||||
<Icon
|
||||
key={`${crumb.key}-icon`}
|
||||
icon="chevron-right"
|
||||
className="size-3"
|
||||
/>
|
||||
) : null}
|
||||
<span key={crumb.key} className="line-clamp-1">
|
||||
{crumb}
|
||||
</span>
|
||||
</>
|
||||
))}
|
||||
{item.spaceTitle}
|
||||
</div>
|
||||
) : null}
|
||||
<HighlightQuery query={query} text={item.title} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use server';
|
||||
|
||||
import { resolvePageId } from '@/lib/pages';
|
||||
import { findSiteSpaceById, getSiteStructureSections } from '@/lib/sites';
|
||||
import { findSiteSpaceById } from '@/lib/sites';
|
||||
import { filterOutNullable } from '@/lib/typescript';
|
||||
import { getV1BaseContext } from '@/lib/v1';
|
||||
import type {
|
||||
@@ -10,8 +10,6 @@ import type {
|
||||
SearchAIRecommendedQuestionStream,
|
||||
SearchPageResult,
|
||||
SearchSpaceResult,
|
||||
SiteSection,
|
||||
SiteSectionGroup,
|
||||
Space,
|
||||
} from '@gitbook/api';
|
||||
import type { GitBookBaseContext, GitBookSiteContext } from '@v2/lib/context';
|
||||
@@ -21,7 +19,6 @@ import type * as React from 'react';
|
||||
|
||||
import { joinPathWithBaseURL } from '@/lib/paths';
|
||||
import { isV2 } from '@/lib/v2';
|
||||
import type { IconName } from '@gitbook/icons';
|
||||
import { throwIfDataError } from '@v2/lib/data';
|
||||
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
|
||||
import { DocumentView } from '../DocumentView';
|
||||
@@ -49,7 +46,8 @@ export interface ComputedPageResult {
|
||||
pageId: string;
|
||||
spaceId: string;
|
||||
|
||||
breadcrumbs?: Array<{ icon?: IconName; label: string }>;
|
||||
/** When part of a multi-spaces search, the title of the space */
|
||||
spaceTitle?: string;
|
||||
}
|
||||
|
||||
export interface AskAnswerSource {
|
||||
@@ -260,16 +258,7 @@ async function searchSiteContent(
|
||||
return (
|
||||
await Promise.all(
|
||||
searchResults.map(async (spaceItem) => {
|
||||
const sections = getSiteStructureSections(structure).flatMap((item) =>
|
||||
item.object === 'site-section-group' ? [item, ...item.sections] : item
|
||||
);
|
||||
const siteSpace = findSiteSpaceById(structure, spaceItem.id);
|
||||
const siteSection = sections.find(
|
||||
(section) => section.id === siteSpace?.section
|
||||
) as SiteSection;
|
||||
const siteSectionGroup = siteSection?.sectionGroup
|
||||
? sections.find((sectionGroup) => sectionGroup.id === siteSection.sectionGroup)
|
||||
: null;
|
||||
|
||||
return Promise.all(
|
||||
spaceItem.pages.map((pageItem) =>
|
||||
@@ -278,8 +267,6 @@ async function searchSiteContent(
|
||||
spaceItem,
|
||||
space: siteSpace?.space,
|
||||
spaceURL: siteSpace?.urls.published,
|
||||
siteSection: siteSection ?? undefined,
|
||||
siteSectionGroup: (siteSectionGroup as SiteSectionGroup) ?? undefined,
|
||||
})
|
||||
)
|
||||
);
|
||||
@@ -361,11 +348,9 @@ async function transformSitePageResult(
|
||||
spaceItem: SearchSpaceResult;
|
||||
space?: Space;
|
||||
spaceURL?: string;
|
||||
siteSection?: SiteSection;
|
||||
siteSectionGroup?: SiteSectionGroup;
|
||||
}
|
||||
): Promise<OrderedComputedResult[]> {
|
||||
const { pageItem, spaceItem, space, spaceURL, siteSection, siteSectionGroup } = args;
|
||||
const { pageItem, spaceItem, space, spaceURL } = args;
|
||||
const { linker } = context;
|
||||
|
||||
const page: ComputedPageResult = {
|
||||
@@ -375,26 +360,12 @@ async function transformSitePageResult(
|
||||
href: spaceURL
|
||||
? linker.toLinkForContent(joinPathWithBaseURL(spaceURL, pageItem.path))
|
||||
: linker.toPathInSpace(pageItem.path),
|
||||
spaceTitle: space?.title,
|
||||
pageId: pageItem.id,
|
||||
spaceId: spaceItem.id,
|
||||
breadcrumbs: [
|
||||
siteSectionGroup && {
|
||||
icon: siteSectionGroup?.icon as IconName,
|
||||
label: siteSectionGroup.title,
|
||||
},
|
||||
siteSection && {
|
||||
icon: siteSection?.icon as IconName,
|
||||
label: siteSection.title,
|
||||
},
|
||||
(!siteSection || siteSection?.siteSpaces.length > 1) && space
|
||||
? {
|
||||
label: space?.title,
|
||||
}
|
||||
: undefined,
|
||||
].filter((item) => item !== undefined),
|
||||
};
|
||||
|
||||
const pageSections = await Promise.all(
|
||||
const sections = await Promise.all(
|
||||
pageItem.sections?.map<Promise<ComputedSectionResult>>(async (section) => ({
|
||||
type: 'section',
|
||||
id: `${page.id}/${section.id}`,
|
||||
@@ -408,5 +379,5 @@ async function transformSitePageResult(
|
||||
})) ?? []
|
||||
);
|
||||
|
||||
return [page, ...pageSections];
|
||||
return [page, ...sections];
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export function SiteSectionList(props: { sections: ClientSiteSections; className
|
||||
<nav
|
||||
aria-label="Sections"
|
||||
className={tcls(
|
||||
'-mx-5 before:contents[] relative border-tint-subtle border-b from-transparent sidebar-filled:to-tint-subtle theme-muted:to-tint-subtle to-tint-base text-sm text-tint before:pointer-events-none before:absolute before:right-2 before:bottom-0 before:left-0 before:h-12 before:bg-gradient-to-b [html.sidebar-filled.theme-bold.tint_&]:to-tint-base [html.sidebar-filled.theme-bold.tint_&]:to-tint-subtle [html.sidebar-filled.theme-muted_&]:to-tint-base',
|
||||
'-mx-5 before:contents[] relative border-tint-subtle border-b from-transparent sidebar-filled:to-tint-subtle theme-bold-tint:to-tint-subtle theme-muted:to-tint-subtle to-tint-base text-sm text-tint before:pointer-events-none before:absolute before:right-2 before:bottom-0 before:left-0 before:h-12 before:bg-gradient-to-b [html.sidebar-filled.theme-bold.tint_&]:to-tint-base [html.sidebar-filled.theme-muted_&]:to-tint-base',
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -40,7 +40,7 @@ export function PageGroupItem(props: {
|
||||
'bg-tint-base',
|
||||
'sidebar-filled:bg-tint-subtle',
|
||||
'theme-muted:bg-tint-subtle',
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
|
||||
'theme-bold-tint:bg-tint-subtle',
|
||||
'[html.sidebar-filled.theme-muted_&]:bg-tint-base',
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
|
||||
'[html.sidebar-default.theme-gradient_&]:bg-gradient-primary',
|
||||
|
||||
@@ -75,7 +75,7 @@ export function TableOfContents(props: {
|
||||
|
||||
'sidebar-filled:bg-tint-subtle',
|
||||
'theme-muted:bg-tint-subtle',
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
|
||||
'theme-bold-tint:bg-tint-subtle',
|
||||
'[html.sidebar-filled.theme-muted_&]:bg-tint-base',
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
|
||||
'page-no-toc:!bg-transparent',
|
||||
|
||||
@@ -52,7 +52,7 @@ export function Trademark(props: {
|
||||
'before:to-tint-base',
|
||||
'sidebar-filled:before:to-tint-subtle',
|
||||
'theme-muted:before:to-tint-subtle',
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:before:to-tint-subtle',
|
||||
'theme-bold-tint:before:to-tint-subtle',
|
||||
'[html.sidebar-filled.theme-muted_&]:before:to-tint-base',
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:before:to-tint-base',
|
||||
'page-no-toc:before:!to-transparent'
|
||||
|
||||
@@ -79,7 +79,7 @@ function ThemeButton(props: {
|
||||
active && [
|
||||
'bg-primary',
|
||||
'theme-muted:bg-primary-hover',
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:bg-primary-hover',
|
||||
'theme-bold-tint:bg-primary-hover',
|
||||
'hover:bg-primary',
|
||||
'text-primary-strong',
|
||||
'contrast-more:text-primary-strong',
|
||||
|
||||
@@ -1,80 +1,62 @@
|
||||
'use client';
|
||||
|
||||
import type { HTMLAttributeAnchorTarget, HTMLAttributes } from 'react';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
|
||||
import { Icon, type IconName } from '@gitbook/icons';
|
||||
import { Link, type LinkInsightsProps } from './Link';
|
||||
|
||||
type ButtonProps = {
|
||||
href?: string;
|
||||
variant?: 'primary' | 'secondary' | 'blank';
|
||||
icon?: IconName;
|
||||
iconOnly?: boolean;
|
||||
variant?: 'primary' | 'secondary';
|
||||
size?: 'default' | 'medium' | 'small';
|
||||
className?: ClassValue;
|
||||
label?: string;
|
||||
} & LinkInsightsProps &
|
||||
HTMLAttributes<HTMLElement>;
|
||||
|
||||
const variantClasses = {
|
||||
primary: [
|
||||
'bg-primary-solid',
|
||||
'text-contrast-primary-solid',
|
||||
'hover:bg-primary-solid-hover',
|
||||
'hover:text-contrast-primary-solid-hover',
|
||||
'ring-0',
|
||||
'contrast-more:ring-1',
|
||||
],
|
||||
blank: [
|
||||
'bg-transparent',
|
||||
'text-tint',
|
||||
'ring-0',
|
||||
'shadow-none',
|
||||
'hover:bg-primary-hover',
|
||||
'hover:text-primary',
|
||||
'hover:scale-1',
|
||||
'hover:shadow-none',
|
||||
'contrast-more:bg-tint-subtle',
|
||||
],
|
||||
secondary: [
|
||||
'bg-tint',
|
||||
'text-tint',
|
||||
'hover:bg-tint-hover',
|
||||
'hover:text-primary',
|
||||
'contrast-more:bg-tint-subtle',
|
||||
],
|
||||
};
|
||||
|
||||
export function Button({
|
||||
href,
|
||||
children,
|
||||
variant = 'primary',
|
||||
size = 'default',
|
||||
className,
|
||||
insights,
|
||||
target,
|
||||
label,
|
||||
icon,
|
||||
iconOnly = false,
|
||||
...rest
|
||||
}: ButtonProps & { target?: HTMLAttributeAnchorTarget }) {
|
||||
}: ButtonProps) {
|
||||
const variantClasses =
|
||||
variant === 'primary'
|
||||
? //PRIMARY
|
||||
[
|
||||
'bg-primary-solid',
|
||||
'text-contrast-primary-solid',
|
||||
'hover:bg-primary-solid-hover',
|
||||
'hover:text-contrast-primary-solid-hover',
|
||||
'ring-0',
|
||||
'contrast-more:ring-1',
|
||||
]
|
||||
: // SECONDARY
|
||||
[
|
||||
'bg-tint',
|
||||
'text-tint',
|
||||
'hover:bg-tint-hover',
|
||||
'hover:text-primary',
|
||||
'contrast-more:bg-tint-subtle',
|
||||
];
|
||||
|
||||
const sizes = {
|
||||
default: ['text-base', 'px-4', 'py-2'],
|
||||
medium: ['text-sm', 'px-3', 'py-1.5'],
|
||||
small: ['text-xs', 'py-2', iconOnly ? 'px-2' : 'px-3'],
|
||||
small: ['text-xs', 'px-3 py-2'],
|
||||
};
|
||||
|
||||
const sizeClasses = sizes[size] || sizes.default;
|
||||
|
||||
const domClassName = tcls(
|
||||
'button',
|
||||
'inline-flex',
|
||||
'items-center',
|
||||
'gap-2',
|
||||
'inline-block',
|
||||
'rounded-md',
|
||||
'straight-corners:rounded-none',
|
||||
// 'place-self-start',
|
||||
'place-self-start',
|
||||
|
||||
'ring-1',
|
||||
'ring-tint',
|
||||
@@ -97,31 +79,22 @@ export function Button({
|
||||
'grow-0',
|
||||
'shrink-0',
|
||||
'truncate',
|
||||
variantClasses[variant],
|
||||
variantClasses,
|
||||
sizeClasses,
|
||||
className
|
||||
);
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className={domClassName}
|
||||
insights={insights}
|
||||
aria-label={label}
|
||||
target={target}
|
||||
{...rest}
|
||||
>
|
||||
{icon ? <Icon icon={icon} className={tcls('size-[1em]')} /> : null}
|
||||
{iconOnly ? null : label}
|
||||
<Link href={href} className={domClassName} insights={insights} {...rest}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button type="button" className={domClassName} aria-label={label} {...rest}>
|
||||
{icon ? <Icon icon={icon} className={tcls('size-[1em]')} /> : null}
|
||||
{iconOnly ? null : label}
|
||||
<button type="button" className={domClassName} {...rest}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,7 @@ import type { SVGProps } from 'react';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
export const Loading = ({
|
||||
busy = true,
|
||||
...props
|
||||
}: { busy?: boolean } & SVGProps<SVGSVGElement>) => {
|
||||
export const Loading = (props: Partial<SVGProps<SVGSVGElement>>) => {
|
||||
return (
|
||||
<svg
|
||||
width="100%"
|
||||
@@ -17,11 +14,7 @@ export const Loading = ({
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
className={tcls(
|
||||
busy
|
||||
? 'animate-[pathLoading_2s_ease_infinite_forwards]'
|
||||
: 'animate-[pathLoading_2s_ease_forwards]'
|
||||
)}
|
||||
className={tcls('animate-[pathLoading_2s_ease_infinite_forwards]')}
|
||||
d="M6 59.5V56.291C6 45.8865 11.5194 36.263 20.5 31.0091V31.0091L60.9857 7.32407C63.4452 5.88525 66.4843 5.86317 68.9643 7.26611L116 33.8734L70.4183 60.2148C67.9468 61.6431 64.9014 61.6462 62.4269 60.223L29.9772 41.5592C19.3106 35.4242 6 43.1236 6 55.4288V64.8776C6 73.4486 10.5708 81.3691 17.9918 85.6575L54.59 106.807C62.0198 111.1 71.1766 111.1 78.6064 106.807L116.364 84.9874C120.074 82.8432 122.36 78.883 122.36 74.5975V59.2647C122.36 57.7248 120.692 56.7626 119.359 57.5331L72.6023 84.5529C68.8874 86.6996 64.309 86.6996 60.5941 84.5529L26 64.5617"
|
||||
stroke="currentColor"
|
||||
pathLength="100"
|
||||
@@ -34,8 +27,7 @@ export const Loading = ({
|
||||
d="M6 59.5V56.291C6 45.8865 11.5194 36.263 20.5 31.0091V31.0091L60.9857 7.32407C63.4452 5.88525 66.4843 5.86317 68.9643 7.26611L116 33.8734L70.4183 60.2148C67.9468 61.6431 64.9014 61.6462 62.4269 60.223L29.9772 41.5592C19.3106 35.4242 6 43.1236 6 55.4288V64.8776C6 73.4486 10.5708 81.3691 17.9918 85.6575L54.59 106.807C62.0198 111.1 71.1766 111.1 78.6064 106.807L116.364 84.9874C120.074 82.8432 122.36 78.883 122.36 74.5975V59.2647C122.36 57.7248 120.692 56.7626 119.359 57.5331L72.6023 84.5529C68.8874 86.6996 64.309 86.6996 60.5941 84.5529L26 64.5617"
|
||||
stroke="currentColor"
|
||||
pathLength="100"
|
||||
strokeOpacity={busy ? 0.24 : 1}
|
||||
className="transition-opacity duration-1000"
|
||||
strokeOpacity="0.24"
|
||||
fill="none"
|
||||
strokeWidth="11"
|
||||
strokeLinecap="round"
|
||||
|
||||
@@ -25,11 +25,11 @@ export const linkStyles = [
|
||||
/**
|
||||
* Styled version of Link component.
|
||||
*/
|
||||
export function StyledLink(props: Omit<LinkProps, 'style'> & { className?: ClassValue }) {
|
||||
const { className, ...rest } = props;
|
||||
export function StyledLink(props: Omit<LinkProps, 'style'> & { style?: ClassValue }) {
|
||||
const { style, ...rest } = props;
|
||||
|
||||
return (
|
||||
<Link {...rest} className={tcls(linkStyles, className)}>
|
||||
<Link {...rest} className={tcls(linkStyles, style)}>
|
||||
{props.children}
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { SiteAPIToken } from '@gitbook/api';
|
||||
import type { SiteURLData } from '@v2/lib/context';
|
||||
import type { PublishedSiteContent, SiteAPIToken } from '@gitbook/api';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
|
||||
/**
|
||||
@@ -10,7 +9,7 @@ export type VisitorAuthClaims = Record<string, any>;
|
||||
/**
|
||||
* Get the visitor auth claims from the API response obtained from `getPublishedContentByUrl`.
|
||||
*/
|
||||
export function getVisitorAuthClaims(siteData: SiteURLData): VisitorAuthClaims {
|
||||
export function getVisitorAuthClaims(siteData: PublishedSiteContent): VisitorAuthClaims {
|
||||
const { apiToken } = siteData;
|
||||
|
||||
return getVisitorAuthClaimsFromToken(jwtDecode<SiteAPIToken>(apiToken));
|
||||
|
||||
@@ -19,7 +19,6 @@ import { getGitbookAppHref } from './links';
|
||||
import { resolvePageId } from './pages';
|
||||
import { findSiteSpaceById } from './sites';
|
||||
import type { ClassValue } from './tailwind';
|
||||
import { filterOutNullable } from './typescript';
|
||||
|
||||
export interface ResolvedContentRef {
|
||||
/** Text to render in the content ref */
|
||||
@@ -28,12 +27,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 */
|
||||
ancestors?: { icon?: React.ReactNode; label: string; href?: string }[];
|
||||
/** URL to open for the content ref */
|
||||
href: string;
|
||||
/** True if the content ref is active */
|
||||
@@ -120,14 +115,6 @@ export async function resolveContentRef(
|
||||
: resolvePageId(pages, contentRef.page);
|
||||
|
||||
const page = resolvePageResult?.page;
|
||||
const ancestors =
|
||||
resolvePageResult?.ancestors.map((ancestor) => ({
|
||||
label: ancestor.title,
|
||||
icon: <PageIcon page={ancestor} style={iconStyle} />,
|
||||
href: resolveAsAbsoluteURL
|
||||
? linker.toAbsoluteURL(linker.toPathForPage({ page: ancestor, pages }))
|
||||
: linker.toPathForPage({ page: ancestor, pages }),
|
||||
})) ?? [];
|
||||
if (!page) {
|
||||
return null;
|
||||
}
|
||||
@@ -138,16 +125,10 @@ export async function resolveContentRef(
|
||||
let text = '';
|
||||
let icon: React.ReactNode | undefined = undefined;
|
||||
let emoji: string | undefined = undefined;
|
||||
const href = linker.toPathForPage({ page, pages, anchor });
|
||||
|
||||
// Compute the text to display for the link
|
||||
if (anchor) {
|
||||
text = `#${anchor}`;
|
||||
ancestors.push({
|
||||
label: page.title,
|
||||
icon: <PageIcon page={page} style={iconStyle} />,
|
||||
href: resolveAsAbsoluteURL ? linker.toAbsoluteURL(href) : href,
|
||||
});
|
||||
|
||||
if (resolveAnchorText) {
|
||||
const document = await getPageDocument(dataFetcher, space, page);
|
||||
@@ -170,14 +151,13 @@ export async function resolveContentRef(
|
||||
icon = <PageIcon page={page} style={iconStyle} />;
|
||||
}
|
||||
|
||||
const href = linker.toPathForPage({ page, pages, anchor });
|
||||
|
||||
return {
|
||||
href: resolveAsAbsoluteURL ? linker.toAbsoluteURL(href) : href,
|
||||
text,
|
||||
subText: page.description,
|
||||
ancestors: ancestors,
|
||||
emoji,
|
||||
icon,
|
||||
id: page.id,
|
||||
active: !anchor && page.id === activePage?.id,
|
||||
};
|
||||
}
|
||||
@@ -366,12 +346,6 @@ async function resolveContentRefInSpace(
|
||||
|
||||
return {
|
||||
...resolved,
|
||||
ancestors: [
|
||||
{
|
||||
label: space.title,
|
||||
href: baseURL.toString(),
|
||||
},
|
||||
...(resolved.ancestors ?? []),
|
||||
].filter(filterOutNullable),
|
||||
subText: space.title,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ export async function getV1BaseContext(): Promise<GitBookBaseContext> {
|
||||
const dataFetcher = await getDataFetcherV1();
|
||||
|
||||
const imageResizer = createImageResizer({
|
||||
imagesContextId: host,
|
||||
host,
|
||||
// In V1, we always resize at the top level of the hostname, not relative to the content.
|
||||
linker: createLinker({
|
||||
host,
|
||||
|
||||
@@ -17,9 +17,9 @@ export async function serveResizedImage(
|
||||
request: Request,
|
||||
requestOptions: {
|
||||
/**
|
||||
* The site identifier to use for verifying the image signature.
|
||||
* The host to use for the image.
|
||||
*/
|
||||
imagesContextId?: string;
|
||||
host?: string;
|
||||
} = {}
|
||||
) {
|
||||
const requestURL = new URL(request.url);
|
||||
@@ -45,8 +45,8 @@ export async function serveResizedImage(
|
||||
}
|
||||
|
||||
// Verify the signature
|
||||
const imagesContextId =
|
||||
requestOptions.imagesContextId ??
|
||||
const host =
|
||||
requestOptions.host ??
|
||||
request.headers.get('x-gitbook-host') ?? // Only for v1, to be removed
|
||||
request.headers.get('x-forwarded-host') ??
|
||||
request.headers.get('host') ??
|
||||
@@ -54,13 +54,13 @@ export async function serveResizedImage(
|
||||
const verified = await verifyImageSignature(
|
||||
{
|
||||
url,
|
||||
imagesContextId,
|
||||
host,
|
||||
},
|
||||
{ signature, version: signatureVersion }
|
||||
);
|
||||
if (!verified) {
|
||||
return new Response(
|
||||
`Invalid signature "${signature ?? ''}" (version ${signatureVersion}) for "${url}" on identifier "${imagesContextId}"`,
|
||||
`Invalid signature "${signature ?? ''}" (version ${signatureVersion}) for "${url}" on "${host}"`,
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -295,7 +295,7 @@ const config: Config = {
|
||||
),
|
||||
},
|
||||
animation: {
|
||||
present: 'present 200ms cubic-bezier(0.25, 1, 0.5, 1) both',
|
||||
present: 'present .5s ease-out both',
|
||||
scaleIn: 'scaleIn 200ms ease',
|
||||
scaleOut: 'scaleOut 200ms ease',
|
||||
fadeIn: 'fadeIn 200ms ease forwards',
|
||||
@@ -330,7 +330,7 @@ const config: Config = {
|
||||
present: {
|
||||
from: {
|
||||
opacity: '0',
|
||||
transform: 'translateY(1rem) scale(90%)',
|
||||
transform: 'translateY(2rem) scale(0.9)',
|
||||
},
|
||||
to: {
|
||||
opacity: '1',
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
# @gitbook/openapi-parser
|
||||
|
||||
## 2.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7bb37c7: Move filterSelectedOpenAPISchemas to @gitbook/openapi-parser
|
||||
- 5b1e01c: Support for x-stability property
|
||||
- 813b2af: Support for x-enumDescriptions and x-gitbook-enum
|
||||
|
||||
## 2.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "2.1.2",
|
||||
"version": "2.1.1",
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@scalar/openapi-parser": "^0.10.10",
|
||||
|
||||
@@ -47,31 +47,8 @@ export interface OpenAPICustomOperationProperties {
|
||||
* Description in Document format.
|
||||
*/
|
||||
'x-gitbook-description-document'?: object;
|
||||
|
||||
/**
|
||||
* Enums with name and description
|
||||
*/
|
||||
'x-enumDescriptions'?: object;
|
||||
|
||||
/**
|
||||
* Enums with name and description
|
||||
*/
|
||||
'x-gitbook-enum'?: {
|
||||
[key: string]: {
|
||||
description?: string;
|
||||
name?: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Stability of the operation.
|
||||
* @enum 'experimental' | 'alpha' | 'beta'
|
||||
*/
|
||||
'x-stability'?: OpenAPIStability;
|
||||
}
|
||||
|
||||
export type OpenAPIStability = 'experimental' | 'alpha' | 'beta';
|
||||
|
||||
/**
|
||||
* Custom code samples that can be defined at the operation level.
|
||||
* It follows the spec defined by Redocly.
|
||||
|
||||
@@ -1,38 +1,5 @@
|
||||
# @gitbook/react-openapi
|
||||
|
||||
## 1.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- da7b369: Fix missing headers in OpenAPIResponses
|
||||
- da485f5: Fix read-only in generateSchemaExample
|
||||
- 139a805: Fix OpenAPI enum display
|
||||
|
||||
## 1.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7d0b422: Handle grouped OpenAPISchemas
|
||||
|
||||
## 1.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- bd35348: Fix missing alternative schemas
|
||||
- ae78fc5: Fix XML in code sample
|
||||
- 7bb37c7: Move filterSelectedOpenAPISchemas to @gitbook/openapi-parser
|
||||
- 373183a: Safe parse OpenAPI JSON schema
|
||||
- 1505ddb: Fix multiple request examples selector not showing
|
||||
- 61db166: Add OpenAPI write-only indicator
|
||||
- 5b1e01c: Support for x-stability property
|
||||
- cd99ed5: Fix spec properties rendering and missing keys
|
||||
- 813b2af: Support for x-enumDescriptions and x-gitbook-enum
|
||||
- a25fded: Replace $ref with $reference in json-decycle
|
||||
- Updated dependencies [7bb37c7]
|
||||
- Updated dependencies [5b1e01c]
|
||||
- Updated dependencies [813b2af]
|
||||
- @gitbook/openapi-parser@2.1.2
|
||||
|
||||
## 1.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "1.1.9",
|
||||
"version": "1.1.6",
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@gitbook/openapi-parser": "workspace:*",
|
||||
|
||||
@@ -6,16 +6,10 @@ import { Button, type ButtonProps, Tooltip, TooltipTrigger } from 'react-aria-co
|
||||
export function OpenAPICopyButton(
|
||||
props: ButtonProps & {
|
||||
value: string;
|
||||
children: React.ReactNode;
|
||||
label?: string;
|
||||
/**
|
||||
* Whether to show a tooltip.
|
||||
* @default true
|
||||
*/
|
||||
withTooltip?: boolean;
|
||||
}
|
||||
) {
|
||||
const { value, label, children, onPress, className, withTooltip = true } = props;
|
||||
const { value } = props;
|
||||
const { children, onPress, className } = props;
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
@@ -27,19 +21,12 @@ export function OpenAPICopyButton(
|
||||
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
setIsOpen(false);
|
||||
}, 2000);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<TooltipTrigger
|
||||
isOpen={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
isDisabled={!withTooltip}
|
||||
closeDelay={200}
|
||||
delay={200}
|
||||
>
|
||||
<TooltipTrigger isOpen={isOpen} onOpenChange={setIsOpen} closeDelay={200} delay={200}>
|
||||
<Button
|
||||
type="button"
|
||||
preventFocusOnPress
|
||||
@@ -60,7 +47,7 @@ export function OpenAPICopyButton(
|
||||
offset={4}
|
||||
className="openapi-tooltip"
|
||||
>
|
||||
{copied ? 'Copied' : label || 'Copy to clipboard'}
|
||||
{copied ? 'Copied' : 'Copy to clipboard'}{' '}
|
||||
</Tooltip>
|
||||
</TooltipTrigger>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
import type {
|
||||
OpenAPICustomOperationProperties,
|
||||
OpenAPIStability,
|
||||
OpenAPIV3,
|
||||
} from '@gitbook/openapi-parser';
|
||||
import type { OpenAPICustomOperationProperties, OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||
import { Markdown } from './Markdown';
|
||||
import { OpenAPICodeSample } from './OpenAPICodeSample';
|
||||
import { OpenAPIPath } from './OpenAPIPath';
|
||||
@@ -33,24 +29,14 @@ export function OpenAPIOperation(props: {
|
||||
return (
|
||||
<div className={clsx('openapi-operation', className)}>
|
||||
<div className="openapi-summary" id={operation.summary ? undefined : context.id}>
|
||||
{(operation.deprecated || operation['x-stability']) && (
|
||||
<div className="openapi-summary-tags">
|
||||
{operation.deprecated && (
|
||||
<div className="openapi-deprecated">Deprecated</div>
|
||||
)}
|
||||
{operation['x-stability'] && (
|
||||
<OpenAPIOperationStability stability={operation['x-stability']} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{operation.summary
|
||||
? context.renderHeading({
|
||||
deprecated: operation.deprecated ?? false,
|
||||
stability: operation['x-stability'],
|
||||
title: operation.summary,
|
||||
})
|
||||
: null}
|
||||
<OpenAPIPath data={data} context={context} />
|
||||
{operation.deprecated && <div className="openapi-deprecated">Deprecated</div>}
|
||||
</div>
|
||||
<div className="openapi-columns">
|
||||
<div className="openapi-column-spec">
|
||||
@@ -103,25 +89,3 @@ function OpenAPIOperationDescription(props: {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const stabilityEnum = {
|
||||
experimental: 'Experimental',
|
||||
alpha: 'Alpha',
|
||||
beta: 'Beta',
|
||||
} as const;
|
||||
|
||||
function OpenAPIOperationStability(props: { stability: OpenAPIStability }) {
|
||||
const { stability } = props;
|
||||
|
||||
const foundStability = stabilityEnum[stability];
|
||||
|
||||
if (!foundStability) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`openapi-stability openapi-stability-${foundStability.toLowerCase()}`}>
|
||||
{foundStability}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,15 +36,13 @@ export function OpenAPIResponse(props: {
|
||||
/>
|
||||
</OpenAPIDisclosure>
|
||||
) : null}
|
||||
{mediaType.schema && (
|
||||
<div className="openapi-responsebody">
|
||||
<OpenAPISchemaProperties
|
||||
id={`response-${context.blockKey}`}
|
||||
properties={[{ schema: mediaType.schema }]}
|
||||
context={context}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="openapi-responsebody">
|
||||
<OpenAPISchemaProperties
|
||||
id={`response-${context.blockKey}`}
|
||||
properties={mediaType.schema ? [{ schema: mediaType.schema }] : []}
|
||||
context={context}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -268,7 +268,6 @@ function getExamplesFromMediaTypeObject(args: {
|
||||
value: {
|
||||
[root]: generateSchemaExample(mediaTypeObject.schema, {
|
||||
xml: mediaType === 'application/xml',
|
||||
mode: 'read',
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -278,11 +277,7 @@ function getExamplesFromMediaTypeObject(args: {
|
||||
return [
|
||||
{
|
||||
key: 'default',
|
||||
example: {
|
||||
value: generateSchemaExample(mediaTypeObject.schema, {
|
||||
mode: 'read',
|
||||
}),
|
||||
},
|
||||
example: { value: generateSchemaExample(mediaTypeObject.schema) },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -21,42 +21,7 @@ export function OpenAPIResponses(props: {
|
||||
icon={context.icons.chevronRight}
|
||||
groups={Object.entries(responses).map(
|
||||
([statusCode, response]: [string, OpenAPIV3.ResponseObject]) => {
|
||||
const tabs = (() => {
|
||||
// If there is no content, but there are headers, we need to show the headers
|
||||
if (
|
||||
(!response.content || !Object.keys(response.content).length) &&
|
||||
response.headers &&
|
||||
Object.keys(response.headers).length
|
||||
) {
|
||||
return [
|
||||
{
|
||||
id: 'default',
|
||||
body: (
|
||||
<OpenAPIResponse
|
||||
response={response}
|
||||
mediaType={{}}
|
||||
context={context}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return Object.entries(response.content ?? {}).map(
|
||||
([contentType, mediaType]) => ({
|
||||
id: contentType,
|
||||
label: contentType,
|
||||
body: (
|
||||
<OpenAPIResponse
|
||||
response={response}
|
||||
mediaType={mediaType}
|
||||
context={context}
|
||||
/>
|
||||
),
|
||||
})
|
||||
);
|
||||
})();
|
||||
|
||||
const content = Object.entries(response.content ?? {});
|
||||
const description = response.description;
|
||||
|
||||
return {
|
||||
@@ -74,7 +39,17 @@ export function OpenAPIResponses(props: {
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
tabs,
|
||||
tabs: content.map(([contentType, mediaType]) => ({
|
||||
id: contentType,
|
||||
label: contentType,
|
||||
body: (
|
||||
<OpenAPIResponse
|
||||
response={response}
|
||||
mediaType={mediaType}
|
||||
context={context}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
};
|
||||
}
|
||||
)}
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
// This component does not use any client feature but we don't want to
|
||||
// render it server-side because it has recursion.
|
||||
|
||||
import type { OpenAPICustomOperationProperties, OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||
import { useId } from 'react';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { Markdown } from './Markdown';
|
||||
import { OpenAPICopyButton } from './OpenAPICopyButton';
|
||||
import { OpenAPIDisclosure } from './OpenAPIDisclosure';
|
||||
import { OpenAPISchemaName } from './OpenAPISchemaName';
|
||||
import { retrocycle } from './decycle';
|
||||
@@ -51,7 +50,7 @@ function OpenAPISchemaProperty(props: {
|
||||
circularRefs.set(schema, id);
|
||||
|
||||
const properties = getSchemaProperties(schema);
|
||||
if (properties?.length) {
|
||||
if (properties) {
|
||||
return (
|
||||
<OpenAPIDisclosure context={context} label={getDisclosureLabel(schema)}>
|
||||
<OpenAPISchemaProperties
|
||||
@@ -237,58 +236,21 @@ function OpenAPISchemaCircularRef(props: { id: string; schema: OpenAPIV3.SchemaO
|
||||
/**
|
||||
* Render the enum value for a schema.
|
||||
*/
|
||||
function OpenAPISchemaEnum(props: {
|
||||
schema: OpenAPIV3.SchemaObject & OpenAPICustomOperationProperties;
|
||||
}) {
|
||||
const { schema } = props;
|
||||
|
||||
const enumValues = (() => {
|
||||
// Render x-gitbook-enum first, as it has a different format
|
||||
if (schema['x-gitbook-enum']) {
|
||||
return Object.entries(schema['x-gitbook-enum']).map(([name, { description }]) => {
|
||||
return {
|
||||
value: name,
|
||||
description,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (schema['x-enumDescriptions']) {
|
||||
return Object.entries(schema['x-enumDescriptions']).map(([value, description]) => {
|
||||
return {
|
||||
value,
|
||||
description,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return schema.enum?.map((value) => {
|
||||
return {
|
||||
value,
|
||||
description: undefined,
|
||||
};
|
||||
});
|
||||
})();
|
||||
|
||||
if (!enumValues?.length) {
|
||||
return null;
|
||||
}
|
||||
function OpenAPISchemaEnum(props: { enumValues: any[] }) {
|
||||
const { enumValues } = props;
|
||||
|
||||
return (
|
||||
<span className="openapi-schema-enum">
|
||||
Available options:{' '}
|
||||
{enumValues.map((item, index) => (
|
||||
<span key={index} className="openapi-schema-enum-value">
|
||||
<OpenAPICopyButton
|
||||
value={item.value}
|
||||
label={item.description}
|
||||
withTooltip={!!item.description}
|
||||
>
|
||||
<code>{`${item.value}`}</code>
|
||||
</OpenAPICopyButton>
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
<div className="openapi-schema-enum">
|
||||
<span>
|
||||
Options:{' '}
|
||||
{enumValues.map((value, index) => (
|
||||
<span key={index} className="openapi-schema-enum-value">
|
||||
<code>{`${value}`}</code>
|
||||
{index < enumValues.length - 1 ? ', ' : ''}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -332,7 +294,9 @@ function OpenAPISchemaPresentation(props: { property: OpenAPISchemaPropertyEntry
|
||||
Pattern: <code>{schema.pattern}</code>
|
||||
</div>
|
||||
) : null}
|
||||
<OpenAPISchemaEnum schema={schema} />
|
||||
{schema.enum && schema.enum.length > 0 ? (
|
||||
<OpenAPISchemaEnum enumValues={schema.enum} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -457,7 +421,7 @@ function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string {
|
||||
// Otherwise try to infer a nice title
|
||||
let type = 'any';
|
||||
|
||||
if (schema.enum || schema['x-enumDescriptions'] || schema['x-gitbook-enum']) {
|
||||
if (schema.enum) {
|
||||
type = `${schema.type} · enum`;
|
||||
// check array AND schema.items as this is sometimes null despite what the type indicates
|
||||
} else if (schema.type === 'array' && !!schema.items) {
|
||||
|
||||
@@ -16,10 +16,14 @@ export function generateSchemaExample(
|
||||
schema: OpenAPIV3.SchemaObject,
|
||||
options?: GenerateSchemaExampleOptions
|
||||
): JSONValue | undefined {
|
||||
return getExampleFromSchema(schema, {
|
||||
emptyString: 'text',
|
||||
...options,
|
||||
});
|
||||
return getExampleFromSchema(
|
||||
schema,
|
||||
{
|
||||
emptyString: 'text',
|
||||
...options,
|
||||
},
|
||||
3 // Max depth for circular references
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,6 +103,21 @@ function guessFromFormat(schema: Record<string, any>, fallback = '') {
|
||||
return genericExampleValues[schema.format] ?? fallback;
|
||||
}
|
||||
|
||||
/** Map of all the results */
|
||||
const resultCache = new WeakMap<Record<string, any>, any>();
|
||||
|
||||
/** Store result in the cache, and return the result */
|
||||
function cache(schema: Record<string, any>, result: unknown) {
|
||||
// Avoid unnecessary WeakMap operations for primitive values
|
||||
if (typeof result !== 'object' || result === null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
resultCache.set(schema, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function takes an OpenAPI schema and generates an example from it
|
||||
* Forked from : https://github.com/scalar/scalar/blob/main/packages/oas-utils/src/spec-getters/getExampleFromSchema.ts
|
||||
@@ -133,20 +152,8 @@ const getExampleFromSchema = (
|
||||
},
|
||||
level = 0,
|
||||
parentSchema?: Record<string, any>,
|
||||
name?: string,
|
||||
resultCache = new WeakMap<Record<string, any>, any>()
|
||||
name?: string
|
||||
): any => {
|
||||
// Store result in the cache, and return the result
|
||||
function cache(schema: Record<string, any>, result: unknown) {
|
||||
// Avoid unnecessary WeakMap operations for primitive values
|
||||
if (typeof result !== 'object' || result === null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
resultCache.set(schema, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check if the result is already cached
|
||||
if (resultCache.has(schema)) {
|
||||
return resultCache.get(schema);
|
||||
@@ -238,8 +245,7 @@ const getExampleFromSchema = (
|
||||
options,
|
||||
level + 1,
|
||||
schema,
|
||||
propertyName,
|
||||
resultCache
|
||||
propertyName
|
||||
);
|
||||
|
||||
if (typeof response[propertyXmlTagName ?? propertyName] === 'undefined') {
|
||||
@@ -263,8 +269,7 @@ const getExampleFromSchema = (
|
||||
options,
|
||||
level + 1,
|
||||
schema,
|
||||
exampleKey,
|
||||
resultCache
|
||||
exampleKey
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -285,51 +290,21 @@ const getExampleFromSchema = (
|
||||
response.ANY_ADDITIONAL_PROPERTY = getExampleFromSchema(
|
||||
schema.additionalProperties,
|
||||
options,
|
||||
level + 1,
|
||||
undefined,
|
||||
undefined,
|
||||
resultCache
|
||||
level + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.anyOf !== undefined) {
|
||||
Object.assign(
|
||||
response,
|
||||
getExampleFromSchema(
|
||||
schema.anyOf[0],
|
||||
options,
|
||||
level + 1,
|
||||
undefined,
|
||||
undefined,
|
||||
resultCache
|
||||
)
|
||||
);
|
||||
Object.assign(response, getExampleFromSchema(schema.anyOf[0], options, level + 1));
|
||||
} else if (schema.oneOf !== undefined) {
|
||||
Object.assign(
|
||||
response,
|
||||
getExampleFromSchema(
|
||||
schema.oneOf[0],
|
||||
options,
|
||||
level + 1,
|
||||
undefined,
|
||||
undefined,
|
||||
resultCache
|
||||
)
|
||||
);
|
||||
Object.assign(response, getExampleFromSchema(schema.oneOf[0], options, level + 1));
|
||||
} else if (schema.allOf !== undefined) {
|
||||
Object.assign(
|
||||
response,
|
||||
...schema.allOf
|
||||
.map((item: Record<string, any>) =>
|
||||
getExampleFromSchema(
|
||||
item,
|
||||
options,
|
||||
level + 1,
|
||||
schema,
|
||||
undefined,
|
||||
resultCache
|
||||
)
|
||||
getExampleFromSchema(item, options, level + 1, schema)
|
||||
)
|
||||
.filter((item: any) => item !== undefined)
|
||||
);
|
||||
@@ -360,9 +335,7 @@ const getExampleFromSchema = (
|
||||
{ type: 'object', allOf: schema.items.allOf },
|
||||
options,
|
||||
level + 1,
|
||||
schema,
|
||||
undefined,
|
||||
resultCache
|
||||
schema
|
||||
);
|
||||
|
||||
return cache(
|
||||
@@ -373,14 +346,7 @@ const getExampleFromSchema = (
|
||||
// For non-objects (like strings), collect all examples
|
||||
const examples = schema.items.allOf
|
||||
.map((item: Record<string, any>) =>
|
||||
getExampleFromSchema(
|
||||
item,
|
||||
options,
|
||||
level + 1,
|
||||
schema,
|
||||
undefined,
|
||||
resultCache
|
||||
)
|
||||
getExampleFromSchema(item, options, level + 1, schema)
|
||||
)
|
||||
.filter((item: any) => item !== undefined);
|
||||
|
||||
@@ -402,14 +368,7 @@ const getExampleFromSchema = (
|
||||
const schemas = schema.items[rule].slice(0, 1);
|
||||
const exampleFromRule = schemas
|
||||
.map((item: Record<string, any>) =>
|
||||
getExampleFromSchema(
|
||||
item,
|
||||
options,
|
||||
level + 1,
|
||||
schema,
|
||||
undefined,
|
||||
resultCache
|
||||
)
|
||||
getExampleFromSchema(item, options, level + 1, schema)
|
||||
)
|
||||
.filter((item: any) => item !== undefined);
|
||||
|
||||
@@ -421,14 +380,7 @@ const getExampleFromSchema = (
|
||||
}
|
||||
|
||||
if (schema.items?.type) {
|
||||
const exampleFromSchema = getExampleFromSchema(
|
||||
schema.items,
|
||||
options,
|
||||
level + 1,
|
||||
undefined,
|
||||
undefined,
|
||||
resultCache
|
||||
);
|
||||
const exampleFromSchema = getExampleFromSchema(schema.items, options, level + 1);
|
||||
|
||||
return wrapItems ? [{ [itemsXmlTagName]: exampleFromSchema }] : [exampleFromSchema];
|
||||
}
|
||||
@@ -455,14 +407,7 @@ const getExampleFromSchema = (
|
||||
const firstOneOfItem = discriminateSchema[0];
|
||||
|
||||
// Return an example for the first item
|
||||
return getExampleFromSchema(
|
||||
firstOneOfItem,
|
||||
options,
|
||||
level + 1,
|
||||
undefined,
|
||||
undefined,
|
||||
resultCache
|
||||
);
|
||||
return getExampleFromSchema(firstOneOfItem, options, level + 1);
|
||||
}
|
||||
|
||||
// Check if schema has the `allOf` key
|
||||
@@ -472,14 +417,7 @@ const getExampleFromSchema = (
|
||||
// Loop through all `allOf` schemas
|
||||
schema.allOf.forEach((allOfItem: Record<string, any>) => {
|
||||
// Return an example from the schema
|
||||
const newExample = getExampleFromSchema(
|
||||
allOfItem,
|
||||
options,
|
||||
level + 1,
|
||||
undefined,
|
||||
undefined,
|
||||
resultCache
|
||||
);
|
||||
const newExample = getExampleFromSchema(allOfItem, options, level + 1);
|
||||
|
||||
// Merge or overwrite the example
|
||||
example =
|
||||
|
||||
@@ -16,12 +16,8 @@ export function OpenAPISchemas(props: {
|
||||
className?: string;
|
||||
data: OpenAPISchemasData;
|
||||
context: OpenAPISchemasContextProps;
|
||||
/**
|
||||
* Whether to show the schema directly if there is only one.
|
||||
*/
|
||||
grouped?: boolean;
|
||||
}) {
|
||||
const { className, data, context, grouped } = props;
|
||||
const { className, data, context } = props;
|
||||
const { schemas } = data;
|
||||
|
||||
const clientContext: OpenAPIClientContext = {
|
||||
@@ -36,7 +32,7 @@ export function OpenAPISchemas(props: {
|
||||
|
||||
return (
|
||||
<div className={clsx('openapi-schemas', className)}>
|
||||
<OpenAPIRootSchemasSchema grouped={grouped} schemas={schemas} context={clientContext} />
|
||||
<OpenAPIRootSchemasSchema schemas={schemas} context={clientContext} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -48,12 +44,11 @@ export function OpenAPISchemas(props: {
|
||||
function OpenAPIRootSchemasSchema(props: {
|
||||
schemas: OpenAPISchemasData['schemas'];
|
||||
context: OpenAPIClientContext;
|
||||
grouped?: boolean;
|
||||
}) {
|
||||
const { schemas, context, grouped } = props;
|
||||
const { schemas, context } = props;
|
||||
|
||||
// If there is only one model and we are not grouping, we show it directly.
|
||||
if (schemas.length === 1 && !grouped) {
|
||||
// If there is only one model, we show it directly.
|
||||
if (schemas.length === 1) {
|
||||
const schema = schemas?.[0]?.schema;
|
||||
|
||||
if (!schema) {
|
||||
|
||||
@@ -13,11 +13,7 @@ export interface OpenAPIContextProps extends OpenAPIClientContext {
|
||||
/**
|
||||
* Render the heading of the operation.
|
||||
*/
|
||||
renderHeading: (props: {
|
||||
deprecated: boolean;
|
||||
title: string;
|
||||
stability?: string;
|
||||
}) => React.ReactNode;
|
||||
renderHeading: (props: { deprecated: boolean; title: string }) => React.ReactNode;
|
||||
/**
|
||||
* Render the document of the operation.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user