Compare commits

..

2 Commits

Author SHA1 Message Date
Samy Pessé e30c249498 Standalone output for next 2024-06-09 21:01:57 +02:00
Samy Pessé 5288f76ded Define top-level dependencies 2024-06-09 20:39:03 +02:00
368 changed files with 625 additions and 2332 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": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
+10 -10
View File
@@ -20,7 +20,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:
@@ -58,7 +58,7 @@ jobs:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: ${{ vars.CLOUDFLARE_PROJECT_NAME }}
directory: ./packages/gitbook/.vercel/output/static
directory: ./.vercel/output/static
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
workingDirectory: ./
- name: Outputs
@@ -82,7 +82,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,13 +102,13 @@ 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
- name: Run pagespeed tests
run: bun ./packages/gitbook/tests/pagespeed-testing.ts $DEPLOYMENT_URL
run: bun ./tests/pagespeed-testing.ts $DEPLOYMENT_URL
env:
DEPLOYMENT_URL: ${{needs.deploy.outputs.deployment_url}}
PAGESPEED_API_KEY: ${{ secrets.PAGESPEED_API_KEY }}
@@ -121,12 +121,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 +136,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 +151,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 +166,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:
-37
View File
@@ -1,37 +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:
# Using a PAT instead of GITHUB_TOKEN because we need to run workflows when releases are created
# https://github.com/orgs/community/discussions/26875#discussioncomment-3253761
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+29 -3
View File
@@ -5,6 +5,16 @@ node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
@@ -14,9 +24,25 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Turbo
.turbo
# local env files
.env*.local
# Vercel
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# visual tests
screenshots/
# Sentry Config File
.sentryclirc
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
# Generated public files
/public/~gitbook/static/
+1 -6
View File
@@ -1,6 +1 @@
.next
.vercel
# Generated
packages/emoji-codepoints/index.ts
packages/gitbook/public/~gitbook/static/
.next
-6
View File
@@ -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.
@@ -106,93 +106,6 @@ const testCases: TestsCase[] = [
},
],
},
{
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',
@@ -363,11 +276,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',
@@ -2,6 +2,8 @@ const { withSentryConfig } = require('@sentry/nextjs');
module.exports = withSentryConfig(
{
output: 'standalone',
env: {
BUILD_VERSION: (process.env.GITHUB_SHA ?? '').slice(0, 7),
SENTRY_DSN: process.env.SENTRY_DSN ?? '',
@@ -9,7 +11,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 +21,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;
},
@@ -60,9 +47,9 @@ module.exports = withSentryConfig(
{
protocol: 'https',
hostname: '*.gitbook.io',
},
],
},
}
]
}
},
{
silent: true,
+81 -12
View File
@@ -2,26 +2,95 @@
"name": "gitbook",
"version": "0.1.0",
"private": true,
"packageManager": "bun@1.1.18",
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build",
"build:cloudflare": "turbo run build:cloudflare",
"lint": "turbo run lint",
"typecheck": "turbo run typecheck",
"dev": "next dev",
"build": "next build",
"build:cloudflare": "next-on-pages",
"start": "next start",
"lint": "next lint",
"format": "prettier ./ --ignore-unknown --write",
"format:check": "prettier ./ --ignore-unknown --list-different",
"unit": "turbo run unit",
"e2e": "turbo run e2e",
"changeset": "changeset",
"release": "turbo run build && changeset publish"
"typecheck": "tsc --noEmit",
"unit": "bun test {src,packages}/**/*.test.ts",
"e2e": "playwright test",
"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": [
"packages/*"
],
"dependencies": {
"@geist-ui/icons": "^1.0.2",
"@gitbook/api": "^0.48.0",
"@gitbook/react-math": "workspace:*",
"@gitbook/react-openapi": "workspace:*",
"@gitbook/react-contentkit": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-popover": "^1.0.7",
"@sentry/nextjs": "^7.94.1",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.10",
"@upstash/redis": "^1.27.1",
"ajv": "^8.12.0",
"assert-never": "^1.2.1",
"bun-types": "^1.0.7",
"classnames": "^2.5.1",
"content-security-policy-merger": "^1.0.0",
"framer-motion": "^10.16.14",
"js-cookie": "^3.0.5",
"jsontoxml": "^1.0.1",
"katex": "^0.16.9",
"mathjax": "^3.2.2",
"memoizee": "^0.4.15",
"next": "^14.1.3",
"next-themes": "^0.2.1",
"nuqs": "^1.15.4",
"object-hash": "^3.0.0",
"openapi-types": "^12.1.3",
"p-map": "^7.0.0",
"parse-cache-control": "^1.0.1",
"react": "^18",
"react-dom": "^18",
"react-hotkeys-hook": "^4.4.1",
"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",
"server-only": "^0.0.1",
"shiki": "^1.2.0",
"tailwind-merge": "^2.2.0",
"tailwind-shades": "^1.1.2",
"unified": "^11.0.4",
"url-join": "^5.0.0"
},
"devDependencies": {
"@changesets/cli": "^2.27.7",
"@argos-ci/playwright": "^2.0.0",
"@cloudflare/next-on-pages": "^1.11.3",
"@cloudflare/workers-types": "^4.20231218.0",
"@playwright/test": "^1.42.1",
"@types/js-cookie": "^3.0.6",
"@types/jsontoxml": "^1.0.5",
"@types/jsonwebtoken": "^9.0.6",
"@types/katex": "^0.16.5",
"@types/node": "^20",
"@types/object-hash": "^3.0.6",
"@types/parse-cache-control": "^1.0.4",
"@types/psi": "^4.1.6",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/rison": "^0.0.9",
"autoprefixer": "^10",
"eslint": "^8",
"eslint-config-next": "13.5.6",
"eslint-plugin-import": "^2.29.0",
"jsonwebtoken": "^9.0.2",
"postcss": "^8",
"prettier": "^3.0.3",
"turbo": "^2.0.9"
"psi": "^4.1.0",
"tailwindcss": "^3.4.0",
"typescript": "^5"
}
}
-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": {},
-35
View File
@@ -1,35 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# visual tests
screenshots/
# Sentry Config File
.sentryclirc
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
# Generated public files
/public/~gitbook/static/
-24
View File
@@ -1,24 +0,0 @@
# gitbook
## 0.2.1
### Patch Changes
- Updated dependencies [24cd72e]
- @gitbook/react-contentkit@0.4.0
- @gitbook/react-math@0.4.0
- @gitbook/react-openapi@0.4.0
## 0.2.0
### Minor Changes
- de747b7: Refactor the repository to be a proper monorepo and publish JS files on NPM instead of TypeScript files.
### Patch Changes
- Updated dependencies [de747b7]
- Updated dependencies [de747b7]
- @gitbook/react-contentkit@0.3.0
- @gitbook/react-openapi@0.3.0
- @gitbook/react-math@0.3.0
-89
View File
@@ -1,89 +0,0 @@
{
"name": "gitbook",
"version": "0.2.1",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"build:cloudflare": "next-on-pages",
"start": "next start",
"lint": "next lint",
"typecheck": "tsc --noEmit",
"e2e": "playwright test",
"unit": "bun test {src,packages}",
"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"
},
"dependencies": {
"@geist-ui/icons": "^1.0.2",
"@gitbook/api": "^0.56.0",
"@gitbook/react-math": "workspace:*",
"@gitbook/react-openapi": "workspace:*",
"@gitbook/react-contentkit": "workspace:*",
"@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",
"ajv": "^8.12.0",
"assert-never": "^1.2.1",
"bun-types": "^1.1.20",
"classnames": "^2.5.1",
"content-security-policy-merger": "^1.0.0",
"framer-motion": "^10.16.14",
"js-cookie": "^3.0.5",
"jsontoxml": "^1.0.1",
"katex": "^0.16.9",
"mathjax": "^3.2.2",
"memoizee": "^0.4.15",
"next": "^14.1.3",
"next-themes": "^0.2.1",
"nuqs": "^1.17.4",
"object-hash": "^3.0.0",
"openapi-types": "^12.1.3",
"p-map": "^7.0.0",
"parse-cache-control": "^1.0.1",
"react": "^18",
"react-dom": "^18",
"react-hotkeys-hook": "^4.4.1",
"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",
"server-only": "^0.0.1",
"shiki": "^1.2.0",
"tailwind-merge": "^2.2.0",
"tailwind-shades": "^1.1.2",
"unified": "^11.0.4",
"url-join": "^5.0.0"
},
"devDependencies": {
"@argos-ci/playwright": "^2.0.0",
"@playwright/test": "^1.42.1",
"@cloudflare/next-on-pages": "^1.11.3",
"@cloudflare/workers-types": "^4.20231218.0",
"@types/js-cookie": "^3.0.6",
"@types/jsontoxml": "^1.0.5",
"@types/jsonwebtoken": "^9.0.6",
"@types/node": "^20",
"@types/object-hash": "^3.0.6",
"@types/parse-cache-control": "^1.0.4",
"@types/psi": "^4.1.6",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/rison": "^0.0.9",
"autoprefixer": "^10",
"eslint": "^8",
"eslint-config-next": "^14.2.5",
"eslint-plugin-import": "^2.29.1",
"jsonwebtoken": "^9.0.2",
"postcss": "^8",
"psi": "^4.1.0",
"tailwindcss": "^3.4.0",
"typescript": "^5.5.3"
}
}
-27
View File
@@ -1,27 +0,0 @@
import {
BrowserClient,
makeFetchTransport,
defaultStackParser,
getCurrentScope,
} 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({
debug: false,
dsn,
integrations: [],
sampleRate: 0.1,
enableTracing: false,
beforeSendTransaction: () => {
return null;
},
transport: makeFetchTransport,
stackParser: defaultStackParser,
});
getCurrentScope().setClient(client);
client.init();
}
@@ -1,40 +0,0 @@
'use client';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import React from 'react';
import { useScrollToHash } from '@/components/hooks';
/**
* Client component to initialize interactivity for a page.
*/
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]);
}
-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>
);
}
@@ -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>
);
}
@@ -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 +0,0 @@
export * from './Ad';
@@ -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 | {}>;
}
@@ -1,72 +0,0 @@
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 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}
href={resolved.href}
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,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>
);
}
@@ -1,112 +0,0 @@
'use client';
import type { RequestSiteTrackPageView, 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;
React.useEffect(() => {
trackPageView({ apiHost, sitePointer, spaceId, pageId });
}, [apiHost, spaceId, pageId, sitePointer]);
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;
/**
* Track the page view for the current page to GitBook.
* 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;
if (pageId === latestPageId) {
// The hook can be called multiple times, we only want to track once.
return;
}
latestPageId = pageId;
const visitorId = await getVisitorId();
const sharedTrackedProps = {
url: window.location.href,
pageId,
visitor: {
anonymousId: visitorId,
userAgent: window.navigator.userAgent,
language: window.navigator.language,
cookies: cookies.get(),
},
referrer: document.referrer,
};
try {
sitePointer
? await sendSiteTrackPageViewRequest({
apiHost,
sitePointer,
body: {
...sharedTrackedProps,
spaceId,
},
})
: await sendSpaceTrackPageViewRequest({ apiHost, spaceId, body: sharedTrackedProps });
} catch (error) {
console.error('Failed to track page view', error);
}
}
@@ -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,2 +0,0 @@
export * from './types';
export * from './LogoIcon';
@@ -1,56 +0,0 @@
export const de = {
locale: 'de',
powered_by_gitbook: 'Bereitgestellt von GitBook',
switch_to_dark_theme: 'Zum dunklen Modus wechseln',
switch_to_light_theme: 'Zum hellen Modus wechseln',
switch_to_system_theme: 'Zum Systemmodus wechseln',
search: 'Suche',
search_or_ask: 'Fragen oder Suchen',
search_input_placeholder: 'Inhalt durchsuchen',
search_ask_input_placeholder: 'Inhalt durchsuchen oder eine Frage stellen',
search_no_results: 'Keine Ergebnisse für "${1}".',
search_scope_space: 'Nur in ${1}',
search_scope_all: 'Alle Inhalte',
search_ask: 'Fragen "${1}"',
search_ask_sources: 'Quellen',
search_ask_sources_no_answer: 'Verwandte Seiten',
search_ask_no_answer:
'Es konnte keine Antwort auf Ihre Frage gefunden werden. Versuchen Sie, sie umzuformulieren oder genauer zu sein.',
search_ask_error: 'Etwas ist schief gelaufen. Bitte versuchen Sie es später noch einmal.',
on_this_page: 'Auf dieser Seite',
next_page: 'Nächste',
previous_page: 'Vorherige',
page_last_modified: 'Zuletzt aktualisiert ${1}',
was_this_helpful: 'War das hilfreich?',
was_this_helpful_positive: 'Ja, das war es!',
was_this_helpful_neutral: 'Nicht sicher',
was_this_helpful_negative: 'Nein',
was_this_helpful_thank_you: 'Danke!',
annotation_button_label: 'Kommentar öffnen',
code_copied: 'Kopiert!',
code_copy: 'Kopieren',
table_of_contents_button_label: 'Inhaltsverzeichnis öffnen',
cookies_title: 'Cookies',
cookies_prompt:
'Diese Website verwendet Cookies, um ihre Dienste bereitzustellen und den Datenverkehr zu analysieren. Durch die Nutzung dieser Website akzeptieren Sie die ${1}.',
cookies_prompt_privacy: 'Datenschutzrichtlinie',
cookies_accept: 'Akzeptieren',
cookies_reject: 'Ablehnen',
cookies_close: 'Schließen',
edit_on_git: 'Bearbeiten auf ${1}',
notfound_title: 'Seite nicht gefunden',
notfound: 'Die gesuchte Seite existiert nicht.',
unexpected_error_title: 'Ein Fehler ist aufgetreten',
unexpected_error:
'Entschuldigung, ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.',
unexpected_error_retry: 'Erneut versuchen',
pdf_download: 'Als PDF exportieren',
pdf_goback: 'Zurück zum Inhalt',
pdf_print: 'Drucken oder als PDF speichern',
pdf_page_of: '${1} von ${2}',
pdf_mode_only_page: 'Nur diese Seite',
pdf_mode_all: 'Alle Seiten',
pdf_limit_reached:
'Das PDF konnte für ${1} Seiten nicht generiert werden, Generierung wurde bei ${2} gestoppt.',
pdf_limit_reached_continue: 'Mit ${1} weiteren Seiten erweitern.',
};
@@ -1,56 +0,0 @@
export const pt_br = {
locale: 'pt-br',
powered_by_gitbook: 'Powered by GitBook',
switch_to_dark_theme: 'Mudar para modo escuro',
switch_to_light_theme: 'Mudar para modo claro',
switch_to_system_theme: 'Mudar para configuração do sistema',
search: 'Busca',
search_or_ask: 'Perguntar ou buscar',
search_input_placeholder: 'Buscar conteúdo',
search_ask_input_placeholder: 'Buscar conteúdo ou fazer uma pergunta',
search_no_results: 'Sem resultados para "${1}".',
search_scope_space: 'Somente em ${1}',
search_scope_all: 'Todo o conteúdo',
search_ask: 'Perguntar "${1}"',
search_ask_sources: 'Fontes',
search_ask_sources_no_answer: 'Páginas relacionadas',
search_ask_no_answer:
'Não foi possível encontrar uma resposta para sua pergunta. Você pode tentar reformulá-la ou ser mais específico.',
search_ask_error: 'Algo deu errado. Por favor tente novamente mais tarde.',
on_this_page: 'Nesta página',
next_page: 'Próximo',
previous_page: 'Anterior',
page_last_modified: 'Atualizado ${1}',
was_this_helpful: 'Isto foi útil?',
was_this_helpful_positive: 'Sim, foi!',
was_this_helpful_neutral: 'Não sei',
was_this_helpful_negative: 'Não',
was_this_helpful_thank_you: 'Obrigado!',
annotation_button_label: 'Abrir anotação',
code_copied: 'Copiado!',
code_copy: 'Copiar',
table_of_contents_button_label: 'Abrir o índice',
cookies_title: 'Cookies',
cookies_prompt:
'Este site usa cookies para entregar suas funcionalidades e analizar o tráfego. Ao navegar neste site, você aceita a ${1}.',
cookies_prompt_privacy: 'política de privacidade',
cookies_accept: 'Aceitar',
cookies_reject: 'Rejeitar',
cookies_close: 'Fechar',
edit_on_git: 'Editar no ${1}',
notfound_title: 'Página não encontrada',
notfound: 'A página que você está procurando não existe.',
unexpected_error_title: 'Aconteceu um erro',
unexpected_error:
'Desculpe, aconteceu um erro inesperado. Por favor tente novamente mais tarde.',
unexpected_error_retry: ' Tentar novamente',
pdf_download: 'Exportar como PDF',
pdf_goback: 'Voltar ao conteúdo',
pdf_print: 'Imprimir ou salvar como PDF',
pdf_page_of: '${1} de ${2}',
pdf_mode_only_page: 'Somente esta página',
pdf_mode_all: 'Todas as páginas',
pdf_limit_reached:
'Não foi possível gerar o PDF para ${1} páginas, generation stopped at ${2}.',
pdf_limit_reached_continue: 'Extender com mais ${1} páginas.',
};
-25
View File
@@ -1,25 +0,0 @@
import {
Collection,
CustomizationSettings,
Site,
SiteCustomizationSettings,
Space,
} from '@gitbook/api';
/**
* Get the title to display for a content.
*/
export function getContentTitle(
space: Space,
customization: CustomizationSettings | SiteCustomizationSettings,
parent: Site | Collection | null,
) {
// When we are rendering a site, always give priority to the customization title first
// and then fallback to the site title
if (parent?.object === 'site') {
return customization.title ?? parent.title ?? space.title;
}
// Otherwise the legacy behavior is not changed to avoid regressions
return parent ? parent.title : customization.title ?? space.title;
}
-1
View File
@@ -1 +0,0 @@
dist/
-25
View File
@@ -1,25 +0,0 @@
# @gitbook/react-contentkit
## 0.4.0
### Minor Changes
- 24cd72e: Fix changeset CI workflow
## 0.3.0
### Minor Changes
- de747b7: Publish to NPM with JS files and TypeScript declaration files
## 0.2.0
### Minor Changes
- 57adb3e: Second release to fix publishing with changeset
## 0.1.0
### Minor Changes
- 5f8a8fe: Initial release
+2 -17
View File
@@ -1,27 +1,12 @@
{
"name": "@gitbook/react-contentkit",
"version": "0.4.0",
"exports": {
".": {
"types": "./dist/index.d.ts",
"development": "./src/index.ts",
"default": "./dist/index.js"
}
},
"exports": "./src/index.ts",
"dependencies": {
"classnames": "^2.5.1",
"@gitbook/api": "^0.51.0",
"@gitbook/api": "^0.48.0",
"assert-never": "^1.2.1"
},
"peerDependencies": {
"react": "*"
},
"devDependencies": {
"typescript": "^5.5.3"
},
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit",
"dev": "tsc -w"
}
}
@@ -1,5 +1,4 @@
import { type ContentKitRenderOutput } from '@gitbook/api';
import React from 'react';
import { Element } from './Element';
import { ContentKitServerContext } from './types';
+1 -1
View File
@@ -3,7 +3,6 @@ import {
ContentKitInlineElement,
ContentKitRootElement,
} from '@gitbook/api';
import React from 'react';
import { ElementBlock } from './ElementBlock';
import { ElementWebframe } from './ElementWebframe';
@@ -14,6 +13,7 @@ import { ElementCodeBlock } from './ElementCodeBlock';
import { ElementImage } from './ElementImage';
import { ElementStack } from './ElementStack';
import { ElementText } from './ElementText';
import React from 'react';
import { ElementButton } from './ElementButton';
import { ElementModal } from './ElementModal';
import { ElementBox } from './ElementBox';
@@ -1,5 +1,4 @@
import { ContentKitBlock } from '@gitbook/api';
import React from 'react';
import { ContentKitServerElementProps } from './types';
@@ -1,5 +1,4 @@
import { ContentKitBox } from '@gitbook/api';
import React from 'react';
import { ContentKitServerElementProps } from './types';
@@ -1,11 +1,11 @@
'use client';
import React from 'react';
import { ContentKitButton } from '@gitbook/api';
import { ContentKitClientElementProps } from './types';
import classNames from 'classnames';
import { useContentKitClientContext } from './context';
import React from 'react';
export function ElementButton(
props: ContentKitClientElementProps<ContentKitButton> & {
@@ -1,5 +1,4 @@
import { ContentKitCodeBlock } from '@gitbook/api';
import React from 'react';
import { ContentKitServerElementProps } from './types';
import { resolveDynamicBinding } from './dynamic';
@@ -1,5 +1,4 @@
import { ContentKitCodeBlock } from '@gitbook/api';
import React from 'react';
import { ContentKitClientElementProps } from './types';
@@ -1,5 +1,4 @@
import { ContentKitDivider } from '@gitbook/api';
import React from 'react';
import { ContentKitServerElementProps } from './types';
import classNames from 'classnames';
@@ -1,5 +1,4 @@
import { ContentKitIcon } from '@gitbook/api';
import React from 'react';
import { ContentKitServerContext } from './types';
export function ElementIcon(props: { icon: ContentKitIcon; context: ContentKitServerContext }) {
@@ -1,5 +1,4 @@
import { ContentKitImage } from '@gitbook/api';
import React from 'react';
import { ContentKitServerElementProps } from './types';
@@ -1,5 +1,4 @@
import { ContentKitMarkdown } from '@gitbook/api';
import React from 'react';
import { ContentKitServerElementProps } from './types';
import { ElementMarkdownClient } from './ElementMarkdownClient';
@@ -1,9 +1,9 @@
'use client';
import { ContentKitMarkdown } from '@gitbook/api';
import React from 'react';
import { ContentKitClientElementProps } from './types';
import { resolveDynamicBinding } from './dynamic';
import React from 'react';
import { useContentKitClientContext } from './context';
/**
@@ -1,11 +1,11 @@
'use client';
import { ContentKitModal } from '@gitbook/api';
import React from 'react';
import { ContentKitClientElementProps } from './types';
import classNames from 'classnames';
import { useContentKitClientContext } from './context';
import React from 'react';
export function ElementModal(
props: ContentKitClientElementProps<ContentKitModal> & {
@@ -1,5 +1,4 @@
import { ContentKitHStack, ContentKitVStack } from '@gitbook/api';
import React from 'react';
import { ContentKitServerElementProps } from './types';
import classNames from 'classnames';
@@ -1,5 +1,4 @@
import { ContentKitText } from '@gitbook/api';
import React from 'react';
import { ContentKitServerElementProps } from './types';
import classNames from 'classnames';
@@ -1,11 +1,11 @@
'use client';
import { ContentKitTextInput } from '@gitbook/api';
import React from 'react';
import { ContentKitClientElementProps } from './types';
import classNames from 'classnames';
import { useContentKitClientContext } from './context';
import React from 'react';
import { getStateStringValue } from './dynamic';
export function ElementTextInput(props: ContentKitClientElementProps<ContentKitTextInput>) {
-21
View File
@@ -1,21 +0,0 @@
{
"compilerOptions": {
"target": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": false,
"declaration": true,
"outDir": "dist",
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react",
"incremental": true
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}
-1
View File
@@ -1 +0,0 @@
dist/
-25
View File
@@ -1,25 +0,0 @@
# @gitbook/react-math
## 0.4.0
### Minor Changes
- 24cd72e: Fix changeset CI workflow
## 0.3.0
### Minor Changes
- de747b7: Refactor the repository to be a proper monorepo and publish JS files on NPM instead of TypeScript files.
## 0.2.0
### Minor Changes
- 57adb3e: Second release to fix publishing with changeset
## 0.1.0
### Minor Changes
- 5f8a8fe: Initial release
-3
View File
@@ -1,3 +0,0 @@
# `@gitbook/react-math`
React component to render a Math formula. It uses KaTeX when possible and fallbacks to MathJaX if needed.
+1 -17
View File
@@ -1,26 +1,10 @@
{
"name": "@gitbook/react-math",
"exports": {
".": {
"types": "./dist/index.d.ts",
"development": "./src/index.ts",
"default": "./dist/index.js"
}
},
"version": "0.4.0",
"exports": "./src/index.ts",
"dependencies": {
"object-hash": "^3.0.0"
},
"peerDependencies": {
"react": "*"
},
"devDependencies": {
"typescript": "^5.5.3",
"@types/katex": "^0.16.5"
},
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit",
"dev": "tsc -w"
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ import React from 'react';
import { KaTeX } from './KaTeX';
import { MathJaXLazy } from './MathJaXLazy';
import '../css/default.css';
import './default.css';
export interface MathFormulaProps {
/**
+1 -2
View File
@@ -1,6 +1,6 @@
'use client';
import React from 'react';
import * as React from 'react';
declare var MathJax: {
startup: {
@@ -24,7 +24,6 @@ export interface MathJaXFormulaProps {
export default function MathJaXFormula(props: MathJaXFormulaProps) {
const { formula, inline, className, mathJaxUrl } = props;
// @ts-ignore - React.use doesn't seem define in typing
React.use(loadMathJaxScript(mathJaxUrl));
const [html, setHTML] = React.useState('');
-21
View File
@@ -1,21 +0,0 @@
{
"compilerOptions": {
"target": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": false,
"declaration": true,
"outDir": "dist",
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react",
"incremental": true
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}
-1
View File
@@ -1 +0,0 @@
dist
-31
View File
@@ -1,31 +0,0 @@
# @gitbook/react-openapi
## 0.4.0
### Minor Changes
- 24cd72e: Fix changeset CI workflow
## 0.3.0
### Minor Changes
- de747b7: Refactor the repository to be a proper monorepo and publish JS files on NPM instead of TypeScript files.
## 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
+10 -1
View File
@@ -1,3 +1,12 @@
# `@gitbook/react-openapi`
Style-less React components to render OpenAPI operation blocks.
React components to render OpenAPI operations.
## Features
- Generate code samples for the request
- Support custom cde samples with `x-codeSamples` (Redocly syntax)
## TODO
- Support for trying out the request
+2 -17
View File
@@ -1,13 +1,6 @@
{
"name": "@gitbook/react-openapi",
"exports": {
".": {
"types": "./dist/index.d.ts",
"development": "./src/index.ts",
"default": "./dist/index.js"
}
},
"version": "0.4.0",
"exports": "./src/index.ts",
"dependencies": {
"@scalar/api-client-react": "^0.3.7",
"@scalar/oas-utils": "0.1.6",
@@ -18,17 +11,9 @@
"swagger2openapi": "^7.0.8"
},
"devDependencies": {
"@types/swagger2openapi": "^7.0.4",
"bun-types": "^1.1.20",
"typescript": "^5.5.3"
"@types/swagger2openapi": "^7.0.4"
},
"peerDependencies": {
"react": "*"
},
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit",
"unit": "bun test",
"dev": "tsc -w"
}
}
-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,
@@ -19,7 +19,7 @@ export function generateSchemaExample(
return undefined;
}
if (typeof schema.example !== 'undefined') {
if (schema.example) {
return schema.example;
}
-24
View File
@@ -1,24 +0,0 @@
{
"compilerOptions": {
"target": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": false,
"declaration": true,
"outDir": "dist",
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react",
"incremental": true,
"types": [
"bun-types" // add Bun global
]
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}
File diff suppressed because one or more lines are too long
+15
View File
@@ -0,0 +1,15 @@
import * as Sentry from '@sentry/nextjs';
const dsn = process.env.SENTRY_DSN;
if (dsn) {
Sentry.init({
debug: false,
dsn,
integrations: [],
sampleRate: 0.1,
enableTracing: false,
beforeSendTransaction: () => {
return null;
},
});
}
@@ -1,8 +1,8 @@
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,
@@ -1,8 +1,8 @@
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,
@@ -1,11 +1,6 @@
import { NextRequest } from 'next/server';
import {
verifyImageSignature,
resizeImage,
CloudflareImageOptions,
checkIsSizableImageURL,
} from '@/lib/images';
import { verifyImageSignature, resizeImage, CloudflareImageOptions } from '@/lib/images';
import { parseImageAPIURL } from '@/lib/urls';
export const runtime = 'edge';
@@ -18,9 +13,6 @@ export const runtime = 'edge';
export async function GET(request: NextRequest) {
let urlParam = 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) {
return new Response('Missing url/sign parameters', { status: 400 });
}
@@ -32,14 +24,8 @@ export async function GET(request: NextRequest) {
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 });
}
@@ -0,0 +1,13 @@
'use client';
import { useScrollToHash } from '@/components/hooks';
/**
* Client component to initialize interactivity for a page.
*/
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();
return null;
}
@@ -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, parent } = await fetchPageData(params);
if (!page) {
notFound();
}
return {
title: [page.title, getContentTitle(space, customization, parent)]
title: [page.title, customization.title ?? space.title, parent?.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,
};
}
@@ -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';
@@ -41,11 +38,6 @@ export default async function ContentLayout(props: { children: React.ReactNode }
scripts,
} = await fetchSpaceData();
ReactDOM.preconnect(api().endpoint);
if (assetsDomain) {
ReactDOM.preconnect(assetsDomain);
}
scripts.forEach(({ script }) => {
ReactDOM.preload(script, {
as: 'script',
@@ -112,7 +104,7 @@ export async function generateMetadata(): Promise<Metadata> {
const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null;
return {
title: getContentTitle(space, customization, parent),
title: `${parent ? parent.title : customization.title ?? space.title}`,
generator: `GitBook (${buildVersion()})`,
metadataBase: new URL(baseUrl()),
icons: {
@@ -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),
),
@@ -14,7 +14,6 @@ import {
} from '@/lib/api';
import { getEmojiForCode } from '@/lib/emojis';
import { tcls } from '@/lib/tailwind';
import { getContentTitle } from '@/lib/utils';
export const runtime = 'edge';
@@ -55,7 +54,7 @@ export async function GET(req: NextRequest) {
: space.visibility === ContentVisibility.InCollection && space.parent
? await getCollection(space.parent)
: null;
const contentTitle = getContentTitle(space, customization, parent);
const contentTitle = parent?.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';
@@ -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)}
{parent?.title ?? customization.title ?? space.title}
</h2>
<div tw="flex flex-1">
<p tw="text-4xl">{page ? page.title : 'Not found'}</p>

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