mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-14 06:35:17 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c0cd40a2d | |||
| 5b30483b64 |
@@ -1,9 +1,5 @@
|
||||
on: [push]
|
||||
name: CI
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
|
||||
@@ -75,7 +75,7 @@ GitBook's rendering engine is fully open-source and built on top of [Next.js](ht
|
||||
|
||||
### Types of contributions
|
||||
|
||||
We encourage you to contribute to GitBook to help us build the best tool for documenting technical knowledge. If you're looking for some quick ways to contribute, continue reading to learn more about popular contributions.
|
||||
We encourage you to contribute to GitBook to help us build the best tool for doucmenting techincal knowledge. If you're looking for some quick ways to contribute, continue reading to learn more about popular contributions.
|
||||
|
||||
#### Translations
|
||||
|
||||
@@ -92,7 +92,7 @@ Encounter a bug or find an issue you'd like to fix? Helping us fix issues relate
|
||||
>
|
||||
> _Looking to add a specific feature in GitBook? Head to our [contributing guide](/.github/CONTRIBUTING.md) to get started._
|
||||
>
|
||||
> Self-hosting this project puts the responsibility of maintaining and merging future updates on **you**. We cannot guarantee support, maintenance, or updates to forked and self-hosted instances of this project.
|
||||
> Self-hosting this project puts the responsibility of maintaining and merging future updates on **you**. We cannot guarantee support, maintainance, or updates to forked and self-hosted instances of this project.
|
||||
>
|
||||
> We want to make it as easy as possible for our community to collaborate and push the future of GitBook, which is why we encourage you to contribute to our product directly instead of creating your own version.
|
||||
|
||||
|
||||
+1
-73
@@ -40,72 +40,6 @@ async function waitForCookiesDialog(page: Page) {
|
||||
}
|
||||
|
||||
const testCases: TestsCase[] = [
|
||||
{
|
||||
name: 'GitBook Site (Single Variant)',
|
||||
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Home',
|
||||
url: '',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'No variants dropdown',
|
||||
url: '',
|
||||
run: async (page) => {
|
||||
await expect(page.locator('[data-testid="space-dropdown-button"]')).toHaveCount(
|
||||
0,
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Search',
|
||||
url: '?q=',
|
||||
},
|
||||
{
|
||||
name: 'Search Results',
|
||||
url: '?q=gitbook',
|
||||
run: async (page) => {
|
||||
await page.waitForSelector('[data-test="search-results"]');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AI Search',
|
||||
url: '?q=What+is+GitBook%3F&ask=true',
|
||||
run: async (page) => {
|
||||
await page.waitForSelector('[data-test="search-ask-answer"]');
|
||||
},
|
||||
screenshot: false,
|
||||
},
|
||||
{
|
||||
name: 'Not found',
|
||||
url: 'content-not-found',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'GitBook Site (Multi Variants)',
|
||||
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/multi-variants/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Variants dropdown',
|
||||
url: '',
|
||||
run: async (page) => {
|
||||
const spaceDrowpdown = page.locator('[data-testid="space-dropdown-button"]');
|
||||
await spaceDrowpdown.waitFor();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Default variant',
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
name: 'RFC variant',
|
||||
url: 'v/rfcs',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'GitBook',
|
||||
baseUrl: 'https://docs.gitbook.com',
|
||||
@@ -243,11 +177,6 @@ const testCases: TestsCase[] = [
|
||||
url: 'blocks/lists',
|
||||
fullPage: true,
|
||||
},
|
||||
{
|
||||
name: 'Code',
|
||||
url: 'blocks/code',
|
||||
fullPage: true,
|
||||
},
|
||||
{
|
||||
name: 'Cards',
|
||||
url: 'blocks/cards',
|
||||
@@ -260,7 +189,6 @@ const testCases: TestsCase[] = [
|
||||
{
|
||||
name: 'Embeds',
|
||||
url: 'blocks/embeds',
|
||||
fullPage: true,
|
||||
},
|
||||
{
|
||||
name: 'Annotations',
|
||||
@@ -548,7 +476,7 @@ for (const testCase of testCases) {
|
||||
}
|
||||
if (testEntry.screenshot !== false) {
|
||||
await argosScreenshot(page, `${testCase.name} - ${testEntry.name}`, {
|
||||
viewports: ['macbook-16', 'macbook-13', 'iphone-x', 'ipad-2'],
|
||||
viewports: ['macbook-13', 'iphone-x', 'ipad-2'],
|
||||
argosCSS: `
|
||||
/* Hide Intercom */
|
||||
.intercom-lightweight-app {
|
||||
|
||||
@@ -39,15 +39,6 @@ module.exports = withSentryConfig(
|
||||
|
||||
assetPrefix: process.env.GITBOOK_ASSETS_PREFIX,
|
||||
poweredByHeader: false,
|
||||
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: '*.gitbook.io',
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
silent: true,
|
||||
|
||||
+2
-3
@@ -20,7 +20,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@geist-ui/icons": "^1.0.2",
|
||||
"@gitbook/api": "^0.46.0",
|
||||
"@gitbook/api": "^0.39.0",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@sentry/nextjs": "^7.94.1",
|
||||
@@ -51,7 +51,6 @@
|
||||
"recoil": "^0.7.7",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"rehype-stringify": "^10.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.0",
|
||||
"rison": "^0.1.1",
|
||||
@@ -64,7 +63,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@argos-ci/playwright": "^2.0.0",
|
||||
"@cloudflare/next-on-pages": "^1.11.3",
|
||||
"@cloudflare/next-on-pages": "^1.9.0",
|
||||
"@cloudflare/workers-types": "^4.20231218.0",
|
||||
"@playwright/test": "^1.42.1",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"exports": "./src/index.ts",
|
||||
"dependencies": {
|
||||
"classnames": "^2.5.1",
|
||||
"@gitbook/api": "^0.46.0",
|
||||
"@gitbook/api": "^0.36.0",
|
||||
"assert-never": "^1.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"name": "@gitbook/react-openapi",
|
||||
"exports": "./src/index.ts",
|
||||
"dependencies": {
|
||||
"@scalar/api-client-react": "^0.3.7",
|
||||
"@scalar/oas-utils": "0.1.6",
|
||||
"@scalar/api-client-react": "^0.2.9",
|
||||
"@scalar/oas-utils": "0.1.1",
|
||||
"classnames": "^2.5.1",
|
||||
"flatted": "^3.2.9",
|
||||
"openapi-types": "^12.1.3",
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
getRequestFromOperation,
|
||||
Query,
|
||||
Header,
|
||||
RequestBody,
|
||||
} from '@scalar/oas-utils';
|
||||
import React from 'react';
|
||||
|
||||
@@ -99,15 +98,7 @@ export function ScalarApiClient(props: { children: React.ReactNode }) {
|
||||
{
|
||||
url: operationData.path,
|
||||
},
|
||||
getRequestFromOperation(
|
||||
{
|
||||
...operation,
|
||||
information: {
|
||||
requestBody: operationData.operation.requestBody as RequestBody,
|
||||
},
|
||||
},
|
||||
{ requiredOnly: false },
|
||||
),
|
||||
getRequestFromOperation(operation, { requiredOnly: false }),
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -6,8 +6,6 @@ if (dsn) {
|
||||
debug: false,
|
||||
dsn,
|
||||
|
||||
sampleRate: 0.1,
|
||||
|
||||
// Disable tracing as it creates additional requests in an env where subrequests are limited.
|
||||
enableTracing: false,
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ if (dsn) {
|
||||
debug: false,
|
||||
dsn,
|
||||
|
||||
sampleRate: 0.1,
|
||||
|
||||
// Disable tracing as it creates additional requests in an env where subrequests are limited.
|
||||
enableTracing: false,
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
import { verifyImageSignature, resizeImage, CloudflareImageOptions } from '@/lib/images';
|
||||
import { parseImageAPIURL } from '@/lib/urls';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
@@ -11,14 +10,12 @@ export const runtime = 'edge';
|
||||
* Fetch and resize an image.
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
let urlParam = request.nextUrl.searchParams.get('url');
|
||||
const url = request.nextUrl.searchParams.get('url');
|
||||
const signature = request.nextUrl.searchParams.get('sign');
|
||||
if (!urlParam || !signature) {
|
||||
if (!url || !signature) {
|
||||
return new Response('Missing url/sign parameters', { status: 400 });
|
||||
}
|
||||
|
||||
const url = parseImageAPIURL(urlParam);
|
||||
|
||||
// Prevent infinite loops
|
||||
if (url.includes('/~gitbook/image')) {
|
||||
return new Response('Invalid url parameter', { status: 400 });
|
||||
@@ -27,7 +24,7 @@ export async function GET(request: NextRequest) {
|
||||
// Verify the signature
|
||||
const verified = await verifyImageSignature(url, signature);
|
||||
if (!verified) {
|
||||
return new Response(`Invalid signature "${signature ?? ''}" for "${url}"`, { status: 400 });
|
||||
return new Response('Invalid signature', { status: 400 });
|
||||
}
|
||||
|
||||
// Cloudflare-specific options are in the cf object.
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { getSpaceLanguage, t } from '@/intl/server';
|
||||
import { getCurrentSiteLayoutData, getSpaceLayoutData } from '@/lib/api';
|
||||
import { getSpaceLayoutData } from '@/lib/api';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { getContentPointer } from '../../fetch';
|
||||
|
||||
export default async function NotFound() {
|
||||
const pointer = getContentPointer();
|
||||
const { customization } = await ('siteId' in pointer
|
||||
? getCurrentSiteLayoutData(pointer)
|
||||
: getSpaceLayoutData(pointer.spaceId));
|
||||
const { customization } = await getSpaceLayoutData(getContentPointer().spaceId);
|
||||
|
||||
const language = getSpaceLanguage(customization);
|
||||
|
||||
|
||||
@@ -104,13 +104,13 @@ export async function generateViewport({ params }: { params: PagePathParams }):
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: PagePathParams }): Promise<Metadata> {
|
||||
const { space, pages, page, customization, parent } = await fetchPageData(params);
|
||||
const { space, pages, page, customization, collection } = await fetchPageData(params);
|
||||
if (!page) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return {
|
||||
title: [page.title, customization.title ?? space.title, parent?.title]
|
||||
title: [page.title, customization.title ?? space.title, collection?.title]
|
||||
.filter(Boolean)
|
||||
.join(' | '),
|
||||
description: page.description ?? '',
|
||||
|
||||
@@ -32,8 +32,8 @@ export default async function ContentLayout(props: { children: React.ReactNode }
|
||||
contentTarget,
|
||||
customization,
|
||||
pages,
|
||||
parent,
|
||||
spaces,
|
||||
collection,
|
||||
collectionSpaces,
|
||||
ancestors,
|
||||
scripts,
|
||||
} = await fetchSpaceData();
|
||||
@@ -56,8 +56,8 @@ export default async function ContentLayout(props: { children: React.ReactNode }
|
||||
<SpaceLayout
|
||||
space={space}
|
||||
contentTarget={contentTarget}
|
||||
parent={parent}
|
||||
spaces={spaces}
|
||||
collection={collection}
|
||||
collectionSpaces={collectionSpaces}
|
||||
customization={customization}
|
||||
pages={pages}
|
||||
ancestors={ancestors}
|
||||
@@ -100,11 +100,11 @@ export async function generateViewport(): Promise<Viewport> {
|
||||
}
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const { space, parent, customization } = await fetchSpaceData();
|
||||
const { space, collection, customization } = await fetchSpaceData();
|
||||
const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null;
|
||||
|
||||
return {
|
||||
title: `${parent ? parent.title : customization.title ?? space.title}`,
|
||||
title: `${collection ? collection.title : customization.title ?? space.title}`,
|
||||
generator: `GitBook (${buildVersion()})`,
|
||||
metadataBase: new URL(baseUrl()),
|
||||
icons: {
|
||||
@@ -125,7 +125,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
||||
},
|
||||
],
|
||||
},
|
||||
robots: shouldIndexSpace({ space, parent }) ? 'index, follow' : 'noindex, nofollow',
|
||||
robots: shouldIndexSpace({ space, collection }) ? 'index, follow' : 'noindex, nofollow',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ContentVisibility } from '@gitbook/api';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
import { getCollection, getSite, getSpace } from '@/lib/api';
|
||||
import { getCollection, getSpace } from '@/lib/api';
|
||||
import { absoluteHref } from '@/lib/links';
|
||||
import { shouldIndexSpace } from '@/lib/seo';
|
||||
|
||||
@@ -13,19 +13,16 @@ export const runtime = 'edge';
|
||||
* Generate a robots.txt for the current space.
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
const pointer = getContentPointer();
|
||||
const space = await getSpace(pointer.spaceId);
|
||||
const parent =
|
||||
'siteId' in pointer
|
||||
? await getSite(pointer.organizationId, pointer.siteId)
|
||||
: space.visibility === ContentVisibility.InCollection && space.parent
|
||||
? await getCollection(space.parent)
|
||||
: null;
|
||||
const space = await getSpace(getContentPointer().spaceId);
|
||||
const collection =
|
||||
space.visibility === ContentVisibility.InCollection && space.parent
|
||||
? await getCollection(space.parent)
|
||||
: null;
|
||||
|
||||
const lines = [
|
||||
`User-agent: *`,
|
||||
'Disallow: /~gitbook/',
|
||||
...(shouldIndexSpace({ space, parent })
|
||||
...(shouldIndexSpace({ space, collection })
|
||||
? [`Allow: /`, `Sitemap: ${absoluteHref(`/sitemap.xml`, true)}`]
|
||||
: [`Disallow: /`]),
|
||||
];
|
||||
|
||||
@@ -5,13 +5,7 @@ import { NextRequest } from 'next/server';
|
||||
import React from 'react';
|
||||
|
||||
import { getContentPointer } from '@/app/(space)/fetch';
|
||||
import {
|
||||
getCollection,
|
||||
getCurrentSiteCustomization,
|
||||
getSite,
|
||||
getSpace,
|
||||
getSpaceCustomization,
|
||||
} from '@/lib/api';
|
||||
import { getCollection, getSpace, getSpaceCustomization } from '@/lib/api';
|
||||
import { getEmojiForCode } from '@/lib/emojis';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
@@ -41,20 +35,17 @@ export async function GET(req: NextRequest) {
|
||||
const options = getOptions(req.url);
|
||||
const size = SIZES[options.size];
|
||||
|
||||
const pointer = getContentPointer();
|
||||
const spaceId = pointer.spaceId;
|
||||
const spaceId = getContentPointer().spaceId;
|
||||
|
||||
const [space, customization] = await Promise.all([
|
||||
getSpace(spaceId),
|
||||
'siteId' in pointer ? getCurrentSiteCustomization(pointer) : getSpaceCustomization(spaceId),
|
||||
getSpaceCustomization(spaceId),
|
||||
]);
|
||||
const parent =
|
||||
'siteId' in pointer
|
||||
? await getSite(pointer.organizationId, pointer.siteId)
|
||||
: space.visibility === ContentVisibility.InCollection && space.parent
|
||||
? await getCollection(space.parent)
|
||||
: null;
|
||||
const contentTitle = parent?.title ?? customization.title ?? space.title;
|
||||
const collection =
|
||||
space.visibility === ContentVisibility.InCollection && space.parent
|
||||
? await getCollection(space.parent)
|
||||
: null;
|
||||
const contentTitle = collection?.title ?? customization.title ?? space.title;
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
|
||||
@@ -11,7 +11,7 @@ export const runtime = 'edge';
|
||||
* Render the OpenGraph image for a space.
|
||||
*/
|
||||
export async function GET(req: NextRequest, { params }: { params: PageIdParams }) {
|
||||
const { space, page, customization, parent } = await fetchPageData(params);
|
||||
const { space, page, customization, collection } = await fetchPageData(params);
|
||||
const url = new URL(space.urls.published ?? space.urls.app);
|
||||
|
||||
if (customization.socialPreview.url) {
|
||||
@@ -31,7 +31,7 @@ export async function GET(req: NextRequest, { params }: { params: PageIdParams }
|
||||
}}
|
||||
>
|
||||
<h2 tw="text-7xl font-bold tracking-tight text-left">
|
||||
{parent?.title ?? customization.title ?? space.title}
|
||||
{collection?.title ?? customization.title ?? space.title}
|
||||
</h2>
|
||||
<div tw="flex flex-1">
|
||||
<p tw="text-4xl">{page ? page.title : 'Not found'}</p>
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
Revision,
|
||||
RevisionPageDocument,
|
||||
RevisionPageGroup,
|
||||
SiteCustomizationSettings,
|
||||
Space,
|
||||
} from '@gitbook/api';
|
||||
import { Metadata } from 'next';
|
||||
@@ -12,18 +11,12 @@ import { notFound } from 'next/navigation';
|
||||
import * as React from 'react';
|
||||
|
||||
import { getContentPointer } from '@/app/(space)/fetch';
|
||||
import { DocumentView, createHighlightingContext } from '@/components/DocumentView';
|
||||
import { DocumentView } from '@/components/DocumentView';
|
||||
import { TrademarkLink } from '@/components/TableOfContents/Trademark';
|
||||
import { PolymorphicComponentProp } from '@/components/utils/types';
|
||||
import { getSpaceLanguage } from '@/intl/server';
|
||||
import { tString } from '@/intl/translate';
|
||||
import {
|
||||
getDocument,
|
||||
getSpace,
|
||||
getSpaceCustomization,
|
||||
getSpaceContentData,
|
||||
getCurrentSiteCustomization,
|
||||
} from '@/lib/api';
|
||||
import { getDocument, getSpace, getSpaceCustomization, getSpaceContentData } from '@/lib/api';
|
||||
import { pagePDFContainerId, PageHrefContext, absoluteHref } from '@/lib/links';
|
||||
import { resolvePageId } from '@/lib/pages';
|
||||
import { ContentRefContext, resolveContentRef } from '@/lib/references';
|
||||
@@ -42,9 +35,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
||||
const contentPointer = getContentPointer();
|
||||
const [space, customization] = await Promise.all([
|
||||
getSpace(contentPointer.spaceId),
|
||||
'siteId' in contentPointer
|
||||
? getCurrentSiteCustomization(contentPointer)
|
||||
: getSpaceCustomization(contentPointer.spaceId),
|
||||
getSpaceCustomization(contentPointer.spaceId),
|
||||
]);
|
||||
|
||||
return {
|
||||
@@ -68,9 +59,7 @@ export default async function PDFHTMLOutput(props: { searchParams: { [key: strin
|
||||
|
||||
// Load the content,
|
||||
const [customization, { space, contentTarget, pages: rootPages }] = await Promise.all([
|
||||
'siteId' in contentPointer
|
||||
? getCurrentSiteCustomization(contentPointer)
|
||||
: getSpaceCustomization(contentPointer.spaceId),
|
||||
getSpaceCustomization(contentPointer.spaceId),
|
||||
getSpaceContentData(contentPointer),
|
||||
]);
|
||||
const language = getSpaceLanguage(customization);
|
||||
@@ -183,10 +172,7 @@ export default async function PDFHTMLOutput(props: { searchParams: { [key: strin
|
||||
);
|
||||
}
|
||||
|
||||
async function PDFSpaceIntro(props: {
|
||||
space: Space;
|
||||
customization: CustomizationSettings | SiteCustomizationSettings;
|
||||
}) {
|
||||
async function PDFSpaceIntro(props: { space: Space; customization: CustomizationSettings }) {
|
||||
const { space, customization } = props;
|
||||
|
||||
return (
|
||||
@@ -230,7 +216,6 @@ async function PDFPageDocument(props: {
|
||||
const { space, page, refContext } = props;
|
||||
|
||||
const document = page.documentId ? await getDocument(space.id, page.documentId) : null;
|
||||
const shouldHighlightCode = createHighlightingContext();
|
||||
|
||||
return (
|
||||
<PrintPage id={pagePDFContainerId(page)}>
|
||||
@@ -253,7 +238,6 @@ async function PDFPageDocument(props: {
|
||||
contentRefContext: refContext,
|
||||
resolveContentRef: (ref) => resolveContentRef(ref, refContext),
|
||||
getId: (id) => pagePDFContainerId(page, id),
|
||||
shouldHighlightCode,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
+16
-68
@@ -9,10 +9,6 @@ import {
|
||||
getDocument,
|
||||
getSpaceData,
|
||||
ContentTarget,
|
||||
SiteContentPointer,
|
||||
getCurrentSiteData,
|
||||
getSite,
|
||||
getSiteSpaces,
|
||||
} from '@/lib/api';
|
||||
import { resolvePagePath, resolvePageId } from '@/lib/pages';
|
||||
|
||||
@@ -27,7 +23,7 @@ export interface PageIdParams {
|
||||
/**
|
||||
* Get the current content pointer from the params.
|
||||
*/
|
||||
export function getContentPointer(): ContentPointer | SiteContentPointer {
|
||||
export function getContentPointer() {
|
||||
const headerSet = headers();
|
||||
const spaceId = headerSet.get('x-gitbook-content-space');
|
||||
if (!spaceId) {
|
||||
@@ -36,31 +32,13 @@ export function getContentPointer(): ContentPointer | SiteContentPointer {
|
||||
);
|
||||
}
|
||||
|
||||
const siteId = headerSet.get('x-gitbook-content-site');
|
||||
if (siteId) {
|
||||
const organizationId = headerSet.get('x-gitbook-content-organization');
|
||||
const siteSpaceId = headerSet.get('x-gitbook-content-site-space');
|
||||
if (!organizationId) {
|
||||
throw new Error('Missing site content headers');
|
||||
}
|
||||
const content: ContentPointer = {
|
||||
spaceId,
|
||||
revisionId: headerSet.get('x-gitbook-content-revision') ?? undefined,
|
||||
changeRequestId: headerSet.get('x-gitbook-content-changerequest') ?? undefined,
|
||||
};
|
||||
|
||||
const siteContent: SiteContentPointer = {
|
||||
siteId,
|
||||
spaceId,
|
||||
siteSpaceId: siteSpaceId ?? undefined,
|
||||
organizationId,
|
||||
revisionId: headerSet.get('x-gitbook-content-revision') ?? undefined,
|
||||
changeRequestId: headerSet.get('x-gitbook-content-changerequest') ?? undefined,
|
||||
};
|
||||
return siteContent;
|
||||
} else {
|
||||
const content: ContentPointer = {
|
||||
spaceId,
|
||||
revisionId: headerSet.get('x-gitbook-content-revision') ?? undefined,
|
||||
changeRequestId: headerSet.get('x-gitbook-content-changerequest') ?? undefined,
|
||||
};
|
||||
return content;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,14 +46,8 @@ export function getContentPointer(): ContentPointer | SiteContentPointer {
|
||||
*/
|
||||
export async function fetchSpaceData() {
|
||||
const content = getContentPointer();
|
||||
|
||||
const [{ space, contentTarget, pages, customization, scripts }, parentSite] = await Promise.all(
|
||||
'siteId' in content
|
||||
? [getCurrentSiteData(content), fetchParentSite(content.organizationId, content.siteId)]
|
||||
: [getSpaceData(content)],
|
||||
);
|
||||
|
||||
const parent = await (parentSite ?? fetchParentCollection(space));
|
||||
const { space, contentTarget, pages, customization, scripts } = await getSpaceData(content);
|
||||
const collection = await fetchParentCollection(space);
|
||||
|
||||
return {
|
||||
content,
|
||||
@@ -85,7 +57,7 @@ export async function fetchSpaceData() {
|
||||
customization,
|
||||
scripts,
|
||||
ancestors: [],
|
||||
...parent,
|
||||
...collection,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -95,15 +67,11 @@ export async function fetchSpaceData() {
|
||||
*/
|
||||
export async function fetchPageData(params: PagePathParams | PageIdParams) {
|
||||
const content = getContentPointer();
|
||||
const { space, contentTarget, pages, customization, scripts } = await ('siteId' in content
|
||||
? getCurrentSiteData(content)
|
||||
: getSpaceData(content));
|
||||
const { space, contentTarget, pages, customization, scripts } = await getSpaceData(content);
|
||||
|
||||
const page = await resolvePage(contentTarget, pages, params);
|
||||
const [parent, document] = await Promise.all([
|
||||
'siteId' in content
|
||||
? fetchParentSite(content.organizationId, content.siteId)
|
||||
: fetchParentCollection(space),
|
||||
const [collection, document] = await Promise.all([
|
||||
fetchParentCollection(space),
|
||||
page?.page.documentId ? getDocument(space.id, page.page.documentId) : null,
|
||||
]);
|
||||
|
||||
@@ -116,7 +84,7 @@ export async function fetchPageData(params: PagePathParams | PageIdParams) {
|
||||
scripts,
|
||||
ancestors: [],
|
||||
...page,
|
||||
...parent,
|
||||
...collection,
|
||||
document,
|
||||
};
|
||||
}
|
||||
@@ -165,32 +133,12 @@ async function resolvePage(
|
||||
async function fetchParentCollection(space: Space) {
|
||||
const parentCollectionId =
|
||||
space.visibility === ContentVisibility.InCollection ? space.parent : undefined;
|
||||
const [collection, spaces] = await Promise.all([
|
||||
const [collection, collectionSpaces] = await Promise.all([
|
||||
parentCollectionId ? getCollection(parentCollectionId) : null,
|
||||
parentCollectionId ? getCollectionSpaces(parentCollectionId) : ([] as Space[]),
|
||||
]);
|
||||
|
||||
return { parent: collection, spaces };
|
||||
}
|
||||
|
||||
async function fetchParentSite(organizationId: string, siteId: string) {
|
||||
const [site, siteSpaces] = await Promise.all([
|
||||
getSite(organizationId, siteId),
|
||||
getSiteSpaces(organizationId, siteId),
|
||||
]);
|
||||
|
||||
const spaces: Record<string, Space> = {};
|
||||
siteSpaces.forEach((siteSpace) => {
|
||||
spaces[siteSpace.space.id] = {
|
||||
...siteSpace.space,
|
||||
urls: {
|
||||
...siteSpace.space.urls,
|
||||
published: siteSpace.urls.published,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return { parent: site, spaces: Object.values(spaces) };
|
||||
return { collection, collectionSpaces };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
CustomizationCorners,
|
||||
CustomizationHeaderPreset,
|
||||
CustomizationSettings,
|
||||
SiteCustomizationSettings,
|
||||
} from '@gitbook/api';
|
||||
import assertNever from 'assert-never';
|
||||
import colors from 'tailwindcss/colors';
|
||||
@@ -11,7 +10,7 @@ import colors from 'tailwindcss/colors';
|
||||
import { emojiFontClassName } from '@/components/primitives';
|
||||
import { fonts, ibmPlexMono } from '@/fonts';
|
||||
import { getSpaceLanguage } from '@/intl/server';
|
||||
import { getCurrentSiteLayoutData, getSpaceLayoutData } from '@/lib/api';
|
||||
import { getSpaceLayoutData } from '@/lib/api';
|
||||
import { hexToRgb, shadesOfColor } from '@/lib/colors';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
@@ -26,10 +25,7 @@ import { getContentPointer } from './fetch';
|
||||
export default async function SpaceRootLayout(props: { children: React.ReactNode }) {
|
||||
const { children } = props;
|
||||
|
||||
const pointer = getContentPointer();
|
||||
const { customization } = await ('siteId' in pointer
|
||||
? getCurrentSiteLayoutData(pointer)
|
||||
: getSpaceLayoutData(pointer.spaceId));
|
||||
const { customization } = await getSpaceLayoutData(getContentPointer().spaceId);
|
||||
const headerTheme = generateHeaderTheme(customization);
|
||||
const language = getSpaceLanguage(customization);
|
||||
|
||||
@@ -124,7 +120,7 @@ function generateColorVariable(name: string, color: ColorInput) {
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function generateHeaderTheme(customization: CustomizationSettings | SiteCustomizationSettings): {
|
||||
function generateHeaderTheme(customization: CustomizationSettings): {
|
||||
backgroundColor: { light: ColorInput; dark: ColorInput };
|
||||
linkColor: { light: ColorInput; dark: ColorInput };
|
||||
} {
|
||||
|
||||
@@ -76,7 +76,6 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setShow(false)}
|
||||
aria-label={tString(language, 'cookies_close')}
|
||||
className={tcls(
|
||||
'absolute',
|
||||
'top-3',
|
||||
@@ -98,7 +97,6 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
aria-label={tString(language, 'cookies_accept')}
|
||||
onClick={() => {
|
||||
onUpdateState(true);
|
||||
}}
|
||||
@@ -108,7 +106,6 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
aria-label={tString(language, 'cookies_reject')}
|
||||
onClick={() => {
|
||||
onUpdateState(false);
|
||||
}}
|
||||
|
||||
@@ -34,15 +34,7 @@ export function Blocks<T extends DocumentBlock, Tag extends React.ElementType =
|
||||
<Block
|
||||
key={node.key}
|
||||
block={node}
|
||||
style={[
|
||||
node.data && 'fullWidth' in node.data && node.data.fullWidth
|
||||
? 'max-w-screen-xl'
|
||||
: 'max-w-3xl',
|
||||
'w-full',
|
||||
'mx-auto',
|
||||
'decoration-primary/6',
|
||||
blockStyle,
|
||||
]}
|
||||
style={['max-w-3xl', 'w-full', 'mx-auto', 'decoration-primary/6', blockStyle]}
|
||||
{...contextProps}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DocumentBlockCode, JSONDocument } from '@gitbook/api';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { CopyCodeButton } from './CopyCodeButton';
|
||||
import { highlight, HighlightLine, HighlightToken, plainHighlighting } from './highlight';
|
||||
import { highlight, HighlightLine, HighlightToken } from './highlight';
|
||||
import { BlockProps } from '../Block';
|
||||
import { DocumentContext } from '../DocumentView';
|
||||
import { Inline } from '../Inline';
|
||||
@@ -15,14 +15,16 @@ import './theme.css';
|
||||
*/
|
||||
export async function CodeBlock(props: BlockProps<DocumentBlockCode>) {
|
||||
const { block, document, style, context } = props;
|
||||
const withHighlighting = context.shouldHighlightCode(context.content?.spaceId);
|
||||
const lines = withHighlighting ? await highlight(block) : plainHighlighting(block);
|
||||
const lines = await highlight(block);
|
||||
|
||||
const id = block.key!;
|
||||
|
||||
const withLineNumbers = !!block.data.lineNumbers && block.nodes.length > 1;
|
||||
const withWrap = block.data.overflow === 'wrap';
|
||||
const title = block.data.title;
|
||||
const fullWidth = block.data.fullWidth;
|
||||
|
||||
const fullWidthStyle = fullWidth ? 'max-w-4xl' : 'max-w-3xl';
|
||||
const titleRoundingStyle = [
|
||||
'rounded-md',
|
||||
'straight-corners:rounded-sm',
|
||||
@@ -30,7 +32,7 @@ export async function CodeBlock(props: BlockProps<DocumentBlockCode>) {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={tcls('group/codeblock', 'grid', 'grid-flow-col', style)}>
|
||||
<div className={tcls('group/codeblock', 'grid', 'grid-flow-col', fullWidthStyle, style)}>
|
||||
<div
|
||||
className={tcls(
|
||||
'flex',
|
||||
|
||||
@@ -50,7 +50,6 @@ export function PlainCodeBlock(props: { code: string; syntax: string }) {
|
||||
mode: 'default',
|
||||
contentRefContext: null,
|
||||
resolveContentRef: async () => null,
|
||||
shouldHighlightCode: () => true,
|
||||
}}
|
||||
block={block}
|
||||
ancestorBlocks={[]}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
const CODE_HIGHLIGHT_BLOCK_LIMIT = 50;
|
||||
|
||||
/**
|
||||
* Spaces where we'll always use plain highlighting.
|
||||
*/
|
||||
const PLAIN_HIGHLIGHTING_SPACES: string[] = [
|
||||
'V9geAO9ITPi8WOYK5o0r',
|
||||
'puRmcwVxGFtHph8IjXaf',
|
||||
'e3jwbMOrr4RhKtZ9C0XL',
|
||||
];
|
||||
|
||||
/**
|
||||
* Protect against memory issues when highlighting a large number of code blocks.
|
||||
* This context only allows 50 code blocks per render to be highlighted.
|
||||
* Once highlighting can scale up to a large number of code blocks, it can be removed.
|
||||
*
|
||||
* https://linear.app/gitbook-x/issue/RND-3588/gitbook-open-code-syntax-highlighting-runs-out-of-memory-after-a
|
||||
*/
|
||||
export function createHighlightingContext() {
|
||||
let count = 0;
|
||||
return (spaceId?: string | undefined) => {
|
||||
if (!spaceId || PLAIN_HIGHLIGHTING_SPACES.includes(spaceId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
count += 1;
|
||||
return count < CODE_HIGHLIGHT_BLOCK_LIMIT;
|
||||
};
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
import { DocumentBlockCode, DocumentBlockCodeLine, DocumentInlineAnnotation } from '@gitbook/api';
|
||||
import {
|
||||
loadWasm,
|
||||
bundledLanguages,
|
||||
ThemedToken,
|
||||
getHighlighter,
|
||||
createCssVariablesTheme,
|
||||
HighlighterGeneric,
|
||||
bundledLanguages,
|
||||
bundledThemes,
|
||||
} from 'shiki';
|
||||
// @ts-ignore - onigWasm is a Wasm module
|
||||
import onigWasm from 'shiki/onig.wasm?module';
|
||||
@@ -15,8 +13,6 @@ import { asyncMutexFunction, singleton } from '@/lib/async';
|
||||
import { getNodeText } from '@/lib/document';
|
||||
import { trace } from '@/lib/tracing';
|
||||
|
||||
import { DocumentContext } from '../DocumentView';
|
||||
|
||||
export type HighlightLine = {
|
||||
highlighted: boolean;
|
||||
tokens: HighlightToken[];
|
||||
@@ -49,14 +45,12 @@ export async function highlight(block: DocumentBlockCode): Promise<HighlightLine
|
||||
});
|
||||
|
||||
const highlighter = await loadHighlighter();
|
||||
await loadHighlighterLanguage(highlighter, langName);
|
||||
|
||||
await loadHighlighterLanguage(langName);
|
||||
const lines = highlighter.codeToTokensBase(code, {
|
||||
lang: langName,
|
||||
tokenizeMaxLineLength: 120,
|
||||
});
|
||||
|
||||
let currentIndex = 0;
|
||||
|
||||
return lines.map((tokens, index) => {
|
||||
const lineBlock = block.nodes[index];
|
||||
const result: HighlightToken[] = [];
|
||||
@@ -321,15 +315,9 @@ const loadHighlighter = singleton(async () => {
|
||||
});
|
||||
|
||||
const loadLanguagesMutex = asyncMutexFunction();
|
||||
async function loadHighlighterLanguage(
|
||||
highlighter: HighlighterGeneric<keyof typeof bundledLanguages, keyof typeof bundledThemes>,
|
||||
lang: keyof typeof bundledLanguages,
|
||||
) {
|
||||
async function loadHighlighterLanguage(lang: keyof typeof bundledLanguages) {
|
||||
await loadLanguagesMutex.runBlocking(async () => {
|
||||
if (highlighter.getLoadedLanguages().includes(lang)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const highlighter = await loadHighlighter();
|
||||
await trace(
|
||||
`highlighting.loadLanguage(${lang})`,
|
||||
async () => await highlighter.loadLanguage(lang),
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from './CodeBlock';
|
||||
export * from './PlainCodeBlock';
|
||||
export * from './createHighlightingContext';
|
||||
|
||||
@@ -36,16 +36,6 @@ export interface DocumentContext {
|
||||
* Transform an ID to be added to the DOM.
|
||||
*/
|
||||
getId?: (id: string) => string;
|
||||
|
||||
/**
|
||||
* Returns true if the given code block should be highlighted.
|
||||
* This function was added to protect against memory issues when highlighting
|
||||
* a large number of code blocks.
|
||||
* Once highlighting can scale up to a large number of code blocks, it can be removed.
|
||||
*
|
||||
* https://linear.app/gitbook-x/issue/RND-3588/gitbook-open-code-syntax-highlighting-runs-out-of-memory-after-a
|
||||
*/
|
||||
shouldHighlightCode: (spaceId: string | undefined) => boolean;
|
||||
}
|
||||
|
||||
export interface DocumentContextProps {
|
||||
|
||||
@@ -1,38 +1,26 @@
|
||||
import * as gitbookAPI from '@gitbook/api';
|
||||
import Script from 'next/script';
|
||||
import { DocumentBlockEmbed } from '@gitbook/api';
|
||||
|
||||
import { Card } from '@/components/primitives';
|
||||
import { api } from '@/lib/api';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { getNodeFragmentByName, isNodeEmpty } from '@/lib/document';
|
||||
import { ClassValue, tcls } from '@/lib/tailwind';
|
||||
|
||||
import { BlockProps } from './Block';
|
||||
import { Caption } from './Caption';
|
||||
import { IntegrationBlock } from './Integration';
|
||||
import { Inlines } from './Inlines';
|
||||
|
||||
export async function Embed(props: BlockProps<gitbookAPI.DocumentBlockEmbed>) {
|
||||
const { block, context, ...otherProps } = props;
|
||||
export async function Embed(props: BlockProps<DocumentBlockEmbed>) {
|
||||
const { block } = props;
|
||||
|
||||
const { data: embed } = await (context.content
|
||||
? api().spaces.getEmbedByUrlInSpace(context.content.spaceId, { url: block.data.url })
|
||||
: api().urls.getEmbedByUrl({ url: block.data.url }));
|
||||
const { data: embed } = await api().urls.getEmbedByUrl({ url: block.data.url });
|
||||
|
||||
return (
|
||||
<Caption {...props}>
|
||||
{embed.type === 'rich' ? (
|
||||
<>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: embed.html,
|
||||
}}
|
||||
/>
|
||||
{/* We load the iframely script to resize the embed iframes dynamically */}
|
||||
<Script src="https://cdn.iframe.ly/embed.js" defer async />
|
||||
</>
|
||||
) : embed.type === 'integration' ? (
|
||||
<IntegrationBlock
|
||||
{...otherProps}
|
||||
context={context}
|
||||
block={createIntegrationBlock(block.data.url, embed.integration, embed.block)}
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: embed.html,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Card
|
||||
@@ -49,28 +37,3 @@ export async function Embed(props: BlockProps<gitbookAPI.DocumentBlockEmbed>) {
|
||||
</Caption>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an integration block with an unfurl action from the GitBook Embed response.
|
||||
*/
|
||||
function createIntegrationBlock(
|
||||
url: string,
|
||||
integration: string,
|
||||
block: gitbookAPI.IntegrationBlock,
|
||||
): gitbookAPI.DocumentBlockIntegration {
|
||||
return {
|
||||
object: 'block',
|
||||
type: 'integration',
|
||||
isVoid: true,
|
||||
data: {
|
||||
integration,
|
||||
block: block.id,
|
||||
props: {},
|
||||
action: {
|
||||
action: '@link.unfurl',
|
||||
url,
|
||||
},
|
||||
url,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import IconDownload from '@geist-ui/icons/download';
|
||||
import IconFileText from '@geist-ui/icons/fileText';
|
||||
import IconImage from '@geist-ui/icons/image';
|
||||
import IconPaperClip from '@geist-ui/icons/paperclip';
|
||||
import { DocumentBlockFile } from '@gitbook/api';
|
||||
|
||||
import { getSimplifiedContentType } from '@/lib/files';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { BlockProps } from './Block';
|
||||
import { FileIcon } from './FileIcon';
|
||||
|
||||
export async function File(props: BlockProps<DocumentBlockFile>) {
|
||||
const { block, context, style } = props;
|
||||
@@ -16,7 +18,42 @@ export async function File(props: BlockProps<DocumentBlockFile>) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const contentType = getSimplifiedContentType(file.contentType);
|
||||
const contentType = (() => {
|
||||
switch (file.contentType) {
|
||||
case 'application/pdf':
|
||||
case 'application/x-pdf':
|
||||
return 'pdf';
|
||||
case 'image/png':
|
||||
case 'image/jpeg':
|
||||
case 'image/gif':
|
||||
case 'image/webp':
|
||||
case 'image/tiff':
|
||||
case 'image/svg+xml':
|
||||
return 'image';
|
||||
case 'application/zip':
|
||||
case 'application/x-7z-compressed':
|
||||
case 'application/x-zip-compressed':
|
||||
case 'application/x-tar':
|
||||
case 'application/x-rar-compressed':
|
||||
case 'application/vnd.rar':
|
||||
return 'archive';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
const icon = (() => {
|
||||
switch (contentType) {
|
||||
case 'pdf':
|
||||
return <IconFileText />;
|
||||
case 'image':
|
||||
return <IconImage />;
|
||||
case 'archive':
|
||||
return <IconPaperClip />;
|
||||
default:
|
||||
return <IconDownload />;
|
||||
}
|
||||
})();
|
||||
|
||||
return (
|
||||
<a
|
||||
@@ -66,9 +103,7 @@ export async function File(props: BlockProps<DocumentBlockFile>) {
|
||||
'dark:border-light/2',
|
||||
)}
|
||||
>
|
||||
<div className={tcls('*:w-5', '*:h-5', '*:stroke-primary')}>
|
||||
<FileIcon contentType={contentType} />
|
||||
</div>
|
||||
<div className={tcls('*:w-5', '*:h-5', '*:stroke-primary')}>{icon}</div>
|
||||
<div
|
||||
className={tcls(
|
||||
'text-xs',
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import IconDownload from '@geist-ui/icons/download';
|
||||
import IconFileText from '@geist-ui/icons/fileText';
|
||||
import IconImage from '@geist-ui/icons/image';
|
||||
import IconPaperClip from '@geist-ui/icons/paperclip';
|
||||
|
||||
import { SimplifiedFileType } from '@/lib/files';
|
||||
|
||||
/**
|
||||
* Render an appropriate icon for a file.
|
||||
*/
|
||||
export function FileIcon(props: { contentType: SimplifiedFileType | null }) {
|
||||
const { contentType } = props;
|
||||
|
||||
switch (contentType) {
|
||||
case 'pdf':
|
||||
return <IconFileText />;
|
||||
case 'image':
|
||||
return <IconImage />;
|
||||
case 'archive':
|
||||
return <IconPaperClip />;
|
||||
default:
|
||||
return <IconDownload />;
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ export function Images(props: BlockProps<DocumentBlockImages>) {
|
||||
align === 'right' && 'justify-end',
|
||||
align === 'left' && 'justify-start',
|
||||
isMultipleImages && ['grid', 'grid-flow-col', 'max-w-none'],
|
||||
block.data.fullWidth ? 'max-w-screen-2xl' : null,
|
||||
)}
|
||||
>
|
||||
{block.nodes.map((node: any, i: number) => (
|
||||
|
||||
@@ -70,7 +70,6 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
|
||||
const initialInput = {
|
||||
componentId: block.data.block,
|
||||
props: block.data.props,
|
||||
action: block.data.action,
|
||||
context: contentKitContext,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,123 +1,115 @@
|
||||
.light .scalar-modal-layout,
|
||||
.light .scalar {
|
||||
--scalar-color-1: color-mix(
|
||||
--theme-color-1: color-mix(
|
||||
in srgb,
|
||||
rgb(var(--primary-base-300, 180 180 180)),
|
||||
rgb(var(--dark-base, 23 23 23)) 96%
|
||||
);
|
||||
--scalar-color-2: color-mix(in srgb, var(--scalar-color-1), transparent calc(100% - 100% * 0.72));
|
||||
--scalar-color-3: color-mix(in srgb, var(--scalar-color-1), transparent calc(100% - 100% * 0.4));
|
||||
--scalar-color-accent: #007d9c;
|
||||
--theme-color-2: color-mix(in srgb, var(--theme-color-1), transparent calc(100% - 100% * 0.72));
|
||||
--theme-color-3: color-mix(in srgb, var(--theme-color-1), transparent calc(100% - 100% * 0.4));
|
||||
--theme-color-accent: #007d9c;
|
||||
|
||||
--scalar-background-1: rgb(var(--light-base, 255 255 255));
|
||||
--scalar-background-2: color-mix(
|
||||
--theme-background-1: rgb(var(--light-base, 255 255 255));
|
||||
--theme-background-2: color-mix(
|
||||
in srgb,
|
||||
rgb(var(--primary-base-800, 30 30 30)),
|
||||
var(--scalar-background-1) 96%
|
||||
var(--theme-background-1) 96%
|
||||
);
|
||||
--scalar-background-3: color-mix(
|
||||
--theme-background-3: color-mix(
|
||||
in srgb,
|
||||
rgb(var(--primary-base-800, 30 30 30)),
|
||||
var(--scalar-background-1) 90%
|
||||
var(--theme-background-1) 90%
|
||||
);
|
||||
--scalar-background-accent: #007d9c1f;
|
||||
--scalar-code-language-color-supersede: var(--scalar-color-1);
|
||||
--scalar-code-languages-background-supersede: var(--scalar-background-1);
|
||||
--scalar-border-color: color-mix(
|
||||
--theme-background-accent: #007d9c1f;
|
||||
--theme-code-language-color-supersede: var(--theme-color-1);
|
||||
--theme-code-languages-background-supersede: var(--theme-background-1);
|
||||
--theme-border-color: color-mix(
|
||||
in srgb,
|
||||
var(--scalar-color-1),
|
||||
var(--theme-color-1),
|
||||
transparent calc(100% - 100% * 0.08)
|
||||
);
|
||||
|
||||
--scalar-color-green: #0a6355;
|
||||
--scalar-color-red: #dc1b19;
|
||||
--scalar-color-yellow: #ffc90d;
|
||||
--scalar-color-blue: rgb(var(--primary-color-500, 52 109 219));
|
||||
--scalar-color-orange: #ff8d4d;
|
||||
--scalar-color-purple: #8250df;
|
||||
--theme-color-green: #0a6355;
|
||||
--theme-color-red: #dc1b19;
|
||||
--theme-color-yellow: #ffc90d;
|
||||
--theme-color-blue: rgb(var(--primary-color-500, 52 109 219));
|
||||
--theme-color-orange: #ff8d4d;
|
||||
--theme-color-purple: #8250df;
|
||||
|
||||
--scalar-scrollbar-color: rgba(255, 255, 255, 0.24);
|
||||
--scalar-scrollbar-color-active: rgba(255, 255, 255, 0.48);
|
||||
|
||||
--scalar-button-1: rgb(49 53 56);
|
||||
--scalar-button-1-color: #fff;
|
||||
--scalar-button-1-hover: rgb(28 31 33);
|
||||
--theme-scrollbar-color: rgba(255, 255, 255, 0.24);
|
||||
--theme-scrollbar-color-active: rgba(255, 255, 255, 0.48);
|
||||
}
|
||||
.dark .scalar-modal-layout,
|
||||
.dark .scalar {
|
||||
--scalar-color-1: color-mix(
|
||||
--theme-color-1: color-mix(
|
||||
in srgb,
|
||||
rgb(var(--primary-base-700, 70 70 70)),
|
||||
rgb(var(--light-base, 255 255 255)) 100%
|
||||
);
|
||||
--scalar-color-2: color-mix(in srgb, var(--scalar-color-1), transparent calc(100% - 100% * 0.64));
|
||||
--scalar-color-3: color-mix(in srgb, var(--scalar-color-1), transparent calc(100% - 100% * 0.4));
|
||||
--scalar-color-accent: #50b7e0;
|
||||
--theme-color-2: color-mix(in srgb, var(--theme-color-1), transparent calc(100% - 100% * 0.64));
|
||||
--theme-color-3: color-mix(in srgb, var(--theme-color-1), transparent calc(100% - 100% * 0.4));
|
||||
--theme-color-accent: #50b7e0;
|
||||
|
||||
--scalar-background-1: rgb(var(--dark-base, 22 22 22));
|
||||
--scalar-background-2: color-mix(
|
||||
--theme-background-1: rgb(var(--dark-base, 22 22 22));
|
||||
--theme-background-2: color-mix(
|
||||
in srgb,
|
||||
rgb(var(--primary-base-200, 200 200 200)),
|
||||
var(--scalar-background-1) 92%
|
||||
var(--theme-background-1) 92%
|
||||
);
|
||||
--scalar-background-3: color-mix(
|
||||
--theme-background-3: color-mix(
|
||||
in srgb,
|
||||
rgb(var(--primary-base-200, 200 200 200)),
|
||||
var(--scalar-background-1) 88%
|
||||
var(--theme-background-1) 88%
|
||||
);
|
||||
--scalar-background-accent: #8ab4f81f;
|
||||
--scalar-code-languages-background-supersede: var(--scalar-background-1);
|
||||
--scalar-border-color: color-mix(
|
||||
--theme-background-accent: #8ab4f81f;
|
||||
--theme-code-languages-background-supersede: var(--theme-background-1);
|
||||
--theme-border-color: color-mix(
|
||||
in srgb,
|
||||
var(--scalar-color-1),
|
||||
var(--theme-color-1),
|
||||
transparent calc(100% - 100% * 0.08)
|
||||
);
|
||||
|
||||
--scalar-color-green: #56b6c2;
|
||||
--scalar-color-red: rgb(245 124 97);
|
||||
--scalar-color-yellow: #edbe20;
|
||||
--scalar-color-blue: rgb(var(--primary-color-400, 93 138 226));
|
||||
--scalar-color-orange: #d19a66;
|
||||
--scalar-color-purple: #5203d1;
|
||||
--theme-color-green: #56b6c2;
|
||||
--theme-color-red: rgb(245 124 97);
|
||||
--theme-color-yellow: #edbe20;
|
||||
--theme-color-blue: rgb(var(--primary-color-400, 93 138 226));
|
||||
--theme-color-orange: #d19a66;
|
||||
--theme-color-purple: #5203d1;
|
||||
|
||||
--scalar-scrollbar-color: rgba(0, 0, 0, 0.18);
|
||||
--scalar-scrollbar-color-active: rgba(0, 0, 0, 0.36);
|
||||
|
||||
--scalar-button-1: #f6f6f6;
|
||||
--scalar-button-1-color: #000;
|
||||
--scalar-button-1-hover: #e7e7e7;
|
||||
--theme-scrollbar-color: rgba(0, 0, 0, 0.18);
|
||||
--theme-scrollbar-color-active: rgba(0, 0, 0, 0.36);
|
||||
}
|
||||
.scalar-modal-layout,
|
||||
.scalar {
|
||||
--scalar-font: initial;
|
||||
--scalar-font-code: var(--font-mono);
|
||||
--theme-font: initial;
|
||||
--theme-font-code: var(--font-mono);
|
||||
|
||||
--scalar-paragraph: 16px;
|
||||
--scalar-small: 14px;
|
||||
--scalar-mini: 13px;
|
||||
--scalar-micro: 12px;
|
||||
--theme-paragraph: 16px;
|
||||
--theme-small: 14px;
|
||||
--theme-mini: 13px;
|
||||
--theme-micro: 12px;
|
||||
|
||||
--scalar-bold: 600;
|
||||
--scalar-semibold: 500;
|
||||
--scalar-regular: 400;
|
||||
--theme-bold: 600;
|
||||
--theme-semibold: 500;
|
||||
--theme-regular: 400;
|
||||
|
||||
/* Font sizes for interactive applications (not rendered text content) */
|
||||
--scalar-font-size-1: 24px;
|
||||
--scalar-font-size-2: 16px;
|
||||
--scalar-font-size-3: 14px;
|
||||
--scalar-font-size-4: 13px;
|
||||
--scalar-font-size-5: 12px;
|
||||
--theme-font-size-1: 24px;
|
||||
--theme-font-size-2: 16px;
|
||||
--theme-font-size-3: 14px;
|
||||
--theme-font-size-4: 13px;
|
||||
--theme-font-size-5: 12px;
|
||||
|
||||
--scalar-line-height-1: 32px;
|
||||
--scalar-line-height-2: 24px;
|
||||
--scalar-line-height-3: 20px;
|
||||
--scalar-line-height-4: 18px;
|
||||
--scalar-line-height-5: 16px;
|
||||
--theme-line-height-1: 32px;
|
||||
--theme-line-height-2: 24px;
|
||||
--theme-line-height-3: 20px;
|
||||
--theme-line-height-4: 18px;
|
||||
--theme-line-height-5: 16px;
|
||||
|
||||
--scalar-app-header-height: 35px;
|
||||
}
|
||||
.scalar input::placeholder {
|
||||
color: var(--scalar-color-3);
|
||||
color: var(--theme-color-3);
|
||||
}
|
||||
.scalar .scalar-app-header {
|
||||
width: 100%;
|
||||
@@ -142,15 +134,15 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
color: var(--scalar-color-1);
|
||||
font-size: var(--scalar-small);
|
||||
font-weight: var(--scalar-semibold);
|
||||
color: var(--theme-color-1);
|
||||
font-size: var(--theme-small);
|
||||
font-weight: var(--theme-semibold);
|
||||
}
|
||||
.scalar-api-client__close:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.scalar .scalar-app {
|
||||
background: var(--scalar-background-3);
|
||||
background: var(--theme-background-3);
|
||||
height: calc(100dvh - 100px);
|
||||
max-width: 1280px;
|
||||
width: 100%;
|
||||
@@ -235,10 +227,10 @@
|
||||
line-height: normal;
|
||||
}
|
||||
.scalar .scalar-app-header span {
|
||||
color: var(--scalar-color-3);
|
||||
color: var(--theme-color-3);
|
||||
}
|
||||
.scalar .scalar-app-header a {
|
||||
color: var(--scalar-color-1);
|
||||
color: var(--theme-color-1);
|
||||
}
|
||||
.scalar .scalar-app-header a:hover {
|
||||
text-decoration: underline;
|
||||
@@ -259,7 +251,7 @@
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
color: var(--scalar-color-blue);
|
||||
color: var(--theme-color-blue);
|
||||
appearance: none;
|
||||
outline: none;
|
||||
border: none;
|
||||
@@ -269,7 +261,7 @@
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
.scalar-activate:hover .scalar-activate-button {
|
||||
background: var(--scalar-background-3);
|
||||
background: var(--theme-background-3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@@ -301,12 +293,12 @@
|
||||
scrollbar-color: rgba(255, 255, 255, 0.24) transparent;
|
||||
}
|
||||
.scalar .custom-scroll:hover::-webkit-scrollbar-thumb {
|
||||
background: var(--scalar-scrollbar-color, var(--default-theme-scrollbar-color));
|
||||
background: var(--theme-scrollbar-color, var(--default-theme-scrollbar-color));
|
||||
background-clip: content-box;
|
||||
border: 3px solid transparent;
|
||||
}
|
||||
.scalar .custom-scroll::-webkit-scrollbar-thumb:active {
|
||||
background: var(--scalar-scrollbar-color-active, var(--default-theme-scrollbar-color-active));
|
||||
background: var(--theme-scrollbar-color-active, var(--default-theme-scrollbar-color-active));
|
||||
background-clip: content-box;
|
||||
border: 3px solid transparent;
|
||||
}
|
||||
@@ -330,4 +322,4 @@
|
||||
.scalar .custom-scroll {
|
||||
padding-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@ import assertNever from 'assert-never';
|
||||
|
||||
import { Checkbox, Emoji } from '@/components/primitives';
|
||||
import { StyledLink } from '@/components/primitives';
|
||||
import { Image } from '@/components/utils';
|
||||
import { getNodeFragmentByName } from '@/lib/document';
|
||||
import { getSimplifiedContentType } from '@/lib/files';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { filterOutNullable } from '@/lib/typescript';
|
||||
|
||||
@@ -14,7 +12,6 @@ import { TableRecordKV } from './Table';
|
||||
import { getColumnAlignment } from './utils';
|
||||
import { BlockProps } from '../Block';
|
||||
import { Blocks } from '../Blocks';
|
||||
import { FileIcon } from '../FileIcon';
|
||||
|
||||
/**
|
||||
* Render the value for a column in a record.
|
||||
@@ -128,41 +125,11 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
|
||||
return (
|
||||
<Tag className={tcls('text-base')}>
|
||||
{files.filter(filterOutNullable).map((ref, index) => {
|
||||
const contentType = ref.file
|
||||
? getSimplifiedContentType(ref.file.contentType)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<StyledLink
|
||||
key={index}
|
||||
href={ref.href}
|
||||
target="_blank"
|
||||
style={['flex', 'flex-row', 'items-center', 'gap-2']}
|
||||
>
|
||||
{contentType === 'image' ? (
|
||||
<Image
|
||||
style={['max-h-[1lh]', 'h-[1lh]']}
|
||||
alt={ref.text}
|
||||
sizes={[{ width: 24 }]}
|
||||
sources={{
|
||||
light: {
|
||||
src: ref.href,
|
||||
size: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
},
|
||||
},
|
||||
}}
|
||||
priority="lazy"
|
||||
/>
|
||||
) : (
|
||||
<FileIcon contentType={contentType} />
|
||||
)}
|
||||
{ref.text}
|
||||
</StyledLink>
|
||||
);
|
||||
})}
|
||||
{files.filter(filterOutNullable).map((file, index) => (
|
||||
<StyledLink key={index} href={file.href}>
|
||||
{file.text}
|
||||
</StyledLink>
|
||||
))}
|
||||
</Tag>
|
||||
);
|
||||
case 'content-ref': {
|
||||
|
||||
@@ -12,12 +12,14 @@ export function ViewCards(props: TableViewProps<DocumentTableViewCards>) {
|
||||
<div
|
||||
className={tcls(
|
||||
style,
|
||||
'max-w-full',
|
||||
'md:max-w-3xl',
|
||||
'inline-grid',
|
||||
'gap-4',
|
||||
'grid-cols-1',
|
||||
'min-[432px]:grid-cols-2',
|
||||
view.cardSize === 'large' ? 'md:grid-cols-2' : 'md:grid-cols-3',
|
||||
block.data.fullWidth ? 'large:flex-column' : null,
|
||||
block.data.fullWidth ? ['max-w-full', 'large:flex-column'] : null,
|
||||
)}
|
||||
>
|
||||
{records.map((record) => {
|
||||
|
||||
@@ -14,6 +14,7 @@ export function ViewGrid(props: TableViewProps<DocumentTableViewGrid>) {
|
||||
const tableWrapper = columnsOverThreshold
|
||||
? [
|
||||
// has over X columns
|
||||
'w-full',
|
||||
'overflow-x-auto',
|
||||
'overflow-y-hidden',
|
||||
'mx-auto',
|
||||
@@ -21,8 +22,32 @@ export function ViewGrid(props: TableViewProps<DocumentTableViewGrid>) {
|
||||
'border',
|
||||
'border-dark/3',
|
||||
'dark:border-light/2',
|
||||
block.data.fullWidth
|
||||
? [
|
||||
// has over X columns, and is full width
|
||||
'max-w-full',
|
||||
]
|
||||
: [
|
||||
// NOT full width, but has over X columns
|
||||
'max-w-4xl',
|
||||
],
|
||||
]
|
||||
: ['overflow-x-auto', 'overflow-y-hidden', 'mx-auto'];
|
||||
: [
|
||||
'w-full',
|
||||
'overflow-x-auto',
|
||||
'overflow-y-hidden',
|
||||
'mx-auto',
|
||||
// has under X columns
|
||||
block.data.fullWidth
|
||||
? [
|
||||
// has under X columns, and is full width
|
||||
'max-w-full',
|
||||
]
|
||||
: [
|
||||
// NOT full width, but has under X columns
|
||||
'max-w-3xl',
|
||||
],
|
||||
];
|
||||
|
||||
const tableTR = columnsOverThreshold
|
||||
? ['[&>*+*]:border-l', '[&>*]:px-4']
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export * from './DocumentView';
|
||||
export { createHighlightingContext } from './CodeBlock';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CustomizationSettings, SiteCustomizationSettings, Space } from '@gitbook/api';
|
||||
import { CustomizationSettings, Space } from '@gitbook/api';
|
||||
import React from 'react';
|
||||
|
||||
import { Image } from '@/components/utils';
|
||||
@@ -12,7 +12,7 @@ import { ThemeToggler } from '../ThemeToggler';
|
||||
export function Footer(props: {
|
||||
space: Space;
|
||||
context: ContentRefContext;
|
||||
customization: CustomizationSettings | SiteCustomizationSettings;
|
||||
customization: CustomizationSettings;
|
||||
}) {
|
||||
const { context, customization } = props;
|
||||
|
||||
|
||||
+7
-4
@@ -4,15 +4,18 @@ import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { Dropdown, DropdownChevron, DropdownMenu, DropdownMenuItem } from './Dropdown';
|
||||
|
||||
export function SpacesDropdown(props: { space: Space; spaces: Space[] }) {
|
||||
const { space, spaces } = props;
|
||||
export function CollectionSpacesDropdown(props: {
|
||||
space: Space;
|
||||
collection: Collection;
|
||||
collectionSpaces: Space[];
|
||||
}) {
|
||||
const { space, collectionSpaces } = props;
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
button={(buttonProps) => (
|
||||
<div
|
||||
{...buttonProps}
|
||||
data-testid="space-dropdown-button"
|
||||
className={tcls(
|
||||
'justify-self-start',
|
||||
'flex',
|
||||
@@ -29,7 +32,7 @@ export function SpacesDropdown(props: { space: Space; spaces: Space[] }) {
|
||||
)}
|
||||
>
|
||||
<DropdownMenu>
|
||||
{spaces.map((otherSpace) => (
|
||||
{collectionSpaces.map((otherSpace) => (
|
||||
<DropdownMenuItem
|
||||
key={otherSpace.id}
|
||||
href={otherSpace.urls.published ?? otherSpace.urls.app}
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
Collection,
|
||||
CustomizationSettings,
|
||||
Site,
|
||||
SiteCustomizationSettings,
|
||||
Space,
|
||||
} from '@gitbook/api';
|
||||
import { Collection, CustomizationSettings, Space } from '@gitbook/api';
|
||||
import React from 'react';
|
||||
|
||||
import { t } from '@/intl/server';
|
||||
@@ -19,11 +13,11 @@ import { SearchButton } from '../Search';
|
||||
*/
|
||||
export function CompactHeader(props: {
|
||||
space: Space;
|
||||
parent: Site | Collection | null;
|
||||
spaces: Space[];
|
||||
customization: CustomizationSettings | SiteCustomizationSettings;
|
||||
collection: Collection | null;
|
||||
collectionSpaces: Space[];
|
||||
customization: CustomizationSettings;
|
||||
}) {
|
||||
const { space, parent, customization } = props;
|
||||
const { space, collection, customization } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -40,7 +34,7 @@ export function CompactHeader(props: {
|
||||
)}
|
||||
>
|
||||
<div className={tcls('flex-grow-0', 'mt-5')}>
|
||||
<HeaderLogo parent={parent} space={space} customization={customization} />
|
||||
<HeaderLogo collection={collection} space={space} customization={customization} />
|
||||
</div>
|
||||
<div
|
||||
className={tcls(
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
Collection,
|
||||
CustomizationSettings,
|
||||
Site,
|
||||
SiteCustomizationSettings,
|
||||
Space,
|
||||
} from '@gitbook/api';
|
||||
import { Collection, CustomizationSettings, Space } from '@gitbook/api';
|
||||
import { CustomizationHeaderPreset } from '@gitbook/api';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
@@ -13,29 +7,26 @@ import { t, getSpaceLanguage } from '@/intl/server';
|
||||
import { ContentRefContext } from '@/lib/references';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { CollectionSpacesDropdown } from './CollectionSpacesDropdown';
|
||||
import { HeaderLink } from './HeaderLink';
|
||||
import { HeaderLinks } from './HeaderLinks';
|
||||
import { HeaderLogo } from './HeaderLogo';
|
||||
import { SpacesDropdown } from './SpacesDropdown';
|
||||
import { SearchButton } from '../Search';
|
||||
/**
|
||||
* Render the header for the space.
|
||||
*/
|
||||
export function Header(props: {
|
||||
space: Space;
|
||||
parent: Site | Collection | null;
|
||||
spaces: Space[];
|
||||
collection: Collection | null;
|
||||
collectionSpaces: Space[];
|
||||
context: ContentRefContext;
|
||||
customization: CustomizationSettings | SiteCustomizationSettings;
|
||||
customization: CustomizationSettings;
|
||||
withTopHeader?: boolean;
|
||||
}) {
|
||||
const { context, space, parent, spaces, customization, withTopHeader } = props;
|
||||
const { context, space, collection, collectionSpaces, customization, withTopHeader } = props;
|
||||
|
||||
const isCustomizationDefault =
|
||||
customization.header.preset === CustomizationHeaderPreset.Default;
|
||||
const isMultiVariants =
|
||||
parent?.object === 'collection' ||
|
||||
(parent && parent.object === 'site' && spaces.length > 1);
|
||||
|
||||
return (
|
||||
<header
|
||||
@@ -75,9 +66,19 @@ export function Header(props: {
|
||||
CONTAINER_STYLE,
|
||||
)}
|
||||
>
|
||||
<HeaderLogo parent={parent} space={space} customization={customization} />
|
||||
<HeaderLogo
|
||||
collection={collection}
|
||||
space={space}
|
||||
customization={customization}
|
||||
/>
|
||||
<span>
|
||||
{isMultiVariants ? <SpacesDropdown space={space} spaces={spaces} /> : null}
|
||||
{collection ? (
|
||||
<CollectionSpacesDropdown
|
||||
space={space}
|
||||
collection={collection}
|
||||
collectionSpaces={collectionSpaces}
|
||||
/>
|
||||
) : null}
|
||||
</span>
|
||||
<HeaderLinks>
|
||||
{customization.header.links.map((link, index) => {
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
CustomizationHeaderLink,
|
||||
CustomizationSettings,
|
||||
CustomizationHeaderPreset,
|
||||
SiteCustomizationSettings,
|
||||
} from '@gitbook/api';
|
||||
|
||||
import { ContentRefContext, resolveContentRef } from '@/lib/references';
|
||||
@@ -21,7 +20,7 @@ import { Link } from '../primitives';
|
||||
export async function HeaderLink(props: {
|
||||
context: ContentRefContext;
|
||||
link: CustomizationHeaderLink;
|
||||
customization: CustomizationSettings | SiteCustomizationSettings;
|
||||
customization: CustomizationSettings;
|
||||
}) {
|
||||
const { context, link, customization } = props;
|
||||
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import {
|
||||
Collection,
|
||||
CustomizationHeaderPreset,
|
||||
CustomizationSettings,
|
||||
Site,
|
||||
SiteCustomizationSettings,
|
||||
Space,
|
||||
} from '@gitbook/api';
|
||||
import { Collection, CustomizationHeaderPreset, CustomizationSettings, Space } from '@gitbook/api';
|
||||
|
||||
import { HeaderMobileMenu } from '@/components/Header/HeaderMobileMenu';
|
||||
import { Image } from '@/components/utils';
|
||||
@@ -15,9 +8,9 @@ import { tcls } from '@/lib/tailwind';
|
||||
import { Link } from '../primitives';
|
||||
|
||||
interface HeaderLogoProps {
|
||||
parent: Site | Collection | null;
|
||||
collection: Collection | null;
|
||||
space: Space;
|
||||
customization: CustomizationSettings | SiteCustomizationSettings;
|
||||
customization: CustomizationSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,7 +86,7 @@ export function HeaderLogo(props: HeaderLogoProps) {
|
||||
}
|
||||
|
||||
function LogoFallback(props: HeaderLogoProps) {
|
||||
const { parent, space, customization } = props;
|
||||
const { collection, space, customization } = props;
|
||||
const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null;
|
||||
|
||||
return (
|
||||
@@ -145,7 +138,7 @@ function LogoFallback(props: HeaderLogoProps) {
|
||||
: 'text-header-link',
|
||||
)}
|
||||
>
|
||||
{parent ? parent.title : customization.title ?? space.title}
|
||||
{collection ? collection.title : customization.title ?? space.title}
|
||||
</h1>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,13 +2,7 @@ import { Menu } from '@geist-ui/icons';
|
||||
import DownloadCloud from '@geist-ui/icons/downloadCloud';
|
||||
import Github from '@geist-ui/icons/github';
|
||||
import Gitlab from '@geist-ui/icons/gitlab';
|
||||
import {
|
||||
CustomizationSettings,
|
||||
JSONDocument,
|
||||
RevisionPageDocument,
|
||||
SiteCustomizationSettings,
|
||||
Space,
|
||||
} from '@gitbook/api';
|
||||
import { CustomizationSettings, JSONDocument, RevisionPageDocument, Space } from '@gitbook/api';
|
||||
import React from 'react';
|
||||
import urlJoin from 'url-join';
|
||||
|
||||
@@ -27,7 +21,7 @@ import { PageFeedbackForm } from '../PageFeedback';
|
||||
*/
|
||||
export async function PageAside(props: {
|
||||
space: Space;
|
||||
customization: CustomizationSettings | SiteCustomizationSettings;
|
||||
customization: CustomizationSettings;
|
||||
page: RevisionPageDocument;
|
||||
document: JSONDocument | null;
|
||||
context: ContentRefContext;
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
CustomizationSettings,
|
||||
JSONDocument,
|
||||
RevisionPageDocument,
|
||||
SiteCustomizationSettings,
|
||||
Space,
|
||||
} from '@gitbook/api';
|
||||
import { CustomizationSettings, JSONDocument, RevisionPageDocument, Space } from '@gitbook/api';
|
||||
import React from 'react';
|
||||
|
||||
import { getSpaceLanguage } from '@/intl/server';
|
||||
@@ -19,14 +13,14 @@ import { PageCover } from './PageCover';
|
||||
import { PageFooterNavigation } from './PageFooterNavigation';
|
||||
import { PageHeader } from './PageHeader';
|
||||
import { TrackPageView } from './TrackPageView';
|
||||
import { DocumentView, createHighlightingContext } from '../DocumentView';
|
||||
import { DocumentView } from '../DocumentView';
|
||||
import { PageFeedbackForm } from '../PageFeedback';
|
||||
import { DateRelative } from '../primitives';
|
||||
|
||||
export function PageBody(props: {
|
||||
space: Space;
|
||||
contentTarget: ContentTarget;
|
||||
customization: CustomizationSettings | SiteCustomizationSettings;
|
||||
customization: CustomizationSettings;
|
||||
page: RevisionPageDocument;
|
||||
document: JSONDocument | null;
|
||||
context: ContentRefContext;
|
||||
@@ -38,7 +32,6 @@ export function PageBody(props: {
|
||||
const asFullWidth = document ? hasFullWidthBlock(document) : false;
|
||||
const language = getSpaceLanguage(customization);
|
||||
const updatedAt = page.updatedAt ?? page.createdAt;
|
||||
const shouldHighlightCode = createHighlightingContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -80,7 +73,6 @@ export function PageBody(props: {
|
||||
contentRefContext: context,
|
||||
resolveContentRef: (ref, options) =>
|
||||
resolveContentRef(ref, context, options),
|
||||
shouldHighlightCode,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import ChevronLeft from '@geist-ui/icons/chevronLeft';
|
||||
import ChevronRight from '@geist-ui/icons/chevronRight';
|
||||
import {
|
||||
CustomizationSettings,
|
||||
Revision,
|
||||
RevisionPageDocument,
|
||||
SiteCustomizationSettings,
|
||||
Space,
|
||||
} from '@gitbook/api';
|
||||
import { CustomizationSettings, Revision, RevisionPageDocument, Space } from '@gitbook/api';
|
||||
import React from 'react';
|
||||
|
||||
import { t, getSpaceLanguage } from '@/intl/server';
|
||||
@@ -21,7 +15,7 @@ import { Link } from '../primitives';
|
||||
*/
|
||||
export function PageFooterNavigation(props: {
|
||||
space: Space;
|
||||
customization: CustomizationSettings | SiteCustomizationSettings;
|
||||
customization: CustomizationSettings;
|
||||
pages: Revision['pages'];
|
||||
page: RevisionPageDocument;
|
||||
}) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import IconSearch from '@geist-ui/icons/search';
|
||||
import { Collection, Site } from '@gitbook/api';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React from 'react';
|
||||
@@ -21,7 +20,7 @@ interface SearchModalProps {
|
||||
spaceId: string;
|
||||
revisionId: string;
|
||||
spaceTitle: string;
|
||||
parent: Site | Collection | null;
|
||||
collectionId: string | null;
|
||||
withAsk: boolean;
|
||||
}
|
||||
|
||||
@@ -136,8 +135,16 @@ function SearchModalBody(
|
||||
onClose: (to?: string) => void;
|
||||
},
|
||||
) {
|
||||
const { spaceId, revisionId, spaceTitle, withAsk, parent, state, onChangeQuery, onClose } =
|
||||
props;
|
||||
const {
|
||||
spaceId,
|
||||
revisionId,
|
||||
spaceTitle,
|
||||
withAsk,
|
||||
collectionId,
|
||||
state,
|
||||
onChangeQuery,
|
||||
onClose,
|
||||
} = props;
|
||||
|
||||
const language = useLanguage();
|
||||
const resultsRef = React.useRef<SearchResultsRef>(null);
|
||||
@@ -237,7 +244,7 @@ function SearchModalBody(
|
||||
ref={resultsRef}
|
||||
spaceId={spaceId}
|
||||
revisionId={revisionId}
|
||||
parent={state.global ? parent : null}
|
||||
collectionId={state.global ? collectionId : null}
|
||||
query={state.query}
|
||||
withAsk={withAsk}
|
||||
onSwitchToAsk={() => {
|
||||
@@ -249,7 +256,7 @@ function SearchModalBody(
|
||||
}}
|
||||
onClose={onClose}
|
||||
>
|
||||
{parent && state.query ? (
|
||||
{collectionId && state.query ? (
|
||||
<SearchScopeToggle spaceTitle={spaceTitle} />
|
||||
) : null}
|
||||
</SearchResults>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Collection, Site } from '@gitbook/api';
|
||||
import assertNever from 'assert-never';
|
||||
import React from 'react';
|
||||
|
||||
@@ -12,7 +11,7 @@ import { SearchSectionResultItem } from './SearchSectionResultItem';
|
||||
import {
|
||||
getRecommendedQuestions,
|
||||
OrderedComputedResult,
|
||||
searchParentContent,
|
||||
searchCollectionContent,
|
||||
searchSpaceContent,
|
||||
} from './server-actions';
|
||||
import { Loading } from '../primitives';
|
||||
@@ -40,14 +39,15 @@ export const SearchResults = React.forwardRef(function SearchResults(
|
||||
query: string;
|
||||
spaceId: string;
|
||||
revisionId: string;
|
||||
parent: Site | Collection | null;
|
||||
collectionId: string | null;
|
||||
withAsk: boolean;
|
||||
onSwitchToAsk: () => void;
|
||||
onClose: (to?: string) => void;
|
||||
},
|
||||
ref: React.Ref<SearchResultsRef>,
|
||||
) {
|
||||
const { children, query, spaceId, revisionId, parent, withAsk, onSwitchToAsk, onClose } = props;
|
||||
const { children, query, spaceId, revisionId, collectionId, withAsk, onSwitchToAsk, onClose } =
|
||||
props;
|
||||
|
||||
const language = useLanguage();
|
||||
const debounceTimeout = React.useRef<NodeJS.Timeout | null>(null);
|
||||
@@ -94,8 +94,8 @@ export const SearchResults = React.forwardRef(function SearchResults(
|
||||
debounceTimeout.current = setTimeout(async () => {
|
||||
setCursor(null);
|
||||
|
||||
const fetchedResults = await (parent
|
||||
? searchParentContent(parent, query)
|
||||
const fetchedResults = await (collectionId
|
||||
? searchCollectionContent(collectionId, query)
|
||||
: searchSpaceContent(spaceId, revisionId, query));
|
||||
setResults(withAsk ? withQuestionResult(fetchedResults, query) : fetchedResults);
|
||||
}, 250);
|
||||
@@ -107,7 +107,7 @@ export const SearchResults = React.forwardRef(function SearchResults(
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [query, spaceId, revisionId, parent, withAsk]);
|
||||
}, [query, spaceId, revisionId, collectionId, withAsk]);
|
||||
|
||||
// Scroll to the active result.
|
||||
React.useEffect(() => {
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
'use server';
|
||||
|
||||
import {
|
||||
Collection,
|
||||
RevisionPage,
|
||||
SearchAIAnswer,
|
||||
SearchPageResult,
|
||||
Site,
|
||||
Space,
|
||||
} from '@gitbook/api';
|
||||
import { headers } from 'next/headers';
|
||||
import { RevisionPage, SearchAIAnswer, SearchPageResult, Space } from '@gitbook/api';
|
||||
|
||||
import { getContentPointer } from '@/app/(space)/fetch';
|
||||
import { streamResponse } from '@/lib/actions';
|
||||
import * as api from '@/lib/api';
|
||||
import { absoluteHref, pageHref } from '@/lib/links';
|
||||
@@ -64,41 +55,20 @@ export async function searchSpaceContent(
|
||||
}
|
||||
|
||||
/**
|
||||
* Server action to search content in a parent (site or collection)
|
||||
* Server action to search content in a collection
|
||||
*/
|
||||
export async function searchParentContent(
|
||||
parent: Site | Collection,
|
||||
export async function searchCollectionContent(
|
||||
collectionId: string,
|
||||
query: string,
|
||||
): Promise<OrderedComputedResult[]> {
|
||||
const pointer = getContentPointer();
|
||||
|
||||
const [data, collectionSpaces, siteSpaces] = await Promise.all([
|
||||
api.searchParentContent(parent.id, query),
|
||||
parent.object === 'collection' ? api.getCollectionSpaces(parent.id) : null,
|
||||
parent.object === 'site' && 'organizationId' in pointer
|
||||
? api.getSiteSpaces(pointer.organizationId, parent.id)
|
||||
: null,
|
||||
const [data, collectionSpaces] = await Promise.all([
|
||||
api.searchCollectionContent(collectionId, query),
|
||||
api.getCollectionSpaces(collectionId),
|
||||
]);
|
||||
|
||||
let spaces: Space[] = [];
|
||||
|
||||
if (collectionSpaces) {
|
||||
spaces = collectionSpaces;
|
||||
} else if (siteSpaces) {
|
||||
spaces = Object.values(
|
||||
siteSpaces.reduce(
|
||||
(acc, siteSpace) => {
|
||||
acc[siteSpace.space.id] = siteSpace.space;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, Space>,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return data.items
|
||||
.map((spaceItem) => {
|
||||
const space = spaces.find((space) => space.id === spaceItem.id);
|
||||
const space = collectionSpaces.find((space) => space.id === spaceItem.id);
|
||||
return spaceItem.pages.map((item) => transformPageResult(item, space));
|
||||
})
|
||||
.flat(2);
|
||||
@@ -161,7 +131,6 @@ function transformAnswer(
|
||||
mode: 'default',
|
||||
contentRefContext: null,
|
||||
resolveContentRef: async () => null,
|
||||
shouldHighlightCode: () => false,
|
||||
}}
|
||||
style={['space-y-5']}
|
||||
/>
|
||||
|
||||
@@ -5,8 +5,6 @@ import {
|
||||
Revision,
|
||||
RevisionPageDocument,
|
||||
RevisionPageGroup,
|
||||
Site,
|
||||
SiteCustomizationSettings,
|
||||
Space,
|
||||
} from '@gitbook/api';
|
||||
import React from 'react';
|
||||
@@ -28,9 +26,9 @@ export function SpaceLayout(props: {
|
||||
content: ContentPointer;
|
||||
contentTarget: ContentTarget;
|
||||
space: Space;
|
||||
parent: Site | Collection | null;
|
||||
spaces: Space[];
|
||||
customization: CustomizationSettings | SiteCustomizationSettings;
|
||||
collection: Collection | null;
|
||||
collectionSpaces: Space[];
|
||||
customization: CustomizationSettings;
|
||||
pages: Revision['pages'];
|
||||
ancestors: Array<RevisionPageDocument | RevisionPageGroup>;
|
||||
children: React.ReactNode;
|
||||
@@ -38,8 +36,8 @@ export function SpaceLayout(props: {
|
||||
const {
|
||||
space,
|
||||
contentTarget,
|
||||
parent,
|
||||
spaces,
|
||||
collection,
|
||||
collectionSpaces,
|
||||
content,
|
||||
pages,
|
||||
customization,
|
||||
@@ -61,8 +59,8 @@ export function SpaceLayout(props: {
|
||||
<Header
|
||||
withTopHeader={withTopHeader}
|
||||
space={space}
|
||||
parent={parent}
|
||||
spaces={spaces}
|
||||
collection={collection}
|
||||
collectionSpaces={collectionSpaces}
|
||||
context={contentRefContext}
|
||||
customization={customization}
|
||||
/>
|
||||
@@ -91,8 +89,8 @@ export function SpaceLayout(props: {
|
||||
withTopHeader ? null : (
|
||||
<CompactHeader
|
||||
space={space}
|
||||
parent={parent}
|
||||
spaces={spaces}
|
||||
collection={collection}
|
||||
collectionSpaces={collectionSpaces}
|
||||
customization={customization}
|
||||
/>
|
||||
)
|
||||
@@ -116,7 +114,7 @@ export function SpaceLayout(props: {
|
||||
revisionId={contentTarget.revisionId}
|
||||
spaceTitle={customization.title ?? space.title}
|
||||
withAsk={customization.aiSearch.enabled}
|
||||
parent={parent && spaces.length > 1 ? parent : null}
|
||||
collectionId={collection && collectionSpaces.length > 1 ? collection.id : null}
|
||||
/>
|
||||
</React.Suspense>
|
||||
</>
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
Revision,
|
||||
RevisionPageDocument,
|
||||
RevisionPageGroup,
|
||||
SiteCustomizationSettings,
|
||||
Space,
|
||||
} from '@gitbook/api';
|
||||
import React from 'react';
|
||||
@@ -17,7 +16,7 @@ import { Trademark } from './Trademark';
|
||||
|
||||
export function TableOfContents(props: {
|
||||
space: Space;
|
||||
customization: CustomizationSettings | SiteCustomizationSettings;
|
||||
customization: CustomizationSettings;
|
||||
content: ContentPointer;
|
||||
context: ContentRefContext;
|
||||
pages: Revision['pages'];
|
||||
@@ -75,7 +74,7 @@ export function TableOfContents(props: {
|
||||
'dark:group-hover:[&::-webkit-scrollbar-thumb]:bg-light/3',
|
||||
'navigation-open:flex', // can be auto height animated as such https://stackoverflow.com/a/76944290
|
||||
'lg:-ml-5',
|
||||
customization.trademark.enabled ? 'lg:pb-20' : 'lg:pb-4',
|
||||
customization.trademark.enabled ? 'lg:pb-16' : 'lg:pb-4',
|
||||
)}
|
||||
>
|
||||
<PagesList
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CustomizationSettings, SiteCustomizationSettings, Space } from '@gitbook/api';
|
||||
import { CustomizationSettings, Space } from '@gitbook/api';
|
||||
|
||||
import { t, getSpaceLanguage } from '@/intl/server';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
@@ -8,10 +8,7 @@ import { IconLogo } from '../icons/IconLogo';
|
||||
/**
|
||||
* Trademark link to the GitBook.
|
||||
*/
|
||||
export function Trademark(props: {
|
||||
space: Space;
|
||||
customization: CustomizationSettings | SiteCustomizationSettings;
|
||||
}) {
|
||||
export function Trademark(props: { space: Space; customization: CustomizationSettings }) {
|
||||
return (
|
||||
<div
|
||||
className={tcls(
|
||||
@@ -57,10 +54,7 @@ export function Trademark(props: {
|
||||
/**
|
||||
* Trademark link to the GitBook.
|
||||
*/
|
||||
export function TrademarkLink(props: {
|
||||
space: Space;
|
||||
customization: CustomizationSettings | SiteCustomizationSettings;
|
||||
}) {
|
||||
export function TrademarkLink(props: { space: Space; customization: CustomizationSettings }) {
|
||||
const { space, customization } = props;
|
||||
const language = getSpaceLanguage(customization);
|
||||
|
||||
@@ -84,20 +78,16 @@ export function TrademarkLink(props: {
|
||||
'flex-row',
|
||||
'items-center',
|
||||
'hover:bg-dark/1',
|
||||
'bg-light',
|
||||
'dark:bg-dark',
|
||||
'px-4',
|
||||
'py-4',
|
||||
'rounded-lg',
|
||||
'py-2',
|
||||
'rounded-md',
|
||||
'straight-corners:rounded-none',
|
||||
'hover:backdrop-blur-sm',
|
||||
'lg:ring-0',
|
||||
'tracking-[-0.016em]',
|
||||
'dark:hover:bg-light/1',
|
||||
'dark:ring-light/1',
|
||||
'border',
|
||||
'border-dark/2',
|
||||
'dark:border-light/2',
|
||||
'dark:font-normal',
|
||||
)}
|
||||
>
|
||||
<IconLogo className={tcls('w-5', 'h-5', 'mr-3')} />
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { ClassValue, tcls } from '@/lib/tailwind';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { Link, LinkProps } from '../primitives/Link';
|
||||
|
||||
/**
|
||||
* Styled version of Link component.
|
||||
*/
|
||||
export function StyledLink(props: Omit<LinkProps, 'style'> & { style?: ClassValue }) {
|
||||
const { style, ...rest } = props;
|
||||
|
||||
export function StyledLink(props: LinkProps) {
|
||||
return (
|
||||
<Link
|
||||
{...rest}
|
||||
{...props}
|
||||
className={tcls(
|
||||
'underline',
|
||||
'underline-offset-2',
|
||||
@@ -18,7 +16,6 @@ export function StyledLink(props: Omit<LinkProps, 'style'> & { style?: ClassValu
|
||||
'text-primary',
|
||||
'hover:text-primary-700',
|
||||
'transition-colors',
|
||||
style,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
|
||||
+2
-4
@@ -1,4 +1,4 @@
|
||||
import { CustomizationSettings, SiteCustomizationSettings } from '@gitbook/api';
|
||||
import { CustomizationSettings } from '@gitbook/api';
|
||||
|
||||
import { languages, TranslationLanguage } from './translations';
|
||||
|
||||
@@ -7,9 +7,7 @@ export * from './translate';
|
||||
/**
|
||||
* Create the translation context for a space to use in the server components.
|
||||
*/
|
||||
export function getSpaceLanguage(
|
||||
customization: CustomizationSettings | SiteCustomizationSettings,
|
||||
): TranslationLanguage {
|
||||
export function getSpaceLanguage(customization: CustomizationSettings): TranslationLanguage {
|
||||
const fallback = languages.en;
|
||||
|
||||
const { locale } = customization.internationalization;
|
||||
|
||||
+5
-260
@@ -9,10 +9,8 @@ import {
|
||||
HttpResponse,
|
||||
List,
|
||||
PublishedContentLookup,
|
||||
PublishedSiteContentLookup,
|
||||
RequestRenderIntegrationUI,
|
||||
RevisionFile,
|
||||
SiteCustomizationSettings,
|
||||
} from '@gitbook/api';
|
||||
import assertNever from 'assert-never';
|
||||
import { headers } from 'next/headers';
|
||||
@@ -37,18 +35,6 @@ export interface ContentPointer {
|
||||
revisionId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pointer to a relative content, it might change overtime, the pointer is relative in the content history.
|
||||
*/
|
||||
export interface SiteContentPointer extends ContentPointer {
|
||||
organizationId: string;
|
||||
siteId: string;
|
||||
/**
|
||||
* ID of the siteSpace can be undefined when rendering in multi-id mode (for site previews)
|
||||
*/
|
||||
siteSpaceId: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pointer to a content that is immutable, it will never change.
|
||||
*/
|
||||
@@ -123,7 +109,7 @@ export function withAPI<T>(client: GitBookAPI, fn: () => Promise<T>): Promise<T>
|
||||
}
|
||||
|
||||
export type PublishedContentWithCache =
|
||||
| ((PublishedContentLookup | PublishedSiteContentLookup) & {
|
||||
| (PublishedContentLookup & {
|
||||
cacheMaxAge?: number;
|
||||
cacheTags?: string[];
|
||||
})
|
||||
@@ -595,240 +581,6 @@ export const getDocument = cache(
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Get the customization settings for a site-space from the API.
|
||||
*/
|
||||
const getSiteSpaceCustomizationFromAPI = cache(
|
||||
'api.getSiteSpaceCustomizationById',
|
||||
async (
|
||||
organizationId: string,
|
||||
siteId: string,
|
||||
siteSpaceId: string,
|
||||
options: CacheFunctionOptions,
|
||||
) => {
|
||||
const response = await api().orgs.getSiteSpaceCustomizationById(
|
||||
organizationId,
|
||||
siteId,
|
||||
siteSpaceId,
|
||||
{
|
||||
signal: options.signal,
|
||||
...noCacheFetchOptions,
|
||||
},
|
||||
);
|
||||
return cacheResponse(response, {
|
||||
revalidateBefore: 60 * 60,
|
||||
tags: [
|
||||
getAPICacheTag({
|
||||
tag: 'site',
|
||||
site: siteId,
|
||||
}),
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Get the customization settings for a site from the API.
|
||||
*/
|
||||
const getSiteCustomizationFromAPI = cache(
|
||||
'api.getSiteCustomizationById',
|
||||
async (organizationId: string, siteId: string, options: CacheFunctionOptions) => {
|
||||
const response = await api().orgs.getSiteCustomizationById(organizationId, siteId, {
|
||||
signal: options.signal,
|
||||
...noCacheFetchOptions,
|
||||
});
|
||||
return cacheResponse(response, {
|
||||
revalidateBefore: 60 * 60,
|
||||
tags: [
|
||||
getAPICacheTag({
|
||||
tag: 'site',
|
||||
site: siteId,
|
||||
}),
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Get the customization settings for a site space from the API.
|
||||
*/
|
||||
async function getSiteSpaceCustomization(args: {
|
||||
organizationId: string;
|
||||
siteId: string;
|
||||
siteSpaceId: string;
|
||||
}): Promise<SiteCustomizationSettings> {
|
||||
const headersList = headers();
|
||||
const raw = await getSiteSpaceCustomizationFromAPI(
|
||||
args.organizationId,
|
||||
args.siteId,
|
||||
args.siteSpaceId,
|
||||
);
|
||||
|
||||
const extend = headersList.get('x-gitbook-customization');
|
||||
if (extend) {
|
||||
try {
|
||||
const parsed = rison.decode_object<Partial<SiteCustomizationSettings>>(extend);
|
||||
return { ...raw, ...parsed };
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to parse x-gitbook-customization header (ignored): ${
|
||||
(error as Error).stack ?? (error as Error).message ?? error
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the customization settings for a site space from the API.
|
||||
*/
|
||||
async function getSiteCustomization(args: {
|
||||
organizationId: string;
|
||||
siteId: string;
|
||||
}): Promise<SiteCustomizationSettings> {
|
||||
const headersList = headers();
|
||||
const raw = await getSiteCustomizationFromAPI(args.organizationId, args.siteId);
|
||||
|
||||
const extend = headersList.get('x-gitbook-customization');
|
||||
if (extend) {
|
||||
try {
|
||||
const parsed = rison.decode_object<Partial<SiteCustomizationSettings>>(extend);
|
||||
return { ...raw, ...parsed };
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to parse x-gitbook-customization header (ignored): ${
|
||||
(error as Error).stack ?? (error as Error).message ?? error
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the infos about a site by its ID.
|
||||
*/
|
||||
export const getSite = cache(
|
||||
'api.getSite',
|
||||
async (organizationId: string, siteId: string, options: CacheFunctionOptions) => {
|
||||
const response = await api().orgs.getSiteById(organizationId, siteId, {
|
||||
...noCacheFetchOptions,
|
||||
signal: options.signal,
|
||||
});
|
||||
return cacheResponse(response, {
|
||||
revalidateBefore: 60 * 60,
|
||||
tags: [getAPICacheTag({ tag: 'site', site: siteId })],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* List all the site-spaces variants published in a site.
|
||||
*/
|
||||
export const getSiteSpaces = cache(
|
||||
'api.getSiteSpaces',
|
||||
async (organizationId: string, siteId: string, options: CacheFunctionOptions) => {
|
||||
const response = await getAll((params) =>
|
||||
api().orgs.listSiteSpaces(organizationId, siteId, params, {
|
||||
...noCacheFetchOptions,
|
||||
signal: options.signal,
|
||||
}),
|
||||
);
|
||||
|
||||
return cacheResponse(response, {
|
||||
revalidateBefore: 60 * 60,
|
||||
data: response.data.items.map((siteSpace) => siteSpace),
|
||||
tags: [getAPICacheTag({ tag: 'site', site: siteId })],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* List the scripts to load for the site.
|
||||
*/
|
||||
export const getSiteIntegrationScripts = cache(
|
||||
'api.getSiteIntegrationScripts',
|
||||
async (organizationId: string, siteId: string, options: CacheFunctionOptions) => {
|
||||
const response = await api().orgs.listSiteIntegrationScripts(organizationId, siteId, {
|
||||
...noCacheFetchOptions,
|
||||
signal: options.signal,
|
||||
});
|
||||
return cacheResponse(response, {
|
||||
revalidateBefore: 60 * 60,
|
||||
tags: [getAPICacheTag({ tag: 'site', site: siteId })],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Fetch all the data to render the current site at once.
|
||||
*/
|
||||
export async function getCurrentSiteData(pointer: SiteContentPointer) {
|
||||
const [{ space, pages, contentTarget }, { customization, scripts }] = await Promise.all([
|
||||
getSpaceData(pointer),
|
||||
getCurrentSiteLayoutData(pointer),
|
||||
]);
|
||||
|
||||
return {
|
||||
space,
|
||||
pages,
|
||||
contentTarget,
|
||||
customization,
|
||||
scripts,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the layout data about the current site at once.
|
||||
*/
|
||||
export async function getCurrentSiteLayoutData(args: {
|
||||
organizationId: string;
|
||||
siteId: string;
|
||||
siteSpaceId: string | undefined;
|
||||
}) {
|
||||
const [customization, scripts] = await Promise.all([
|
||||
args.siteSpaceId
|
||||
? getSiteSpaceCustomization({
|
||||
organizationId: args.organizationId,
|
||||
siteId: args.siteId,
|
||||
siteSpaceId: args.siteSpaceId,
|
||||
})
|
||||
: getSiteCustomization({
|
||||
organizationId: args.organizationId,
|
||||
siteId: args.siteId,
|
||||
}),
|
||||
getSiteIntegrationScripts(args.organizationId, args.siteId),
|
||||
]);
|
||||
|
||||
return {
|
||||
customization,
|
||||
scripts,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the customization settings for the current site from the API.
|
||||
*/
|
||||
export async function getCurrentSiteCustomization(args: {
|
||||
organizationId: string;
|
||||
siteId: string;
|
||||
siteSpaceId: string | undefined;
|
||||
}): Promise<SiteCustomizationSettings> {
|
||||
return args.siteSpaceId
|
||||
? getSiteSpaceCustomization({
|
||||
organizationId: args.organizationId,
|
||||
siteId: args.siteId,
|
||||
siteSpaceId: args.siteSpaceId,
|
||||
})
|
||||
: getSiteCustomization({
|
||||
organizationId: args.organizationId,
|
||||
siteId: args.siteId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the customization settings for a space from the API.
|
||||
*/
|
||||
@@ -1000,11 +752,11 @@ export const searchSpaceContent = cache(
|
||||
);
|
||||
|
||||
/**
|
||||
* Search content accross all spaces in a parent (site or collection).
|
||||
* Search content accross all spaces in a collection.
|
||||
*/
|
||||
export const searchParentContent = cache(
|
||||
'api.searchParentContent',
|
||||
async (parentId: string, query: string, options: CacheFunctionOptions) => {
|
||||
export const searchCollectionContent = cache(
|
||||
'api.searchCollectionContent',
|
||||
async (collectionId: string, query: string, options: CacheFunctionOptions) => {
|
||||
const response = await api().search.searchContent(
|
||||
{ query },
|
||||
{
|
||||
@@ -1082,11 +834,6 @@ export function getAPICacheTag(
|
||||
| {
|
||||
tag: 'synced-block';
|
||||
syncedBlock: string;
|
||||
}
|
||||
// All data related to a site
|
||||
| {
|
||||
tag: 'site';
|
||||
site: string;
|
||||
},
|
||||
): string {
|
||||
switch (spec.tag) {
|
||||
@@ -1098,8 +845,6 @@ export function getAPICacheTag(
|
||||
return `collection:${spec.collection}`;
|
||||
case 'synced-block':
|
||||
return `synced-block:${spec.syncedBlock}`;
|
||||
case 'site':
|
||||
return `site:${spec.site}`;
|
||||
default:
|
||||
assertNever(spec);
|
||||
}
|
||||
|
||||
Vendored
+9
@@ -108,12 +108,14 @@ export const cloudflareKVCache: CacheBackend = {
|
||||
|
||||
const kv = await getKVNamespace();
|
||||
if (!kv) {
|
||||
console.log('no kv');
|
||||
return result;
|
||||
}
|
||||
|
||||
const pendingDeletions: Array<Promise<unknown>> = [];
|
||||
|
||||
const iterateKVPage = async (prefix: string, cursor: string | null, max: number = 3) => {
|
||||
console.log(`iterateKVPage prefix=${prefix} cursor=${cursor} max=${max}`)
|
||||
const entries = await kv.list<KVTagMetadata>({
|
||||
prefix,
|
||||
cursor,
|
||||
@@ -125,15 +127,20 @@ export const cloudflareKVCache: CacheBackend = {
|
||||
const metadata = entry.metadata;
|
||||
const key = metadata.meta.key;
|
||||
|
||||
console.log(`clear ${JSON.stringify(entry)}`)
|
||||
|
||||
result.metas.push(metadata.meta);
|
||||
result.keys.push(key);
|
||||
|
||||
// Delete the tag key and the value key
|
||||
pendingDeletions.push(kv.delete(getValueKey(key)));
|
||||
pendingDeletions.push(kv.delete(entry.name));
|
||||
} else {
|
||||
console.log(`entry ${JSON.stringify(entry)} has no metadata`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`cacheStatus = ${entries.cacheStatus} list_complete=${entries.list_complete}`);
|
||||
if (!entries.list_complete && max > 0) {
|
||||
await iterateKVPage(prefix, entries.cursor, max - 1);
|
||||
}
|
||||
@@ -173,9 +180,11 @@ async function getKVNamespace(): Promise<KVNamespace | null> {
|
||||
|
||||
const cloudflare = getOptionalRequestContext();
|
||||
if (cloudflare) {
|
||||
console.log(`return cloudflare.env.CACHE_KV: ${'CACHE_KV' in cloudflare.env ? !!cloudflare.env.CACHE_KV : null}`);
|
||||
// @ts-ignore
|
||||
return cloudflare.env.CACHE_KV ?? null;
|
||||
}
|
||||
|
||||
console.log(`no cloudflare :(`);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
export type SimplifiedFileType = 'image' | 'pdf' | 'archive';
|
||||
|
||||
/**
|
||||
* Get a simplified content type for the given mime type.
|
||||
*/
|
||||
export function getSimplifiedContentType(mimeType: string): SimplifiedFileType | null {
|
||||
if (mimeType.startsWith('image')) {
|
||||
return 'image';
|
||||
}
|
||||
|
||||
switch (mimeType) {
|
||||
case 'application/pdf':
|
||||
case 'application/x-pdf':
|
||||
return 'pdf';
|
||||
case 'application/zip':
|
||||
case 'application/x-7z-compressed':
|
||||
case 'application/x-zip-compressed':
|
||||
case 'application/x-tar':
|
||||
case 'application/x-rar-compressed':
|
||||
case 'application/vnd.rar':
|
||||
return 'archive';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -3,7 +3,6 @@ import 'server-only';
|
||||
import { noCacheFetchOptions } from '@/lib/cache/http';
|
||||
|
||||
import { rootUrl } from './links';
|
||||
import { getImageAPIUrl } from './urls';
|
||||
|
||||
export interface CloudflareImageJsonFormat {
|
||||
width: number;
|
||||
@@ -77,7 +76,7 @@ export async function getResizedImageURL(
|
||||
|
||||
return (options) => {
|
||||
const url = new URL('/~gitbook/image', rootUrl());
|
||||
url.searchParams.set('url', getImageAPIUrl(input));
|
||||
url.searchParams.set('url', input);
|
||||
|
||||
if (options.width) {
|
||||
url.searchParams.set('width', options.width.toString());
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { describe, it, expect } from 'bun:test';
|
||||
|
||||
import { parseMarkdown } from './markdown';
|
||||
|
||||
describe('parseMarkdown', () => {
|
||||
it('should parse a simple table', async () => {
|
||||
const result = await parseMarkdown(`## Table
|
||||
|
||||
| a | b | c | d |
|
||||
| - | :- | -: | :-: |`);
|
||||
|
||||
expect(result).toContain('<table>');
|
||||
});
|
||||
|
||||
it('should parse a complex table', async () => {
|
||||
const result =
|
||||
await parseMarkdown(`Returns information for all non-fungible tokens for an account.
|
||||
|
||||
## Ordering
|
||||
When considering NFTs, their order is governed by a combination of their numerical **token.Id** and **serialnumber** values, with **token.id** being the parent column.
|
||||
A serialnumbers value governs its order within the given token.id
|
||||
|
||||
In that regard, if a user acquired a set of NFTs in the order (2-2, 2-4 1-5, 1-1, 1-3, 3-3, 3-4), the following layouts illustrate the ordering expectations for ownership listing
|
||||
1. **All NFTs in ASC order**: 1-1, 1-3, 1-5, 2-2, 2-4, 3-3, 3-4
|
||||
2. **All NFTs in DESC order**: 3-4, 3-3, 2-4, 2-2, 1-5, 1-3, 1-1
|
||||
3. **NFTs above 1-1 in ASC order**: 1-3, 1-5, 2-2, 2-4, 3-3, 3-4
|
||||
4. **NFTs below 3-3 in ASC order**: 1-1, 1-3, 1-5, 2-2, 2-4
|
||||
5. **NFTs between 1-3 and 3-3 inclusive in DESC order**: 3-4, 3-3, 2-4, 2-2, 1-5, 1-3
|
||||
|
||||
Note: The default order for this API is currently DESC
|
||||
|
||||
## Filtering
|
||||
When filtering there are some restrictions enforced to ensure correctness and scalability.
|
||||
|
||||
**The table below defines the restrictions and support for the NFT ownership endpoint**
|
||||
|
||||
| Query Param | Comparison Operator | Support | Description | Example |
|
||||
| ------------- | ------------------- | ------- | --------------------- | ------- |
|
||||
| token.id | eq | Y | Single occurrence only. | ?token.id=X |
|
||||
| | ne | N | | |
|
||||
| | lt(e) | Y | Single occurrence only. | ?token.id=lte:X |
|
||||
| | gt(e) | Y | Single occurrence only. | ?token.id=gte:X |
|
||||
| serialnumber | eq | Y | Single occurrence only. Requires the presence of a **token.id** query | ?serialnumber=Y |
|
||||
| | ne | N | | |
|
||||
| | lt(e) | Y | Single occurrence only. Requires the presence of an **lte** or **eq** **token.id** query | ?token.id=lte:X&serialnumber=lt:Y |
|
||||
| | gt(e) | Y | Single occurrence only. Requires the presence of an **gte** or **eq** **token.id** query | ?token.id=gte:X&serialnumber=gt:Y |
|
||||
| spender.id | eq | Y | | ?spender.id=Z |
|
||||
| | ne | N | | |
|
||||
| | lt(e) | Y | | ?spender.id=lt:Z |
|
||||
| | gt(e) | Y | | ?spender.id=gt:Z |
|
||||
|
||||
Note: When searching across a range for individual NFTs a **serialnumber** with an additional **token.id** query filter must be provided.
|
||||
Both filters must be a single occurrence of **gt(e)** or **lt(e)** which provide a lower and or upper boundary for search.`);
|
||||
|
||||
expect(result).toContain('<table>');
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
import rehypeSanitize from 'rehype-sanitize';
|
||||
import rehypeStringify from 'rehype-stringify';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkParse from 'remark-parse';
|
||||
import remarkRehype from 'remark-rehype';
|
||||
import { unified } from 'unified';
|
||||
@@ -11,7 +10,6 @@ import { unified } from 'unified';
|
||||
export async function parseMarkdown(markdown: string): Promise<string> {
|
||||
const file = await unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGfm)
|
||||
.use(remarkRehype)
|
||||
.use(rehypeSanitize)
|
||||
.use(rehypeStringify)
|
||||
|
||||
+5
-11
@@ -1,4 +1,4 @@
|
||||
import { Collection, ContentVisibility, Site, SiteVisibility, Space } from '@gitbook/api';
|
||||
import { Collection, ContentVisibility, Space } from '@gitbook/api';
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
/**
|
||||
@@ -6,10 +6,10 @@ import { headers } from 'next/headers';
|
||||
*/
|
||||
export function shouldIndexSpace({
|
||||
space,
|
||||
parent,
|
||||
collection,
|
||||
}: {
|
||||
space: Space;
|
||||
parent: Site | Collection | null;
|
||||
collection: Collection | null;
|
||||
}) {
|
||||
const headerSet = headers();
|
||||
|
||||
@@ -28,18 +28,12 @@ export function shouldIndexSpace({
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parent && parent.object === 'site') {
|
||||
return shouldIndexVisibility(parent.visibility);
|
||||
}
|
||||
|
||||
if (space.visibility === ContentVisibility.InCollection) {
|
||||
return parent && parent.object === 'collection'
|
||||
? shouldIndexVisibility(parent.visibility)
|
||||
: false;
|
||||
return collection ? shouldIndexVisibility(collection.visibility) : false;
|
||||
}
|
||||
return shouldIndexVisibility(space.visibility);
|
||||
}
|
||||
|
||||
function shouldIndexVisibility(visibility: ContentVisibility | SiteVisibility) {
|
||||
function shouldIndexVisibility(visibility: ContentVisibility) {
|
||||
return visibility === ContentVisibility.Public;
|
||||
}
|
||||
|
||||
@@ -65,21 +65,3 @@ export function getPDFUrlSearchParams(
|
||||
|
||||
return searchParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Because of a bug in Cloudflare, 127.0.0.1 is replaced by localhost.
|
||||
* We protect against it by converting to a special token, and then parsing
|
||||
* the token in the image API.
|
||||
*/
|
||||
const GITBOOK_LOCALHOST_TOKEN = '$GITBOOK_LOCALHOST$';
|
||||
|
||||
/**
|
||||
* Prepare a URL for the GitBook Open Image API.
|
||||
*/
|
||||
export function getImageAPIUrl(url: string): string {
|
||||
return url.replaceAll('127.0.0.1', GITBOOK_LOCALHOST_TOKEN);
|
||||
}
|
||||
|
||||
export function parseImageAPIURL(url: string): string {
|
||||
return url.replaceAll(GITBOOK_LOCALHOST_TOKEN, '127.0.0.1');
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { it, describe, expect } from 'bun:test';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
import {
|
||||
VisitorAuthCookieValue,
|
||||
getVisitorAuthCookieName,
|
||||
getVisitorAuthCookieValue,
|
||||
getVisitorAuthToken,
|
||||
@@ -18,18 +17,14 @@ describe('getVisitorAuthToken', () => {
|
||||
const request = nextRequest('https://example.com', {
|
||||
[getVisitorAuthCookieName('/')]: { value: getVisitorAuthCookieValue('/', '123') },
|
||||
});
|
||||
const visitorAuth = getVisitorAuthToken(request, request.nextUrl);
|
||||
assertVisitorAuthCookieValue(visitorAuth);
|
||||
expect(visitorAuth.token).toEqual('123');
|
||||
expect(getVisitorAuthToken(request, request.nextUrl)).toEqual('123');
|
||||
});
|
||||
|
||||
it('should return the token from the cookie root basepath for a sub-path', () => {
|
||||
const request = nextRequest('https://example.com/hello/world', {
|
||||
[getVisitorAuthCookieName('/')]: { value: getVisitorAuthCookieValue('/', '123') },
|
||||
});
|
||||
const visitorAuth = getVisitorAuthToken(request, request.nextUrl);
|
||||
assertVisitorAuthCookieValue(visitorAuth);
|
||||
expect(visitorAuth.token).toEqual('123');
|
||||
expect(getVisitorAuthToken(request, request.nextUrl)).toEqual('123');
|
||||
});
|
||||
|
||||
it('should return the closest token from the path', () => {
|
||||
@@ -39,9 +34,7 @@ describe('getVisitorAuthToken', () => {
|
||||
value: getVisitorAuthCookieValue('/hello/', '123'),
|
||||
},
|
||||
});
|
||||
const visitorAuth = getVisitorAuthToken(request, request.nextUrl);
|
||||
assertVisitorAuthCookieValue(visitorAuth);
|
||||
expect(visitorAuth.token).toEqual('123');
|
||||
expect(getVisitorAuthToken(request, request.nextUrl)).toEqual('123');
|
||||
});
|
||||
|
||||
it('should return the token from the cookie in a collection type url', () => {
|
||||
@@ -50,9 +43,7 @@ describe('getVisitorAuthToken', () => {
|
||||
value: getVisitorAuthCookieValue('/hello/v/space1/', '123'),
|
||||
},
|
||||
});
|
||||
const visitorAuth = getVisitorAuthToken(request, request.nextUrl);
|
||||
assertVisitorAuthCookieValue(visitorAuth);
|
||||
expect(visitorAuth.token).toEqual('123');
|
||||
expect(getVisitorAuthToken(request, request.nextUrl)).toEqual('123');
|
||||
});
|
||||
|
||||
it('should return undefined if no cookie and no query param', () => {
|
||||
@@ -61,14 +52,6 @@ describe('getVisitorAuthToken', () => {
|
||||
});
|
||||
});
|
||||
|
||||
function assertVisitorAuthCookieValue(value: unknown): asserts value is VisitorAuthCookieValue {
|
||||
if (value && typeof value === 'object' && 'token' in value) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('Expected a VisitorAuthCookieValue');
|
||||
}
|
||||
|
||||
function nextRequest(url: string, cookies: Record<string, { value: string }> = {}) {
|
||||
const nextUrl = new URL(url);
|
||||
// @ts-ignore
|
||||
|
||||
+11
-20
@@ -16,10 +16,7 @@ export type VisitorAuthCookieValue = {
|
||||
* Get the visitor authentication token for the request. This token can either be in the
|
||||
* query parameters or stored as a cookie.
|
||||
*/
|
||||
export function getVisitorAuthToken(
|
||||
request: NextRequest,
|
||||
url: URL,
|
||||
): string | VisitorAuthCookieValue | undefined {
|
||||
export function getVisitorAuthToken(request: NextRequest, url: URL): string | undefined {
|
||||
return url.searchParams.get(VISITOR_AUTH_PARAM) ?? getVisitorAuthTokenFromCookies(request, url);
|
||||
}
|
||||
|
||||
@@ -71,10 +68,7 @@ function getUrlBasePathCombinations(url: URL): string[] {
|
||||
* checking all cookies for a matching "visitor authentication cookie" and returning the
|
||||
* best possible match for the current URL.
|
||||
*/
|
||||
function getVisitorAuthTokenFromCookies(
|
||||
request: NextRequest,
|
||||
url: URL,
|
||||
): VisitorAuthCookieValue | undefined {
|
||||
function getVisitorAuthTokenFromCookies(request: NextRequest, url: URL): string | undefined {
|
||||
const urlBasePaths = getUrlBasePathCombinations(url);
|
||||
// Try to find a visitor authentication token for the current URL. The request
|
||||
// for the content could be hosted on a base path like `/foo/v/bar` or `/foo` or just `/`
|
||||
@@ -96,17 +90,14 @@ function getVisitorAuthTokenFromCookies(
|
||||
function findVisitorAuthCookieForBasePath(
|
||||
request: NextRequest,
|
||||
basePath: string,
|
||||
): VisitorAuthCookieValue | undefined {
|
||||
return Array.from(request.cookies).reduce<VisitorAuthCookieValue | undefined>(
|
||||
(acc, [name, cookie]) => {
|
||||
if (name === getVisitorAuthCookieName(basePath)) {
|
||||
const value = JSON.parse(cookie.value) as VisitorAuthCookieValue;
|
||||
if (value.basePath === basePath) {
|
||||
acc = value;
|
||||
}
|
||||
): string | undefined {
|
||||
return Array.from(request.cookies).reduce<string | undefined>((acc, [name, cookie]) => {
|
||||
if (name === getVisitorAuthCookieName(basePath)) {
|
||||
const value = JSON.parse(cookie.value) as VisitorAuthCookieValue;
|
||||
if (value.basePath === basePath) {
|
||||
acc = value.token;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
return acc;
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
+19
-83
@@ -1,7 +1,6 @@
|
||||
import { GitBookAPI } from '@gitbook/api';
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import assertNever from 'assert-never';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import type { ResponseCookie } from 'next/dist/compiled/@edge-runtime/cookies';
|
||||
import { NextResponse, NextRequest } from 'next/server';
|
||||
|
||||
@@ -15,14 +14,12 @@ import {
|
||||
withAPI,
|
||||
getSpaceLayoutData,
|
||||
DEFAULT_API_ENDPOINT,
|
||||
getCurrentSiteLayoutData,
|
||||
} from '@/lib/api';
|
||||
import { race } from '@/lib/async';
|
||||
import { buildVersion } from '@/lib/build';
|
||||
import { createContentSecurityPolicyNonce, getContentSecurityPolicy } from '@/lib/csp';
|
||||
import { getURLLookupAlternatives, normalizeURL } from '@/lib/middleware';
|
||||
import {
|
||||
VisitorAuthCookieValue,
|
||||
getVisitorAuthCookieName,
|
||||
getVisitorAuthCookieValue,
|
||||
getVisitorAuthToken,
|
||||
@@ -75,14 +72,6 @@ export type LookupResult = PublishedContentWithCache & {
|
||||
cookies?: LookupCookies;
|
||||
};
|
||||
|
||||
interface ContentAPITokenPayload {
|
||||
organization: string;
|
||||
spaces: string[];
|
||||
collection?: string;
|
||||
site?: string;
|
||||
siteSpace?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware to lookup the space to render.
|
||||
* It takes as input a request with an URL, and a set of headers:
|
||||
@@ -140,10 +129,8 @@ export async function middleware(request: NextRequest) {
|
||||
// Make sure the URL is clean of any va token after a successful lookup
|
||||
// The token is stored in a cookie that is set on the redirect response
|
||||
const normalizedVA = normalizeVisitorAuthURL(normalized);
|
||||
|
||||
// We don't do this redirect when working locally as we can't write cookies reliably on localhost.
|
||||
// On localhost we just keep the jwt_token in the URL.
|
||||
if (url.hostname !== 'localhost' && normalizedVA.toString() !== normalized.toString()) {
|
||||
if (normalizedVA.toString() !== normalized.toString()) {
|
||||
console.log(`redirecting to ${normalizedVA.toString()}`);
|
||||
return writeCookies(NextResponse.redirect(normalizedVA.toString()), resolved.cookies);
|
||||
}
|
||||
|
||||
@@ -152,12 +139,13 @@ export async function middleware(request: NextRequest) {
|
||||
space: resolved.space,
|
||||
changeRequest: resolved.changeRequest,
|
||||
revision: resolved.revision,
|
||||
...('site' in resolved ? { site: resolved.site, siteSpace: resolved.siteSpace } : {}),
|
||||
});
|
||||
|
||||
// Because of how Next will encode, we need to encode ourselves the pathname before rewriting to it.
|
||||
const rewritePathname = normalizePathname(encodePathname(resolved.pathname));
|
||||
|
||||
console.log(`${request.method} (${resolved.space}) ${rewritePathname}`);
|
||||
|
||||
// Resolution might have changed the API endpoint
|
||||
apiEndpoint = resolved.apiEndpoint ?? apiEndpoint;
|
||||
|
||||
@@ -179,13 +167,7 @@ export async function middleware(request: NextRequest) {
|
||||
}),
|
||||
);
|
||||
|
||||
const { scripts } = await ('site' in resolved
|
||||
? getCurrentSiteLayoutData({
|
||||
organizationId: resolved.organization,
|
||||
siteId: resolved.site,
|
||||
siteSpaceId: resolved.siteSpace,
|
||||
})
|
||||
: getSpaceLayoutData(resolved.space));
|
||||
const { scripts } = await getSpaceLayoutData(resolved.space);
|
||||
return getContentSecurityPolicy(scripts, nonce);
|
||||
},
|
||||
);
|
||||
@@ -202,13 +184,6 @@ export async function middleware(request: NextRequest) {
|
||||
headers.set('x-gitbook-origin-basepath', originBasePath);
|
||||
headers.set('x-gitbook-basepath', joinPath(originBasePath, resolved.basePath));
|
||||
headers.set('x-gitbook-content-space', resolved.space);
|
||||
if ('site' in resolved) {
|
||||
headers.set('x-gitbook-content-organization', resolved.organization);
|
||||
headers.set('x-gitbook-content-site', resolved.site);
|
||||
if (resolved.siteSpace) {
|
||||
headers.set('x-gitbook-content-site-space', resolved.siteSpace);
|
||||
}
|
||||
}
|
||||
if (resolved.revision) {
|
||||
headers.set('x-gitbook-content-revision', resolved.revision);
|
||||
}
|
||||
@@ -246,10 +221,6 @@ export async function middleware(request: NextRequest) {
|
||||
|
||||
// Add Content Security Policy header
|
||||
response.headers.set('content-security-policy', csp);
|
||||
// Basic security headers
|
||||
response.headers.set('strict-transport-security', 'max-age=31536000');
|
||||
response.headers.set('referrer-policy', 'no-referrer-when-downgrade');
|
||||
response.headers.set('x-content-type-options', 'nosniff');
|
||||
|
||||
const isPrefetch = request.headers.has('x-middleware-prefetch');
|
||||
|
||||
@@ -486,20 +457,10 @@ async function lookupSpaceInMultiIdMode(request: NextRequest, url: URL): Promise
|
||||
};
|
||||
}
|
||||
|
||||
const { organization, site, siteSpace } = jwt.decode(apiToken) as ContentAPITokenPayload;
|
||||
const siteLookupResult =
|
||||
typeof organization === 'string' && organization && typeof site === 'string' && site
|
||||
? {
|
||||
organization,
|
||||
site,
|
||||
...(typeof siteSpace === 'string' && siteSpace ? { siteSpace } : {}),
|
||||
}
|
||||
: {};
|
||||
return {
|
||||
space: spaceId,
|
||||
changeRequest: changeRequestId,
|
||||
revision: revisionId,
|
||||
...siteLookupResult,
|
||||
basePath: normalizePathname(basePathParts.join('/')),
|
||||
pathname: normalizePathname(pathSegments.join('/')),
|
||||
apiToken,
|
||||
@@ -589,7 +550,7 @@ async function lookupSpaceInMultiPathMode(request: NextRequest, url: URL): Promi
|
||||
*/
|
||||
async function lookupSpaceByAPI(
|
||||
lookupURL: URL,
|
||||
visitorAuthToken: ReturnType<typeof getVisitorAuthToken>,
|
||||
visitorAuthToken: string | undefined,
|
||||
): Promise<LookupResult> {
|
||||
const url = stripURLSearch(lookupURL);
|
||||
const lookup = getURLLookupAlternatives(url);
|
||||
@@ -599,17 +560,9 @@ async function lookupSpaceByAPI(
|
||||
);
|
||||
|
||||
const result = await race(lookup.urls, async (alternative, { signal }) => {
|
||||
const data = await getPublishedContentByUrl(
|
||||
alternative.url,
|
||||
typeof visitorAuthToken === 'undefined'
|
||||
? undefined
|
||||
: typeof visitorAuthToken === 'string'
|
||||
? visitorAuthToken
|
||||
: visitorAuthToken.token,
|
||||
{
|
||||
signal,
|
||||
},
|
||||
);
|
||||
const data = await getPublishedContentByUrl(alternative.url, visitorAuthToken, {
|
||||
signal,
|
||||
});
|
||||
|
||||
if ('error' in data) {
|
||||
if (alternative.primary) {
|
||||
@@ -659,9 +612,6 @@ async function lookupSpaceByAPI(
|
||||
apiToken: data.apiToken,
|
||||
cacheMaxAge: data.cacheMaxAge,
|
||||
cacheTags: data.cacheTags,
|
||||
...('site' in data
|
||||
? { site: data.site, siteSpace: data.siteSpace, organization: data.organization }
|
||||
: {}),
|
||||
} as PublishedContentWithCache;
|
||||
});
|
||||
|
||||
@@ -681,36 +631,22 @@ async function lookupSpaceByAPI(
|
||||
*/
|
||||
function getLookupResultForVisitorAuth(
|
||||
basePath: string,
|
||||
visitorAuthToken: string | VisitorAuthCookieValue,
|
||||
visitorAuthToken: string,
|
||||
): Partial<LookupResult> {
|
||||
return {
|
||||
// No caching for content served with visitor auth
|
||||
cacheMaxAge: undefined,
|
||||
cacheTags: [],
|
||||
cookies: {
|
||||
/**
|
||||
* If the visitorAuthToken has been retrieved from a cookie, we set it back only
|
||||
* if the basePath matches the current one. This is to avoid setting cookie for
|
||||
* different base paths.
|
||||
*/
|
||||
...(typeof visitorAuthToken === 'string' || visitorAuthToken.basePath === basePath
|
||||
? {
|
||||
[getVisitorAuthCookieName(basePath)]: {
|
||||
value: getVisitorAuthCookieValue(
|
||||
basePath,
|
||||
typeof visitorAuthToken === 'string'
|
||||
? visitorAuthToken
|
||||
: visitorAuthToken.token,
|
||||
),
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 7 * 24 * 60 * 60,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
[getVisitorAuthCookieName(basePath)]: {
|
||||
value: getVisitorAuthCookieValue(basePath, visitorAuthToken),
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 7 * 24 * 60 * 60,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user