mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-26 04:07:07 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e1fdce020f | |||
| 84e01f255e |
@@ -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)
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Avoid multiple <h1> in the page by using a <div> for the title in the header
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
RND-3532: drop down menu for hidden links at small screen size
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Change dark mode shadow for multi-space search toolbar
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
bump: scalar from 1.0.5 to 1.0.7
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Replace all icons, previously imported from Geist, by new package `@gitbook/icons`
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Add support for page icons
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/icons': minor
|
||||
---
|
||||
|
||||
Initial release for `@gitbook/icons`, a package of content icons to be used for pages, documents, etc.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Use url hash to open Expandable and scroll to anchor
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix margin for paragraphs in quote blocks
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Optimize images rendered in community ads
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/react-contentkit': patch
|
||||
---
|
||||
|
||||
Fix rendering of webframe with SSR causing wrong communication between frame and renderer
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Add optional env `GITBOOK_INTEGRATIONS_HOST` to configure the host serving the integrations
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Add support for icons style customization for sites
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Update Scalar client to latest version
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Update rendering of community ads to match new API response, and make it possible to preview ads.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/react-math': minor
|
||||
---
|
||||
|
||||
Export binary `gitbook-math` to copy assets to a directory
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix "ip" being passed to BSA for community ads
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/icons': minor
|
||||
---
|
||||
|
||||
Fix bin gitbook-icons when installed without the Pro token
|
||||
@@ -7,7 +7,7 @@ runs:
|
||||
- name: 🏗 Prepare Playwright env
|
||||
shell: bash
|
||||
run: |
|
||||
PLAYWRIGHT_VERSION=$(npm ls --json @playwright/test | jq --raw-output '.dependencies["gitbook"].dependencies["@playwright/test"].version')
|
||||
PLAYWRIGHT_VERSION=$(npm ls --json @playwright/test | jq --raw-output '.dependencies["@playwright/test"].version')
|
||||
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV
|
||||
|
||||
# Cache browser binaries, cache key is based on Playwright version and OS
|
||||
@@ -24,7 +24,6 @@ runs:
|
||||
- name: 🏗 Install Playwright browser binaries & OS dependencies
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
working-directory: packages/gitbook
|
||||
run: |
|
||||
bun x playwright install --with-deps chromium
|
||||
|
||||
@@ -32,6 +31,5 @@ runs:
|
||||
- name: 🏗 Install Playwright OS dependencies
|
||||
if: steps.playwright-cache.outputs.cache-hit == 'true'
|
||||
shell: bash
|
||||
working-directory: packages/gitbook
|
||||
run: |
|
||||
bun x playwright install-deps
|
||||
|
||||
+12
-35
@@ -1,11 +1,6 @@
|
||||
on: [push]
|
||||
name: CI
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
env:
|
||||
NPMRC_FONT_AWESOME_TOKEN: ${{ secrets.NPMRC_FONT_AWESOME_TOKEN }}
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy to Cloudflare Pages
|
||||
@@ -21,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:
|
||||
@@ -59,7 +54,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
|
||||
@@ -83,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
|
||||
@@ -103,13 +98,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 }}
|
||||
@@ -122,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
|
||||
@@ -137,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:
|
||||
@@ -152,30 +147,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 unit
|
||||
build-oss:
|
||||
# CI to check that the repository builds correctly on a machine without the credentials
|
||||
runs-on: ubuntu-latest
|
||||
name: Build (Open Source)
|
||||
env:
|
||||
NPMRC_FONT_AWESOME_TOKEN: ''
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- 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
|
||||
- run: bun run build
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
name: Typecheck
|
||||
@@ -185,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:
|
||||
|
||||
@@ -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
-5
@@ -5,6 +5,16 @@ node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
@@ -14,11 +24,25 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Turbo
|
||||
.turbo
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# Vercel
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# Env files
|
||||
.env.local
|
||||
# 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
-10
@@ -1,10 +1 @@
|
||||
.next
|
||||
.vercel
|
||||
|
||||
# Generated
|
||||
packages/emoji-codepoints/index.ts
|
||||
packages/gitbook/public/~gitbook/static/
|
||||
packages/icons/src/data/*.json
|
||||
|
||||
# Build files
|
||||
dist/
|
||||
.next
|
||||
@@ -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.
|
||||
|
||||
@@ -106,7 +106,7 @@ On the con side, you become responsible for the reliability of your published si
|
||||
|
||||
Distributed under the [GNU GPLv3 License](https://github.com/GitBookIO/gitbook/blob/main/LICENSE).
|
||||
|
||||
If you plan to distribute the code, you must make the source code public to comply with the GNU GPLv3. To clone in a private repository, acquire a [commercial license](https://www.gitbook.com/pricing).
|
||||
If you plan to distribute the code, you must the source code public to comply with GNU GPLv3. To clone in a private repository, acquire a [commercial license](https://www.gitbook.com/pricing).
|
||||
|
||||
See `LICENSE` for more information.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
[install.scopes]
|
||||
"awesome.me" = { token = "$NPMRC_FONT_AWESOME_TOKEN", url = "https://npm.fontawesome.com/" }
|
||||
+1
-1
@@ -10,7 +10,7 @@ Invalidate cache can be done at two levels using tags:
|
||||
To invalidate and refetch the data cache, you can execute a POST request to `/~/gitbook/revalidate`:
|
||||
|
||||
```bash
|
||||
curl --location --request POST 'https://gitbook/mycompany.com/~gitbook/revalidate' \
|
||||
curl --location --request POST 'https://gitbook.mycompany.com/~gitbook/revalidate' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{"tags": ["space.id"]}'
|
||||
```
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { DeepPartial } from 'ts-essentials';
|
||||
import { argosScreenshot } from '@argos-ci/playwright';
|
||||
import {
|
||||
CustomizationHeaderPreset,
|
||||
CustomizationIconsStyle,
|
||||
CustomizationLocale,
|
||||
SiteCustomizationSettings,
|
||||
CustomizationSettings,
|
||||
} from '@gitbook/api';
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
import jwt from 'jsonwebtoken';
|
||||
@@ -42,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',
|
||||
@@ -332,11 +177,6 @@ const testCases: TestsCase[] = [
|
||||
url: 'blocks/lists',
|
||||
fullPage: true,
|
||||
},
|
||||
{
|
||||
name: 'Code',
|
||||
url: 'blocks/code',
|
||||
fullPage: true,
|
||||
},
|
||||
{
|
||||
name: 'Cards',
|
||||
url: 'blocks/cards',
|
||||
@@ -349,12 +189,6 @@ const testCases: TestsCase[] = [
|
||||
{
|
||||
name: 'Embeds',
|
||||
url: 'blocks/embeds',
|
||||
fullPage: true,
|
||||
},
|
||||
{
|
||||
name: 'Page links',
|
||||
url: 'blocks/page-links',
|
||||
fullPage: true,
|
||||
},
|
||||
{
|
||||
name: 'Annotations',
|
||||
@@ -370,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',
|
||||
@@ -390,11 +219,6 @@ const testCases: TestsCase[] = [
|
||||
url: 'page-options/page-with-cover-and-no-toc',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'With icon',
|
||||
url: 'page-options/page-with-icon',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -411,37 +235,16 @@ const testCases: TestsCase[] = [
|
||||
}),
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'With duotone icons',
|
||||
url:
|
||||
'page-options/page-with-icon' +
|
||||
getCustomizationURL({
|
||||
styling: {
|
||||
icons: CustomizationIconsStyle.Duotone,
|
||||
},
|
||||
}),
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Ads',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Without previewed ads',
|
||||
url: 'text-page?ads_preview=1',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Share links',
|
||||
baseUrl: 'https://gitbook.gitbook.io/gbo-tests-share-links/',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-share-links/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Valid link',
|
||||
url: 'TGs8PkF4GWVtbmPnWhYL/',
|
||||
url: 'Fc6mMII9FKgnwm7qqynx/',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Invalid link',
|
||||
@@ -650,6 +453,7 @@ const testCases: TestsCase[] = [
|
||||
url: getCustomizationURL({
|
||||
internationalization: {
|
||||
locale,
|
||||
inherit: false,
|
||||
},
|
||||
}),
|
||||
run: async (page) => {
|
||||
@@ -672,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 {
|
||||
@@ -690,7 +494,7 @@ for (const testCase of testCases) {
|
||||
/**
|
||||
* Create a URL with customization settings.
|
||||
*/
|
||||
function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettings>): string {
|
||||
function getCustomizationURL(partial: Partial<CustomizationSettings>): string {
|
||||
const encoded = rison.encode_object(partial);
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
@@ -7,11 +7,9 @@ module.exports = withSentryConfig(
|
||||
SENTRY_DSN: process.env.SENTRY_DSN ?? '',
|
||||
SENTRY_ENVIRONMENT: process.env.SENTRY_ENVIRONMENT ?? 'development',
|
||||
GITBOOK_ASSETS_PREFIX: process.env.GITBOOK_ASSETS_PREFIX,
|
||||
GITBOOK_ICONS_URL: process.env.GITBOOK_ICONS_URL,
|
||||
GITBOOK_ICONS_TOKEN: process.env.GITBOOK_ICONS_TOKEN,
|
||||
},
|
||||
|
||||
webpack(config, { dev, webpack }) {
|
||||
webpack(config) {
|
||||
config.resolve.fallback = {
|
||||
...config.resolve.fallback,
|
||||
|
||||
@@ -21,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;
|
||||
},
|
||||
|
||||
@@ -56,15 +39,6 @@ module.exports = withSentryConfig(
|
||||
|
||||
assetPrefix: process.env.GITBOOK_ASSETS_PREFIX,
|
||||
poweredByHeader: false,
|
||||
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: '*.gitbook.io',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
silent: true,
|
||||
+77
-18
@@ -2,31 +2,90 @@
|
||||
"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",
|
||||
"lint:fix": "turbo run lint -- --fix",
|
||||
"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 release && changeset publish",
|
||||
"download:env": "op read op://gitbook-x-dev/gitbook-open/.env.local >> .env.local"
|
||||
"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/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.27.7",
|
||||
"prettier": "^3.0.3",
|
||||
"turbo": "^2.0.9"
|
||||
"dependencies": {
|
||||
"@geist-ui/icons": "^1.0.2",
|
||||
"@gitbook/api": "^0.39.0",
|
||||
"@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-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"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@vercel/next@4.3.6": "patches/@vercel%2Fnext@4.3.6.patch"
|
||||
"devDependencies": {
|
||||
"@argos-ci/playwright": "^2.0.0",
|
||||
"@cloudflare/next-on-pages": "^1.9.0",
|
||||
"@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",
|
||||
"psi": "^4.1.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
dist/
|
||||
index.ts
|
||||
|
||||
@@ -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,5 +1,4 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import fs from 'fs';
|
||||
import emojisRaws from 'emoji-assets/emoji.json';
|
||||
|
||||
interface EmojiData {
|
||||
@@ -21,8 +20,7 @@ Object.entries(emojis).forEach(([key, value]) => {
|
||||
}
|
||||
});
|
||||
|
||||
fs.mkdirSync(path.resolve(__dirname, 'dist'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, 'dist/index.ts'),
|
||||
'index.ts',
|
||||
`export const emojiCodepoints: Record<string, string> = ${JSON.stringify(output, null, 4)};`,
|
||||
);
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
{
|
||||
"name": "@gitbook/emoji-codepoints",
|
||||
"description": "Optimized mapping of codepoints to the fully qualified emoji codepoints",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"exports": "./dist/index.ts",
|
||||
"exports": "./index.ts",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"emoji-assets": "^8.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"generate": "bun ./build.ts"
|
||||
"postinstall": "bun ./build.ts"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/
|
||||
@@ -1,47 +0,0 @@
|
||||
# gitbook
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 24b785c: Update shiki for code block syntax highlighting, with support for more languages and fixes for diffs. It also patches the deployment on Cloudflare to support edge functions larger than 4MB.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- acc3f2f: Fix error with the "Try it" of OpenAPI block because of the Scalar proxy failing on Cloudflare with the `cache` option
|
||||
- Updated dependencies [709f1a1]
|
||||
- Updated dependencies [ede2335]
|
||||
- Updated dependencies [0426312]
|
||||
- @gitbook/react-openapi@0.6.0
|
||||
|
||||
## 0.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3445db4]
|
||||
- @gitbook/react-contentkit@0.5.0
|
||||
- @gitbook/react-openapi@0.5.0
|
||||
- @gitbook/react-math@0.5.0
|
||||
|
||||
## 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
|
||||
@@ -1,93 +0,0 @@
|
||||
{
|
||||
"name": "gitbook",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "env-cmd --silent -f ../../.env.local 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}",
|
||||
"generate": "gitbook-icons ./public/~gitbook/static/icons custom-icons && gitbook-math ./public/~gitbook/static/math",
|
||||
"copy:icons": "gitbook-icons ./public/~gitbook/static/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gitbook/api": "^0.60.0",
|
||||
"@gitbook/icons": "workspace:*",
|
||||
"@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",
|
||||
"@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.2.5",
|
||||
"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.11.1",
|
||||
"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.12.1",
|
||||
"@cloudflare/workers-types": "^4.20240725.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",
|
||||
"env-cmd": "^10.1.0",
|
||||
"ts-essentials": "^10.0.1"
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { ClassValue, tcls } from '@/lib/tailwind';
|
||||
|
||||
import { renderAd } from './renderAd';
|
||||
|
||||
/**
|
||||
* Zone ID provided by BuySellAds for the preview.
|
||||
*/
|
||||
const PREVIEW_ZONE_ID = 'CVAIKKQM';
|
||||
|
||||
/**
|
||||
* 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 | null;
|
||||
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 [ad, setAd] = React.useState<React.ReactNode | 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;
|
||||
|
||||
const preview = new URL(window.location.href).searchParams.has('ads_preview');
|
||||
const realZoneId = preview ? PREVIEW_ZONE_ID : zoneId;
|
||||
|
||||
if (!realZoneId) {
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const result = await renderAd({
|
||||
spaceId,
|
||||
placement,
|
||||
ignore: ignore || preview,
|
||||
zoneId: realZoneId,
|
||||
mode,
|
||||
});
|
||||
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
setAd(result);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [visible, spaceId, zoneId, ignore, placement, mode]);
|
||||
|
||||
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 : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { getResizedImageURL } from '@/lib/images';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { AdItem } from './types';
|
||||
|
||||
/**
|
||||
* Classic rendering for an ad.
|
||||
*/
|
||||
export async 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={await getResizedImageURL(ad.smallImage, { width: 192, dpr: 2 })}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={tcls('px-6', 'py-4', 'rounded-md')}
|
||||
style={{ backgroundColor: ad.backgroundColor }}
|
||||
>
|
||||
<img
|
||||
alt="Ads logo"
|
||||
src={await getResizedImageURL(ad.logo, { width: 192 - 48, dpr: 2 })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={tcls('flex', 'flex-col')}>
|
||||
<div className={tcls('text-xs')}>{ad.description}</div>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { hexToRgba } from '@/lib/colors';
|
||||
import { getResizedImageURL } from '@/lib/images';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { AdCover } from './types';
|
||||
|
||||
/**
|
||||
* Cover rendering for an ad.
|
||||
*/
|
||||
export async function AdCoverRendering({ ad }: { ad: AdCover }) {
|
||||
const largeImage = await getResizedImageURL(ad.largeImage, { width: 128, dpr: 2 });
|
||||
|
||||
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(${largeImage})`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className={tcls('z-[2]')}>
|
||||
<img
|
||||
alt="Large image"
|
||||
src={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,29 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
/**
|
||||
* Render attribution or verification pixels.
|
||||
* https://docs.buysellads.com/ad-serving-api#pixels
|
||||
*/
|
||||
export 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>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './Ad';
|
||||
@@ -1,91 +0,0 @@
|
||||
'use server';
|
||||
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { AdClassicRendering } from './AdClassicRendering';
|
||||
import { AdCoverRendering } from './AdCoverRendering';
|
||||
import { AdPixels } from './AdPixels';
|
||||
import { AdItem, AdsResponse } from './types';
|
||||
|
||||
interface FetchAdOptions {
|
||||
/** ID of the zone to fetch Ads for */
|
||||
zoneId: string;
|
||||
/** ID of the space */
|
||||
spaceId: string;
|
||||
/** Mode to render the Ad */
|
||||
mode: 'classic' | 'auto' | 'cover';
|
||||
/** Name of the placement for the ad */
|
||||
placement: string;
|
||||
/** If true, we'll not track it as an impression */
|
||||
ignore: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Server action to render the Ad placement.
|
||||
* We use a server-action to avoid caching issues with server-side components,
|
||||
* and properly access user-agent and IP.
|
||||
*/
|
||||
export async function renderAd(options: FetchAdOptions) {
|
||||
const { spaceId, mode } = options;
|
||||
const ad = await fetchAd(options);
|
||||
if (!ad || !ad.description || !ad.statlink) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
{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')}>
|
||||
Sponsored via GitBook
|
||||
</a>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
async function fetchAd({ zoneId, placement, ignore }: FetchAdOptions): Promise<AdItem | null> {
|
||||
const headersSet = headers();
|
||||
const ip = headersSet.get('cf-connecting-ip') ?? headersSet.get('x-forwarded-for') ?? '';
|
||||
const userAgent = headersSet.get('user-agent') ?? '';
|
||||
|
||||
const url = new URL(`https://srv.buysellads.com/ads/${zoneId}.json`);
|
||||
url.searchParams.set('segment', `placement:${placement}`);
|
||||
url.searchParams.set('v', 'true');
|
||||
url.searchParams.set('forwardedip', ip);
|
||||
url.searchParams.set('useragent', userAgent);
|
||||
if (ignore) {
|
||||
url.searchParams.set('ignore', 'true');
|
||||
}
|
||||
|
||||
const res = await fetch(url);
|
||||
const json: AdsResponse = await res.json();
|
||||
|
||||
const first = json.ads[0];
|
||||
if (first && 'active' in first) {
|
||||
return first;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -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,73 +0,0 @@
|
||||
import { DocumentBlockContentRef } from '@gitbook/api';
|
||||
|
||||
import { Card, Emoji } from '@/components/primitives';
|
||||
import { getSpaceCustomization, ignoreAPIError } from '@/lib/api';
|
||||
import { ResolvedContentRef } from '@/lib/references';
|
||||
|
||||
import { BlockProps } from './Block';
|
||||
import { SpaceIcon } from '../Space/SpaceIcon';
|
||||
|
||||
export async function BlockContentRef(props: BlockProps<DocumentBlockContentRef>) {
|
||||
const { block, context, style } = props;
|
||||
|
||||
const resolved = await context.resolveContentRef(block.data.ref, {
|
||||
resolveAnchorText: true,
|
||||
iconStyle: ['text-xl', 'text-dark/6', 'dark:text-light/6'],
|
||||
});
|
||||
|
||||
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.icon ? resolved.icon : 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={
|
||||
<SpaceIcon
|
||||
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 @@
|
||||
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,80 +0,0 @@
|
||||
import * as gitbookAPI from '@gitbook/api';
|
||||
import { headers } from 'next/headers';
|
||||
import Script from 'next/script';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { Card } from '@/components/primitives';
|
||||
import { api } from '@/lib/api';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { BlockProps } from './Block';
|
||||
import { Caption } from './Caption';
|
||||
import { IntegrationBlock } from './Integration';
|
||||
|
||||
export async function Embed(props: BlockProps<gitbookAPI.DocumentBlockEmbed>) {
|
||||
const { block, context, ...otherProps } = props;
|
||||
const nonce = headers().get('x-nonce') || undefined;
|
||||
|
||||
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 }));
|
||||
|
||||
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)}
|
||||
/>
|
||||
) : (
|
||||
<Card
|
||||
leadingIcon={
|
||||
embed.icon ? (
|
||||
<img src={embed.icon} className={tcls('w-5', 'h-5')} alt="Logo" />
|
||||
) : null
|
||||
}
|
||||
href={block.data.url}
|
||||
title={embed.title}
|
||||
postTitle={embed.site}
|
||||
/>
|
||||
)}
|
||||
</Caption>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an integration block with an unfurl action from the GitBook Embed response.
|
||||
*/
|
||||
function createIntegrationBlock(
|
||||
url: string,
|
||||
integration: string,
|
||||
block: gitbookAPI.IntegrationBlock,
|
||||
): gitbookAPI.DocumentBlockIntegration {
|
||||
return {
|
||||
object: 'block',
|
||||
type: 'integration',
|
||||
isVoid: true,
|
||||
data: {
|
||||
integration,
|
||||
block: block.id,
|
||||
props: {},
|
||||
action: {
|
||||
action: '@link.unfurl',
|
||||
url,
|
||||
},
|
||||
url,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useHash } from '@/components/hooks';
|
||||
import { ClassValue, tcls } from '@/lib/tailwind';
|
||||
|
||||
/**
|
||||
* Details component rendered on client so it can expand dependent on url hash changes.
|
||||
*/
|
||||
export function Details(props: {
|
||||
children: React.ReactNode;
|
||||
id: string;
|
||||
contentIds?: string[];
|
||||
open?: boolean;
|
||||
className?: ClassValue;
|
||||
}) {
|
||||
const { children, id, className } = props;
|
||||
|
||||
const detailsRef = React.useRef<HTMLDetailsElement>(null);
|
||||
|
||||
const [openFromHash, setOpenFromHash] = React.useState(false);
|
||||
|
||||
const hash = useHash();
|
||||
/**
|
||||
* Open the details element if the url hash refers to the id of the details element
|
||||
* or the id of some element contained within the details element.
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
if (!hash || !detailsRef.current) {
|
||||
return;
|
||||
}
|
||||
if (hash === id) {
|
||||
setOpenFromHash(true);
|
||||
}
|
||||
const activeElement = document.getElementById(hash);
|
||||
setOpenFromHash(Boolean(activeElement && detailsRef.current?.contains(activeElement)));
|
||||
}, [hash, id]);
|
||||
|
||||
return (
|
||||
<details
|
||||
ref={detailsRef}
|
||||
id={id}
|
||||
open={props.open || openFromHash}
|
||||
className={tcls(
|
||||
className,
|
||||
'group/expandable',
|
||||
'shadow-dark/1',
|
||||
'bg-gradient-to-t',
|
||||
'from-light-1',
|
||||
'to-light-1',
|
||||
'border',
|
||||
'border-b-0',
|
||||
'border-dark-3/3',
|
||||
//all
|
||||
'[&]:mt-[0px]',
|
||||
//select first child
|
||||
'[&:first-child]:mt-5',
|
||||
'[&:first-child]:rounded-t-lg',
|
||||
//select first in group
|
||||
'[:not(&)_+&]:mt-5',
|
||||
'[:not(&)_+&]:rounded-t-lg',
|
||||
//select last in group
|
||||
'[&:not(:has(+_&))]:mb-5',
|
||||
'[&:not(:has(+_&))]:rounded-b-lg',
|
||||
'[&:not(:has(+_&))]:border-b',
|
||||
/* '[&:not(:has(+_&))]:shadow-1xs', */
|
||||
|
||||
'dark:border-light-2/[0.06]',
|
||||
'dark:from-dark-2',
|
||||
'dark:to-dark-2',
|
||||
'dark:shadow-none',
|
||||
|
||||
'group open:dark:to-dark-2/8',
|
||||
'group open:to-light-1/6',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</details>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Icon } from '@gitbook/icons';
|
||||
|
||||
import { SimplifiedFileType } from '@/lib/files';
|
||||
|
||||
/**
|
||||
* Render an appropriate icon for a file.
|
||||
*/
|
||||
export function FileIcon(props: { contentType: SimplifiedFileType | null; className: string }) {
|
||||
const { contentType, className } = props;
|
||||
|
||||
switch (contentType) {
|
||||
case 'pdf':
|
||||
return <Icon icon="file-pdf" className={className} />;
|
||||
case 'image':
|
||||
return <Icon icon="file-image" className={className} />;
|
||||
case 'archive':
|
||||
return <Icon icon="file-archive" className={className} />;
|
||||
default:
|
||||
return <Icon icon="file-download" className={className} />;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './DocumentView';
|
||||
export { createHighlightingContext } from './CodeBlock';
|
||||
@@ -1,49 +0,0 @@
|
||||
import { CustomizationHeaderLink } from '@gitbook/api';
|
||||
import React from 'react';
|
||||
|
||||
import { ContentRefContext, resolveContentRef } from '@/lib/references';
|
||||
|
||||
import { Dropdown, DropdownChevron, DropdownMenu, DropdownMenuItem } from './Dropdown';
|
||||
import styles from './headerLinks.module.css';
|
||||
|
||||
/**
|
||||
* Dropdown menu for header links hidden at small screen size.
|
||||
*/
|
||||
export function HeaderLinkMore(props: {
|
||||
label: React.ReactNode;
|
||||
links: CustomizationHeaderLink[];
|
||||
context: ContentRefContext;
|
||||
}) {
|
||||
const { label, links, context } = props;
|
||||
|
||||
const renderButton = () => (
|
||||
<button className="px-1">
|
||||
<span className="sr-only">{label}</span>
|
||||
<DropdownChevron />
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`${styles.linkEllipsis} items-center`}>
|
||||
<Dropdown button={renderButton} className="-translate-x-48 md:translate-x-0">
|
||||
<DropdownMenu>
|
||||
{links.map((link, index) => (
|
||||
<MoreMenuLink key={index} link={link} context={context} />
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function MoreMenuLink(props: { context: ContentRefContext; link: CustomizationHeaderLink }) {
|
||||
const { context, link } = props;
|
||||
|
||||
const target = await resolveContentRef(link.to, context);
|
||||
|
||||
if (!target) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <DropdownMenuItem href={target.href}>{link.title}</DropdownMenuItem>;
|
||||
}
|
||||
@@ -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,86 +0,0 @@
|
||||
.containerHeaderlinks {
|
||||
container-type: inline-size;
|
||||
container-name: headerlinks;
|
||||
}
|
||||
|
||||
.containerHeaderlinks > * {
|
||||
display: flex;
|
||||
}
|
||||
.linkEllipsis {
|
||||
display: none;
|
||||
& div > a {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@container headerlinks ( width < 150px ) {
|
||||
.containerHeaderlinks > :nth-child(n + 1) {
|
||||
display: none;
|
||||
}
|
||||
.containerHeaderlinks > :nth-child(n + 1) ~ .linkEllipsis {
|
||||
display: flex;
|
||||
& div > a:nth-of-type(n + 1) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@container headerlinks ( width < 300px ) {
|
||||
.containerHeaderlinks > :nth-child(n + 2) {
|
||||
display: none;
|
||||
}
|
||||
.containerHeaderlinks > :nth-child(n + 2) ~ .linkEllipsis {
|
||||
display: flex;
|
||||
& div > a:nth-of-type(n + 2) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@container headerlinks ( width < 450px ) {
|
||||
.containerHeaderlinks > :nth-child(n + 3) {
|
||||
display: none;
|
||||
}
|
||||
.containerHeaderlinks > :nth-child(n + 3) ~ .linkEllipsis {
|
||||
display: flex;
|
||||
& div > a:nth-of-type(n + 3) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@container headerlinks ( width < 600px ) {
|
||||
.containerHeaderlinks > :nth-child(n + 4) {
|
||||
display: none;
|
||||
}
|
||||
.containerHeaderlinks > :nth-child(n + 4) ~ .linkEllipsis {
|
||||
display: flex;
|
||||
& div > a:nth-of-type(n + 4) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@container headerlinks ( width < 750px ) {
|
||||
.containerHeaderlinks > :nth-child(n + 5) {
|
||||
display: none;
|
||||
}
|
||||
.containerHeaderlinks > :nth-child(n + 5) ~ .linkEllipsis {
|
||||
display: flex;
|
||||
& div > :nth-child(n + 5) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@container headerlinks ( width < 900px ) {
|
||||
.containerHeaderlinks > :nth-child(n + 6) {
|
||||
display: none;
|
||||
}
|
||||
.containerHeaderlinks > :nth-child(n + 6) ~ .linkEllipsis {
|
||||
display: flex;
|
||||
& div > a:nth-of-type(n + 6) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@container headerlinks ( width > 900px ) {
|
||||
.containerHeaderlinks > *:not(.linkEllipsis) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@@ -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,27 +0,0 @@
|
||||
import { RevisionPage } from '@gitbook/api';
|
||||
import { Icon, IconName } from '@gitbook/icons';
|
||||
|
||||
import { Emoji } from '@/components/primitives';
|
||||
import { ClassValue, tcls } from '@/lib/tailwind';
|
||||
|
||||
export function PageIcon(props: { page: RevisionPage; style?: ClassValue }) {
|
||||
const { page, style } = props;
|
||||
|
||||
if (page.emoji) {
|
||||
return (
|
||||
<Emoji
|
||||
code={page.emoji}
|
||||
style={[
|
||||
style,
|
||||
// We reset the color that could be passed as "style"
|
||||
// as emojis should always be rendered with normal text opacity
|
||||
'text-inherit',
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (page.icon) {
|
||||
return <Icon icon={page.icon as IconName} className={tcls('size-[1em]', style)} />;
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './PageIcon';
|
||||
@@ -1,294 +0,0 @@
|
||||
'use server';
|
||||
|
||||
import {
|
||||
Collection,
|
||||
RevisionPage,
|
||||
SearchAIAnswer,
|
||||
SearchPageResult,
|
||||
Site,
|
||||
SiteSpace,
|
||||
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';
|
||||
import { resolvePageId } from '@/lib/pages';
|
||||
import { filterOutNullable } from '@/lib/typescript';
|
||||
|
||||
import { DocumentView } from '../DocumentView';
|
||||
|
||||
export type OrderedComputedResult = ComputedPageResult | ComputedSectionResult;
|
||||
|
||||
export interface ComputedSectionResult {
|
||||
type: 'section';
|
||||
id: string;
|
||||
title: string;
|
||||
href: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface ComputedPageResult {
|
||||
type: 'page';
|
||||
id: string;
|
||||
title: string;
|
||||
href: string;
|
||||
|
||||
/** When part of a multi-spaces search, the title of the space */
|
||||
spaceTitle?: string;
|
||||
}
|
||||
|
||||
export interface AskAnswerSource {
|
||||
id: string;
|
||||
title: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export interface AskAnswerResult {
|
||||
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 [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Server action to search content in a space
|
||||
*/
|
||||
export async function searchSpaceContent(
|
||||
spaceId: string,
|
||||
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)
|
||||
*/
|
||||
export async function searchParentContent(
|
||||
parent: Site | Collection,
|
||||
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,
|
||||
]);
|
||||
|
||||
let spaces: Space[] = collectionSpaces ? collectionSpaces : [];
|
||||
|
||||
return data.items
|
||||
.map((spaceItem) => {
|
||||
const space = spaces.find((space) => space.id === spaceItem.id);
|
||||
return spaceItem.pages.map((item) => transformPageResult(item, space));
|
||||
})
|
||||
.flat(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 pagesPromise = api.getSpaceContentData({ spaceId }, undefined);
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* List suggested questions for a space.
|
||||
*/
|
||||
export async function getRecommendedQuestions(spaceId: string): Promise<string[]> {
|
||||
const data = await api.getRecommendedQuestionsInSpace(spaceId);
|
||||
return data.questions;
|
||||
}
|
||||
|
||||
function transformAnswer(
|
||||
answer: SearchAIAnswer | undefined,
|
||||
pages: RevisionPage[],
|
||||
): AskAnswerResult | null {
|
||||
if (!answer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sources = answer.sources
|
||||
.map((source) => {
|
||||
if (source.type !== 'page') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const page = resolvePageId(pages, source.page);
|
||||
if (!page) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: page.page.id,
|
||||
title: page.page.title,
|
||||
href: pageHref(pages, page.page),
|
||||
};
|
||||
})
|
||||
.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,
|
||||
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;
|
||||
|
||||
// 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 += '/';
|
||||
}
|
||||
if (path.startsWith('/')) {
|
||||
path = path.slice(1);
|
||||
}
|
||||
return spaceURL + path;
|
||||
} else {
|
||||
return absoluteHref(path);
|
||||
}
|
||||
};
|
||||
|
||||
const sections =
|
||||
item.sections?.map<ComputedSectionResult>((section) => ({
|
||||
type: 'section',
|
||||
id: item.id + '/' + section.id,
|
||||
title: section.title,
|
||||
href: getURL(section.path, spaceURL),
|
||||
body: section.body,
|
||||
})) ?? [];
|
||||
|
||||
const page: ComputedPageResult = {
|
||||
type: 'page',
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
href: getURL(item.path, spaceURL),
|
||||
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];
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { CustomizationThemedURL } from '@gitbook/api';
|
||||
|
||||
import { Image } from '@/components/utils';
|
||||
import { absoluteHref } from '@/lib/links';
|
||||
|
||||
import { Emoji } from '../primitives';
|
||||
|
||||
/**
|
||||
* Icon for a space.
|
||||
*/
|
||||
export function SpaceIcon(
|
||||
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,23 +0,0 @@
|
||||
import { RevisionPage } from '@gitbook/api';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { PageIcon } from '../PageIcon';
|
||||
|
||||
/**
|
||||
* Styled page icon for the table of contents.
|
||||
*/
|
||||
export function TOCPageIcon({ page }: { page: RevisionPage }) {
|
||||
return (
|
||||
<PageIcon
|
||||
page={page}
|
||||
style={tcls(
|
||||
'text-base',
|
||||
'text-dark/6',
|
||||
'dark:text-light/6',
|
||||
'group-aria-selected/toclink:text-primary',
|
||||
'group-aria-selected/toclink:dark:text-primary-400',
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { useParams } from 'next/navigation';
|
||||
import React from 'react';
|
||||
|
||||
export function useHash() {
|
||||
const params = useParams();
|
||||
const [hash, setHash] = React.useState<string>(global.location?.hash?.slice(1));
|
||||
React.useEffect(() => {
|
||||
function updateHash() {
|
||||
setHash(global.location?.hash?.slice(1));
|
||||
}
|
||||
global.addEventListener('hashchange', updateHash);
|
||||
updateHash();
|
||||
return () => global.removeEventListener('hashchange', updateHash);
|
||||
// With next.js, the hashchange event is not triggered when the hash changes
|
||||
// Instead a hack is to use the `useParams` hook to listen to changes in the hash
|
||||
// https://github.com/vercel/next.js/discussions/49465#discussioncomment-5845312
|
||||
}, [params]);
|
||||
return hash;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useHash } from './useHash';
|
||||
|
||||
/**
|
||||
* Scroll to the current URL hash everytime the URL changes.
|
||||
*/
|
||||
export function useScrollToHash() {
|
||||
const hash = useHash();
|
||||
React.useLayoutEffect(() => {
|
||||
if (hash) {
|
||||
const element = document.getElementById(hash);
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
block: 'start',
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [hash]);
|
||||
}
|
||||
@@ -1,57 +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.',
|
||||
more: 'Mehr',
|
||||
};
|
||||
@@ -1,57 +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.',
|
||||
more: 'Mais',
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
export type SimplifiedFileType = 'image' | 'pdf' | 'archive';
|
||||
|
||||
/**
|
||||
* Get a simplified content type for the given mime type.
|
||||
*/
|
||||
export function getSimplifiedContentType(mimeType: string): SimplifiedFileType | null {
|
||||
if (mimeType.startsWith('image')) {
|
||||
return 'image';
|
||||
}
|
||||
|
||||
switch (mimeType) {
|
||||
case 'application/pdf':
|
||||
case 'application/x-pdf':
|
||||
return 'pdf';
|
||||
case 'application/zip':
|
||||
case 'application/x-7z-compressed':
|
||||
case 'application/x-zip-compressed':
|
||||
case 'application/x-tar':
|
||||
case 'application/x-rar-compressed':
|
||||
case 'application/vnd.rar':
|
||||
return 'archive';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { describe, it, expect } from 'bun:test';
|
||||
|
||||
import { parseMarkdown } from './markdown';
|
||||
|
||||
describe('parseMarkdown', () => {
|
||||
it('should parse a simple table', async () => {
|
||||
const result = await parseMarkdown(`## Table
|
||||
|
||||
| a | b | c | d |
|
||||
| - | :- | -: | :-: |`);
|
||||
|
||||
expect(result).toContain('<table>');
|
||||
});
|
||||
|
||||
it('should parse a complex table', async () => {
|
||||
const result =
|
||||
await parseMarkdown(`Returns information for all non-fungible tokens for an account.
|
||||
|
||||
## Ordering
|
||||
When considering NFTs, their order is governed by a combination of their numerical **token.Id** and **serialnumber** values, with **token.id** being the parent column.
|
||||
A serialnumbers value governs its order within the given token.id
|
||||
|
||||
In that regard, if a user acquired a set of NFTs in the order (2-2, 2-4 1-5, 1-1, 1-3, 3-3, 3-4), the following layouts illustrate the ordering expectations for ownership listing
|
||||
1. **All NFTs in ASC order**: 1-1, 1-3, 1-5, 2-2, 2-4, 3-3, 3-4
|
||||
2. **All NFTs in DESC order**: 3-4, 3-3, 2-4, 2-2, 1-5, 1-3, 1-1
|
||||
3. **NFTs above 1-1 in ASC order**: 1-3, 1-5, 2-2, 2-4, 3-3, 3-4
|
||||
4. **NFTs below 3-3 in ASC order**: 1-1, 1-3, 1-5, 2-2, 2-4
|
||||
5. **NFTs between 1-3 and 3-3 inclusive in DESC order**: 3-4, 3-3, 2-4, 2-2, 1-5, 1-3
|
||||
|
||||
Note: The default order for this API is currently DESC
|
||||
|
||||
## Filtering
|
||||
When filtering there are some restrictions enforced to ensure correctness and scalability.
|
||||
|
||||
**The table below defines the restrictions and support for the NFT ownership endpoint**
|
||||
|
||||
| Query Param | Comparison Operator | Support | Description | Example |
|
||||
| ------------- | ------------------- | ------- | --------------------- | ------- |
|
||||
| token.id | eq | Y | Single occurrence only. | ?token.id=X |
|
||||
| | ne | N | | |
|
||||
| | lt(e) | Y | Single occurrence only. | ?token.id=lte:X |
|
||||
| | gt(e) | Y | Single occurrence only. | ?token.id=gte:X |
|
||||
| serialnumber | eq | Y | Single occurrence only. Requires the presence of a **token.id** query | ?serialnumber=Y |
|
||||
| | ne | N | | |
|
||||
| | lt(e) | Y | Single occurrence only. Requires the presence of an **lte** or **eq** **token.id** query | ?token.id=lte:X&serialnumber=lt:Y |
|
||||
| | gt(e) | Y | Single occurrence only. Requires the presence of an **gte** or **eq** **token.id** query | ?token.id=gte:X&serialnumber=gt:Y |
|
||||
| spender.id | eq | Y | | ?spender.id=Z |
|
||||
| | ne | N | | |
|
||||
| | lt(e) | Y | | ?spender.id=lt:Z |
|
||||
| | gt(e) | Y | | ?spender.id=gt:Z |
|
||||
|
||||
Note: When searching across a range for individual NFTs a **serialnumber** with an additional **token.id** query filter must be provided.
|
||||
Both filters must be a single occurrence of **gt(e)** or **lt(e)** which provide a lower and or upper boundary for search.`);
|
||||
|
||||
expect(result).toContain('<table>');
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
/**
|
||||
* Return true if a space track page views.
|
||||
*/
|
||||
export function shouldTrackPageViews(): boolean {
|
||||
const headerSet = headers();
|
||||
|
||||
if (
|
||||
process.env.GITBOOK_BLOCK_PAGE_VIEWS_TRACKING &&
|
||||
!headerSet.has('x-gitbook-track-page-views')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -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,2 +0,0 @@
|
||||
dist/
|
||||
src/data/*.json
|
||||
@@ -1,11 +0,0 @@
|
||||
# `@gitbook/icons`
|
||||
|
||||
Icon sets useed in GitBook content.
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { Icon } from '@gitbook/icons';
|
||||
|
||||
<Icon name="x" className={'size-5'} />;
|
||||
```
|
||||
@@ -1,75 +0,0 @@
|
||||
import fs from 'fs/promises';
|
||||
import { existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
import url from 'url';
|
||||
|
||||
import { getKitPath } from './kit.js';
|
||||
|
||||
/**
|
||||
* Scripts to generate the list of all icons.
|
||||
*/
|
||||
async function main() {
|
||||
const source = getKitPath();
|
||||
const icons = JSON.parse(
|
||||
await fs.readFile(path.join(source, 'metadata/icon-families.json'), 'utf8'),
|
||||
);
|
||||
|
||||
// Only these families have exceptions
|
||||
const potentialOnly = ['brands', 'custom-icons'];
|
||||
|
||||
const keyRename = {
|
||||
// Font Awesome stores custom icons under "custom-icons"
|
||||
// but indicates them as svg.kit.custom in the JSON data
|
||||
'kit-custom': 'custom-icons',
|
||||
};
|
||||
|
||||
const onlyStyles = {};
|
||||
|
||||
const result = [];
|
||||
Object.entries(icons).forEach(([icon, iconSpec]) => {
|
||||
const stylesCovered = [];
|
||||
|
||||
Object.entries(iconSpec.svgs).forEach(([family, familyEntries]) => {
|
||||
Object.keys(familyEntries).forEach((style) => {
|
||||
let key = family !== 'classic' ? `${family}-${style}` : style;
|
||||
key = keyRename[key] ?? key;
|
||||
stylesCovered.push(key);
|
||||
});
|
||||
});
|
||||
|
||||
potentialOnly.forEach((style) => {
|
||||
if (stylesCovered.includes(style)) {
|
||||
onlyStyles[style] = onlyStyles[style] || [];
|
||||
onlyStyles[style].push(icon);
|
||||
}
|
||||
});
|
||||
|
||||
result.push({
|
||||
icon,
|
||||
label: iconSpec.label,
|
||||
search: iconSpec.search?.terms,
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
writeDataFile('styles-map', JSON.stringify(onlyStyles, null, 2)),
|
||||
writeDataFile('icons', JSON.stringify(result, null, 2)),
|
||||
]);
|
||||
console.log(`🎉 ${result.length} icons found`);
|
||||
}
|
||||
|
||||
async function writeDataFile(name, content) {
|
||||
const dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
||||
await Promise.all([
|
||||
fs.writeFile(path.resolve(dirname, `../src/data/${name}.json`), content),
|
||||
// Write to dist folder if it exists
|
||||
existsSync(path.resolve(dirname, '../dist/'))
|
||||
? fs.writeFile(path.resolve(dirname, `../dist/data/${name}.json`), content)
|
||||
: null,
|
||||
]);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,57 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'fs/promises';
|
||||
import { existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { getKitPath } from './kit.js';
|
||||
|
||||
const allStyles = ['brands', 'duotone', 'solid', 'regular', 'light', 'thin', 'custom-icons'];
|
||||
|
||||
/**
|
||||
* Scripts to copy the assets to a public folder.
|
||||
*/
|
||||
async function main() {
|
||||
const outputFolder = path.resolve(process.cwd(), process.argv[2] ?? 'public/icons');
|
||||
const stylesToCopy = (process.argv[3] ? process.argv[3].split(',') : allStyles).filter(
|
||||
(style) => allStyles.includes(style),
|
||||
);
|
||||
const source = getKitPath();
|
||||
|
||||
// Create the output folder if it doesn't exist
|
||||
await fs.mkdir(outputFolder, { recursive: true });
|
||||
|
||||
const printOutputFolder = path.relative(process.cwd(), outputFolder);
|
||||
console.log(`🚚 Copying icons of styles ${stylesToCopy.join(', ')} to ${printOutputFolder}`);
|
||||
|
||||
// Copy the assets from
|
||||
// source/sprites to outputFolder/sprites
|
||||
// source/svgs to outputFolder/svgs
|
||||
await Promise.all([
|
||||
...stylesToCopy.map((style) => {
|
||||
const stylePath = path.join(source, 'svgs', style);
|
||||
if (!existsSync(stylePath)) {
|
||||
console.warn(`❌ Style ${style} does not exist`);
|
||||
} else {
|
||||
return fs.cp(stylePath, path.join(outputFolder, 'svgs', style), {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
}),
|
||||
...stylesToCopy.map((style) => {
|
||||
const spritePath = path.join(source, `sprites/${style}.svg`);
|
||||
if (existsSync(spritePath)) {
|
||||
return fs.cp(
|
||||
path.join(source, `sprites/${style}.svg`),
|
||||
path.join(outputFolder, 'sprites', style + '.svg'),
|
||||
);
|
||||
}
|
||||
}),
|
||||
]);
|
||||
|
||||
console.log(`🎉 Icons copied to ${printOutputFolder}`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
import path from 'path';
|
||||
import url from 'url';
|
||||
|
||||
/**
|
||||
* Get the path to the kit, depending on the Pro or Free version.
|
||||
*/
|
||||
export function getKitPath() {
|
||||
let source = path.dirname(
|
||||
url.fileURLToPath(import.meta.resolve('@fortawesome/fontawesome-free/package.json')),
|
||||
);
|
||||
try {
|
||||
source = path.resolve(
|
||||
path.dirname(
|
||||
url.fileURLToPath(import.meta.resolve('@awesome.me/kit-a463935e93/package.json')),
|
||||
),
|
||||
'icons',
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Could not find the Pro kit, using the free kit instead');
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"name": "@gitbook/icons",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./icons": {
|
||||
"types": "./dist/icons.d.ts",
|
||||
"development": "./src/icons.ts",
|
||||
"default": "./dist/icons.js"
|
||||
},
|
||||
"./style.css": {
|
||||
"default": "./src/style.css"
|
||||
}
|
||||
},
|
||||
"version": "0.0.3",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
||||
"@fortawesome/fontawesome-free": "^6.6.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@awesome.me/kit-a463935e93": "1.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"generate": "node ./bin/gen-list.js",
|
||||
"build": "tsc",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "tsc -w"
|
||||
},
|
||||
"bin": {
|
||||
"gitbook-icons": "./bin/gitbook-icons.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"bin",
|
||||
"data",
|
||||
"README.md",
|
||||
"CHANGELOG.md"
|
||||
]
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { IconName, IconStyle } from './types';
|
||||
import { getIconStyle } from './getIconStyle';
|
||||
import { getIconAssetURL, useIcons } from './IconsProvider';
|
||||
|
||||
/**
|
||||
* Props for the icon component.
|
||||
*/
|
||||
export type IconProps = React.SVGProps<SVGSVGElement> & {
|
||||
/**
|
||||
* Name of the icon to render.
|
||||
*/
|
||||
icon: IconName;
|
||||
|
||||
/**
|
||||
* Style of the icon to render.
|
||||
* @default 'solid'
|
||||
*/
|
||||
iconStyle?: IconStyle;
|
||||
|
||||
/**
|
||||
* Class name to apply to the icon.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* Size of the icon.
|
||||
* If none is defined, the size should be defined using CSS / class name.
|
||||
*/
|
||||
size?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders an icon component from the library.
|
||||
* The icon is loaded as a standalone image.
|
||||
*/
|
||||
export const Icon = React.forwardRef(function Icon(
|
||||
props: IconProps,
|
||||
ref: React.Ref<SVGSVGElement>,
|
||||
) {
|
||||
const context = useIcons();
|
||||
const {
|
||||
icon: propIcon,
|
||||
iconStyle: propIconStyle = context.iconStyle,
|
||||
className = '',
|
||||
size,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const [iconStyle, icon] = getIconStyle(propIconStyle, propIcon);
|
||||
const url = getIconAssetURL(context, iconStyle, icon);
|
||||
|
||||
return (
|
||||
<svg
|
||||
ref={ref}
|
||||
{...rest}
|
||||
style={{
|
||||
maskImage: `url(${url})`,
|
||||
maskRepeat: 'no-repeat',
|
||||
maskPosition: 'center',
|
||||
...(size ? { width: size, height: size } : {}),
|
||||
...rest.style,
|
||||
}}
|
||||
className={'gb-icon ' + className}
|
||||
></svg>
|
||||
);
|
||||
});
|
||||
@@ -1,77 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { IconStyle } from './types';
|
||||
|
||||
const version = 1;
|
||||
|
||||
export interface IconsAssetsLocation {
|
||||
/** Rroot url where the icon assets are served */
|
||||
assetsURL: string;
|
||||
/** Token to be passed in the URL for assets */
|
||||
assetsURLToken?: string;
|
||||
}
|
||||
|
||||
export type IconsContextType = Partial<IconsAssetsLocation> & {
|
||||
/** Assets location for special styles */
|
||||
assetsByStyles?: Record<string, IconsAssetsLocation>;
|
||||
/** Current default style for icons */
|
||||
iconStyle: IconStyle;
|
||||
};
|
||||
|
||||
const IconsContext = React.createContext<IconsContextType>({
|
||||
iconStyle: IconStyle.Regular,
|
||||
});
|
||||
|
||||
/**
|
||||
* Provider to control the loading of icons.
|
||||
*/
|
||||
export function IconsProvider(props: React.PropsWithChildren<Partial<IconsContextType>>) {
|
||||
const parent = React.useContext(IconsContext);
|
||||
const {
|
||||
children,
|
||||
assetsURL = parent.assetsURL,
|
||||
assetsURLToken = parent.assetsURLToken,
|
||||
iconStyle = parent.iconStyle,
|
||||
assetsByStyles = parent.assetsByStyles,
|
||||
} = props;
|
||||
const value = React.useMemo(() => {
|
||||
return { assetsURL, assetsURLToken, iconStyle, assetsByStyles };
|
||||
}, [assetsURL, assetsURLToken, iconStyle, assetsByStyles]);
|
||||
|
||||
return <IconsContext.Provider value={value}>{children}</IconsContext.Provider>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to access the current icons context.
|
||||
*/
|
||||
export function useIcons(): IconsContextType {
|
||||
return React.useContext(IconsContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL for an asset.
|
||||
*/
|
||||
export function getAssetURL(location: Partial<IconsAssetsLocation>, path: string): string {
|
||||
if (!location.assetsURL) {
|
||||
throw new Error('You first need to pass a assetsURL to <IconsProvider>');
|
||||
}
|
||||
const rawUrl =
|
||||
location.assetsURL + (location.assetsURL.endsWith('/') ? '' : '/') + path + `?v=${version}`;
|
||||
|
||||
if (location.assetsURLToken) {
|
||||
const url = new URL(rawUrl);
|
||||
url.searchParams.set('token', location.assetsURLToken);
|
||||
return url.toString();
|
||||
} else {
|
||||
return rawUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL for the SVG of an icon.
|
||||
*/
|
||||
export function getIconAssetURL(context: IconsContextType, style: string, icon: string): string {
|
||||
const location = context.assetsByStyles?.[style] ?? context;
|
||||
return getAssetURL(location, `svgs/${style}/${icon}.svg`);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
Files in this folder are generated in postinstall script.
|
||||
@@ -1,33 +0,0 @@
|
||||
import stylesMap from './data/styles-map.json';
|
||||
import { IconName, IconStyle } from './types';
|
||||
|
||||
const cache = new Map<IconName, Map<IconStyle, [string, IconName]>>();
|
||||
|
||||
/**
|
||||
* Return the style to load an icon from by its name.
|
||||
* Some icons are only available for certain styles.
|
||||
*/
|
||||
export function getIconStyle(style: IconStyle, icon: IconName): [string, IconName] {
|
||||
const cached = cache.get(icon)?.get(style);
|
||||
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Check for exceptions
|
||||
let result = [style, icon] as [string, IconName];
|
||||
|
||||
for (const [onlyStyle, icons] of Object.entries(stylesMap)) {
|
||||
if (icons.includes(icon)) {
|
||||
result = [onlyStyle, icon];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!cache.has(icon)) {
|
||||
cache.set(icon, new Map());
|
||||
}
|
||||
cache.get(icon)!.set(style, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { IconName, IconStyle } from './types';
|
||||
import rawIcons from './data/icons.json';
|
||||
|
||||
export interface IconStyleDefinition {
|
||||
title: string;
|
||||
style: IconStyle;
|
||||
}
|
||||
|
||||
export interface IconDefinition {
|
||||
icon: IconName;
|
||||
label: string;
|
||||
search?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* List of all icons available in the library.
|
||||
*/
|
||||
// @ts-ignore
|
||||
export const icons: IconDefinition[] = rawIcons;
|
||||
|
||||
let iconNamesSet: Set<string> | null = null;
|
||||
|
||||
/**
|
||||
* Validate that the icon name is valid.
|
||||
*/
|
||||
export function validateIconName(icon: IconName | string): icon is IconName {
|
||||
if (!iconNamesSet) {
|
||||
iconNamesSet = new Set(icons.map((icon) => icon.icon));
|
||||
}
|
||||
|
||||
return iconNamesSet.has(icon);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './Icon';
|
||||
export * from './types';
|
||||
export * from './IconsProvider';
|
||||
@@ -1,11 +0,0 @@
|
||||
/* Normal */
|
||||
svg.gb-icon {
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
/* Sprite */
|
||||
svg.gb-icon-s path,
|
||||
svg.gb-icon-s use {
|
||||
fill: currentColor;
|
||||
stroke: currentColor;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { IconName as BasicIconName } from '@fortawesome/fontawesome-svg-core';
|
||||
|
||||
export enum IconStyle {
|
||||
// Regular
|
||||
Regular = 'regular',
|
||||
SharpRegular = 'sharp-regular',
|
||||
// Solid
|
||||
Solid = 'solid',
|
||||
SharpSolid = 'sharp-solid',
|
||||
// Duotone
|
||||
Duotone = 'duotone',
|
||||
SharpDuotoneSolid = 'sharp-duotone-solid',
|
||||
// Light
|
||||
Light = 'light',
|
||||
SharpLight = 'sharp-light',
|
||||
// Thin
|
||||
Thin = 'thin',
|
||||
SharpThin = 'sharp-thin',
|
||||
}
|
||||
|
||||
type CustomIconName = 'gitbook';
|
||||
|
||||
export type IconName = BasicIconName | CustomIconName;
|
||||
@@ -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 +0,0 @@
|
||||
dist/
|
||||
@@ -1,31 +0,0 @@
|
||||
# @gitbook/react-contentkit
|
||||
|
||||
## 0.5.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 3445db4: Fix files published in the NPM packages by defining "files" in "package.json"
|
||||
|
||||
## 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
|
||||
@@ -1,33 +1,12 @@
|
||||
{
|
||||
"name": "@gitbook/react-contentkit",
|
||||
"version": "0.5.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.58.0",
|
||||
"@gitbook/api": "^0.36.0",
|
||||
"assert-never": "^1.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "tsc -w"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"README.md",
|
||||
"CHANGELOG.md"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { type ContentKitRenderOutput } from '@gitbook/api';
|
||||
import React from 'react';
|
||||
|
||||
import { Element } from './Element';
|
||||
import { ContentKitServerContext } from './types';
|
||||
|
||||
@@ -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';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user