Compare commits

..

2 Commits

Author SHA1 Message Date
Steven Hall 7c0cd40a2d type' 2024-04-05 13:54:40 +02:00
Steven Hall 5b30483b64 Logs 2024-04-05 13:50:00 +02:00
131 changed files with 708 additions and 2990 deletions
-8
View File
@@ -1,8 +0,0 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
-11
View File
@@ -1,11 +0,0 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.0.2/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
+9 -13
View File
@@ -1,9 +1,5 @@
on: [push]
name: CI
on:
pull_request:
push:
branches:
- main
jobs:
deploy:
@@ -20,7 +16,7 @@ jobs:
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
bun-version: 1.0.33
- name: Install dependencies
run: bun install --frozen-lockfile
env:
@@ -82,7 +78,7 @@ jobs:
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
bun-version: 1.0.33
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Setup Playwright
@@ -102,7 +98,7 @@ jobs:
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
bun-version: 1.0.33
- name: Install dependencies
run: bun install --frozen-lockfile
env:
@@ -121,12 +117,12 @@ jobs:
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
bun-version: 1.0.33
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- run: bun format:check
- run: bun format --check .
lint:
runs-on: ubuntu-latest
name: Lint
@@ -136,7 +132,7 @@ jobs:
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
bun-version: 1.0.33
- name: Install dependencies
run: bun install --frozen-lockfile
env:
@@ -151,7 +147,7 @@ jobs:
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
bun-version: 1.0.33
- name: Install dependencies
run: bun install --frozen-lockfile
env:
@@ -166,7 +162,7 @@ jobs:
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
bun-version: 1.0.33
- name: Install dependencies
run: bun install --frozen-lockfile
env:
-35
View File
@@ -1,35 +0,0 @@
name: Publish
on:
push:
branches:
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
publish:
name: Publish
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v3
with:
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
fetch-depth: 0
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Create Release Pull Request or Publish to npm
id: changesets
uses: changesets/action@v1
with:
publish: npm run release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+1 -4
View File
@@ -1,4 +1 @@
.next
# Generated
packages/emoji-codepoints/index.ts
.next
+2 -8
View File
@@ -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.
@@ -119,12 +119,6 @@ GitBook wouldn't be possible without these projects:
- [Tailwind CSS](https://tailwindcss.com/)
- [Framer Motion](https://www.npmjs.com/package/framer-motion)
## Contributors
<a href="https://github.com/gitbookIO/gitbook/graphs/contributors">
<img src="https://contrib.rocks/image?repo=gitbookIO/gitbook" />
</a>
## Legacy GitBook (Deprecated)
Our previous version of GitBook and it's CLI tool are now deprecated. You can still view the old repository and it's commits on this [branch](https://github.com/GitbookIO/gitbook/tree/legacy).
BIN
View File
Binary file not shown.
+1 -165
View File
@@ -40,159 +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 Site (Navigation when switching variant)',
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/',
tests: [
{
name: 'Keep navigation path/route when switching variant (Public)',
url: 'api-multi-versions/reference/api-reference/pets',
screenshot: false,
run: async (page) => {
const spaceDrowpdown = await page.waitForSelector(
'[data-testid="space-dropdown-button"]',
);
await spaceDrowpdown.click();
// Click the second variant in the dropdown
await page
.getByRole('link', {
name: '2.0',
})
.click();
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
await page.waitForURL(
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions/v/2.0/reference/api-reference/pets?fallback=true',
);
},
},
{
name: 'Keep navigation path/route when switching variant (Share link)',
url: 'api-multi-versions-share-links/bRfQbzwsK8rbN1GRxx7K/reference/api-reference/pets',
screenshot: false,
run: async (page) => {
const spaceDrowpdown = await page.waitForSelector(
'[data-testid="space-dropdown-button"]',
);
await spaceDrowpdown.click();
// Click the second variant in the dropdown
await page
.getByRole('link', {
name: '2.0',
})
.click();
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
await page.waitForURL(
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-share-links/bRfQbzwsK8rbN1GRxx7K/v/2.0/reference/api-reference/pets?fallback=true',
);
},
},
{
name: 'Keep navigation path/route when switching variant (VA)',
screenshot: false,
url: (() => {
const privateKey = 'c26190fc-74b2-4b54-9fc7-df9941104953';
const token = jwt.sign(
{
name: 'gitbook-open-tests',
},
privateKey,
{
expiresIn: '24h',
},
);
return `api-multi-versions-va/reference/api-reference/pets?jwt_token=${token}`;
})(),
run: async (page) => {
const spaceDrowpdown = await page.waitForSelector(
'[data-testid="space-dropdown-button"]',
);
await spaceDrowpdown.click();
// Click the second variant in the dropdown
await page
.getByRole('link', {
name: '2.0',
})
.click();
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
await page.waitForURL(
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-va/v/2.0/reference/api-reference/pets?fallback=true',
);
},
},
],
},
{
name: 'GitBook',
baseUrl: 'https://docs.gitbook.com',
@@ -330,11 +177,6 @@ const testCases: TestsCase[] = [
url: 'blocks/lists',
fullPage: true,
},
{
name: 'Code',
url: 'blocks/code',
fullPage: true,
},
{
name: 'Cards',
url: 'blocks/cards',
@@ -347,7 +189,6 @@ const testCases: TestsCase[] = [
{
name: 'Embeds',
url: 'blocks/embeds',
fullPage: true,
},
{
name: 'Annotations',
@@ -363,11 +204,6 @@ const testCases: TestsCase[] = [
name: 'Page options',
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
tests: [
{
name: 'Hidden',
url: 'page-options/page-hidden',
run: waitForCookiesDialog,
},
{
name: 'With cover',
url: 'page-options/page-with-cover',
@@ -640,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 {
+1 -25
View File
@@ -9,7 +9,7 @@ module.exports = withSentryConfig(
GITBOOK_ASSETS_PREFIX: process.env.GITBOOK_ASSETS_PREFIX,
},
webpack(config, { dev, webpack }) {
webpack(config) {
config.resolve.fallback = {
...config.resolve.fallback,
@@ -19,21 +19,6 @@ module.exports = withSentryConfig(
http: false,
};
// Tree shake debug code for Sentry
// https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/tree-shaking/#tree-shaking-with-nextjs
if (!dev) {
config.plugins.push(
new webpack.DefinePlugin({
__SENTRY_DEBUG__: false,
// We always init Sentry with enableTracing: false for now, so this is useless
__SENTRY_TRACING__: false,
__RRWEB_EXCLUDE_IFRAME__: true,
__RRWEB_EXCLUDE_SHADOW_DOM__: true,
__SENTRY_EXCLUDE_REPLAY_WORKER__: true,
}),
);
}
return config;
},
@@ -54,15 +39,6 @@ module.exports = withSentryConfig(
assetPrefix: process.env.GITBOOK_ASSETS_PREFIX,
poweredByHeader: false,
images: {
remotePatterns: [
{
protocol: 'https',
hostname: '*.gitbook.io',
},
],
},
},
{
silent: true,
+4 -9
View File
@@ -11,10 +11,8 @@
"format": "prettier ./ --ignore-unknown --write",
"format:check": "prettier ./ --ignore-unknown --list-different",
"typecheck": "tsc --noEmit",
"unit": "bun test {src,packages}",
"unit": "bun test {src,packages}/**/*.test.ts",
"e2e": "playwright test",
"changeset": "changeset",
"release": "changeset publish",
"postinstall": "rm -rf ./public/~gitbook/static/mathjax@3.2.2 && mkdir -p ./public/~gitbook/static/ && cp -R node_modules/mathjax/es5 ./public/~gitbook/static/mathjax@3.2.2"
},
"workspaces": [
@@ -22,11 +20,10 @@
],
"dependencies": {
"@geist-ui/icons": "^1.0.2",
"@gitbook/api": "^0.56.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",
"@sindresorhus/fnv1a": "^3.1.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.10",
"@upstash/redis": "^1.27.1",
@@ -43,7 +40,7 @@
"memoizee": "^0.4.15",
"next": "^14.1.3",
"next-themes": "^0.2.1",
"nuqs": "^1.17.4",
"nuqs": "^1.15.4",
"object-hash": "^3.0.0",
"openapi-types": "^12.1.3",
"p-map": "^7.0.0",
@@ -54,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",
@@ -67,8 +63,7 @@
},
"devDependencies": {
"@argos-ci/playwright": "^2.0.0",
"@changesets/cli": "^2.27.7",
"@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",
-13
View File
@@ -1,13 +0,0 @@
# @gitbook/emoji-codepoints
## 0.2.0
### Minor Changes
- 57adb3e: Second release to fix publishing with changeset
## 0.1.0
### Minor Changes
- 5f8a8fe: Initial release
-1
View File
@@ -1,7 +1,6 @@
{
"name": "@gitbook/emoji-codepoints",
"description": "Optimized mapping of codepoints to the fully qualified emoji codepoints",
"version": "0.2.0",
"private": true,
"exports": "./index.ts",
"dependencies": {},
-13
View File
@@ -1,13 +0,0 @@
# @gitbook/react-contentkit
## 0.2.0
### Minor Changes
- 57adb3e: Second release to fix publishing with changeset
## 0.1.0
### Minor Changes
- 5f8a8fe: Initial release
+1 -2
View File
@@ -1,10 +1,9 @@
{
"name": "@gitbook/react-contentkit",
"version": "0.2.0",
"exports": "./src/index.ts",
"dependencies": {
"classnames": "^2.5.1",
"@gitbook/api": "^0.51.0",
"@gitbook/api": "^0.36.0",
"assert-never": "^1.2.1"
},
"peerDependencies": {
-13
View File
@@ -1,13 +0,0 @@
# @gitbook/react-math
## 0.2.0
### Minor Changes
- 57adb3e: Second release to fix publishing with changeset
## 0.1.0
### Minor Changes
- 5f8a8fe: Initial release
-1
View File
@@ -1,7 +1,6 @@
{
"name": "@gitbook/react-math",
"exports": "./src/index.ts",
"version": "0.2.0",
"dependencies": {
"object-hash": "^3.0.0"
},
-19
View File
@@ -1,19 +0,0 @@
# @gitbook/react-openapi
## 0.3.0
### Minor Changes
- bd0ca5b: Fix missing react imports
## 0.2.0
### Minor Changes
- 57adb3e: Second release to fix publishing with changeset
## 0.1.0
### Minor Changes
- 5f8a8fe: Initial release
+9 -2
View File
@@ -1,5 +1,12 @@
# `@gitbook/react-openapi`
Style-less React components to render OpenAPI operation blocks.
React components to render OpenAPI operations.
Note: The package is a TypeScript package and do not expose JS built files.
## Features
- Generate code samples for the request
- Support custom cde samples with `x-codeSamples` (Redocly syntax)
## TODO
- Support for trying out the request
+2 -3
View File
@@ -1,10 +1,9 @@
{
"name": "@gitbook/react-openapi",
"exports": "./src/index.ts",
"version": "0.3.0",
"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",
-1
View File
@@ -1,4 +1,3 @@
import * as React from 'react';
import classNames from 'classnames';
export function Markdown(props: { source: string; className?: string }) {
@@ -1,4 +1,4 @@
import * as React from 'react';
import { OpenAPIV3 } from 'openapi-types';
import { CodeSampleInput, codeSampleGenerators } from './code-samples';
import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation';
@@ -1,4 +1,3 @@
import * as React from 'react';
import classNames from 'classnames';
import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation';
@@ -1,5 +1,3 @@
import * as React from 'react';
import { OpenAPIV3 } from 'openapi-types';
import { OpenAPIRootSchema } from './OpenAPISchema';
import { noReference } from './utils';
@@ -1,4 +1,3 @@
import * as React from 'react';
import classNames from 'classnames';
import { OpenAPIV3 } from 'openapi-types';
import { OpenAPIRootSchema, OpenAPISchemaProperties } from './OpenAPISchema';
@@ -1,4 +1,3 @@
import * as React from 'react';
import { InteractiveSection } from './InteractiveSection';
import { OpenAPIOperationData } from './fetchOpenAPIOperation';
import { generateSchemaExample } from './generateSchemaExample';
@@ -63,10 +62,7 @@ export function OpenAPIResponseExample(props: {
return (
<InteractiveSection header="Response" className="openapi-response-example">
<context.CodeBlock
code={typeof example === 'string' ? example : JSON.stringify(example, null, 2)}
syntax="json"
/>
<context.CodeBlock code={JSON.stringify(example, null, 2)} syntax="json" />
</InteractiveSection>
);
}
@@ -1,4 +1,3 @@
import * as React from 'react';
import classNames from 'classnames';
import { OpenAPIV3 } from 'openapi-types';
import { noReference } from './utils';
+2 -17
View File
@@ -47,13 +47,6 @@ export function OpenAPISchemaProperty(
? null
: getSchemaAlternatives(schema, new Set(circularRefs.keys()));
const shouldDisplayExample = (schema: OpenAPIV3.SchemaObject): boolean => {
return (
typeof schema.example === 'string' ||
typeof schema.example === 'number' ||
typeof schema.example === 'boolean'
);
};
return (
<InteractiveSection
id={id}
@@ -99,11 +92,6 @@ export function OpenAPISchemaProperty(
className="openapi-schema-description"
/>
) : null}
{shouldDisplayExample(schema) ? (
<span className="openapi-schema-example">
Example: <code>{JSON.stringify(schema.example)}</code>
</span>
) : null}
</div>
}
>
@@ -182,8 +170,6 @@ export function OpenAPIRootSchema(props: {
/**
* Render a tab for an alternative schema.
* It renders directly the properties if relevant;
* for primitives, it renders the schema itself.
*/
function OpenAPISchemaAlternative(props: {
schema: OpenAPIV3.SchemaObject;
@@ -192,13 +178,12 @@ function OpenAPISchemaAlternative(props: {
}) {
const { schema, circularRefs, context } = props;
const id = useId();
const subProperties = getSchemaProperties(schema);
return (
<OpenAPISchemaProperties
id={id}
properties={subProperties ?? [{ schema }]}
circularRefs={subProperties ? new Map(circularRefs).set(schema, id) : circularRefs}
properties={getSchemaProperties(schema) ?? []}
circularRefs={new Map(circularRefs).set(schema, id)}
context={context}
/>
);
@@ -1,4 +1,3 @@
import * as React from 'react';
import { OpenAPIV3 } from 'openapi-types';
import { OpenAPIClientContext } from './types';
import { InteractiveSection } from './InteractiveSection';
@@ -1,4 +1,3 @@
import * as React from 'react';
import { OpenAPIV3 } from 'openapi-types';
import { OpenAPIServerURLVariable } from './OpenAPIServerURLVariable';
@@ -1,8 +1,8 @@
'use client';
import * as React from 'react';
import classNames from 'classnames';
import { OpenAPIV3 } from 'openapi-types';
import React from 'react';
/**
* Interactive component to show the value of a server variable and let the user change it.
@@ -1,6 +1,5 @@
'use client';
import * as React from 'react';
import { OpenAPIV3 } from 'openapi-types';
import { OpenAPIOperationData, fromJSON } from './fetchOpenAPIOperation';
@@ -49,7 +48,6 @@ export function OpenAPISpec(props: { rawData: any; context: OpenAPIClientContext
// Description of the parameter is defined at the parameter level
// we use display it if the schema doesn't override it
description: parameter.description,
example: parameter.example,
...(noReference(parameter.schema) ?? {}),
},
required: parameter.required,
+1 -10
View File
@@ -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 {
@@ -19,7 +19,7 @@ export function generateSchemaExample(
return undefined;
}
if (typeof schema.example !== 'undefined') {
if (schema.example) {
return schema.example;
}
+2 -14
View File
@@ -1,15 +1,8 @@
import {
BrowserClient,
makeFetchTransport,
defaultStackParser,
getCurrentScope,
} from '@sentry/nextjs';
import * as Sentry from '@sentry/nextjs';
const dsn = process.env.SENTRY_DSN;
if (dsn) {
// To tree shake default integrations that we don't use
// https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/tree-shaking/#tree-shaking-default-integrations
const client = new BrowserClient({
Sentry.init({
debug: false,
dsn,
integrations: [],
@@ -18,10 +11,5 @@ if (dsn) {
beforeSendTransaction: () => {
return null;
},
transport: makeFetchTransport,
stackParser: defaultStackParser,
});
getCurrentScope().setClient(client);
client.init();
}
+2 -4
View File
@@ -1,13 +1,11 @@
import { init } from '@sentry/nextjs';
import * as Sentry from '@sentry/nextjs';
const dsn = process.env.SENTRY_DSN;
if (dsn) {
init({
Sentry.init({
debug: false,
dsn,
sampleRate: 0.1,
// Disable tracing as it creates additional requests in an env where subrequests are limited.
enableTracing: false,
+2 -4
View File
@@ -1,13 +1,11 @@
import { init } from '@sentry/nextjs';
import * as Sentry from '@sentry/nextjs';
const dsn = process.env.SENTRY_DSN;
if (dsn) {
init({
Sentry.init({
debug: false,
dsn,
sampleRate: 0.1,
// Disable tracing as it creates additional requests in an env where subrequests are limited.
enableTracing: false,
+5 -22
View File
@@ -1,12 +1,6 @@
import { NextRequest } from 'next/server';
import {
verifyImageSignature,
resizeImage,
CloudflareImageOptions,
checkIsSizableImageURL,
} from '@/lib/images';
import { parseImageAPIURL } from '@/lib/urls';
import { verifyImageSignature, resizeImage, CloudflareImageOptions } from '@/lib/images';
export const runtime = 'edge';
@@ -16,32 +10,21 @@ 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');
// The current signature algorithm sets version as 1, but we need to support the older version as well
// for previously generated content. In this case, we default to version 0.
const signatureVersion = (request.nextUrl.searchParams.get('sv') as '1') || '0';
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 });
}
// Check again if the image can be sized, even though we checked when rendering the Image component
// Otherwise, it's possible to pass just any link to this endpoint and trigger HTML injection on the domain
if (!checkIsSizableImageURL(url)) {
return new Response('Invalid url parameter', { status: 400 });
}
// Verify the signature
const verified = await verifyImageSignature(url, { signature, version: signatureVersion });
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,8 +1,5 @@
'use client';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import React from 'react';
import { useScrollToHash } from '@/components/hooks';
/**
@@ -12,29 +9,5 @@ export function PageClientLayout(props: {}) {
// We use this hook in the page layout to ensure the elements for the blocks
// are rendered before we scroll to the hash.
useScrollToHash();
useStripFallbackQueryParam();
return null;
}
/**
* Strip the fallback query parameter from current URL.
*
* When the user switches variants using the space dropdown, we pass a fallback=true parameter.
* This parameter indicates that we should redirect to the root page if the path from the
* previous variant doesn't exist in the new variant. If the path does exist, no redirect occurs,
* so we need to remove the fallback parameter.
*/
function useStripFallbackQueryParam() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
React.useEffect(() => {
if (searchParams.has('fallback')) {
const params = new URLSearchParams(searchParams.toString());
params.delete('fallback');
router.push(`${pathname}?${params.toString()}${window.location.hash ?? ''}`);
}
}, [router, pathname, searchParams]);
}
@@ -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);
@@ -6,10 +6,9 @@ import React from 'react';
import { PageAside } from '@/components/PageAside';
import { PageBody, PageCover } from '@/components/PageBody';
import { PageHrefContext, absoluteHref, pageHref } from '@/lib/links';
import { getPagePath, resolveFirstDocument } from '@/lib/pages';
import { getPagePath } from '@/lib/pages';
import { ContentRefContext } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
import { getContentTitle } from '@/lib/utils';
import { PageClientLayout } from './PageClientLayout';
import { PagePathParams, fetchPageData, getPathnameParam, normalizePathname } from '../../fetch';
@@ -19,29 +18,14 @@ export const runtime = 'edge';
/**
* Fetch and render a page.
*/
export default async function Page(props: {
params: PagePathParams;
searchParams: { fallback?: string };
}) {
const { params, searchParams } = props;
export default async function Page(props: { params: PagePathParams }) {
const { params } = props;
const {
content: contentPointer,
contentTarget,
space,
parent,
customization,
pages,
page,
document,
} = await getPageDataWithFallback({
pagePathParams: params,
searchParams,
redirectOnFallback: true,
});
const linksContext: PageHrefContext = {};
const rawPathname = getPathnameParam(params);
const { contentTarget, space, customization, pages, page, document } =
await fetchPageData(params);
const linksContext: PageHrefContext = {};
if (!page) {
const pathname = normalizePathname(rawPathname);
if (pathname !== rawPathname) {
@@ -78,7 +62,6 @@ export default async function Page(props: {
<div className={tcls('flex', 'flex-row')}>
<PageBody
space={space}
contentPointer={contentPointer}
contentTarget={contentTarget}
customization={customization}
context={contentRefContext}
@@ -92,7 +75,6 @@ export default async function Page(props: {
{page.layout.outline ? (
<PageAside
space={space}
site={parent?.object === 'site' ? parent : undefined}
customization={customization}
page={page}
document={document}
@@ -121,24 +103,14 @@ export async function generateViewport({ params }: { params: PagePathParams }):
};
}
export async function generateMetadata({
params,
searchParams,
}: {
params: PagePathParams;
searchParams: { fallback?: string };
}): Promise<Metadata> {
const { space, pages, page, customization, parent } = await getPageDataWithFallback({
pagePathParams: params,
searchParams,
});
export async function generateMetadata({ params }: { params: PagePathParams }): Promise<Metadata> {
const { space, pages, page, customization, collection } = await fetchPageData(params);
if (!page) {
notFound();
}
return {
title: [page.title, getContentTitle(space, customization, parent)]
title: [page.title, customization.title ?? space.title, collection?.title]
.filter(Boolean)
.join(' | '),
description: page.description ?? '',
@@ -153,34 +125,3 @@ export async function generateMetadata({
},
};
}
/**
* Fetches the page data matching the requested pathname and fallback to root page when page is not found.
*/
async function getPageDataWithFallback(args: {
pagePathParams: PagePathParams;
searchParams: { fallback?: string };
redirectOnFallback?: boolean;
}) {
const { pagePathParams, searchParams, redirectOnFallback = false } = args;
const { pages, page: targetPage, ...otherPageData } = await fetchPageData(pagePathParams);
let page = targetPage;
const canFallback = !!searchParams.fallback;
if (!page && canFallback) {
const rootPage = resolveFirstDocument(pages, []);
if (redirectOnFallback && rootPage?.page) {
redirect(pageHref(pages, rootPage?.page));
}
page = rootPage?.page;
}
return {
...otherPageData,
pages,
page,
};
}
+7 -15
View File
@@ -8,13 +8,10 @@ import { AdminToolbar } from '@/components/AdminToolbar';
import { CookiesToast } from '@/components/Cookies';
import { LoadIntegrations } from '@/components/Integrations';
import { SpaceLayout } from '@/components/SpaceLayout';
import { api } from '@/lib/api';
import { assetsDomain } from '@/lib/assets';
import { buildVersion } from '@/lib/build';
import { getContentSecurityPolicyNonce } from '@/lib/csp';
import { absoluteHref, baseUrl } from '@/lib/links';
import { shouldIndexSpace } from '@/lib/seo';
import { getContentTitle } from '@/lib/utils';
import { ClientContexts } from './ClientContexts';
import { RocketLoaderDetector } from './RocketLoaderDetector';
@@ -35,17 +32,12 @@ export default async function ContentLayout(props: { children: React.ReactNode }
contentTarget,
customization,
pages,
parent,
spaces,
collection,
collectionSpaces,
ancestors,
scripts,
} = await fetchSpaceData();
ReactDOM.preconnect(api().endpoint);
if (assetsDomain) {
ReactDOM.preconnect(assetsDomain);
}
scripts.forEach(({ script }) => {
ReactDOM.preload(script, {
as: 'script',
@@ -64,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}
@@ -108,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: getContentTitle(space, customization, parent),
title: `${collection ? collection.title : customization.title ?? space.title}`,
generator: `GitBook (${buildVersion()})`,
metadataBase: new URL(baseUrl()),
icons: {
@@ -133,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',
};
}
+7 -10
View File
@@ -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: /`]),
];
+3 -6
View File
@@ -15,7 +15,7 @@ export const runtime = 'edge';
*/
export async function GET(req: NextRequest) {
const { pages: rootPages } = await getSpaceContentData(getContentPointer());
const pages = flattenPages(rootPages, (page) => !page.hidden);
const pages = flattenPages(rootPages);
const urls = pages.map(({ page, depth }) => {
// Decay priority with depth
const priority = Math.pow(2, -0.25 * depth);
@@ -66,16 +66,13 @@ export async function GET(req: NextRequest) {
type FlatPageEntry = { page: RevisionPageDocument; depth: number };
function flattenPages(
rootPags: RevisionPage[],
filter: (page: RevisionPageDocument) => boolean,
): FlatPageEntry[] {
function flattenPages(rootPags: RevisionPage[]): FlatPageEntry[] {
const flattenPage = (
page: RevisionPageDocument | RevisionPageGroup,
depth: number,
): FlatPageEntry[] => {
return [
...(page.type === 'document' && filter(page) ? [{ page, depth }] : []),
...(page.type === 'document' ? [{ page, depth }] : []),
...page.pages.flatMap((child) =>
child.type === 'link' ? [] : flattenPage(child, depth + 1),
),
+8 -18
View File
@@ -5,16 +5,9 @@ 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';
import { getContentTitle } from '@/lib/utils';
export const runtime = 'edge';
@@ -42,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 = getContentTitle(space, customization, parent);
const collection =
space.visibility === ContentVisibility.InCollection && space.parent
? await getCollection(space.parent)
: null;
const contentTitle = collection?.title ?? customization.title ?? space.title;
return new ImageResponse(
(
@@ -3,8 +3,6 @@ import { ImageResponse } from 'next/og';
import { NextRequest } from 'next/server';
import React from 'react';
import { getContentTitle } from '@/lib/utils';
import { PageIdParams, fetchPageData } from '../../../../fetch';
export const runtime = 'edge';
@@ -13,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) {
@@ -33,7 +31,7 @@ export async function GET(req: NextRequest, { params }: { params: PageIdParams }
}}
>
<h2 tw="text-7xl font-bold tracking-tight text-left">
{getContentTitle(space, customization, parent)}
{collection?.title ?? customization.title ?? space.title}
</h2>
<div tw="flex flex-1">
<p tw="text-4xl">{page ? page.title : 'Not found'}</p>
+11 -44
View File
@@ -4,8 +4,6 @@ import {
Revision,
RevisionPageDocument,
RevisionPageGroup,
RevisionPageLink,
SiteCustomizationSettings,
Space,
} from '@gitbook/api';
import { Metadata } from 'next';
@@ -13,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';
@@ -43,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 {
@@ -69,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);
@@ -184,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 (
@@ -231,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)}>
@@ -254,7 +238,6 @@ async function PDFPageDocument(props: {
contentRefContext: refContext,
resolveContentRef: (ref) => resolveContentRef(ref, refContext),
getId: (id) => pagePDFContainerId(page, id),
shouldHighlightCode,
}}
/>
) : null}
@@ -314,17 +297,9 @@ function selectPages(
): FlatPageEntry[] => {
return [
{ page, depth },
...page.pages.flatMap((child) => {
if (child.type === 'link') {
return [];
}
if (child.hidden) {
return [];
}
return flattenPage(child, depth + 1);
}),
...page.pages.flatMap((child) =>
child.type === 'link' ? [] : flattenPage(child, depth + 1),
),
];
};
@@ -349,16 +324,8 @@ function selectPages(
return limitTo(flattenPage(found.page, 0));
}
const allPages = rootPages.flatMap((page) => {
if (page.type === 'link') {
return [];
}
if (page.hidden) {
return [];
}
return flattenPage(page, 0);
});
const allPages = rootPages.flatMap((page) =>
page.type === 'link' ? [] : flattenPage(page, 0),
);
return limitTo(allPages);
}
+2 -2
View File
@@ -1,6 +1,6 @@
'use client';
import { captureException } from '@sentry/nextjs';
import * as Sentry from '@sentry/nextjs';
import React from 'react';
import { Button } from '@/components/primitives/Button';
@@ -15,7 +15,7 @@ export default function ErrorPage(props: {
const language = useLanguage();
React.useEffect(() => {
captureException(error);
Sentry.captureException(error);
}, [error]);
return (
+16 -86
View File
@@ -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,33 +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');
const siteShareKey = headerSet.get('x-gitbook-content-site-share-key');
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,
siteShareKey: siteShareKey ?? 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;
}
/**
@@ -70,21 +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({
organizationId: content.organizationId,
siteId: content.siteId,
siteShareKey: content.siteShareKey,
}),
]
: [getSpaceData(content)],
);
const parent = await (parentSite ?? fetchParentCollection(space));
const { space, contentTarget, pages, customization, scripts } = await getSpaceData(content);
const collection = await fetchParentCollection(space);
return {
content,
@@ -94,7 +57,7 @@ export async function fetchSpaceData() {
customization,
scripts,
ancestors: [],
...parent,
...collection,
};
}
@@ -104,19 +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({
organizationId: content.organizationId,
siteId: content.siteId,
siteShareKey: content.siteShareKey,
})
: fetchParentCollection(space),
const [collection, document] = await Promise.all([
fetchParentCollection(space),
page?.page.documentId ? getDocument(space.id, page.page.documentId) : null,
]);
@@ -129,7 +84,7 @@ export async function fetchPageData(params: PagePathParams | PageIdParams) {
scripts,
ancestors: [],
...page,
...parent,
...collection,
document,
};
}
@@ -178,37 +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(args: {
organizationId: string;
siteId: string;
siteShareKey: string | undefined;
}) {
const { organizationId, siteId, siteShareKey } = args;
const [site, siteSpaces] = await Promise.all([
getSite(organizationId, siteId),
getSiteSpaces({ organizationId, siteId, siteShareKey }),
]);
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
View File
@@ -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 };
} {
+2 -2
View File
@@ -1,12 +1,12 @@
'use client';
import { captureException } from '@sentry/nextjs';
import * as Sentry from '@sentry/nextjs';
import Error from 'next/error';
import { useEffect } from 'react';
export default function GlobalError({ error }: { error: Error }) {
useEffect(() => {
captureException(error);
Sentry.captureException(error);
}, [error]);
return (
-200
View File
@@ -1,200 +0,0 @@
'use client';
import IconHeart from '@geist-ui/icons/heart';
import * as React from 'react';
import { ClassValue, tcls } from '@/lib/tailwind';
import { AdClassicRendering } from './AdClassicRendering';
import { AdCoverRendering } from './AdCoverRendering';
import { AdItem, AdsResponse } from './types';
/**
* Fetch and render the Ad placement.
* https://docs.buysellads.com/ad-serving-api
*/
export function Ad({
zoneId,
spaceId,
placement,
ignore,
style,
mode = 'auto',
}: {
zoneId: string;
spaceId: string;
placement: string;
ignore: boolean;
style?: ClassValue;
mode?: 'classic' | 'auto' | 'cover';
}) {
const containerRef = React.useRef<HTMLDivElement>(null);
const [visible, setVisible] = React.useState(false);
const [failed, setFailed] = React.useState(false);
const [ad, setAd] = React.useState<AdItem | undefined>(undefined);
// Observe the container visibility
React.useEffect(() => {
if (!containerRef.current) {
return;
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setVisible(true);
}
},
{
root: null,
rootMargin: '0px',
threshold: 0.1,
},
);
observer.observe(containerRef.current);
return () => {
observer.disconnect();
};
}, []);
// When the container is visible,
// track an impression on the ad and fetch it
React.useEffect(() => {
if (!visible) {
return;
}
let cancelled = false;
(async () => {
const url = new URL(`https://srv.buysellads.com/ads/${zoneId}.json`);
url.searchParams.set('segment', `placement:${placement}`);
url.searchParams.set('v', 'true');
if (ignore) {
url.searchParams.set('ignore', 'true');
}
try {
const res = await fetch(url);
const json: AdsResponse = await res.json();
if (cancelled) {
return;
}
const first = json.ads[0];
if (first && 'active' in first) {
setAd(first);
}
} catch (error) {
console.error(
'Failed to fetch ad, it might have been blocked by a ad-blocker',
error,
);
setFailed(true);
}
})();
return () => {
cancelled = true;
};
}, [visible, zoneId, ignore, placement]);
const viaUrl = new URL('https://www.gitbook.com');
viaUrl.searchParams.set('utm_source', 'content');
viaUrl.searchParams.set('utm_medium', 'ads');
viaUrl.searchParams.set('utm_campaign', spaceId);
return (
<div ref={containerRef} className={tcls(style)}>
{ad && ad.description && ad.statlink ? (
<>
{mode === 'classic' || !('callToAction' in ad) ? (
<AdClassicRendering ad={ad} />
) : (
<AdCoverRendering ad={ad} />
)}
{ad.pixel ? <AdPixels rawPixel={ad.pixel} /> : null}
<p
className={tcls(
'mt-2',
'mr-2',
'text-xs',
'text-right',
'text-dark/5',
'dark:text-light/5',
)}
>
<a
target="_blank"
href={viaUrl.toString()}
className={tcls('hover:underline')}
>
Ads via GitBook
</a>
</p>
</>
) : failed ? (
<AdBlockerPlaceholder />
) : null}
</div>
);
}
/**
* Render attribution or verification pixels.
* https://docs.buysellads.com/ad-serving-api#pixels
*/
function AdPixels({ rawPixel }: { rawPixel: string }) {
const pixels = rawPixel.split('||');
const time = String(Math.round(Date.now() / 1e4) | 0);
return (
<div className={tcls('hidden')}>
{pixels.map((pixel, index) => {
return (
<img
key={index}
src={pixel.replace('[timestamp]', time)}
width="1"
height="1"
style={{ display: 'none' }}
alt="Ads tracking pixel"
/>
);
})}
</div>
);
}
/**
* Placeholder when visitor has an ad-blocker.
*/
function AdBlockerPlaceholder() {
return (
<div
className={tcls(
'flex',
'flex-col',
'gap-3',
'bg-light-2',
'text-dark/7',
'dark:bg-dark-2',
'dark:text-light/7',
'rounded-lg',
'p-4',
)}
>
<div className={tcls('flex', 'flex-row', 'gap-2', 'items-center')}>
<IconHeart className={tcls('w-4', 'h-4', 'text-primary-500')} />
<p className={tcls('text-xs', 'font-semibold')}>Ad disabled</p>
</div>
<p className={tcls('text-xs')}>
{`It looks like you're using an adblocker. Whitelist this site to help support this
project.`}
</p>
</div>
);
}
-47
View File
@@ -1,47 +0,0 @@
import * as React from 'react';
import { tcls } from '@/lib/tailwind';
import { AdItem } from './types';
/**
* Classic rendering for an ad.
*/
export function AdClassicRendering({ ad }: { ad: AdItem }) {
return (
<a
className={tcls(
'flex',
'flex-col',
'gap-4',
'bg-light-2',
'text-dark/7',
'dark:bg-dark-2',
'dark:text-light/7',
'hover:text-dark/9',
'dark:hover:text-light/9',
'rounded-lg',
'p-4',
)}
href={ad.statlink}
rel="sponsored noopener"
target="_blank"
>
{'smallImage' in ad ? (
<div>
<img alt="Ads logo" className={tcls('rounded-md')} src={ad.smallImage} />
</div>
) : (
<div
className={tcls('px-6', 'py-4', 'rounded-md')}
style={{ backgroundColor: ad.backgroundColor }}
>
<img alt="Ads logo" src={ad.logo} />
</div>
)}
<div className={tcls('flex', 'flex-col')}>
<div className={tcls('text-xs')}>{ad.description}</div>
</div>
</a>
);
}
-108
View File
@@ -1,108 +0,0 @@
import * as React from 'react';
import { hexToRgba } from '@/lib/colors';
import { tcls } from '@/lib/tailwind';
import { AdCover } from './types';
/**
* Cover rendering for an ad.
*/
export function AdCoverRendering({ ad }: { ad: AdCover }) {
return (
<a
className={tcls(
'group/ad',
'relative',
'flex',
'flex-col',
'gap-4',
'bg-light-2',
'text-dark/7',
'dark:bg-dark-2',
'dark:text-light/7',
'hover:text-dark/9',
'dark:hover:text-light/9',
'rounded-lg',
'p-4',
'overflow-hidden',
'shadow-sm',
)}
style={{ backgroundColor: ad.backgroundColor, color: ad.textColor ?? '#ffffff' }}
href={ad.statlink}
rel="sponsored noopener"
target="_blank"
>
<div
className={tcls(
'absolute',
'inset-0',
'bg-center',
'bg-cover',
'bg-no-repeat',
'z-0',
)}
style={{
backgroundImage: `url(${ad.largeImage})`,
}}
/>
<div className={tcls('z-[2]')}>
<img
alt="Large image"
src={ad.largeImage}
className={tcls(
'rounded-md',
'shadow-md',
'max-h-32',
'group-hover/ad:max-h-16',
'transition-all',
)}
/>
</div>
<div className={tcls('z-[2]')}>
<img alt={ad.company} src={ad.logo} className={tcls('max-w-36', 'max-h-12')} />
</div>
<div className={tcls('flex', 'flex-col', 'z-[2]')}>
<div className={tcls('text-sm', 'font-semibold', 'mb-2')}>{ad.companyTagline}</div>
<div
className={tcls(
'text-xs',
'h-0',
'opacity-0',
'group-hover/ad:h-16',
'group-hover/ad:opacity-10',
'transition-all',
)}
>
{ad.description}
</div>
</div>
<div className={tcls('z-[2]')}>
<span
className={tcls(
'text-sm',
'font-semibold',
'shadow-lg',
'rounded-md',
'bg-white',
'py-2',
'px-4',
)}
style={{
backgroundColor: ad.ctaBackgroundColor,
color: ad.ctaTextColor ?? ad.backgroundColor,
}}
>
{ad.callToAction}
</span>
</div>
<div
className={tcls('absolute', 'inset-0', 'backdrop-blur', 'z-[1]')}
style={{
backgroundColor: hexToRgba(ad.backgroundColor, 0.8),
}}
/>
</a>
);
}
-1
View File
@@ -1 +0,0 @@
export * from './Ad';
-51
View File
@@ -1,51 +0,0 @@
export interface AdGeneric {
active: string;
ad_via_link: string;
bannerid: string;
creativeid: string;
evenodd: string;
external_id: string;
height: string;
i: string;
identifier: string;
longimp: string;
longlink: string;
num_slots: string;
statimp: string;
statlink: string;
timestamp: string;
width: string;
zoneid: string;
zonekey: string;
rendering: 'carbon';
pixel?: string;
}
export interface AdClassic extends AdGeneric {
description: string;
smallImage: string;
}
export interface AdCover extends AdGeneric {
backgroundColor: string;
backgroundHoverColor?: string;
textColor?: string;
textColorHover?: string;
callToAction: string;
company: string;
companyTagline: string;
description: string;
largeImage: string;
image?: string;
logo: string;
ctaBackgroundColor?: string;
ctaBackgroundHoverColor?: string;
ctaTextColor?: string;
ctaTextColorHover?: string;
}
export type AdItem = AdClassic | AdCover;
export interface AdsResponse {
ads: Array<AdItem | {}>;
}
-3
View File
@@ -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);
}}
@@ -1,72 +1,27 @@
import { DocumentBlockContentRef } from '@gitbook/api';
import { LogoIcon } from '@/components/icons';
import { Card, Emoji } from '@/components/primitives';
import { getSpaceCustomization, ignoreAPIError } from '@/lib/api';
import { ResolvedContentRef } from '@/lib/references';
import { BlockProps } from './Block';
export async function BlockContentRef(props: BlockProps<DocumentBlockContentRef>) {
const { block, context, style } = props;
const kind = block?.data?.ref?.kind;
const resolved = await context.resolveContentRef(block.data.ref, {
resolveAnchorText: true,
});
if (!resolved) {
return null;
}
const isContentInOtherSpace =
context.contentRefContext?.space &&
'space' in block.data.ref &&
context.contentRefContext.space.id !== block.data.ref.space;
const kind = block?.data?.ref?.kind;
if ((resolved.active && kind === 'space') || isContentInOtherSpace) {
return <SpaceRefCard {...props} resolved={resolved} />;
}
return (
<Card
leadingIcon={resolved.emoji ? <Emoji code={resolved.emoji} style="text-xl" /> : null}
leadingIcon={resolved.emoji ? <Emoji code={resolved.emoji} /> : null}
href={resolved.href}
preTitle={kind}
title={resolved.text}
style={style}
/>
);
}
async function SpaceRefCard(
props: { resolved: ResolvedContentRef } & BlockProps<DocumentBlockContentRef>,
) {
const { context, style, resolved } = props;
const spaceId = context.contentRefContext?.space.id;
if (!spaceId) {
return null;
}
const spaceCustomization = await ignoreAPIError(getSpaceCustomization(spaceId));
const customFavicon = spaceCustomization?.favicon;
const customEmoji = customFavicon && 'emoji' in customFavicon ? customFavicon.emoji : undefined;
const customIcon = customFavicon && 'icon' in customFavicon ? customFavicon.icon : undefined;
return (
<Card
leadingIcon={
<LogoIcon
icon={customIcon}
emoji={customEmoji}
alt=""
sizes={[{ width: 24 }]}
style={['object-contain', 'size-6']}
/>
}
href={resolved.href}
title={resolved.text}
postTitle={resolved.subText}
style={style}
/>
);
}
+1 -9
View File
@@ -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,30 +0,0 @@
const CODE_HIGHLIGHT_BLOCK_LIMIT = 50;
/**
* Spaces where we'll always use plain highlighting.
*/
const PLAIN_HIGHLIGHTING_SPACES: string[] = [
'V9geAO9ITPi8WOYK5o0r',
'puRmcwVxGFtHph8IjXaf',
'e3jwbMOrr4RhKtZ9C0XL',
'ryjzVNizLfd6pYogN3dm',
];
/**
* 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 {
+11 -52
View File
@@ -1,42 +1,26 @@
import * as gitbookAPI from '@gitbook/api';
import { headers } from 'next/headers';
import Script from 'next/script';
import ReactDOM from 'react-dom';
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;
const nonce = headers().get('x-nonce') || undefined;
export async function Embed(props: BlockProps<DocumentBlockEmbed>) {
const { block } = props;
ReactDOM.preload('https://cdn.iframe.ly/embed.js', { as: 'script', nonce });
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,
}}
/>
<Script src="https://cdn.iframe.ly/embed.js" nonce={nonce} />
</>
) : embed.type === 'integration' ? (
<IntegrationBlock
{...otherProps}
context={context}
block={createIntegrationBlock(block.data.url, embed.integration, embed.block)}
<div
dangerouslySetInnerHTML={{
__html: embed.html,
}}
/>
) : (
<Card
@@ -53,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,
},
};
}
+41 -6
View File
@@ -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',
-24
View File
@@ -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 />;
}
}
+1
View File
@@ -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,
};
+73 -97
View File
@@ -1,139 +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%;
@@ -158,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%;
@@ -251,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;
@@ -275,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;
@@ -285,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;
}
@@ -317,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;
}
@@ -172,10 +172,6 @@
@apply prose-sm;
}
.openapi-schema-example {
@apply prose-sm mt-2 text-dark/10 dark:text-light/10;
}
/** Authentication */
.openapi-securities {
@@ -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) => {
+26 -1
View File
@@ -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
View File
@@ -1,2 +1 @@
export * from './DocumentView';
export { createHighlightingContext } from './CodeBlock';
+2 -2
View File
@@ -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;
@@ -1,19 +1,21 @@
import { Space } from '@gitbook/api';
import { Collection, Space } from '@gitbook/api';
import { tcls } from '@/lib/tailwind';
import { Dropdown, DropdownChevron, DropdownMenu } from './Dropdown';
import { SpacesDropdownMenuItem } from './SpacesDropdownMenuItem';
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',
@@ -30,16 +32,14 @@ export function SpacesDropdown(props: { space: Space; spaces: Space[] }) {
)}
>
<DropdownMenu>
{spaces.map((otherSpace, index) => (
<SpacesDropdownMenuItem
key={`${otherSpace.id}-${index}`}
variantSpace={{
id: otherSpace.id,
title: otherSpace.title,
url: otherSpace.urls.published ?? otherSpace.urls.app,
}}
{collectionSpaces.map((otherSpace) => (
<DropdownMenuItem
key={otherSpace.id}
href={otherSpace.urls.published ?? otherSpace.urls.app}
active={otherSpace.id === space.id}
/>
>
{otherSpace.title}
</DropdownMenuItem>
))}
</DropdownMenu>
</Dropdown>
+6 -12
View File
@@ -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(
+18 -17
View File
@@ -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) => {
+1 -2
View File
@@ -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;
+35 -22
View File
@@ -1,25 +1,16 @@
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 { LogoIcon } from '@/components/icons';
import { Image } from '@/components/utils';
import { absoluteHref } from '@/lib/links';
import { tcls } from '@/lib/tailwind';
import { getContentTitle } from '@/lib/utils';
import { Link } from '../primitives';
interface HeaderLogoProps {
parent: Site | Collection | null;
collection: Collection | null;
space: Space;
customization: CustomizationSettings | SiteCustomizationSettings;
customization: CustomizationSettings;
}
/**
@@ -95,19 +86,41 @@ export function HeaderLogo(props: HeaderLogoProps) {
}
function LogoFallback(props: HeaderLogoProps) {
const { parent, space, customization } = props;
const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : undefined;
const customEmoji = 'emoji' in customization.favicon ? customization.favicon.emoji : undefined;
const { collection, space, customization } = props;
const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null;
return (
<>
<LogoIcon
icon={customIcon}
emoji={customEmoji}
alt=""
sizes={[{ width: 32 }]}
style={['object-contain', 'size-8']}
<Image
alt="Logo"
sources={
customIcon
? {
light: { src: customIcon.light, aspectRatio: '1' },
dark: customIcon.dark
? { src: customIcon.dark, aspectRatio: '1' }
: null,
}
: {
light: {
src: absoluteHref('~gitbook/icon?size=medium&theme=light', true),
size: { width: 256, height: 256 },
},
dark: {
src: absoluteHref('~gitbook/icon?size=medium&theme=dark', true),
size: { width: 256, height: 256 },
},
}
}
sizes={[
{
width: 32,
},
]}
fetchPriority="high"
style={['w-8', 'h-8', 'object-contain']}
/>
<h1
className={tcls(
'text-pretty',
@@ -125,7 +138,7 @@ function LogoFallback(props: HeaderLogoProps) {
: 'text-header-link',
)}
>
{getContentTitle(space, customization, parent)}
{collection ? collection.title : customization.title ?? space.title}
</h1>
</>
);
@@ -1,30 +0,0 @@
'use client';
import { Space } from '@gitbook/api';
import { useSelectedLayoutSegment } from 'next/navigation';
import { DropdownMenuItem } from './Dropdown';
function useVariantSpaceHref(variantSpaceUrl: string) {
const currentPathname = useSelectedLayoutSegment() ?? '';
const targetUrl = new URL(variantSpaceUrl);
targetUrl.pathname += `/${currentPathname}`;
targetUrl.pathname = targetUrl.pathname.replace(/\/{2,}/g, '/').replace(/\/$/, '');
targetUrl.searchParams.set('fallback', 'true');
return targetUrl.toString();
}
export function SpacesDropdownMenuItem(props: {
variantSpace: { id: Space['id']; title: Space['title']; url: string };
active: boolean;
}) {
const { variantSpace, active } = props;
const variantHref = useVariantSpaceHref(variantSpace.url);
return (
<DropdownMenuItem key={variantSpace.id} href={variantHref} active={active}>
{variantSpace.title}
</DropdownMenuItem>
);
}
+5 -31
View File
@@ -2,14 +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,
Site,
SiteCustomizationSettings,
Space,
} from '@gitbook/api';
import { CustomizationSettings, JSONDocument, RevisionPageDocument, Space } from '@gitbook/api';
import React from 'react';
import urlJoin from 'url-join';
@@ -18,10 +11,9 @@ import { getDocumentSections } from '@/lib/document';
import { absoluteHref } from '@/lib/links';
import { ContentRefContext, resolveContentRef } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
import { getPDFUrlSearchParams } from '@/lib/urls';
import { getPDFUrl, getPDFUrlSearchParams } from '@/lib/urls';
import { ScrollSectionsList } from './ScrollSectionsList';
import { Ad } from '../Ads';
import { PageFeedbackForm } from '../PageFeedback';
/**
@@ -29,8 +21,7 @@ import { PageFeedbackForm } from '../PageFeedback';
*/
export async function PageAside(props: {
space: Space;
site: Site | undefined;
customization: CustomizationSettings | SiteCustomizationSettings;
customization: CustomizationSettings;
page: RevisionPageDocument;
document: JSONDocument | null;
context: ContentRefContext;
@@ -38,16 +29,8 @@ export async function PageAside(props: {
withFullPageCover: boolean;
withPageFeedback: boolean;
}) {
const {
space,
site,
page,
document,
customization,
withHeaderOffset,
withPageFeedback,
context,
} = props;
const { space, page, document, customization, withHeaderOffset, withPageFeedback, context } =
props;
const language = getSpaceLanguage(customization);
return (
@@ -183,15 +166,6 @@ export async function PageAside(props: {
) : null}
</div>
</div>
{site?.ads ? (
<Ad
zoneId={site.ads.zone}
placement="page.aside"
spaceId={space.id}
ignore={process.env.NODE_ENV !== 'production'}
style={tcls('mt-4')}
/>
) : null}
</aside>
);
}
+7 -33
View File
@@ -1,15 +1,9 @@
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';
import { t } from '@/intl/translate';
import { ContentPointer, ContentTarget, SiteContentPointer, api } from '@/lib/api';
import { ContentTarget, api } from '@/lib/api';
import { hasFullWidthBlock, isNodeEmpty } from '@/lib/document';
import { ContentRefContext, resolveContentRef } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
@@ -19,39 +13,25 @@ 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;
contentPointer: ContentPointer | SiteContentPointer;
contentTarget: ContentTarget;
customization: CustomizationSettings | SiteCustomizationSettings;
customization: CustomizationSettings;
page: RevisionPageDocument;
document: JSONDocument | null;
context: ContentRefContext;
withPageFeedback: boolean;
}) {
const {
space,
contentPointer,
contentTarget,
customization,
context,
page,
document,
withPageFeedback,
} = props;
const { space, contentTarget, customization, context, page, document, withPageFeedback } =
props;
const asFullWidth = document ? hasFullWidthBlock(document) : false;
const language = getSpaceLanguage(customization);
const updatedAt = page.updatedAt ?? page.createdAt;
const shouldHighlightCode = createHighlightingContext();
const sitePointer =
'siteId' in contentPointer
? { organizationId: contentPointer.organizationId, siteId: contentPointer.siteId }
: undefined;
return (
<>
@@ -93,7 +73,6 @@ export function PageBody(props: {
contentRefContext: context,
resolveContentRef: (ref, options) =>
resolveContentRef(ref, context, options),
shouldHighlightCode,
}}
/>
) : (
@@ -142,12 +121,7 @@ export function PageBody(props: {
</div>
</main>
<React.Suspense fallback={null}>
<TrackPageView
sitePointer={sitePointer}
spaceId={space.id}
pageId={page.id}
apiHost={api().endpoint}
/>
<TrackPageView spaceId={space.id} pageId={page.id} apiHost={api().endpoint} />
</React.Suspense>
</>
);
@@ -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;
}) {
+16 -60
View File
@@ -1,66 +1,28 @@
'use client';
import type { RequestSiteTrackPageView, RequestSpaceTrackPageView } from '@gitbook/api';
import type { RequestSpaceTrackPageView } from '@gitbook/api';
import cookies from 'js-cookie';
import * as React from 'react';
import { getVisitorId } from '@/lib/analytics';
import { SiteContentPointer } from '@/lib/api';
/**
* Track the page view for the current page to integrations.
*/
export function TrackPageView(props: {
apiHost: string;
sitePointer?: Pick<SiteContentPointer, 'siteId' | 'organizationId'>;
spaceId: string;
pageId: string | undefined;
}) {
const { apiHost, sitePointer, spaceId, pageId } = props;
const { apiHost, spaceId, pageId } = props;
React.useEffect(() => {
trackPageView({ apiHost, sitePointer, spaceId, pageId });
}, [apiHost, spaceId, pageId, sitePointer]);
trackPageView(apiHost, spaceId, pageId);
}, [apiHost, spaceId, pageId]);
return null;
}
async function sendSpaceTrackPageViewRequest(args: {
apiHost: string;
spaceId: string;
body: RequestSpaceTrackPageView;
}) {
const { apiHost, spaceId, body } = args;
const url = new URL(apiHost);
url.pathname = `/v1/spaces/${spaceId}/insights/track_view`;
await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
}
async function sendSiteTrackPageViewRequest(args: {
apiHost: string;
sitePointer: Pick<SiteContentPointer, 'siteId' | 'organizationId'>;
body: RequestSiteTrackPageView;
}) {
const { apiHost, sitePointer, body } = args;
const url = new URL(apiHost);
url.pathname = `/v1/orgs/${sitePointer.organizationId}/sites/${sitePointer.siteId}/insights/track_view`;
await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
}
let latestPageId: string | undefined | null = null;
/**
@@ -68,13 +30,7 @@ let latestPageId: string | undefined | null = null;
* We don't use the API client to avoid shipping 80kb of JS to the client.
* And instead use a simple fetch.
*/
async function trackPageView(args: {
apiHost: string;
sitePointer?: Pick<SiteContentPointer, 'siteId' | 'organizationId'>;
spaceId: string;
pageId: string | undefined;
}) {
const { apiHost, sitePointer, pageId, spaceId } = args;
async function trackPageView(apiHost: string, spaceId: string, pageId: string | undefined) {
if (pageId === latestPageId) {
// The hook can be called multiple times, we only want to track once.
return;
@@ -83,7 +39,7 @@ async function trackPageView(args: {
latestPageId = pageId;
const visitorId = await getVisitorId();
const sharedTrackedProps = {
const body: RequestSpaceTrackPageView = {
url: window.location.href,
pageId,
visitor: {
@@ -95,17 +51,17 @@ async function trackPageView(args: {
referrer: document.referrer,
};
const url = new URL(apiHost);
url.pathname = `/v1/spaces/${spaceId}/insights/track_view`;
try {
sitePointer
? await sendSiteTrackPageViewRequest({
apiHost,
sitePointer,
body: {
...sharedTrackedProps,
spaceId,
},
})
: await sendSpaceTrackPageViewRequest({ apiHost, spaceId, body: sharedTrackedProps });
await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
} catch (error) {
console.error('Failed to track page view', error);
}
+90 -116
View File
@@ -6,11 +6,10 @@ import { atom, useRecoilState } from 'recoil';
import { Loading } from '@/components/primitives';
import { useLanguage } from '@/intl/client';
import { t } from '@/intl/translate';
import { TranslationLanguage } from '@/intl/translations';
import { iterateStreamResponse } from '@/lib/actions';
import { tcls } from '@/lib/tailwind';
import { AskAnswerResult, AskAnswerSource, streamAskQuestion } from './server-actions';
import { AskAnswerResult, streamAskQuestion } from './server-actions';
import { useSearch, useSearchLink } from './useSearch';
import { Link } from '../primitives';
@@ -95,11 +94,6 @@ export function SearchAskAnswer(props: { spaceId: string; query: string }) {
};
}, [setState]);
let hasAnswer = false;
if (state && 'answer' in state) {
hasAnswer = !!state?.answer?.body;
}
return (
<div
className={tcls(
@@ -135,128 +129,108 @@ export function SearchAskAnswer(props: { spaceId: string; query: string }) {
function AnswerBody(props: { answer: AskAnswerResult }) {
const { answer } = props;
const getSearchLinkProps = useSearchLink();
const language = useLanguage();
const [, setSearchState] = useSearch();
const onClose = () => {
setSearchState(null);
};
return (
<>
<div
data-test="search-ask-answer"
className={tcls('mt-4', 'px-4', 'text-dark/9', 'dark:text-light/8')}
>
{answer.hasAnswer ? answer.body : t(language, 'search_ask_no_answer')}
{answer.body}
</div>
{answer.followupQuestions.length > 0 ? (
<AnswerFollowupQuestions followupQuestions={answer.followupQuestions} />
<div className={tcls('mt-7', 'flex', 'flex-col', 'flex-wrap', 'gap-1')}>
{answer.followupQuestions.map((question) => (
<Link
key={question}
className={tcls(
'text-sm',
'font-medium',
'inline-flex',
'items-start',
'gap-2',
'px-4',
'py-1',
'text-primary-500',
'focus-within:text-primary-700',
'hover:bg-primary/2',
'dark:text-primary-400',
'dark:hover:bg-primary-500/3',
)}
{...getSearchLinkProps({
query: question,
ask: true,
})}
>
<IconSearch
className={tcls(
'w-[15px]',
'h-[15px]',
'shrink-0',
'mt-0.5',
'[&>path]:[stroke-opacity:0.64]',
)}
/>
<span>{question}</span>
</Link>
))}
</div>
) : null}
{answer.sources.length > 0 ? (
<AnswerSources
hasAnswer={answer.hasAnswer}
sources={answer.sources}
language={language}
/>
<div
className={tcls(
'flex',
'flex-wrap',
'gap-2',
'mt-7',
'py-4',
'px-4',
'border-t',
'border-dark/2',
'dark:border-light/1',
)}
>
<span className={tcls('text-sm')}>{t(language, 'search_ask_sources')}</span>
{answer.sources.map((source) => (
<span key={source.id} className={tcls()}>
<Link
onClick={onClose}
className={tcls(
'flex',
'text-sm',
'text-dark/7',
'hover:underline',
'focus-within:text-primary-700',
'dark:text-light/8',
)}
href={source.href}
prefetch={false}
>
<IconBox
className={tcls(
'stroke-dark/6',
'w-[15px]',
'h-[15px]',
'shrink-0',
'mt-0.5',
'mr-0.5',
'dark:stroke-light/6',
)}
/>
{source.title}
</Link>
</span>
))}
</div>
) : null}
</>
);
}
function AnswerFollowupQuestions(props: { followupQuestions: string[] }) {
const { followupQuestions } = props;
const getSearchLinkProps = useSearchLink();
return (
<div className={tcls('mt-7 mb-4', 'flex', 'flex-col', 'flex-wrap', 'gap-1')}>
{followupQuestions.map((question) => (
<Link
key={question}
className={tcls(
'text-sm',
'font-medium',
'inline-flex',
'items-start',
'gap-2',
'px-4',
'py-1',
'text-primary-500',
'focus-within:text-primary-700',
'hover:bg-primary/2',
'dark:text-primary-400',
'dark:hover:bg-primary-500/3',
)}
{...getSearchLinkProps({
query: question,
ask: true,
})}
>
<IconSearch
className={tcls(
'w-[15px]',
'h-[15px]',
'shrink-0',
'mt-0.5',
'[&>path]:[stroke-opacity:0.64]',
)}
/>
<span>{question}</span>
</Link>
))}
</div>
);
}
function AnswerSources(props: {
sources: AskAnswerSource[];
language: TranslationLanguage;
hasAnswer?: boolean;
}) {
const { sources, language, hasAnswer } = props;
return (
<div
className={tcls(
'flex',
'flex-wrap',
'gap-2',
'mt-7',
'py-4',
'px-4',
'border-t',
'border-dark/2',
'dark:border-light/1',
)}
>
<span className={tcls('text-sm')}>
{t(language, hasAnswer ? 'search_ask_sources' : 'search_ask_sources_no_answer')}
</span>
{sources.map((source) => (
<span key={source.id} className={tcls()}>
<Link
className={tcls(
'flex',
'text-sm',
'text-dark/7',
'hover:underline',
'focus-within:text-primary-700',
'dark:text-light/8',
)}
href={source.href}
prefetch={false}
>
<IconBox
className={tcls(
'stroke-dark/6',
'w-[15px]',
'h-[15px]',
'shrink-0',
'mt-0.5',
'mr-0.5',
'dark:stroke-light/6',
)}
/>
{source.title}
</Link>
</span>
))}
</div>
);
}
+33 -21
View File
@@ -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);
@@ -232,23 +239,28 @@ function SearchModalBody(
</div>
</div>
{!state.ask || !withAsk ? (
<SearchResults
ref={resultsRef}
spaceId={spaceId}
revisionId={revisionId}
parent={state.global ? parent : null}
query={state.query}
withAsk={withAsk}
onSwitchToAsk={() => {
onChangeQuery({
ask: true,
query: state.query,
global: state.global,
});
}}
>
{parent && state.query ? <SearchScopeToggle spaceTitle={spaceTitle} /> : null}
</SearchResults>
<>
<SearchResults
ref={resultsRef}
spaceId={spaceId}
revisionId={revisionId}
collectionId={state.global ? collectionId : null}
query={state.query}
withAsk={withAsk}
onSwitchToAsk={() => {
onChangeQuery({
ask: true,
query: state.query,
global: state.global,
});
}}
onClose={onClose}
>
{collectionId && state.query ? (
<SearchScopeToggle spaceTitle={spaceTitle} />
) : null}
</SearchResults>
</>
) : null}
{state.query && state.ask && withAsk ? (
<SearchAskAnswer spaceId={spaceId} query={state.query} />
@@ -11,15 +11,20 @@ export const SearchPageResultItem = React.forwardRef(function SearchPageResultIt
query: string;
item: ComputedPageResult;
active: boolean;
onClick: (to: string) => void;
},
ref: React.Ref<HTMLAnchorElement>,
) {
const { query, item, active } = props;
const { query, item, active, onClick } = props;
return (
<Link
ref={ref}
href={item.href}
onClick={(event) => {
event.preventDefault();
onClick(item.href);
}}
className={tcls(
'flex',
'flex-row',
+10 -8
View File
@@ -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,13 +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 } = props;
const { children, query, spaceId, revisionId, collectionId, withAsk, onSwitchToAsk, onClose } =
props;
const language = useLanguage();
const debounceTimeout = React.useRef<NodeJS.Timeout | null>(null);
@@ -93,10 +94,9 @@ 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(() => {
@@ -211,6 +211,7 @@ export const SearchResults = React.forwardRef(function SearchResults(
query={query}
item={item}
active={index === cursor}
onClick={onClose}
/>
);
}
@@ -251,6 +252,7 @@ export const SearchResults = React.forwardRef(function SearchResults(
query={query}
item={item}
active={index === cursor}
onClick={onClose}
/>
);
}
@@ -11,15 +11,20 @@ export const SearchSectionResultItem = React.forwardRef(function SearchSectionRe
query: string;
item: ComputedSectionResult;
active: boolean;
onClick: (to: string) => void;
},
ref: React.Ref<HTMLAnchorElement>,
) {
const { query, item, active } = props;
const { query, item, active, onClick } = props;
return (
<Link
ref={ref}
href={item.href}
onClick={(event) => {
event.preventDefault();
onClick(item.href);
}}
className={tcls(
'search-section-result-item',
'[&:has(+:not(&))]:mb-6',
+29 -144
View File
@@ -1,16 +1,7 @@
'use server';
import {
Collection,
RevisionPage,
SearchAIAnswer,
SearchPageResult,
Site,
SiteSpace,
Space,
} from '@gitbook/api';
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';
@@ -46,65 +37,9 @@ export interface AskAnswerSource {
}
export interface AskAnswerResult {
body?: React.ReactNode;
body: React.ReactNode;
followupQuestions: string[];
sources: AskAnswerSource[];
hasAnswer: boolean;
}
export async function searchSiteContent(args: {
query: string;
siteSpaceIds?: string[];
cacheBust?: string;
}): Promise<OrderedComputedResult[]> {
const { siteSpaceIds, query, cacheBust } = args;
const pointer = getContentPointer();
if (siteSpaceIds?.length === 0) {
// if we have no siteSpaces to search in then we won't find anything. skip the call.
return [];
}
if ('siteId' in pointer && 'organizationId' in pointer) {
const [searchResults, allSiteSpaces] = await Promise.all([
api.searchSiteContent(
pointer.organizationId,
pointer.siteId,
query,
siteSpaceIds,
cacheBust,
),
siteSpaceIds
? null
: api.getSiteSpaces({
organizationId: pointer.organizationId,
siteId: pointer.siteId,
siteShareKey: pointer.siteShareKey,
}),
]);
if (!siteSpaceIds) {
// We are searching all of this Site's content
return searchResults.items
.map((spaceItem) => {
const siteSpace = allSiteSpaces?.find(
(siteSpace) => siteSpace.space.id === spaceItem.id,
);
return spaceItem.pages.map((item) => transformSitePageResult(item, siteSpace));
})
.flat(2);
}
return searchResults.items
.map((spaceItem) => {
return spaceItem.pages.map((item) => transformPageResult(item));
})
.flat(2);
}
// This should never happen
return [];
}
/**
@@ -115,44 +50,25 @@ export async function searchSpaceContent(
revisionId: string,
query: string,
): Promise<OrderedComputedResult[]> {
const pointer = getContentPointer();
if ('siteId' in pointer && 'organizationId' in pointer) {
const siteSpaceIds = pointer.siteSpaceId ? [pointer.siteSpaceId] : []; // if we don't have a siteSpaceID search all content
// This is a site so use a different function which we can eventually call directly
// We also want to break cache for this specific space if the revisionId is different so use it as a cache busting key
return await searchSiteContent({ siteSpaceIds, query, cacheBust: revisionId });
}
const data = await api.searchSpaceContent(spaceId, revisionId, query);
return data.items.map((item) => transformPageResult(item, undefined)).flat();
}
/**
* 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 isSite = 'siteId' in pointer;
if (isSite) {
return searchSiteContent({ query });
}
const [data, collectionSpaces] = await Promise.all([
api.searchParentContent(parent.id, query),
parent.object === 'collection' ? api.getCollectionSpaces(parent.id) : null,
api.searchCollectionContent(collectionId, query),
api.getCollectionSpaces(collectionId),
]);
let spaces: Space[] = collectionSpaces ? collectionSpaces : [];
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);
@@ -162,15 +78,12 @@ export async function searchParentContent(
* Server action to ask a question in a space.
*/
export const streamAskQuestion = streamResponse(async function* (spaceId: string, query: string) {
const stream = api
.api()
.spaces.streamAskInSpace(spaceId, { query, format: 'document', details: true });
const stream = api.api().spaces.streamAskInSpace(spaceId, { query, format: 'document' });
const pagesPromise = api.getSpaceContentData({ spaceId });
for await (const chunk of stream) {
// We run the AI search and fetch the pages in parallel
const { pages } = await pagesPromise;
yield transformAnswer(chunk.answer, pages);
}
});
@@ -187,7 +100,7 @@ function transformAnswer(
answer: SearchAIAnswer | undefined,
pages: RevisionPage[],
): AskAnswerResult | null {
if (!answer) {
if (!answer || !('document' in answer.answer)) {
return null;
}
@@ -211,43 +124,35 @@ function transformAnswer(
.filter(filterOutNullable);
return {
body:
answer.answer && 'document' in answer.answer ? (
<DocumentView
document={answer.answer.document}
context={{
mode: 'default',
contentRefContext: null,
resolveContentRef: async () => null,
shouldHighlightCode: () => false,
}}
style={['space-y-5']}
/>
) : null,
body: (
<DocumentView
document={answer.answer.document}
context={{
mode: 'default',
contentRefContext: null,
resolveContentRef: async () => null,
}}
style={['space-y-5']}
/>
),
followupQuestions: answer.followupQuestions,
sources,
hasAnswer: !!answer.answer && 'document' in answer.answer,
};
}
function transformSectionsAndPage(args: {
item: SearchPageResult;
space?: Space;
spaceURL?: string;
}): [ComputedPageResult, ComputedSectionResult[]] {
const { item, space, spaceURL } = args;
function transformPageResult(item: SearchPageResult, space?: Space) {
// Resolve a relative path to an absolute URL
// if the search result is relative to another space, we use the space URL
const getURL = (path: string, spaceURL?: string) => {
if (spaceURL) {
if (!spaceURL.endsWith('/')) {
spaceURL += '/';
const getURL = (path: string) => {
if (space) {
let url = space.urls.published ?? space.urls.app;
if (!url.endsWith('/')) {
url += '/';
}
if (path.startsWith('/')) {
path = path.slice(1);
}
return spaceURL + path;
return url + path;
} else {
return absoluteHref(path);
}
@@ -258,7 +163,7 @@ function transformSectionsAndPage(args: {
type: 'section',
id: item.id + '/' + section.id,
title: section.title,
href: getURL(section.path, spaceURL),
href: getURL(section.path),
body: section.body,
})) ?? [];
@@ -270,25 +175,5 @@ function transformSectionsAndPage(args: {
spaceTitle: space?.title,
};
return [page, sections];
}
function transformSitePageResult(item: SearchPageResult, siteSpace?: SiteSpace) {
const [page, sections] = transformSectionsAndPage({
item,
space: siteSpace?.space,
spaceURL: siteSpace?.urls.published,
});
return [page, ...sections];
}
function transformPageResult(item: SearchPageResult, space?: Space) {
const [page, sections] = transformSectionsAndPage({
item,
space,
spaceURL: space?.urls.published ?? space?.urls.app,
});
return [page, ...sections];
}
+10 -12
View File
@@ -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>
</>
+2 -8
View File
@@ -19,14 +19,6 @@ export function PagesList(props: {
return (
<ul className={tcls('flex', 'flex-1', 'flex-col', 'gap-y-0.5', style)}>
{pages.map((page) => {
if (page.type === 'link') {
return <PageLinkItem key={page.id} page={page} context={context} />;
}
if (page.hidden) {
return null;
}
if (page.type === 'group') {
return (
<PageGroupItem
@@ -37,6 +29,8 @@ export function PagesList(props: {
context={context}
/>
);
} else if (page.type === 'link') {
return <PageLinkItem key={page.id} page={page} context={context} />;
}
return (
@@ -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
+6 -16
View File
@@ -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')} />
-49
View File
@@ -1,49 +0,0 @@
import { CustomizationThemedURL } from '@gitbook/api';
import { Image } from '@/components/utils';
import { absoluteHref } from '@/lib/links';
import { Emoji } from '../primitives';
export function LogoIcon(
props: { icon?: CustomizationThemedURL; emoji?: string } & Omit<
React.ComponentProps<typeof Image>,
'sources'
>,
) {
const { icon, emoji, alt, ...imageProps } = props;
if (emoji && !icon) {
return <Emoji code={emoji} style="text-xl" />;
}
return (
<Image
alt={alt ?? ''}
sources={
icon
? {
light: {
src: icon.light,
aspectRatio: '1',
},
dark: {
src: icon.dark,
aspectRatio: '1',
},
}
: {
light: {
src: absoluteHref('~gitbook/icon?size=medium&theme=light', true),
size: { width: 256, height: 256 },
},
dark: {
src: absoluteHref('~gitbook/icon?size=medium&theme=dark', true),
size: { width: 256, height: 256 },
},
}
}
{...imageProps}
/>
);
}
-1
View File
@@ -1,2 +1 @@
export * from './types';
export * from './LogoIcon';
+3 -6
View File
@@ -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
View File
@@ -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;

Some files were not shown because too many files have changed in this diff Show More