mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-19 17:15:24 +00:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 538aea26ac | |||
| 07eab986d5 | |||
| 711cf38f9b | |||
| 4f7c0eea9f | |||
| f3448de2da | |||
| ec92075ef8 | |||
| 1e013cd87c | |||
| f033734308 | |||
| 50c05164f7 | |||
| 7fb9004b43 | |||
| 4721403986 | |||
| df848ef64e | |||
| 0b40ccd286 | |||
| caaa692f0b | |||
| 42d43e09c4 | |||
| 7ccbf55ba7 | |||
| 81f5bfc2f9 | |||
| a7a713bca2 | |||
| d6613c787f | |||
| 427f748e1c | |||
| 4f5fec7213 | |||
| 8c0a53ab40 | |||
| 2dfd5aab20 | |||
| 8d3c6562a2 | |||
| dae019c115 | |||
| 73e0cbb2d6 | |||
| 392f59450c | |||
| b4918f60ce | |||
| 8f7c304d58 | |||
| a3a944d7dd | |||
| 88a35ed057 | |||
| 67998b6f15 | |||
| ff6d1150a5 | |||
| 500c8cb649 | |||
| 6859f7d239 | |||
| dfa8a37be1 | |||
| 11a6511b7a | |||
| af98402655 | |||
| b7a0db3339 | |||
| 382a19885b | |||
| d99da6a3ae | |||
| 72cd0e59e6 | |||
| 42d88da73c | |||
| 2863fe0dc1 | |||
| 87218062be |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix UX issue about highlighting the search term in search result sections
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Hide scrollbar on sections
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Add support for inline icons.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix crash when integration script fails to render block.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Show scrollbars
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
Optimize performances by using a smarter per-request cache arround data cached functions
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Add support for icons in buttons.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Fix rendering of ogimage with SVG logos.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix crash when integration is triggering invalid requests.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
Optimize the fetch of revision files by using only the getRevision cache.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix CodeBlock layout
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Scroll to active TOC when clicking a link
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Add support for text alignment for headings and paragraphs.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@gitbook/react-contentkit": patch
|
||||
---
|
||||
|
||||
Add basic error handling when transitioning between states.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Don't crash ogimage generation on RTL text, as a workaround until we can support it.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Add support for site customization option to change how external links open.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix rendering of ogimage when logo or icon are AVIF images.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix ogimage generation failing with some JPEG images.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix page group not expanded by default
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
Don't cache unexpected API errors for more than a few minutes.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
Fix an issue where PDF export URLs were not keeping their query params.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Ignore case while highlighting search results.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Consistently show variant selector in section bar if site has sections
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Clicking an active TOC item toggles its descendants
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
Fix InlineLinkTooltip having a negative impact on performance, especially on larger pages.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@gitbook/openapi-parser': patch
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Optional label in OpenAPI x-codeSamples
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Add metadata for adding site to Apple devices home
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix crash during rendering of ogimage for VA sites with default icon.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Allow to zoom images on mobile if relevant
|
||||
@@ -0,0 +1,3 @@
|
||||
# Changes to the API data cache functions can invalidate all existing data cache
|
||||
# causing a massive amount of revalidation, impacting our API.
|
||||
packages/gitbook-v2/src/lib/data/api.ts @SamyPesse
|
||||
@@ -56,13 +56,19 @@ git clone https://github.com/gitbookIO/gitbook.git
|
||||
bun install
|
||||
```
|
||||
|
||||
4. Start your local development server.
|
||||
4. Run build.
|
||||
|
||||
```
|
||||
bun build:v2
|
||||
```
|
||||
|
||||
5. Start your local development server.
|
||||
|
||||
```
|
||||
bun dev:v2
|
||||
```
|
||||
|
||||
5. Open a published GitBook space in your web browser, prefixing it with `http://localhost:3000/`.
|
||||
6. Open a published GitBook space in your web browser, prefixing it with `http://localhost:3000/`.
|
||||
|
||||
examples:
|
||||
|
||||
|
||||
@@ -85,8 +85,10 @@
|
||||
"assert-never": "^1.2.1",
|
||||
"bun-types": "^1.1.20",
|
||||
"classnames": "^2.5.1",
|
||||
"direction": "^2.0.1",
|
||||
"event-iterator": "^2.0.0",
|
||||
"framer-motion": "^10.16.14",
|
||||
"image-size": "^2.0.2",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jsontoxml": "^1.0.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
@@ -169,6 +171,7 @@
|
||||
"assert-never": "^1.2.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"next": "^15.3.2",
|
||||
"object-identity": "^0.1.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"rison": "^0.1.1",
|
||||
@@ -254,7 +257,7 @@
|
||||
"version": "1.3.0",
|
||||
"dependencies": {
|
||||
"@gitbook/openapi-parser": "workspace:*",
|
||||
"@scalar/api-client-react": "^1.2.19",
|
||||
"@scalar/api-client-react": "^1.3.16",
|
||||
"@scalar/oas-utils": "^0.2.130",
|
||||
"clsx": "^2.1.1",
|
||||
"flatted": "^3.2.9",
|
||||
@@ -283,7 +286,7 @@
|
||||
"react-dom": "^19.0.0",
|
||||
},
|
||||
"catalog": {
|
||||
"@gitbook/api": "^0.120.0",
|
||||
"@gitbook/api": "^0.123.0",
|
||||
},
|
||||
"packages": {
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@1.1.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew=="],
|
||||
@@ -648,7 +651,7 @@
|
||||
|
||||
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="],
|
||||
|
||||
"@gitbook/api": ["@gitbook/api@0.120.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-FiRmPiSBwobMxmNjd14QkkOdM95BAPLDDRShgpS9Vsd8lHjNMyZfrJKVJTsJUuFcgYoi4cqNw9yu/TiUBUgv3g=="],
|
||||
"@gitbook/api": ["@gitbook/api@0.123.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-BszoIk4H/wWb0jnGSJPSSXSAO8tV4B4/LHwl5ygfERHqk9rOGpM/U7+yOlmbX/tnm30vE+MQ3QQ5i8hN8wVk1w=="],
|
||||
|
||||
"@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"],
|
||||
|
||||
@@ -838,6 +841,8 @@
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@phosphor-icons/core": ["@phosphor-icons/core@2.1.1", "", {}, "sha512-v4ARvrip4qBCImOE5rmPUylOEK4iiED9ZyKjcvzuezqMaiRASCHKcRIuvvxL/twvLpkfnEODCOJp5dM4eZilxQ=="],
|
||||
|
||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||
|
||||
"@playwright/test": ["@playwright/test@1.51.1", "", { "dependencies": { "playwright": "1.51.1" }, "bin": { "playwright": "cli.js" } }, "sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q=="],
|
||||
@@ -1122,19 +1127,21 @@
|
||||
|
||||
"@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="],
|
||||
|
||||
"@scalar/api-client": ["@scalar/api-client@2.3.19", "", { "dependencies": { "@headlessui/tailwindcss": "^0.2.0", "@headlessui/vue": "^1.7.20", "@scalar/components": "0.13.47", "@scalar/draggable": "0.1.11", "@scalar/icons": "0.1.3", "@scalar/import": "0.3.13", "@scalar/oas-utils": "0.2.130", "@scalar/object-utils": "1.1.13", "@scalar/openapi-parser": "0.10.14", "@scalar/openapi-types": "0.2.0", "@scalar/postman-to-openapi": "0.2.3", "@scalar/snippetz": "0.2.19", "@scalar/themes": "0.9.86", "@scalar/types": "0.1.7", "@scalar/use-codemirror": "0.11.92", "@scalar/use-hooks": "0.1.40", "@scalar/use-toasts": "0.7.9", "@scalar/use-tooltip": "1.0.6", "@vueuse/core": "^10.10.0", "@vueuse/integrations": "^11.2.0", "focus-trap": "^7", "fuse.js": "^7.0.0", "microdiff": "^1.4.0", "nanoid": "^5.1.5", "pretty-bytes": "^6.1.1", "pretty-ms": "^8.0.0", "shell-quote": "^1.8.1", "type-fest": "^4.20.0", "vue": "^3.5.12", "vue-router": "^4.3.0", "whatwg-mimetype": "^4.0.0", "yaml": "^2.4.5", "zod": "^3.23.8" } }, "sha512-1Scff4QL6UExxcmSYv5j1dktvQZTXbmDUJp99RqmUROEhneNWEeWaZe+GZWsda5mvDoe4vP9zZKaymkulBDKYQ=="],
|
||||
"@scalar/api-client": ["@scalar/api-client@2.5.11", "", { "dependencies": { "@headlessui/tailwindcss": "^0.2.0", "@headlessui/vue": "^1.7.20", "@scalar/components": "0.14.12", "@scalar/draggable": "0.2.0", "@scalar/helpers": "0.0.4", "@scalar/icons": "0.4.4", "@scalar/import": "0.4.11", "@scalar/oas-utils": "0.4.7", "@scalar/object-utils": "1.2.1", "@scalar/openapi-parser": "0.18.0", "@scalar/openapi-types": "0.3.3", "@scalar/postman-to-openapi": "0.3.10", "@scalar/snippetz": "0.3.1", "@scalar/themes": "0.13.4", "@scalar/types": "0.2.3", "@scalar/use-codemirror": "0.12.13", "@scalar/use-hooks": "0.2.2", "@scalar/use-toasts": "0.8.0", "@scalar/use-tooltip": "1.1.0", "@vueuse/core": "^10.10.0", "@vueuse/integrations": "^11.2.0", "focus-trap": "^7", "fuse.js": "^7.0.0", "microdiff": "^1.4.0", "nanoid": "^5.1.5", "pretty-bytes": "^6.1.1", "pretty-ms": "^8.0.0", "shell-quote": "^1.8.1", "type-fest": "^4.20.0", "vue": "^3.5.12", "vue-router": "^4.3.0", "whatwg-mimetype": "^4.0.0", "yaml": "^2.4.5", "zod": "3.24.1" } }, "sha512-ubREY9whWXH1MdBIVUXdG/Vq4un2m1G918Z3jQhEmrkUP4wyZ0VEiV/h1N71JIYyql9apoj3HqAQb+D2U+rsDQ=="],
|
||||
|
||||
"@scalar/api-client-react": ["@scalar/api-client-react@1.2.19", "", { "dependencies": { "@scalar/api-client": "2.3.19", "@scalar/types": "0.1.7", "vue": "^3.5.12" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-zWUQvzAgOQ+oAbzswOlN42OJhwKku5RIIfBlukFMT31ceun3cp8e7+RT5DS6qwwQ2FROhSaqqsdM+o7VQ6pXkA=="],
|
||||
"@scalar/api-client-react": ["@scalar/api-client-react@1.3.16", "", { "dependencies": { "@scalar/api-client": "2.5.11", "@scalar/types": "0.2.3", "vue": "^3.5.12" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-dt9TxdeEaMrOfwH5WuK2QW/TxVJNkccG3DQle3SHVe9Hfl7nK+i8mMjTR8QMsuUU1MCIFpQzewDtNODq0pVXiw=="],
|
||||
|
||||
"@scalar/code-highlight": ["@scalar/code-highlight@0.0.27", "", { "dependencies": { "hast-util-to-text": "^4.0.2", "highlight.js": "^11.9.0", "highlightjs-curl": "^1.3.0", "highlightjs-vue": "^1.0.0", "lowlight": "^3.1.0", "rehype-external-links": "^3.0.0", "rehype-format": "^5.0.0", "rehype-parse": "^9.0.0", "rehype-raw": "^7.0.0", "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", "remark-stringify": "^11.0.0", "unified": "^11.0.4", "unist-util-visit": "^5.0.0" } }, "sha512-A61FUxqD278L+iLtdbMl4+Pg72wtMrnAYft8v1FNY44uf6UfmM47eDVmzWrc7bSvDevg3ho5QA8cKiJBHXZHJA=="],
|
||||
"@scalar/code-highlight": ["@scalar/code-highlight@0.1.4", "", { "dependencies": { "hast-util-to-text": "^4.0.2", "highlight.js": "^11.9.0", "highlightjs-curl": "^1.3.0", "highlightjs-vue": "^1.0.0", "lowlight": "^3.1.0", "rehype-external-links": "^3.0.0", "rehype-format": "^5.0.0", "rehype-parse": "^9.0.0", "rehype-raw": "^7.0.0", "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", "remark-stringify": "^11.0.0", "unified": "^11.0.4", "unist-util-visit": "^5.0.0" } }, "sha512-vE5q8Jn+I8x99APk4FGVff+ofVQ6y2Fc9qr8zPUncL+ASo0APCqjmVlHiMaFIC1f5nEApHU879B5N4CsLAvnFA=="],
|
||||
|
||||
"@scalar/components": ["@scalar/components@0.13.47", "", { "dependencies": { "@floating-ui/utils": "^0.2.2", "@floating-ui/vue": "^1.0.2", "@headlessui/vue": "^1.7.20", "@scalar/code-highlight": "0.0.27", "@scalar/themes": "0.9.86", "@scalar/use-hooks": "0.1.40", "@scalar/use-toasts": "0.7.9", "@vueuse/core": "^10.10.0", "cva": "1.0.0-beta.2", "nanoid": "^5.1.5", "pretty-bytes": "^6.1.1", "radix-vue": "^1.9.3", "tailwind-merge": "^2.5.5", "vue": "^3.5.12" } }, "sha512-e88mKKsCEspd06bpPQPnhtEvCo/jjoFFOX9yUSV9sr0sWFZHi0ihq1zvnpLKpULyS+C5zzyoN/tGVhmaYpXgyg=="],
|
||||
"@scalar/components": ["@scalar/components@0.14.12", "", { "dependencies": { "@floating-ui/utils": "^0.2.2", "@floating-ui/vue": "^1.0.2", "@headlessui/vue": "^1.7.20", "@scalar/code-highlight": "0.1.4", "@scalar/icons": "0.4.4", "@scalar/oas-utils": "0.4.7", "@scalar/themes": "0.13.4", "@scalar/use-hooks": "0.2.2", "@scalar/use-toasts": "0.8.0", "@vueuse/core": "^10.10.0", "cva": "1.0.0-beta.2", "nanoid": "^5.1.5", "pretty-bytes": "^6.1.1", "radix-vue": "^1.9.3", "vue": "^3.5.12" } }, "sha512-y+2LK84INfD8vCGwyDvpcT+1W+gwfEdjpBGZwrdKXYzJ13Z+fLf5BDDPMCVT4cPWnzQ2MrVgyBh8UnznM8JLFw=="],
|
||||
|
||||
"@scalar/draggable": ["@scalar/draggable@0.1.11", "", { "dependencies": { "vue": "^3.5.12" } }, "sha512-EQW9N1+mDORhsbjdtCI3XDvmUKsuKw1uf6r3kT1Mm2zQKT+rWwA0ChsAkEN6OG62C0YumMuXpH71h1seAWptxw=="],
|
||||
"@scalar/draggable": ["@scalar/draggable@0.2.0", "", { "dependencies": { "vue": "^3.5.12" } }, "sha512-UetHRB5Bqo5egVYlS21roWBcICmyk8CKh2htsidO+bFGAjl2e7Te+rY0borhNrMclr0xezHlPuLpEs1dvgLS2g=="],
|
||||
|
||||
"@scalar/icons": ["@scalar/icons@0.1.3", "", { "dependencies": { "vue": "^3.5.12" } }, "sha512-Bl46u7WsJ7NYjW1Fva7SMvw9c/92pGBP8B68tvDc+QevQ04DVNxw6+ny1NU/PnLtpuu1rUpPdtSCAkV1OdQGZQ=="],
|
||||
"@scalar/helpers": ["@scalar/helpers@0.0.4", "", {}, "sha512-AkWdiU8zhztb4bJ0U2LHgCtJs+kwWB5UFXebfy9QCLwCP0guhhFR2UJtRCU3QZ8xmZnI0AX0i7BjrxU/MN1roQ=="],
|
||||
|
||||
"@scalar/import": ["@scalar/import@0.3.13", "", { "dependencies": { "@scalar/oas-utils": "0.2.130", "@scalar/openapi-parser": "0.10.14", "yaml": "^2.4.5" } }, "sha512-ooKyRxwtvMpxBnoLt9mSJF8er5rCR6RzGJaIMRCj7ViN776eY4mbLiYcXre/LO8XfLSHb1p7XbNDhfFyTHaUlw=="],
|
||||
"@scalar/icons": ["@scalar/icons@0.4.4", "", { "dependencies": { "@phosphor-icons/core": "^2.1.1", "@types/node": "^20.17.10", "chalk": "^5.4.1", "vue": "^3.5.12" } }, "sha512-622QsIkRPw5UG4IU4QzcTUV1UHEw7aBaZMu6QYEHPO0Wn3ue1lfFIqLZOKsoXxvDuuXGvZUqyoZahkXdRHccmQ=="],
|
||||
|
||||
"@scalar/import": ["@scalar/import@0.4.11", "", { "dependencies": { "@scalar/helpers": "0.0.4", "@scalar/openapi-parser": "0.18.0", "yaml": "^2.4.5" } }, "sha512-s8GRknImMjevimqv+jLkW+7LsBuOWgQPv/p29dukLqPtCGddIyNoWgVot6/IaREEXXeu5m/PmoG+uN58AVN4Vg=="],
|
||||
|
||||
"@scalar/oas-utils": ["@scalar/oas-utils@0.2.130", "", { "dependencies": { "@hyperjump/json-schema": "^1.9.6", "@scalar/object-utils": "1.1.13", "@scalar/openapi-types": "0.2.0", "@scalar/themes": "0.9.86", "@scalar/types": "0.1.7", "flatted": "^3.3.1", "microdiff": "^1.4.0", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "yaml": "^2.4.5", "zod": "^3.23.8" } }, "sha512-sVpdc3+3c/WiNrKEIwzJ+ml2ZQBjarMOTDJCM/IrvYhrJE0nHrdkzxlJgNPi++vJbVl0saYt8LhEItALv7NziA=="],
|
||||
|
||||
@@ -1144,21 +1151,21 @@
|
||||
|
||||
"@scalar/openapi-types": ["@scalar/openapi-types@0.1.9", "", {}, "sha512-HQQudOSQBU7ewzfnBW9LhDmBE2XOJgSfwrh5PlUB7zJup/kaRkBGNgV2wMjNz9Af/uztiU/xNrO179FysmUT+g=="],
|
||||
|
||||
"@scalar/postman-to-openapi": ["@scalar/postman-to-openapi@0.2.3", "", { "dependencies": { "@scalar/oas-utils": "0.2.130", "@scalar/openapi-types": "0.2.0" } }, "sha512-/I5QbDFy+Sh29EIEgub/ztI+1eNtHRn+mln726hR+uWOyVyaDk0FfNC0R4XOn8SsDNyu5eqPbXBb9vceTVG6jQ=="],
|
||||
"@scalar/postman-to-openapi": ["@scalar/postman-to-openapi@0.3.10", "", { "dependencies": { "@scalar/helpers": "0.0.4", "@scalar/oas-utils": "0.4.7", "@scalar/openapi-types": "0.3.3" } }, "sha512-Jg+uJg56VASihtfL0sOzB8onyt7lfz/8Ztwo57WIAxUTvLcwBeCBgdTvUIw+Z4h7E2KfmKQY9HdDP/GHWMv6DQ=="],
|
||||
|
||||
"@scalar/snippetz": ["@scalar/snippetz@0.2.19", "", { "dependencies": { "stringify-object": "^5.0.0" } }, "sha512-fxC5mL3AZWiXAM21sMe1QU1/mu5KceN8ZmzFaP3xmdK26o/MkPKSLGVWW7w6OQkZi5hNloLHXXQiaI235qomEg=="],
|
||||
"@scalar/snippetz": ["@scalar/snippetz@0.3.1", "", { "dependencies": { "stringify-object": "^5.0.0" } }, "sha512-r5tjYhHxSgyxEPbzIp21xUVR3oy6hjrG4B4ozQWR8qvzpST+Am1KZI+dS9l69PKzfQ0YvQROOvfCMIh3JAFTFQ=="],
|
||||
|
||||
"@scalar/themes": ["@scalar/themes@0.9.86", "", { "dependencies": { "@scalar/types": "0.1.7" } }, "sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row=="],
|
||||
|
||||
"@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="],
|
||||
"@scalar/types": ["@scalar/types@0.2.3", "", { "dependencies": { "@scalar/openapi-types": "0.3.3", "nanoid": "^5.1.5", "zod": "3.24.1" } }, "sha512-K1/Vp5xaQ8TEGDYwHg88OBuQWJRWrE3ToZ0Z2LJwskcY1Eg+uDiRQXKF4QPngZzu5rrWgRErAq4yte4Y7omqgA=="],
|
||||
|
||||
"@scalar/use-codemirror": ["@scalar/use-codemirror@0.11.92", "", { "dependencies": { "@codemirror/autocomplete": "^6.18.3", "@codemirror/commands": "^6.7.1", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-html": "^6.4.8", "@codemirror/lang-json": "^6.0.0", "@codemirror/lang-xml": "^6.0.0", "@codemirror/lang-yaml": "^6.1.2", "@codemirror/language": "^6.10.7", "@codemirror/lint": "^6.8.4", "@codemirror/state": "^6.5.0", "@codemirror/view": "^6.35.3", "@lezer/common": "^1.2.3", "@lezer/highlight": "^1.2.1", "@lezer/lr": "^1.4.2", "@replit/codemirror-css-color-picker": "^6.3.0", "@scalar/components": "0.13.47", "codemirror": "^6.0.0", "style-mod": "^4.1.2", "vue": "^3.5.12" } }, "sha512-WDd50xGLV+q1T36cKzmhqYP+TyHe4MOW0tIiu09ed9aMXGUk6kQgnf0J3rYPsGeNj9IkVG3znZ7oYT3QplLGVA=="],
|
||||
"@scalar/use-codemirror": ["@scalar/use-codemirror@0.12.13", "", { "dependencies": { "@codemirror/autocomplete": "^6.18.3", "@codemirror/commands": "^6.7.1", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-html": "^6.4.8", "@codemirror/lang-json": "^6.0.0", "@codemirror/lang-xml": "^6.0.0", "@codemirror/lang-yaml": "^6.1.2", "@codemirror/language": "^6.10.7", "@codemirror/lint": "^6.8.4", "@codemirror/state": "^6.5.0", "@codemirror/view": "^6.35.3", "@lezer/common": "^1.2.3", "@lezer/highlight": "^1.2.1", "@replit/codemirror-css-color-picker": "^6.3.0", "@scalar/components": "0.14.12", "codemirror": "^6.0.0", "vue": "^3.5.12" } }, "sha512-XOXCUT3b1l0gkp4CY51e7xMgR6IP/AMpYIZ97fBOOjnrUIJ8B/J+U/BMS6+lPKiN0hexaSALRWlVBf90txdT8w=="],
|
||||
|
||||
"@scalar/use-hooks": ["@scalar/use-hooks@0.1.40", "", { "dependencies": { "@scalar/themes": "0.9.86", "@scalar/use-toasts": "0.7.9", "@vueuse/core": "^10.10.0", "vue": "^3.5.12", "zod": "^3.23.8" } }, "sha512-z8qtgIcW9Z3PCrP2cbKG+D2EVhpNgl1N0ucGtDg5SMl/fvCyXNfqB9j+u3ygxkouatfQ9zRZuhxreNMkW9/H5g=="],
|
||||
"@scalar/use-hooks": ["@scalar/use-hooks@0.2.2", "", { "dependencies": { "@scalar/use-toasts": "0.8.0", "@vueuse/core": "^10.10.0", "cva": "1.0.0-beta.2", "tailwind-merge": "^2.5.5", "vue": "^3.5.12", "zod": "3.24.1" } }, "sha512-9XxfqIKyUp727C2zzyc+cExsK3Gsg5M/v3pEG2TB665RZMAYV1YK8MBT1JGkJDkkugBgHKGl5PQiXsYme1rmTQ=="],
|
||||
|
||||
"@scalar/use-toasts": ["@scalar/use-toasts@0.7.9", "", { "dependencies": { "nanoid": "^5.0.9", "vue": "^3.5.12", "vue-sonner": "^1.0.3" } }, "sha512-EcUDJY8VozLS9sfoQKvvipStQJ9RuH/nKOzf0BBr+mZDmumi1WFZ1iIJnHVXIN3iSLcSAr5ej6rOqa6jIv4bCQ=="],
|
||||
"@scalar/use-toasts": ["@scalar/use-toasts@0.8.0", "", { "dependencies": { "nanoid": "^5.1.5", "vue": "^3.5.12", "vue-sonner": "^1.0.3" } }, "sha512-u+o77cdTNZ5ePqHPu8ZcFw1BLlISv+cthN0bR1zJHXmqBjvanFTy2kL+Gmv3eW9HxZiHdqycKVETlYd0mWiqJQ=="],
|
||||
|
||||
"@scalar/use-tooltip": ["@scalar/use-tooltip@1.0.6", "", { "dependencies": { "tippy.js": "^6.3.7", "vue": "^3.5.12" } }, "sha512-f0gadIaUnILfi9qYAk7g+fNTsvLGXnam8oOUTxovavC1ocYuGTEykdz3g2MTqnAqRS8OkAB64h9mHf0FBfg6mg=="],
|
||||
"@scalar/use-tooltip": ["@scalar/use-tooltip@1.1.0", "", { "dependencies": { "tippy.js": "^6.3.7", "vue": "^3.5.12" } }, "sha512-KJConD/JDyGP8GPGpD+TXA6FEcKT9bmHQb/JyBmME+tJoJGHGtNcGy7kcezFakaKCqfKyY7cgPzL1ctUaGIRag=="],
|
||||
|
||||
"@shikijs/core": ["@shikijs/core@3.2.0", "", { "dependencies": { "@shikijs/types": "3.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-+5dPz8q6HgNqfQ28ycm/vA8dIVd2lNFOUqVRFCQLbs0KZ6emYI+1apLpX+wuL/aDSPLOkMgARwNjkA5UjGKS1Q=="],
|
||||
|
||||
@@ -1318,6 +1325,8 @@
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
|
||||
|
||||
"@types/har-format": ["@types/har-format@1.2.16", "", {}, "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A=="],
|
||||
|
||||
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||
|
||||
"@types/js-cookie": ["@types/js-cookie@3.0.6", "", {}, "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ=="],
|
||||
@@ -1706,6 +1715,8 @@
|
||||
|
||||
"dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="],
|
||||
|
||||
"direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="],
|
||||
|
||||
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
|
||||
|
||||
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
|
||||
@@ -2066,6 +2077,8 @@
|
||||
|
||||
"ignore": ["ignore@7.0.3", "", {}, "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA=="],
|
||||
|
||||
"image-size": ["image-size@2.0.2", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
|
||||
"import-lazy": ["import-lazy@2.1.0", "", {}, "sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A=="],
|
||||
@@ -2438,6 +2451,8 @@
|
||||
|
||||
"object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="],
|
||||
|
||||
"object-identity": ["object-identity@0.1.2", "", {}, "sha512-Px5puVllX5L2aBjbcfXpiG5xXeq6OE8RckryTeP2Zq+0PgYrCGJXmC6LblWgknKSJs11Je2W4U2NOWFj3t/QXQ=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.2", "", {}, "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g=="],
|
||||
|
||||
"object-treeify": ["object-treeify@1.1.33", "", {}, "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A=="],
|
||||
@@ -3860,19 +3875,45 @@
|
||||
|
||||
"@rollup/pluginutils/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||
|
||||
"@scalar/api-client/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="],
|
||||
"@scalar/api-client/@scalar/oas-utils": ["@scalar/oas-utils@0.4.7", "", { "dependencies": { "@hyperjump/browser": "^1.1.0", "@hyperjump/json-schema": "^1.9.6", "@scalar/helpers": "0.0.4", "@scalar/object-utils": "1.2.1", "@scalar/openapi-types": "0.3.3", "@scalar/themes": "0.13.4", "@scalar/types": "0.2.3", "@types/har-format": "^1.2.15", "flatted": "^3.3.1", "microdiff": "^1.4.0", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "yaml": "^2.4.5", "zod": "3.24.1" } }, "sha512-Vp8iZZCjNKXcPpL0Yr6dmy+VOSN1LZ5CdlGAHtp+pxuQTeahV6FxiVjMc9kdFve77fTNyut4seVAD6d7rDUDcA=="],
|
||||
|
||||
"@scalar/api-client/@scalar/object-utils": ["@scalar/object-utils@1.2.1", "", { "dependencies": { "flatted": "^3.3.1", "just-clone": "^6.2.0", "ts-deepmerge": "^7.0.1", "type-fest": "^4.20.0" } }, "sha512-2P/0In6XSrV1Ye8yU3LyOWJKWkzXpxf0thHmTfJriqGBZz41s06td2KKqoqHSEEi+FAsl3O1ngmOOzLM3QUkkw=="],
|
||||
|
||||
"@scalar/api-client/@scalar/openapi-parser": ["@scalar/openapi-parser@0.18.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-qAWEfJR5f/LwSFULFeSkD56LHgU9ovyj+e1uqy9XaW9Hb9P0ewIGKi+Gd4wTyxfFkdPLXTFYXApjviP6jV+new=="],
|
||||
|
||||
"@scalar/api-client/@scalar/openapi-types": ["@scalar/openapi-types@0.3.3", "", { "dependencies": { "zod": "3.24.1" } }, "sha512-mr3OvbCyvuzSDgv9gR9D3pJAsl82BRNBp1P8ldgRgKF4Wiswfa7nQeKibpX0kHSGXLgiyvxWJULUFwgdWMXNNA=="],
|
||||
|
||||
"@scalar/api-client/@scalar/themes": ["@scalar/themes@0.13.4", "", { "dependencies": { "@scalar/types": "0.2.3", "nanoid": "^5.1.5" } }, "sha512-PMiSqX+MZF29u32ogEjTVA5c/LBd5cE/QPF7lK0EJeemXRy2r/nWbvrXtqD9Y7Sw54g1caVNjK8yJcCYJdB6hw=="],
|
||||
|
||||
"@scalar/api-client/pretty-ms": ["pretty-ms@8.0.0", "", { "dependencies": { "parse-ms": "^3.0.0" } }, "sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q=="],
|
||||
|
||||
"@scalar/api-client/zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="],
|
||||
|
||||
"@scalar/components/@scalar/oas-utils": ["@scalar/oas-utils@0.4.7", "", { "dependencies": { "@hyperjump/browser": "^1.1.0", "@hyperjump/json-schema": "^1.9.6", "@scalar/helpers": "0.0.4", "@scalar/object-utils": "1.2.1", "@scalar/openapi-types": "0.3.3", "@scalar/themes": "0.13.4", "@scalar/types": "0.2.3", "@types/har-format": "^1.2.15", "flatted": "^3.3.1", "microdiff": "^1.4.0", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "yaml": "^2.4.5", "zod": "3.24.1" } }, "sha512-Vp8iZZCjNKXcPpL0Yr6dmy+VOSN1LZ5CdlGAHtp+pxuQTeahV6FxiVjMc9kdFve77fTNyut4seVAD6d7rDUDcA=="],
|
||||
|
||||
"@scalar/components/@scalar/themes": ["@scalar/themes@0.13.4", "", { "dependencies": { "@scalar/types": "0.2.3", "nanoid": "^5.1.5" } }, "sha512-PMiSqX+MZF29u32ogEjTVA5c/LBd5cE/QPF7lK0EJeemXRy2r/nWbvrXtqD9Y7Sw54g1caVNjK8yJcCYJdB6hw=="],
|
||||
|
||||
"@scalar/icons/@types/node": ["@types/node@20.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA=="],
|
||||
|
||||
"@scalar/import/@scalar/openapi-parser": ["@scalar/openapi-parser@0.18.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-qAWEfJR5f/LwSFULFeSkD56LHgU9ovyj+e1uqy9XaW9Hb9P0ewIGKi+Gd4wTyxfFkdPLXTFYXApjviP6jV+new=="],
|
||||
|
||||
"@scalar/oas-utils/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="],
|
||||
|
||||
"@scalar/oas-utils/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="],
|
||||
|
||||
"@scalar/object-utils/flatted": ["flatted@3.3.1", "", {}, "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw=="],
|
||||
|
||||
"@scalar/postman-to-openapi/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="],
|
||||
"@scalar/postman-to-openapi/@scalar/oas-utils": ["@scalar/oas-utils@0.4.7", "", { "dependencies": { "@hyperjump/browser": "^1.1.0", "@hyperjump/json-schema": "^1.9.6", "@scalar/helpers": "0.0.4", "@scalar/object-utils": "1.2.1", "@scalar/openapi-types": "0.3.3", "@scalar/themes": "0.13.4", "@scalar/types": "0.2.3", "@types/har-format": "^1.2.15", "flatted": "^3.3.1", "microdiff": "^1.4.0", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "yaml": "^2.4.5", "zod": "3.24.1" } }, "sha512-Vp8iZZCjNKXcPpL0Yr6dmy+VOSN1LZ5CdlGAHtp+pxuQTeahV6FxiVjMc9kdFve77fTNyut4seVAD6d7rDUDcA=="],
|
||||
|
||||
"@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="],
|
||||
"@scalar/postman-to-openapi/@scalar/openapi-types": ["@scalar/openapi-types@0.3.3", "", { "dependencies": { "zod": "3.24.1" } }, "sha512-mr3OvbCyvuzSDgv9gR9D3pJAsl82BRNBp1P8ldgRgKF4Wiswfa7nQeKibpX0kHSGXLgiyvxWJULUFwgdWMXNNA=="],
|
||||
|
||||
"@scalar/use-toasts/nanoid": ["nanoid@5.1.2", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-b+CiXQCNMUGe0Ri64S9SXFcP9hogjAJ2Rd6GdVxhPLRm7mhGaM7VgOvCAJ1ZshfHbqVDI3uqTI5C8/GaKuLI7g=="],
|
||||
"@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="],
|
||||
|
||||
"@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.3.3", "", { "dependencies": { "zod": "3.24.1" } }, "sha512-mr3OvbCyvuzSDgv9gR9D3pJAsl82BRNBp1P8ldgRgKF4Wiswfa7nQeKibpX0kHSGXLgiyvxWJULUFwgdWMXNNA=="],
|
||||
|
||||
"@scalar/types/zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="],
|
||||
|
||||
"@scalar/use-hooks/zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="],
|
||||
|
||||
"@shikijs/core/hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
|
||||
|
||||
@@ -4930,6 +4971,24 @@
|
||||
|
||||
"@scalar/api-client/pretty-ms/parse-ms": ["parse-ms@3.0.0", "", {}, "sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw=="],
|
||||
|
||||
"@scalar/components/@scalar/oas-utils/@scalar/object-utils": ["@scalar/object-utils@1.2.1", "", { "dependencies": { "flatted": "^3.3.1", "just-clone": "^6.2.0", "ts-deepmerge": "^7.0.1", "type-fest": "^4.20.0" } }, "sha512-2P/0In6XSrV1Ye8yU3LyOWJKWkzXpxf0thHmTfJriqGBZz41s06td2KKqoqHSEEi+FAsl3O1ngmOOzLM3QUkkw=="],
|
||||
|
||||
"@scalar/components/@scalar/oas-utils/@scalar/openapi-types": ["@scalar/openapi-types@0.3.3", "", { "dependencies": { "zod": "3.24.1" } }, "sha512-mr3OvbCyvuzSDgv9gR9D3pJAsl82BRNBp1P8ldgRgKF4Wiswfa7nQeKibpX0kHSGXLgiyvxWJULUFwgdWMXNNA=="],
|
||||
|
||||
"@scalar/components/@scalar/oas-utils/zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="],
|
||||
|
||||
"@scalar/icons/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"@scalar/postman-to-openapi/@scalar/oas-utils/@scalar/object-utils": ["@scalar/object-utils@1.2.1", "", { "dependencies": { "flatted": "^3.3.1", "just-clone": "^6.2.0", "ts-deepmerge": "^7.0.1", "type-fest": "^4.20.0" } }, "sha512-2P/0In6XSrV1Ye8yU3LyOWJKWkzXpxf0thHmTfJriqGBZz41s06td2KKqoqHSEEi+FAsl3O1ngmOOzLM3QUkkw=="],
|
||||
|
||||
"@scalar/postman-to-openapi/@scalar/oas-utils/@scalar/themes": ["@scalar/themes@0.13.4", "", { "dependencies": { "@scalar/types": "0.2.3", "nanoid": "^5.1.5" } }, "sha512-PMiSqX+MZF29u32ogEjTVA5c/LBd5cE/QPF7lK0EJeemXRy2r/nWbvrXtqD9Y7Sw54g1caVNjK8yJcCYJdB6hw=="],
|
||||
|
||||
"@scalar/postman-to-openapi/@scalar/oas-utils/zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="],
|
||||
|
||||
"@scalar/postman-to-openapi/@scalar/openapi-types/zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="],
|
||||
|
||||
"@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="],
|
||||
|
||||
"@shikijs/core/hast-util-to-html/property-information": ["property-information@7.0.0", "", {}, "sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg=="],
|
||||
|
||||
"@smithy/chunked-blob-reader-native/@smithy/util-base64/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.0.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug=="],
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@
|
||||
"workspaces": {
|
||||
"packages": ["packages/*"],
|
||||
"catalog": {
|
||||
"@gitbook/api": "^0.120.0"
|
||||
"@gitbook/api": "^0.123.0"
|
||||
}
|
||||
},
|
||||
"patchedDependencies": {
|
||||
|
||||
@@ -122,6 +122,11 @@
|
||||
]
|
||||
},
|
||||
"production": {
|
||||
"vars": {
|
||||
// This is a bit misleading, but it means that we can have 500 concurrent revalidations
|
||||
// This means that we'll have up to 100 durable objects instance running at the same time
|
||||
"MAX_REVALIDATE_CONCURRENCY": "100"
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
|
||||
@@ -98,7 +98,11 @@
|
||||
},
|
||||
"production": {
|
||||
"vars": {
|
||||
// R2 is strongly consistent, so we can disable SQLite
|
||||
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true",
|
||||
// We don't want to pollute the memory with broken cache entries
|
||||
// Most of the time, those are fake requests.
|
||||
"NEXT_CACHE_DO_QUEUE_MAX_RETRIES": "1",
|
||||
"STAGE": "production"
|
||||
},
|
||||
"r2_buckets": [
|
||||
|
||||
@@ -183,7 +183,14 @@ class GitbookIncrementalCache implements IncrementalCache {
|
||||
}
|
||||
|
||||
// Utility function to generate keys for R2/Cache API
|
||||
getR2Key(key: string, cacheType: CacheEntryType = 'cache'): string {
|
||||
getR2Key(initialKey: string, cacheType: CacheEntryType = 'cache'): string {
|
||||
let key = initialKey;
|
||||
if (cacheType === 'composable') {
|
||||
// For composable cache, we need to discard the build ID from the key
|
||||
const [_buildId, ...restOfTheKey] = JSON.parse(initialKey);
|
||||
key = JSON.stringify([...restOfTheKey]);
|
||||
}
|
||||
|
||||
const hash = createHash('sha256').update(key).digest('hex');
|
||||
return `${DEFAULT_PREFIX}/${cacheType === 'cache' ? process.env?.NEXT_BUILD_ID : 'dataCache'}/${hash}.${cacheType}`.replace(
|
||||
/\/+/g,
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"react-dom": "^19.0.0",
|
||||
"rison": "^0.1.1",
|
||||
"server-only": "^0.0.1",
|
||||
"warn-once": "^0.1.1"
|
||||
"warn-once": "^0.1.1",
|
||||
"object-identity": "^0.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gitbook": "*",
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { withStableRef } from './cache';
|
||||
|
||||
describe('withStableRef', () => {
|
||||
it('should return primitive values as is', () => {
|
||||
const toStableRef = withStableRef();
|
||||
|
||||
expect(toStableRef(42)).toBe(42);
|
||||
expect(toStableRef('hello')).toBe('hello');
|
||||
expect(toStableRef(true)).toBe(true);
|
||||
expect(toStableRef(null)).toBe(null);
|
||||
expect(toStableRef(undefined)).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should return the same reference for identical objects', () => {
|
||||
const toStableRef = withStableRef();
|
||||
|
||||
const obj1 = { a: 1, b: 2 };
|
||||
const obj2 = { a: 1, b: 2 };
|
||||
|
||||
const ref1 = toStableRef(obj1);
|
||||
const ref2 = toStableRef(obj2);
|
||||
|
||||
expect(ref1).toBe(ref2);
|
||||
expect(ref1).toBe(obj1);
|
||||
expect(ref1).not.toBe(obj2);
|
||||
});
|
||||
|
||||
it('should return the same reference for identical arrays', () => {
|
||||
const toStableRef = withStableRef();
|
||||
|
||||
const arr1 = [1, 2, 3];
|
||||
const arr2 = [1, 2, 3];
|
||||
|
||||
const ref1 = toStableRef(arr1);
|
||||
const ref2 = toStableRef(arr2);
|
||||
|
||||
expect(ref1).toBe(ref2);
|
||||
expect(ref1).toBe(arr1);
|
||||
expect(ref1).not.toBe(arr2);
|
||||
});
|
||||
|
||||
it('should return the same reference for identical nested objects', () => {
|
||||
const toStableRef = withStableRef();
|
||||
|
||||
const obj1 = { a: { b: 1 }, c: [2, 3] };
|
||||
const obj2 = { a: { b: 1 }, c: [2, 3] };
|
||||
|
||||
const ref1 = toStableRef(obj1);
|
||||
const ref2 = toStableRef(obj2);
|
||||
|
||||
expect(ref1).toBe(ref2);
|
||||
expect(ref1).toBe(obj1);
|
||||
expect(ref1).not.toBe(obj2);
|
||||
});
|
||||
|
||||
it('should return different references for different objects', () => {
|
||||
const toStableRef = withStableRef();
|
||||
|
||||
const obj1 = { a: 1 };
|
||||
const obj2 = { a: 2 };
|
||||
|
||||
const ref1 = toStableRef(obj1);
|
||||
const ref2 = toStableRef(obj2);
|
||||
|
||||
expect(ref1).not.toBe(ref2);
|
||||
});
|
||||
|
||||
it('should maintain reference stability across multiple calls', () => {
|
||||
const toStableRef = withStableRef();
|
||||
|
||||
const obj = { a: 1 };
|
||||
const ref1 = toStableRef(obj);
|
||||
const ref2 = toStableRef(obj);
|
||||
const ref3 = toStableRef(obj);
|
||||
|
||||
expect(ref1).toBe(ref2);
|
||||
expect(ref2).toBe(ref3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { identify } from 'object-identity';
|
||||
import * as React from 'react';
|
||||
|
||||
/**
|
||||
* Equivalent to `React.cache` but with support for non-primitive arguments.
|
||||
* As `React.cache` only uses `Object.is` to compare arguments, it will not work with non-primitive arguments.
|
||||
*/
|
||||
export function cache<Args extends any[], Return>(fn: (...args: Args) => Return) {
|
||||
const cached = React.cache(fn);
|
||||
|
||||
return (...args: Args) => {
|
||||
const toStableRef = getWithStableRef();
|
||||
const stableArgs = args.map((value) => {
|
||||
return toStableRef(value);
|
||||
}) as Args;
|
||||
return cached(...stableArgs);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* To ensure memory is garbage collected between each request, we use a per-request cache to store the ref maps.
|
||||
*/
|
||||
const getWithStableRef = React.cache(withStableRef);
|
||||
|
||||
/**
|
||||
* Create a function that converts a value to a stable reference.
|
||||
*/
|
||||
export function withStableRef(): <T>(value: T) => T {
|
||||
const reverseIndex = new WeakMap<object, string>();
|
||||
const refIndex = new Map<string, object>();
|
||||
|
||||
return <T>(value: T) => {
|
||||
if (isPrimitive(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const objectValue = value as object;
|
||||
const index = reverseIndex.get(objectValue);
|
||||
if (index !== undefined) {
|
||||
return refIndex.get(index) as T;
|
||||
}
|
||||
|
||||
const hash = identify(objectValue);
|
||||
reverseIndex.set(objectValue, hash);
|
||||
|
||||
const existing = refIndex.get(hash);
|
||||
if (existing !== undefined) {
|
||||
return existing as T;
|
||||
}
|
||||
|
||||
// first time we've seen this shape
|
||||
refIndex.set(hash, objectValue);
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
function isPrimitive(value: any): boolean {
|
||||
return value === null || typeof value !== 'object';
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { getSiteStructureSections } from '@/lib/sites';
|
||||
import type {
|
||||
ChangeRequest,
|
||||
PublishedSiteContent,
|
||||
RevisionPage,
|
||||
Revision,
|
||||
RevisionPageDocument,
|
||||
Site,
|
||||
SiteCustomizationSettings,
|
||||
@@ -84,11 +84,8 @@ export type GitBookSpaceContext = GitBookBaseContext & {
|
||||
space: Space;
|
||||
changeRequest: ChangeRequest | null;
|
||||
|
||||
/** ID of the current revision. */
|
||||
revisionId: string;
|
||||
|
||||
/** Pages of the space. */
|
||||
pages: RevisionPage[];
|
||||
/** Revision of the space. */
|
||||
revision: Revision;
|
||||
|
||||
/** Share key of the space. */
|
||||
shareKey: string | undefined;
|
||||
@@ -351,20 +348,17 @@ export async function fetchSpaceContextByIds(
|
||||
|
||||
const revisionId = ids.revision ?? changeRequest?.revision ?? space.revision;
|
||||
|
||||
const pages = await getDataOrNull(
|
||||
dataFetcher.getRevisionPages({
|
||||
const revision = await getDataOrNull(
|
||||
dataFetcher.getRevision({
|
||||
spaceId: ids.space,
|
||||
revisionId,
|
||||
// We only care about the Git metadata when the Git sync is enabled,
|
||||
// otherwise we can optimize performance by not fetching it
|
||||
metadata: !!space.gitSync,
|
||||
}),
|
||||
|
||||
// When trying to render a revision with an invalid / non-existing ID,
|
||||
// we should handle gracefully the 404 and throw notFound.
|
||||
ids.revision ? [404] : undefined
|
||||
);
|
||||
if (!pages) {
|
||||
if (!revision) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
@@ -372,9 +366,8 @@ export async function fetchSpaceContextByIds(
|
||||
...baseContext,
|
||||
organizationId: space.organization,
|
||||
space,
|
||||
pages,
|
||||
revision,
|
||||
changeRequest,
|
||||
revisionId,
|
||||
shareKey: ids.shareKey,
|
||||
};
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
import { GitBookAPIError } from '@gitbook/api';
|
||||
import { unstable_cacheLife as cacheLife } from 'next/cache';
|
||||
import type { DataFetcherErrorData, DataFetcherResponse } from './types';
|
||||
|
||||
export class DataFetcherError extends Error {
|
||||
@@ -47,11 +48,7 @@ export function getDataOrNull<T>(
|
||||
return response.then((result) => getDataOrNull(result, ignoreErrors));
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
if (ignoreErrors.includes(response.error.code)) return null;
|
||||
throw new DataFetcherError(response.error.message, response.error.code);
|
||||
}
|
||||
return response.data;
|
||||
return ignoreDataFetcherErrors(response, ignoreErrors).data ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,6 +90,50 @@ export async function wrapDataFetcherError<T>(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap an async execution to handle errors and return a DataFetcherResponse.
|
||||
* This should be used inside 'use cache' functions.
|
||||
*/
|
||||
export async function wrapCacheDataFetcherError<T>(
|
||||
fn: () => Promise<T>
|
||||
): Promise<DataFetcherResponse<T>> {
|
||||
const result = await wrapDataFetcherError(fn);
|
||||
if (result.error && result.error.code >= 500) {
|
||||
// We don't want to cache errors for too long.
|
||||
// as the API might
|
||||
cacheLife('minutes');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignore some data fetcher errors.
|
||||
*/
|
||||
export function ignoreDataFetcherErrors<T>(
|
||||
response: DataFetcherResponse<T>,
|
||||
ignoreErrors?: number[]
|
||||
): DataFetcherResponse<T>;
|
||||
export function ignoreDataFetcherErrors<T>(
|
||||
response: Promise<DataFetcherResponse<T>>,
|
||||
ignoreErrors?: number[]
|
||||
): Promise<DataFetcherResponse<T>>;
|
||||
export function ignoreDataFetcherErrors<T>(
|
||||
response: DataFetcherResponse<T> | Promise<DataFetcherResponse<T>>,
|
||||
ignoreErrors: number[] = [404]
|
||||
): DataFetcherResponse<T> | Promise<DataFetcherResponse<T>> {
|
||||
if (response instanceof Promise) {
|
||||
return response.then((result) => ignoreDataFetcherErrors(result, ignoreErrors));
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
if (ignoreErrors.includes(response.error.code)) {
|
||||
return response;
|
||||
}
|
||||
throw new DataFetcherError(response.error.message, response.error.code);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a data fetcher exposable error from a JS error.
|
||||
*/
|
||||
|
||||
@@ -5,3 +5,4 @@ export * from './errors';
|
||||
export * from './lookup';
|
||||
export * from './visitor';
|
||||
export * from './pages';
|
||||
export * from './revisions';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { race, tryCatch } from '@/lib/async';
|
||||
import { joinPath, joinPathWithBaseURL } from '@/lib/paths';
|
||||
import { trace } from '@/lib/tracing';
|
||||
import type { GitBookAPI, PublishedSiteContentLookup, SiteVisitorPayload } from '@gitbook/api';
|
||||
import type { PublishedSiteContentLookup, SiteVisitorPayload } from '@gitbook/api';
|
||||
import { apiClient } from './api';
|
||||
import { getExposableError } from './errors';
|
||||
import type { DataFetcherResponse } from './types';
|
||||
@@ -18,85 +18,32 @@ interface LookupPublishedContentByUrlInput {
|
||||
* Lookup a content by its URL using the GitBook resolvePublishedContentByUrl API endpoint.
|
||||
* To optimize caching, we try multiple lookup alternatives and return the first one that matches.
|
||||
*/
|
||||
export async function resolvePublishedContentByUrl(input: LookupPublishedContentByUrlInput) {
|
||||
return lookupPublishedContentByUrl({
|
||||
url: input.url,
|
||||
fetchLookupAPIResult: ({ url, signal }) => {
|
||||
const api = apiClient({ apiToken: input.apiToken });
|
||||
return trace(
|
||||
{
|
||||
operation: 'resolvePublishedContentByUrl',
|
||||
name: url,
|
||||
},
|
||||
() =>
|
||||
tryCatch(
|
||||
api.urls.resolvePublishedContentByUrl(
|
||||
{
|
||||
url,
|
||||
...(input.visitorPayload ? { visitor: input.visitorPayload } : {}),
|
||||
redirectOnError: input.redirectOnError,
|
||||
},
|
||||
{ signal }
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup a content by its URL using the GitBook getPublishedContentByUrl API endpoint.
|
||||
* To optimize caching, we try multiple lookup alternatives and return the first one that matches.
|
||||
*
|
||||
* @deprecated use resolvePublishedContentByUrl.
|
||||
*
|
||||
*/
|
||||
export async function getPublishedContentByURL(input: LookupPublishedContentByUrlInput) {
|
||||
return lookupPublishedContentByUrl({
|
||||
url: input.url,
|
||||
fetchLookupAPIResult: ({ url, signal }) => {
|
||||
const api = apiClient({ apiToken: input.apiToken });
|
||||
return trace(
|
||||
{
|
||||
operation: 'getPublishedContentByURL',
|
||||
name: url,
|
||||
},
|
||||
() =>
|
||||
tryCatch(
|
||||
api.urls.getPublishedContentByUrl(
|
||||
{
|
||||
url,
|
||||
visitorAuthToken: input.visitorPayload.jwtToken ?? undefined,
|
||||
redirectOnError: input.redirectOnError,
|
||||
// @ts-expect-error - cacheVersion is not a real query param
|
||||
cacheVersion: 'v2',
|
||||
},
|
||||
{ signal }
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
type TryCatch<T> = ReturnType<typeof tryCatch<T>>;
|
||||
|
||||
async function lookupPublishedContentByUrl(input: {
|
||||
url: string;
|
||||
fetchLookupAPIResult: (args: {
|
||||
url: string;
|
||||
signal: AbortSignal;
|
||||
}) => TryCatch<Awaited<ReturnType<GitBookAPI['urls']['resolvePublishedContentByUrl']>>>;
|
||||
}): Promise<DataFetcherResponse<PublishedSiteContentLookup>> {
|
||||
export async function lookupPublishedContentByUrl(
|
||||
input: LookupPublishedContentByUrlInput
|
||||
): Promise<DataFetcherResponse<PublishedSiteContentLookup>> {
|
||||
const lookupURL = new URL(input.url);
|
||||
const url = stripURLSearch(lookupURL);
|
||||
const lookup = getURLLookupAlternatives(url);
|
||||
|
||||
const result = await race(lookup.urls, async (alternative, { signal }) => {
|
||||
const callResult = await input.fetchLookupAPIResult({
|
||||
url: alternative.url,
|
||||
signal,
|
||||
});
|
||||
const api = apiClient({ apiToken: input.apiToken });
|
||||
const callResult = await trace(
|
||||
{
|
||||
operation: 'resolvePublishedContentByUrl',
|
||||
name: alternative.url,
|
||||
},
|
||||
() =>
|
||||
tryCatch(
|
||||
api.urls.resolvePublishedContentByUrl(
|
||||
{
|
||||
url: alternative.url,
|
||||
...(input.visitorPayload ? { visitor: input.visitorPayload } : {}),
|
||||
redirectOnError: input.redirectOnError,
|
||||
},
|
||||
{ signal }
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if (callResult.error) {
|
||||
if (alternative.primary) {
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import { cache } from 'react';
|
||||
|
||||
// This is used to create a context specific to the current request.
|
||||
// This version works both in cloudflare and in vercel.
|
||||
const getRequestContext = cache(() => ({}));
|
||||
|
||||
/**
|
||||
* Wrap a function by preventing concurrent executions of the same function.
|
||||
* With a logic to work per-request in Cloudflare Workers.
|
||||
*/
|
||||
export function withoutConcurrentExecution<ArgsType extends any[], ReturnType>(
|
||||
wrapped: (key: string, ...args: ArgsType) => Promise<ReturnType>
|
||||
): (cacheKey: string, ...args: ArgsType) => Promise<ReturnType> {
|
||||
const globalPromiseCache = new WeakMap<object, Map<string, Promise<ReturnType>>>();
|
||||
|
||||
return (key: string, ...args: ArgsType) => {
|
||||
const globalContext = getRequestContext();
|
||||
|
||||
/**
|
||||
* Cache storage that is scoped to the current request when executed in Cloudflare Workers,
|
||||
* to avoid "Cannot perform I/O on behalf of a different request" errors.
|
||||
*/
|
||||
const promiseCache =
|
||||
globalPromiseCache.get(globalContext) ?? new Map<string, Promise<ReturnType>>();
|
||||
globalPromiseCache.set(globalContext, promiseCache);
|
||||
|
||||
const concurrent = promiseCache.get(key);
|
||||
if (concurrent) {
|
||||
return concurrent;
|
||||
}
|
||||
|
||||
const promise = (async () => {
|
||||
try {
|
||||
const result = await wrapped(key, ...args);
|
||||
return result;
|
||||
} finally {
|
||||
promiseCache.delete(key);
|
||||
}
|
||||
})();
|
||||
|
||||
promiseCache.set(key, promise);
|
||||
|
||||
return promise;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a function by passing it a cache key that is computed from the function arguments.
|
||||
*/
|
||||
export function withCacheKey<ArgsType extends any[], ReturnType>(
|
||||
wrapped: (cacheKey: string, ...args: ArgsType) => Promise<ReturnType>
|
||||
): (...args: ArgsType) => Promise<ReturnType> {
|
||||
return (...args: ArgsType) => {
|
||||
const cacheKey = getCacheKey(args);
|
||||
return wrapped(cacheKey, ...args);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a cache key from the function arguments.
|
||||
*/
|
||||
function getCacheKey(args: any[]) {
|
||||
return JSON.stringify(deepSortValue(args));
|
||||
}
|
||||
|
||||
function deepSortValue(value: unknown): unknown {
|
||||
if (
|
||||
typeof value === 'string' ||
|
||||
typeof value === 'number' ||
|
||||
typeof value === 'boolean' ||
|
||||
value === null ||
|
||||
value === undefined
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(deepSortValue);
|
||||
}
|
||||
|
||||
if (value && typeof value === 'object') {
|
||||
return Object.entries(value)
|
||||
.map(([key, subValue]) => {
|
||||
return [key, deepSortValue(subValue)] as const;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return a[0].localeCompare(b[0]);
|
||||
});
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { waitUntil } from '@/lib/waitUntil';
|
||||
import { isV2 } from '@/lib/v2';
|
||||
import type { JSONDocument, RevisionPageDocument } from '@gitbook/api';
|
||||
import type { GitBookSiteContext, GitBookSpaceContext } from '../context';
|
||||
import { getDataOrNull } from './errors';
|
||||
@@ -12,10 +12,7 @@ export async function getPageDocument(
|
||||
): Promise<JSONDocument | null> {
|
||||
const { dataFetcher, space } = context;
|
||||
|
||||
if (
|
||||
'site' in context &&
|
||||
(context.site.id === 'site_JOVzv' || context.site.id === 'site_IxAYj')
|
||||
) {
|
||||
if (isV2()) {
|
||||
return getDataOrNull(
|
||||
dataFetcher.getRevisionPageDocument({
|
||||
spaceId: space.id,
|
||||
@@ -41,33 +38,5 @@ export async function getPageDocument(
|
||||
);
|
||||
}
|
||||
|
||||
// Pre-fetch the document to start filling the cache before we migrate to this API.
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// Disable for now to investigate side-effects
|
||||
if (isInPercentRollout(space.id, 10) || process.env.VERCEL_ENV === 'preview') {
|
||||
await waitUntil(
|
||||
getDataOrNull(
|
||||
dataFetcher.getRevisionPageDocument({
|
||||
spaceId: space.id,
|
||||
revisionId: space.revision,
|
||||
pageId: page.id,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isInPercentRollout(value: string, rollout: number) {
|
||||
return getRandomPercent(value) < rollout;
|
||||
}
|
||||
|
||||
function getRandomPercent(value: string) {
|
||||
const hash = value.split('').reduce((acc, char) => {
|
||||
return acc + char.charCodeAt(0);
|
||||
}, 0);
|
||||
|
||||
return hash % 100;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Revision, RevisionFile, RevisionReusableContent } from '@gitbook/api';
|
||||
import * as React from 'react';
|
||||
|
||||
const getRevisionReusableContents = React.cache((revision: Revision) => {
|
||||
return new Map(
|
||||
revision.reusableContents.map((reusableContent) => [reusableContent.id, reusableContent])
|
||||
);
|
||||
});
|
||||
|
||||
const getRevisionFiles = React.cache((revision: Revision) => {
|
||||
return new Map(revision.files.map((file) => [file.id, file]));
|
||||
});
|
||||
|
||||
/**
|
||||
* Get a revision file by its ID.
|
||||
*/
|
||||
export function getRevisionFile(input: {
|
||||
revision: Revision;
|
||||
fileId: string;
|
||||
}): RevisionFile | null {
|
||||
const { revision, fileId } = input;
|
||||
const files = getRevisionFiles(revision);
|
||||
return files.get(fileId) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a revision reusable content by its ID.
|
||||
*/
|
||||
export function getRevisionReusableContent(input: {
|
||||
revision: Revision;
|
||||
reusableContentId: string;
|
||||
}): RevisionReusableContent | null {
|
||||
const { revision, reusableContentId } = input;
|
||||
const reusableContents = getRevisionReusableContents(revision);
|
||||
return reusableContents.get(reusableContentId) ?? null;
|
||||
}
|
||||
@@ -67,27 +67,8 @@ export interface GitBookDataFetcher {
|
||||
getRevision(params: {
|
||||
spaceId: string;
|
||||
revisionId: string;
|
||||
metadata: boolean;
|
||||
}): Promise<DataFetcherResponse<api.Revision>>;
|
||||
|
||||
/**
|
||||
* Get the revision pages by its space ID and revision ID.
|
||||
*/
|
||||
getRevisionPages(params: {
|
||||
spaceId: string;
|
||||
revisionId: string;
|
||||
metadata: boolean;
|
||||
}): Promise<DataFetcherResponse<api.RevisionPage[]>>;
|
||||
|
||||
/**
|
||||
* Get a revision file by its space ID, revision ID and file ID.
|
||||
*/
|
||||
getRevisionFile(params: {
|
||||
spaceId: string;
|
||||
revisionId: string;
|
||||
fileId: string;
|
||||
}): Promise<DataFetcherResponse<api.RevisionFile>>;
|
||||
|
||||
/**
|
||||
* Get a revision page by its path.
|
||||
*/
|
||||
@@ -132,15 +113,6 @@ export interface GitBookDataFetcher {
|
||||
seed: string;
|
||||
}): Promise<DataFetcherResponse<api.JSONDocument>>;
|
||||
|
||||
/**
|
||||
* Get a reusable content by its space ID, revision ID and reusable content ID.
|
||||
*/
|
||||
getReusableContent(params: {
|
||||
spaceId: string;
|
||||
revisionId: string;
|
||||
reusableContentId: string;
|
||||
}): Promise<DataFetcherResponse<api.RevisionReusableContent>>;
|
||||
|
||||
/**
|
||||
* Get the latest OpenAPI spec version content by its organization ID and slug.
|
||||
*/
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface CloudflareImageJsonFormat {
|
||||
* https://developers.cloudflare.com/images/image-resizing/resize-with-workers/
|
||||
*/
|
||||
export interface CloudflareImageOptions {
|
||||
format?: 'webp' | 'avif' | 'json' | 'jpeg';
|
||||
format?: 'webp' | 'avif' | 'json' | 'jpeg' | 'png';
|
||||
fit?: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad';
|
||||
width?: number;
|
||||
height?: number;
|
||||
|
||||
@@ -35,8 +35,9 @@ describe('toPathInSpace', () => {
|
||||
expect(variantInSection.toPathInSpace('some/path/')).toBe('/section/variant/some/path');
|
||||
});
|
||||
|
||||
it('should not add a trailing slash', () => {
|
||||
expect(root.toPathInSpace('')).toBe('');
|
||||
it('should not add an unnecessary trailing slash', () => {
|
||||
// The index page should not be an empty path
|
||||
expect(root.toPathInSpace('')).toBe('/');
|
||||
expect(variantInSection.toPathInSpace('')).toBe('/section/variant');
|
||||
});
|
||||
});
|
||||
@@ -52,8 +53,9 @@ describe('toPathInSite', () => {
|
||||
expect(siteGitBookIO.toPathInSite('some/path/')).toBe('/sitename/some/path');
|
||||
});
|
||||
|
||||
it('should not add a trailing slash', () => {
|
||||
expect(root.toPathInSite('')).toBe('');
|
||||
it('should not add an unnecessary trailing slash', () => {
|
||||
// The index page should not be an empty path
|
||||
expect(root.toPathInSite('')).toBe('/');
|
||||
expect(siteGitBookIO.toPathInSite('')).toBe('/sitename');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -128,7 +128,8 @@ export function createLinker(
|
||||
function joinPaths(prefix: string, path: string): string {
|
||||
const prefixPath = prefix.endsWith('/') ? prefix : `${prefix}/`;
|
||||
const suffixPath = path.startsWith('/') ? path.slice(1) : path;
|
||||
return removeTrailingSlash(prefixPath + suffixPath);
|
||||
const pathWithoutTrailingSlash = removeTrailingSlash(prefixPath + suffixPath);
|
||||
return pathWithoutTrailingSlash === '' ? '/' : pathWithoutTrailingSlash;
|
||||
}
|
||||
|
||||
function removeTrailingSlash(path: string): string {
|
||||
|
||||
@@ -16,10 +16,9 @@ import {
|
||||
import { serveResizedImage } from '@/routes/image';
|
||||
import {
|
||||
DataFetcherError,
|
||||
getPublishedContentByURL,
|
||||
getVisitorAuthBasePath,
|
||||
lookupPublishedContentByUrl,
|
||||
normalizeURL,
|
||||
resolvePublishedContentByUrl,
|
||||
throwIfDataError,
|
||||
} from '@v2/lib/data';
|
||||
import { isGitBookAssetsHostURL, isGitBookHostURL } from '@v2/lib/env';
|
||||
@@ -34,18 +33,6 @@ export const config = {
|
||||
|
||||
type URLWithMode = { url: URL; mode: 'url' | 'url-host' };
|
||||
|
||||
/**
|
||||
* Temporary list of hosts to test adaptive content using the new resolution API.
|
||||
*/
|
||||
const ADAPTIVE_CONTENT_HOSTS = [
|
||||
'docs.gitbook.com',
|
||||
'paypal.gitbook.com',
|
||||
'adaptive-docs.gitbook-staging.com',
|
||||
'enriched-content-playground.gitbook-staging.io',
|
||||
'docs.testgitbook.com',
|
||||
'launchdarkly-site.gitbook.education',
|
||||
];
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
try {
|
||||
const requestURL = new URL(request.url);
|
||||
@@ -56,6 +43,12 @@ export async function middleware(request: NextRequest) {
|
||||
return NextResponse.redirect(normalized.toString());
|
||||
}
|
||||
|
||||
// Reject malicious requests
|
||||
const rejectResponse = await validateServerActionRequest(request);
|
||||
if (rejectResponse) {
|
||||
return rejectResponse;
|
||||
}
|
||||
|
||||
for (const handler of [serveSiteRoutes, serveSpacePDFRoutes]) {
|
||||
const result = await handler(requestURL, request);
|
||||
if (result) {
|
||||
@@ -69,6 +62,25 @@ export async function middleware(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
async function validateServerActionRequest(request: NextRequest) {
|
||||
// We need to reject incorrect server actions requests
|
||||
// We do not do it in cloudflare workers as there is a bug that prevents us from reading the request body.
|
||||
if (request.headers.has('next-action') && process.env.GITBOOK_RUNTIME !== 'cloudflare') {
|
||||
// We just test that the json body is parseable
|
||||
try {
|
||||
const clonedRequest = request.clone();
|
||||
await clonedRequest.json();
|
||||
} catch (e) {
|
||||
console.warn('Invalid server action request', e);
|
||||
// If the body is not parseable, we reject the request
|
||||
return new Response('Invalid request', {
|
||||
status: 400,
|
||||
headers: { 'content-type': 'text/plain' },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle request that are targetting the site routes group.
|
||||
*/
|
||||
@@ -95,6 +107,14 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
// We want to filter hostnames that contains a port here as this is likely a malicious request.
|
||||
if (siteRequestURL.host.includes(':')) {
|
||||
return new Response('Invalid request', {
|
||||
status: 400,
|
||||
headers: { 'content-type': 'text/plain' },
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Detect and extract the visitor authentication token from the request
|
||||
//
|
||||
@@ -104,11 +124,8 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
});
|
||||
|
||||
const withAPIToken = async (apiToken: string | null) => {
|
||||
const resolve = ADAPTIVE_CONTENT_HOSTS.includes(siteRequestURL.hostname)
|
||||
? resolvePublishedContentByUrl
|
||||
: getPublishedContentByURL;
|
||||
const siteURLData = await throwIfDataError(
|
||||
resolve({
|
||||
lookupPublishedContentByUrl({
|
||||
url: siteRequestURL.toString(),
|
||||
visitorPayload: {
|
||||
jwtToken: visitorToken?.token ?? undefined,
|
||||
@@ -275,6 +292,8 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
console.log(`rewriting ${request.nextUrl.toString()} to ${route}`);
|
||||
|
||||
const rewrittenURL = new URL(`/${route}`, request.nextUrl.toString());
|
||||
rewrittenURL.search = request.nextUrl.search; // Preserve the original search params
|
||||
|
||||
const response = NextResponse.rewrite(rewrittenURL, {
|
||||
request: {
|
||||
headers: requestHeaders,
|
||||
|
||||
@@ -117,11 +117,11 @@ const testCases: TestsCase[] = [
|
||||
contentBaseURL: 'https://docs.gradient.network',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'mygate-network.gitbook.io',
|
||||
contentBaseURL: 'https://mygate-network.gitbook.io',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
// {
|
||||
// name: 'mygate-network.gitbook.io',
|
||||
// contentBaseURL: 'https://mygate-network.gitbook.io',
|
||||
// tests: [{ name: 'Home', url: '/' }],
|
||||
// },
|
||||
{
|
||||
name: 'treasurenft.gitbook.io',
|
||||
contentBaseURL: 'https://treasurenft.gitbook.io',
|
||||
|
||||
@@ -603,11 +603,21 @@ const testCases: TestsCase[] = [
|
||||
url: 'blocks/emojis',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Icons',
|
||||
url: 'blocks/icons',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Links',
|
||||
url: 'blocks/links',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Buttons',
|
||||
url: 'blocks/buttons',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Lists',
|
||||
url: 'blocks/lists',
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import { argosScreenshot } from '@argos-ci/playwright';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { getContentTestURL } from '../tests/utils';
|
||||
import { waitForIcons } from './util';
|
||||
|
||||
test.describe('PDF export', () => {
|
||||
test('export all pages as PDF (e2e)', async ({ page }) => {
|
||||
// Set the header to disable the Vercel toolbar
|
||||
// But only on the main document as it'd cause CORS issues on other resources
|
||||
await page.route('**/*', async (route, request) => {
|
||||
if (request.resourceType() === 'document') {
|
||||
await route.continue({
|
||||
headers: {
|
||||
...request.headers(),
|
||||
'x-vercel-skip-toolbar': '1',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(
|
||||
getContentTestURL(
|
||||
'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/~gitbook/pdf?limit=10'
|
||||
)
|
||||
);
|
||||
|
||||
const printBtn = page.getByTestId('print-button');
|
||||
await expect(printBtn).toBeVisible();
|
||||
|
||||
await argosScreenshot(page, 'pdf - all pages', {
|
||||
viewports: ['macbook-13'],
|
||||
argosCSS: `
|
||||
/* Hide Intercom */
|
||||
.intercom-lightweight-app {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
threshold: undefined,
|
||||
fullPage: true,
|
||||
beforeScreenshot: async ({ runStabilization }) => {
|
||||
await runStabilization();
|
||||
await waitForIcons(page);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('export all pages as PDF (GitBook docs)', async ({ page }) => {
|
||||
// Set the header to disable the Vercel toolbar
|
||||
// But only on the main document as it'd cause CORS issues on other resources
|
||||
await page.route('**/*', async (route, request) => {
|
||||
if (request.resourceType() === 'document') {
|
||||
await route.continue({
|
||||
headers: {
|
||||
...request.headers(),
|
||||
'x-vercel-skip-toolbar': '1',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(getContentTestURL('https://gitbook.com/docs/~gitbook/pdf?limit=10'));
|
||||
|
||||
const printBtn = page.getByTestId('print-button');
|
||||
await expect(printBtn).toBeVisible();
|
||||
|
||||
await argosScreenshot(page, 'pdf - all pages', {
|
||||
viewports: ['macbook-13'],
|
||||
argosCSS: `
|
||||
/* Hide Intercom */
|
||||
.intercom-lightweight-app {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
threshold: undefined,
|
||||
fullPage: true,
|
||||
beforeScreenshot: async ({ runStabilization }) => {
|
||||
await runStabilization();
|
||||
await waitForIcons(page);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('export a single page as PDF (e2e)', async ({ page }) => {
|
||||
// Set the header to disable the Vercel toolbar
|
||||
// But only on the main document as it'd cause CORS issues on other resources
|
||||
await page.route('**/*', async (route, request) => {
|
||||
if (request.resourceType() === 'document') {
|
||||
await route.continue({
|
||||
headers: {
|
||||
...request.headers(),
|
||||
'x-vercel-skip-toolbar': '1',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(
|
||||
getContentTestURL(
|
||||
'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/~gitbook/pdf?page=Bw7LjWwgTjV8nIV4s7rs&only=yes&limit=2'
|
||||
)
|
||||
);
|
||||
|
||||
const printBtn = page.getByTestId('print-button');
|
||||
await expect(printBtn).toBeVisible();
|
||||
|
||||
await argosScreenshot(page, 'pdf - all pages', {
|
||||
viewports: ['macbook-13'],
|
||||
argosCSS: `
|
||||
/* Hide Intercom */
|
||||
.intercom-lightweight-app {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
threshold: undefined,
|
||||
fullPage: true,
|
||||
beforeScreenshot: async ({ runStabilization }) => {
|
||||
await runStabilization();
|
||||
await waitForIcons(page);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('export a single page as PDF (GitBook docs)', async ({ page }) => {
|
||||
// Set the header to disable the Vercel toolbar
|
||||
// But only on the main document as it'd cause CORS issues on other resources
|
||||
await page.route('**/*', async (route, request) => {
|
||||
if (request.resourceType() === 'document') {
|
||||
await route.continue({
|
||||
headers: {
|
||||
...request.headers(),
|
||||
'x-vercel-skip-toolbar': '1',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(
|
||||
getContentTestURL(
|
||||
'https://gitbook.com/docs/~gitbook/pdf?page=DfnNkU49mvLe2ythHAyx&only=yes&limit=2'
|
||||
)
|
||||
);
|
||||
|
||||
const printBtn = page.getByTestId('print-button');
|
||||
await expect(printBtn).toBeVisible();
|
||||
|
||||
await argosScreenshot(page, 'pdf - all pages', {
|
||||
viewports: ['macbook-13'],
|
||||
argosCSS: `
|
||||
/* Hide Intercom */
|
||||
.intercom-lightweight-app {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
threshold: undefined,
|
||||
fullPage: true,
|
||||
beforeScreenshot: async ({ runStabilization }) => {
|
||||
await runStabilization();
|
||||
await waitForIcons(page);
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
CustomizationThemeMode,
|
||||
type CustomizationThemedColor,
|
||||
type SiteCustomizationSettings,
|
||||
SiteExternalLinksTarget,
|
||||
} from '@gitbook/api';
|
||||
import { type BrowserContext, type Page, type Response, expect, test } from '@playwright/test';
|
||||
import deepMerge from 'deepmerge';
|
||||
@@ -317,6 +318,9 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
|
||||
aiSearch: {
|
||||
enabled: true,
|
||||
},
|
||||
externalLinks: {
|
||||
target: SiteExternalLinksTarget.Self,
|
||||
},
|
||||
advancedCustomization: {
|
||||
enabled: true,
|
||||
},
|
||||
@@ -346,7 +350,7 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
|
||||
/**
|
||||
* Wait for all icons present on the page to be loaded.
|
||||
*/
|
||||
async function waitForIcons(page: Page) {
|
||||
export async function waitForIcons(page: Page) {
|
||||
await page.waitForFunction(() => {
|
||||
const urlStates: Record<
|
||||
string,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"build:cloudflare": "next-on-pages --custom-entrypoint=./src/cloudflare-entrypoint.ts",
|
||||
"start": "next start",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"e2e": "playwright test e2e/internal.spec.ts",
|
||||
"e2e": "playwright test e2e/internal.spec.ts e2e/pdf.spec.ts",
|
||||
"e2e-customers": "playwright test e2e/customers.spec.ts",
|
||||
"unit": "bun test {src,packages} --preload ./tests/preload-bun.ts",
|
||||
"generate": "gitbook-icons ./public/~gitbook/static/icons custom-icons && gitbook-math ./public/~gitbook/static/math",
|
||||
@@ -81,7 +81,9 @@
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"zod": "^3.24.2",
|
||||
"zod-to-json-schema": "^3.24.5",
|
||||
"zustand": "^5.0.3"
|
||||
"zustand": "^5.0.3",
|
||||
"image-size": "^2.0.2",
|
||||
"direction": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@argos-ci/playwright": "^5.0.3",
|
||||
|
||||
@@ -4,7 +4,6 @@ import { headers } from 'next/headers';
|
||||
import React from 'react';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { throwIfDataError } from '@v2/lib/data';
|
||||
|
||||
import { DateRelative } from '../primitives';
|
||||
import { RefreshChangeRequestButton } from './RefreshChangeRequestButton';
|
||||
@@ -59,7 +58,7 @@ export async function AdminToolbar(props: AdminToolbarProps) {
|
||||
return <ChangeRequestToolbar context={context} />;
|
||||
}
|
||||
|
||||
if (context.revisionId !== context.space.revision) {
|
||||
if (context.revision.id !== context.space.revision) {
|
||||
return <RevisionToolbar context={context} />;
|
||||
}
|
||||
|
||||
@@ -106,15 +105,7 @@ async function ChangeRequestToolbar(props: { context: GitBookSiteContext }) {
|
||||
|
||||
async function RevisionToolbar(props: { context: GitBookSiteContext }) {
|
||||
const { context } = props;
|
||||
const { space, revisionId } = context;
|
||||
|
||||
const revision = await throwIfDataError(
|
||||
context.dataFetcher.getRevision({
|
||||
spaceId: space.id,
|
||||
revisionId,
|
||||
metadata: true,
|
||||
})
|
||||
);
|
||||
const { revision } = context;
|
||||
|
||||
return (
|
||||
<ToolbarLayout>
|
||||
|
||||
@@ -19,14 +19,14 @@ export async function AdClassicRendering({
|
||||
insightsAd: SiteInsightsAd | null;
|
||||
context: GitBookBaseContext;
|
||||
}) {
|
||||
const smallImgSrc =
|
||||
const [smallImgSrc, logoSrc] = await Promise.all([
|
||||
'smallImage' in ad
|
||||
? await getResizedImageURL(context.imageResizer, ad.smallImage, { width: 192, dpr: 2 })
|
||||
: null;
|
||||
const logoSrc =
|
||||
? getResizedImageURL(context.imageResizer, ad.smallImage, { width: 192, dpr: 2 })
|
||||
: null,
|
||||
'logo' in ad
|
||||
? await getResizedImageURL(context.imageResizer, ad.logo, { width: 192 - 48, dpr: 2 })
|
||||
: null;
|
||||
? getResizedImageURL(context.imageResizer, ad.logo, { width: 192 - 48, dpr: 2 })
|
||||
: null,
|
||||
]);
|
||||
return (
|
||||
<Link
|
||||
rel="sponsored noopener"
|
||||
|
||||
@@ -43,10 +43,12 @@ interface FetchPlaceholderAdOptions {
|
||||
* and properly access user-agent and IP.
|
||||
*/
|
||||
export async function renderAd(options: FetchAdOptions) {
|
||||
const context = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
|
||||
const [context, result] = await Promise.all([
|
||||
isV2() ? getServerActionBaseContext() : getV1BaseContext(),
|
||||
options.source === 'live' ? fetchAd(options) : getPlaceholderAd(),
|
||||
]);
|
||||
|
||||
const mode = options.source === 'live' ? options.mode : 'classic';
|
||||
const result = options.source === 'live' ? await fetchAd(options) : await getPlaceholderAd();
|
||||
if (!result || !result.ad.description || !result.ad.statlink) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import { tcls } from '@/lib/tailwind';
|
||||
import { type CustomizationAnnouncement, SiteInsightsLinkPosition } from '@gitbook/api';
|
||||
import { Icon, type IconName } from '@gitbook/icons';
|
||||
import { CONTAINER_STYLE } from '../layout';
|
||||
import { Link, linkStyles } from '../primitives';
|
||||
import { Link } from '../primitives';
|
||||
import { LinkStyles } from '../primitives/styles';
|
||||
import { ANNOUNCEMENT_CSS_CLASS, ANNOUNCEMENT_STORAGE_KEY } from './constants';
|
||||
|
||||
/**
|
||||
@@ -58,7 +59,7 @@ export function AnnouncementBanner(props: {
|
||||
<div>
|
||||
{announcement.message}
|
||||
{hasLink ? (
|
||||
<div className={tcls(linkStyles, style.link, 'ml-1 inline')}>
|
||||
<div className={tcls(LinkStyles, style.link, 'ml-1 inline')}>
|
||||
{contentRef?.icon ? (
|
||||
<span className="mr-1 ml-2 *:inline">
|
||||
{contentRef?.icon}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
.highlight-line {
|
||||
@apply grid [grid-template-columns:subgrid] col-span-2 relative ring-1 ring-transparent hover:ring-tint-hover hover:z-[1] rounded;
|
||||
@apply only:hover:ring-transparent;
|
||||
@apply [counter-increment:line];
|
||||
|
||||
&.highlighted {
|
||||
@apply bg-tint-active;
|
||||
@apply first:rounded-t-md *:first:mt-1;
|
||||
@apply last:rounded-b-md *:last:mb-1;
|
||||
@apply rounded-none;
|
||||
}
|
||||
|
||||
&:not(.highlighted) + .highlighted {
|
||||
@apply rounded-t-md *:mt-1;
|
||||
}
|
||||
|
||||
&.highlighted:has(+ :not(.highlighted)) {
|
||||
@apply rounded-b-md *:mb-1;
|
||||
}
|
||||
|
||||
&:not(.highlighted) + .highlighted:has(+ :not(.highlighted)) {
|
||||
@apply rounded-md;
|
||||
}
|
||||
}
|
||||
|
||||
.highlight-line-number {
|
||||
@apply text-sm text-right pr-3.5 rounded-l pl-2 sticky left-[-3px] bg-gradient-to-r from-80% from-tint-subtle contrast-more:from-tint-base theme-muted:from-tint-base [html.theme-bold.sidebar-filled_&]:from-tint-base to-transparent;
|
||||
@apply before:text-tint before:content-[counter(line)];
|
||||
|
||||
.highlight-line.highlighted > & {
|
||||
@apply before:text-tint bg-gradient-to-r from-80% from-tint-active to-transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.highlight-line-content {
|
||||
@apply ml-3 block text-sm;
|
||||
}
|
||||
@@ -9,9 +9,6 @@ import type { BlockProps } from '../Block';
|
||||
import { CopyCodeButton } from './CopyCodeButton';
|
||||
import type { HighlightLine, HighlightToken } from './highlight';
|
||||
|
||||
import './theme.css';
|
||||
import './CodeBlockRenderer.css';
|
||||
|
||||
type CodeBlockRendererProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> & {
|
||||
lines: HighlightLine[];
|
||||
'aria-busy'?: boolean;
|
||||
@@ -50,7 +47,7 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
|
||||
/>
|
||||
<pre
|
||||
className={tcls(
|
||||
'hide-scroll relative overflow-auto border border-tint-subtle bg-tint-subtle theme-muted:bg-tint-base [grid-area:2/1] contrast-more:border-tint contrast-more:bg-tint-base [html.theme-bold.sidebar-filled_&]:bg-tint-base',
|
||||
'relative overflow-auto border border-tint-subtle bg-tint-subtle theme-muted:bg-tint-base p-2 [grid-area:2/1] contrast-more:border-tint contrast-more:bg-tint-base [html.theme-bold.sidebar-filled_&]:bg-tint-base',
|
||||
'rounded-md straight-corners:rounded-sm shadow-sm',
|
||||
title && 'rounded-ss-none'
|
||||
)}
|
||||
@@ -58,7 +55,7 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
|
||||
<code
|
||||
id={id}
|
||||
className={tcls(
|
||||
'inline-grid min-w-full grid-cols-[auto_1fr] p-2 [count-reset:line] print:whitespace-pre-wrap',
|
||||
'inline-grid min-w-full grid-cols-[auto_1fr] [count-reset:line] print:whitespace-pre-wrap',
|
||||
withWrap && 'whitespace-pre-wrap'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
|
||||
import { type BundledLanguage, bundledLanguages } from 'shiki/langs';
|
||||
|
||||
import { nullIfNever } from '@/lib/typescript';
|
||||
import { plainHighlight } from './plain-highlight';
|
||||
|
||||
export type HighlightLine = {
|
||||
@@ -263,16 +264,28 @@ function getPlainCodeBlockLine(
|
||||
if (node.object === 'text') {
|
||||
content += cleanupLine(node.leaves.map((leaf) => leaf.text).join(''));
|
||||
} else {
|
||||
const start = index + content.length;
|
||||
content += getPlainCodeBlockLine(node, index + content.length, inlines);
|
||||
const end = index + content.length;
|
||||
switch (node.type) {
|
||||
case 'annotation': {
|
||||
const start = index + content.length;
|
||||
content += getPlainCodeBlockLine(node, index + content.length, inlines);
|
||||
const end = index + content.length;
|
||||
|
||||
if (inlines) {
|
||||
inlines.push({
|
||||
inline: node,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
if (inlines) {
|
||||
inlines.push({
|
||||
inline: node,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'expression': {
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
nullIfNever(node);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
:root {
|
||||
--shiki-color-text: theme("colors.tint.11");
|
||||
--shiki-token-punctuation: theme("colors.tint.11");
|
||||
--shiki-token-comment: theme("colors.neutral.9/.7");
|
||||
--shiki-token-link: theme("colors.primary.10");
|
||||
|
||||
--shiki-token-constant: theme("colors.warning.10");
|
||||
--shiki-token-string: theme("colors.warning.10");
|
||||
--shiki-token-string-expression: theme("colors.success.10");
|
||||
--shiki-token-keyword: theme("colors.danger.10");
|
||||
--shiki-token-parameter: theme("colors.warning.10");
|
||||
--shiki-token-function: theme("colors.primary.10");
|
||||
|
||||
--shiki-token-inserted: theme("colors.success.10");
|
||||
--shiki-token-deleted: theme("colors.danger.10");
|
||||
--shiki-token-changed: theme("colors.tint.12");
|
||||
}
|
||||
|
||||
@media (prefers-contrast: more) {
|
||||
:root {
|
||||
--shiki-color-text: theme("colors.tint.12");
|
||||
--shiki-token-punctuation: theme("colors.tint.12");
|
||||
--shiki-token-comment: theme("colors.neutral.11");
|
||||
--shiki-token-link: theme("colors.primary.11");
|
||||
|
||||
--shiki-token-constant: theme("colors.warning.11");
|
||||
--shiki-token-string: theme("colors.warning.11");
|
||||
--shiki-token-string-expression: theme("colors.success.11");
|
||||
--shiki-token-keyword: theme("colors.danger.11");
|
||||
--shiki-token-parameter: theme("colors.warning.11");
|
||||
--shiki-token-function: theme("colors.primary.11");
|
||||
|
||||
--shiki-token-inserted: theme("colors.success.11");
|
||||
--shiki-token-deleted: theme("colors.danger.11");
|
||||
--shiki-token-changed: theme("colors.tint.12");
|
||||
}
|
||||
}
|
||||
|
||||
html.dark {
|
||||
/* Override select colors to have more contrast */
|
||||
--shiki-token-comment: theme("colors.neutral.9");
|
||||
|
||||
--shiki-token-constant: theme("colors.warning.11");
|
||||
--shiki-token-string: theme("colors.warning.11");
|
||||
--shiki-token-string-expression: theme("colors.success.11");
|
||||
--shiki-token-keyword: theme("colors.danger.11");
|
||||
--shiki-token-parameter: theme("colors.warning.11");
|
||||
--shiki-token-function: theme("colors.primary.11");
|
||||
}
|
||||
|
||||
.code-monochrome {
|
||||
--shiki-token-constant: theme("colors.tint.11");
|
||||
--shiki-token-string: theme("colors.tint.12");
|
||||
--shiki-token-string-expression: theme("colors.tint.12");
|
||||
--shiki-token-keyword: theme("colors.primary.10");
|
||||
--shiki-token-parameter: theme("colors.tint.9");
|
||||
--shiki-token-function: theme("colors.primary.9");
|
||||
}
|
||||
|
||||
html.dark.code-monochrome {
|
||||
--shiki-token-constant: theme("colors.tint.11");
|
||||
--shiki-token-string: theme("colors.tint.12");
|
||||
--shiki-token-string-expression: theme("colors.tint.12");
|
||||
--shiki-token-keyword: theme("colors.primary.11");
|
||||
--shiki-token-parameter: theme("colors.tint.10");
|
||||
--shiki-token-function: theme("colors.primary.10");
|
||||
}
|
||||
@@ -28,6 +28,14 @@ export interface DocumentContext {
|
||||
* @default true
|
||||
*/
|
||||
wrapBlocksInSuspense?: boolean;
|
||||
|
||||
/**
|
||||
* True if link previews should be rendered.
|
||||
* This is used to limit the number of link previews rendered in a document.
|
||||
* If false, no link previews will be rendered.
|
||||
* @default false
|
||||
*/
|
||||
shouldRenderLinkPreviews?: boolean;
|
||||
}
|
||||
|
||||
export interface DocumentContextProps {
|
||||
|
||||
@@ -6,9 +6,10 @@ import type { BlockProps } from './Block';
|
||||
import { HashLinkButton, hashLinkButtonWrapperStyles } from './HashLinkButton';
|
||||
import { Inlines } from './Inlines';
|
||||
import { getBlockTextStyle } from './spacing';
|
||||
import { getTextAlignment } from './utils';
|
||||
|
||||
export function Heading(props: BlockProps<DocumentBlockHeading>) {
|
||||
const { block, style, context, ...rest } = props;
|
||||
const { block, style, context, ancestorBlocks, ...rest } = props;
|
||||
|
||||
const textStyle = getBlockTextStyle(block);
|
||||
|
||||
@@ -42,7 +43,7 @@ export function Heading(props: BlockProps<DocumentBlockHeading>) {
|
||||
'grid-area-1-1',
|
||||
'z-[1]',
|
||||
'justify-self-start',
|
||||
'text-left',
|
||||
getTextAlignment(block.data.align, ancestorBlocks),
|
||||
textStyle.lineHeight,
|
||||
textStyle.marginTop
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Annotation } from './Annotation/Annotation';
|
||||
import type { DocumentContextProps } from './DocumentView';
|
||||
import { Emoji } from './Emoji';
|
||||
import { InlineButton } from './InlineButton';
|
||||
import { InlineIcon } from './InlineIcon';
|
||||
import { InlineImage } from './InlineImage';
|
||||
import { InlineLink } from './InlineLink';
|
||||
import { InlineMath } from './Math';
|
||||
@@ -47,6 +48,8 @@ export function Inline<T extends DocumentInline>(props: InlineProps<T>) {
|
||||
return <InlineImage {...contextProps} inline={inline} />;
|
||||
case 'button':
|
||||
return <InlineButton {...contextProps} inline={inline} />;
|
||||
case 'icon':
|
||||
return <InlineIcon {...contextProps} inline={inline} />;
|
||||
case 'expression':
|
||||
// The GitBook API should take care of evaluating expressions.
|
||||
// We should never need to render them.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
import * as api from '@gitbook/api';
|
||||
import type { IconName } from '@gitbook/icons';
|
||||
import { Button } from '../primitives';
|
||||
import type { InlineProps } from './Inline';
|
||||
|
||||
@@ -25,6 +26,7 @@ export async function InlineButton(props: InlineProps<api.DocumentInlineButton>)
|
||||
// TODO: use a variant specifically for user-defined buttons.
|
||||
variant={inline.data.kind}
|
||||
className="leading-normal"
|
||||
icon={inline.data.icon as IconName | undefined}
|
||||
insights={{
|
||||
type: 'link_click',
|
||||
link: {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { DocumentInlineIcon } from '@gitbook/api';
|
||||
|
||||
import { Icon, type IconName } from '@gitbook/icons';
|
||||
import type { InlineProps } from './Inline';
|
||||
|
||||
export async function InlineIcon(props: InlineProps<DocumentInlineIcon>) {
|
||||
const { inline } = props;
|
||||
|
||||
return <Icon icon={inline.data.icon as IconName} className="inline size-[1em]" />;
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { type DocumentInlineLink, SiteInsightsLinkPosition } from '@gitbook/api';
|
||||
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { StyledLink } from '../primitives';
|
||||
import type { InlineProps } from './Inline';
|
||||
import { InlineLinkTooltip } from './InlineLinkTooltip';
|
||||
import { Inlines } from './Inlines';
|
||||
|
||||
export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
|
||||
const { inline, document, context, ancestorInlines } = props;
|
||||
|
||||
const resolved = context.contentContext
|
||||
? await resolveContentRef(inline.data.ref, context.contentContext, {
|
||||
// We don't want to resolve the anchor text here, as it can be very expensive and will block rendering if there is a lot of anchors link.
|
||||
resolveAnchorText: false,
|
||||
})
|
||||
: null;
|
||||
|
||||
if (!context.contentContext || !resolved) {
|
||||
return (
|
||||
<span title="Broken link" className="underline">
|
||||
<Inlines
|
||||
context={context}
|
||||
document={document}
|
||||
nodes={inline.nodes}
|
||||
ancestorInlines={[...ancestorInlines, inline]}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const isExternal = inline.data.ref.kind === 'url';
|
||||
|
||||
return (
|
||||
<InlineLinkTooltip inline={inline} context={context.contentContext} resolved={resolved}>
|
||||
<StyledLink
|
||||
href={resolved.href}
|
||||
insights={{
|
||||
type: 'link_click',
|
||||
link: {
|
||||
target: inline.data.ref,
|
||||
position: SiteInsightsLinkPosition.Content,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Inlines
|
||||
context={context}
|
||||
document={document}
|
||||
nodes={inline.nodes}
|
||||
ancestorInlines={[...ancestorInlines, inline]}
|
||||
/>
|
||||
{isExternal ? (
|
||||
<Icon
|
||||
icon="arrow-up-right"
|
||||
className="ml-0.5 inline size-3 links-accent:text-tint-subtle"
|
||||
/>
|
||||
) : null}
|
||||
</StyledLink>
|
||||
</InlineLinkTooltip>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import { type DocumentInlineLink, SiteInsightsLinkPosition } from '@gitbook/api';
|
||||
|
||||
import { getSpaceLanguage, tString } from '@/intl/server';
|
||||
import { languages } from '@/intl/translations';
|
||||
import { type ResolvedContentRef, resolveContentRef } from '@/lib/references';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import type { GitBookAnyContext } from '@v2/lib/context';
|
||||
import { StyledLink } from '../../primitives';
|
||||
import type { InlineProps } from '../Inline';
|
||||
import { Inlines } from '../Inlines';
|
||||
import { InlineLinkTooltip } from './InlineLinkTooltip';
|
||||
|
||||
export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
|
||||
const { inline, document, context, ancestorInlines } = props;
|
||||
|
||||
const resolved = context.contentContext
|
||||
? await resolveContentRef(inline.data.ref, context.contentContext, {
|
||||
// We don't want to resolve the anchor text here, as it can be very expensive and will block rendering if there is a lot of anchors link.
|
||||
resolveAnchorText: false,
|
||||
})
|
||||
: null;
|
||||
|
||||
if (!context.contentContext || !resolved) {
|
||||
return (
|
||||
<span title="Broken link" className="underline">
|
||||
<Inlines
|
||||
context={context}
|
||||
document={document}
|
||||
nodes={inline.nodes}
|
||||
ancestorInlines={[...ancestorInlines, inline]}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const isExternal = inline.data.ref.kind === 'url';
|
||||
const content = (
|
||||
<StyledLink
|
||||
href={resolved.href}
|
||||
insights={{
|
||||
type: 'link_click',
|
||||
link: {
|
||||
target: inline.data.ref,
|
||||
position: SiteInsightsLinkPosition.Content,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Inlines
|
||||
context={context}
|
||||
document={document}
|
||||
nodes={inline.nodes}
|
||||
ancestorInlines={[...ancestorInlines, inline]}
|
||||
/>
|
||||
{isExternal ? (
|
||||
<Icon
|
||||
icon="arrow-up-right"
|
||||
className="ml-0.5 inline size-3 links-accent:text-tint-subtle"
|
||||
/>
|
||||
) : null}
|
||||
</StyledLink>
|
||||
);
|
||||
|
||||
if (context.shouldRenderLinkPreviews) {
|
||||
return (
|
||||
<InlineLinkTooltipWrapper
|
||||
inline={inline}
|
||||
context={context.contentContext}
|
||||
resolved={resolved}
|
||||
>
|
||||
{content}
|
||||
</InlineLinkTooltipWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* An SSR component that renders a link with a tooltip.
|
||||
* Essentially it pulls the minimum amount of props from the context to render the tooltip.
|
||||
*/
|
||||
function InlineLinkTooltipWrapper(props: {
|
||||
inline: DocumentInlineLink;
|
||||
context: GitBookAnyContext;
|
||||
children: React.ReactNode;
|
||||
resolved: ResolvedContentRef;
|
||||
}) {
|
||||
const { inline, context, resolved, children } = props;
|
||||
|
||||
let breadcrumbs = resolved.ancestors ?? [];
|
||||
const language =
|
||||
'customization' in context ? getSpaceLanguage(context.customization) : languages.en;
|
||||
const isExternal = inline.data.ref.kind === 'url';
|
||||
const isSamePage = inline.data.ref.kind === 'anchor' && inline.data.ref.page === undefined;
|
||||
if (isExternal) {
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: tString(language, 'link_tooltip_external_link'),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (isSamePage) {
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: tString(language, 'link_tooltip_page_anchor'),
|
||||
icon: <Icon icon="arrow-down-short-wide" className="size-3" />,
|
||||
},
|
||||
];
|
||||
resolved.subText = undefined;
|
||||
}
|
||||
|
||||
const aiSummary: { pageId: string; spaceId: string } | undefined = (() => {
|
||||
if (isExternal) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSamePage) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!('customization' in context) || !context.customization.ai?.pageLinkSummaries.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!('page' in context) || !('page' in inline.data.ref)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (inline.data.ref.kind === 'page' || inline.data.ref.kind === 'anchor') {
|
||||
return {
|
||||
pageId: resolved.page?.id ?? inline.data.ref.page ?? context.page.id,
|
||||
spaceId: inline.data.ref.space ?? context.space.id,
|
||||
};
|
||||
}
|
||||
})();
|
||||
|
||||
return (
|
||||
<InlineLinkTooltip
|
||||
breadcrumbs={breadcrumbs}
|
||||
isExternal={isExternal}
|
||||
isSamePage={isSamePage}
|
||||
aiSummary={aiSummary}
|
||||
openInNewTabLabel={tString(language, 'open_in_new_tab')}
|
||||
target={{
|
||||
href: resolved.href,
|
||||
text: resolved.text,
|
||||
subText: resolved.subText,
|
||||
icon: resolved.icon,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</InlineLinkTooltip>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
'use client';
|
||||
import dynamic from 'next/dynamic';
|
||||
import React from 'react';
|
||||
|
||||
const LoadingValueContext = React.createContext<React.ReactNode>(null);
|
||||
|
||||
// To avoid polluting the RSC payload with the tooltip implementation,
|
||||
// we lazily load it on the client side. This way, the tooltip is only loaded
|
||||
// when the user interacts with the link, and it doesn't block the initial render.
|
||||
|
||||
const InlineLinkTooltipImpl = dynamic(
|
||||
() => import('./InlineLinkTooltipImpl').then((mod) => mod.InlineLinkTooltipImpl),
|
||||
{
|
||||
// Disable server-side rendering for this component, it's only
|
||||
// visible on user interaction.
|
||||
ssr: false,
|
||||
loading: () => {
|
||||
// The fallback should be the children (the content of the link),
|
||||
// but as next/dynamic is aiming for feature parity with React.lazy,
|
||||
// it doesn't support passing children to the loading component.
|
||||
// https://github.com/vercel/next.js/issues/7906
|
||||
const children = React.useContext(LoadingValueContext);
|
||||
return <>{children}</>;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Tooltip for inline links. It's lazily loaded to avoid blocking the initial render
|
||||
* and polluting the RSC payload.
|
||||
*
|
||||
* The link text and href have already been rendered on the server for good SEO,
|
||||
* so we can be as lazy as possible with the tooltip.
|
||||
*/
|
||||
export function InlineLinkTooltip(props: {
|
||||
isSamePage: boolean;
|
||||
isExternal: boolean;
|
||||
aiSummary?: { pageId: string; spaceId: string };
|
||||
breadcrumbs: Array<{ href?: string; label: string; icon?: React.ReactNode }>;
|
||||
target: {
|
||||
href: string;
|
||||
text: string;
|
||||
subText?: string;
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
openInNewTabLabel: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { children, ...rest } = props;
|
||||
const [shouldLoad, setShouldLoad] = React.useState(false);
|
||||
|
||||
// Once the browser is idle, we set shouldLoad to true.
|
||||
// NOTE: to be slightly more performant, we could load when a link is hovered.
|
||||
// But I found this was too much of a delay for the tooltip to appear.
|
||||
// Loading on idle is a good compromise, as it allows the initial render to be fast,
|
||||
// while still loading the tooltip in the background and not polluting the RSC payload.
|
||||
React.useEffect(() => {
|
||||
if ('requestIdleCallback' in window) {
|
||||
(window as globalThis.Window).requestIdleCallback(() => setShouldLoad(true));
|
||||
} else {
|
||||
// fallback for old browsers
|
||||
setTimeout(() => setShouldLoad(true), 2000);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return shouldLoad ? (
|
||||
<LoadingValueContext.Provider value={children}>
|
||||
<InlineLinkTooltipImpl {...rest}>{children}</InlineLinkTooltipImpl>
|
||||
</LoadingValueContext.Provider>
|
||||
) : (
|
||||
children
|
||||
);
|
||||
}
|
||||
+34
-67
@@ -1,55 +1,27 @@
|
||||
import type { DocumentInlineLink } from '@gitbook/api';
|
||||
|
||||
import type { ResolvedContentRef } from '@/lib/references';
|
||||
|
||||
import { getSpaceLanguage } from '@/intl/server';
|
||||
import { tString } from '@/intl/translate';
|
||||
import { languages } from '@/intl/translations';
|
||||
import { getNodeText } from '@/lib/document';
|
||||
'use client';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||
import type { GitBookAnyContext } from '@v2/lib/context';
|
||||
import { Fragment } from 'react';
|
||||
import { AIPageLinkSummary } from '../Adaptive/AIPageLinkSummary';
|
||||
import { Button, StyledLink } from '../primitives';
|
||||
import { AIPageLinkSummary } from '../../Adaptive';
|
||||
import { Button, StyledLink } from '../../primitives';
|
||||
|
||||
export async function InlineLinkTooltip(props: {
|
||||
inline: DocumentInlineLink;
|
||||
context: GitBookAnyContext;
|
||||
export function InlineLinkTooltipImpl(props: {
|
||||
isSamePage: boolean;
|
||||
isExternal: boolean;
|
||||
aiSummary?: { pageId: string; spaceId: string };
|
||||
breadcrumbs: Array<{ href?: string; label: string; icon?: React.ReactNode }>;
|
||||
target: {
|
||||
href: string;
|
||||
text: string;
|
||||
subText?: string;
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
openInNewTabLabel: string;
|
||||
children: React.ReactNode;
|
||||
resolved: ResolvedContentRef;
|
||||
}) {
|
||||
const { inline, context, resolved, children } = props;
|
||||
|
||||
let breadcrumbs = resolved.ancestors;
|
||||
const language =
|
||||
'customization' in context ? getSpaceLanguage(context.customization) : languages.en;
|
||||
const isExternal = inline.data.ref.kind === 'url';
|
||||
const isSamePage = inline.data.ref.kind === 'anchor' && inline.data.ref.page === undefined;
|
||||
if (isExternal) {
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: tString(language, 'link_tooltip_external_link'),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (isSamePage) {
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: tString(language, 'link_tooltip_page_anchor'),
|
||||
icon: <Icon icon="arrow-down-short-wide" className="size-3" />,
|
||||
},
|
||||
];
|
||||
resolved.subText = undefined;
|
||||
}
|
||||
|
||||
const hasAISummary =
|
||||
!isExternal &&
|
||||
!isSamePage &&
|
||||
'customization' in context &&
|
||||
context.customization.ai?.pageLinkSummaries.enabled &&
|
||||
(inline.data.ref.kind === 'page' || inline.data.ref.kind === 'anchor');
|
||||
const { isSamePage, isExternal, aiSummary, openInNewTabLabel, target, breadcrumbs, children } =
|
||||
props;
|
||||
|
||||
return (
|
||||
<Tooltip.Provider delayDuration={200}>
|
||||
@@ -100,15 +72,15 @@ export async function InlineLinkTooltip(props: {
|
||||
isExternal && 'text-sm [overflow-wrap:anywhere]'
|
||||
)}
|
||||
>
|
||||
{resolved.icon ? (
|
||||
{target.icon ? (
|
||||
<div className="mt-1 text-tint-subtle empty:hidden">
|
||||
{resolved.icon}
|
||||
{target.icon}
|
||||
</div>
|
||||
) : null}
|
||||
<h5 className="font-semibold">{resolved.text}</h5>
|
||||
<h5 className="font-semibold">{target.text}</h5>
|
||||
</div>
|
||||
</div>
|
||||
{!isSamePage && resolved.href ? (
|
||||
{!isSamePage && target.href ? (
|
||||
<Button
|
||||
className={tcls(
|
||||
'-mx-2 -my-2 ml-auto',
|
||||
@@ -117,40 +89,35 @@ export async function InlineLinkTooltip(props: {
|
||||
: null
|
||||
)}
|
||||
variant="blank"
|
||||
href={resolved.href}
|
||||
href={target.href}
|
||||
target="_blank"
|
||||
label={tString(language, 'open_in_new_tab')}
|
||||
label={openInNewTabLabel}
|
||||
size="small"
|
||||
icon="arrow-up-right-from-square"
|
||||
iconOnly={true}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{resolved.subText ? (
|
||||
<p className="mt-1 text-sm text-tint">{resolved.subText}</p>
|
||||
{target.subText ? (
|
||||
<p className="mt-1 text-sm text-tint">{target.subText}</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{hasAISummary && 'page' in context && 'page' in inline.data.ref ? (
|
||||
{aiSummary ? (
|
||||
<div className="border-tint-subtle border-t bg-tint p-4">
|
||||
<AIPageLinkSummary
|
||||
targetPageId={
|
||||
resolved.page?.id ??
|
||||
inline.data.ref.page ??
|
||||
context.page.id
|
||||
}
|
||||
targetSpaceId={inline.data.ref.space ?? context.space.id}
|
||||
linkTitle={getNodeText(inline)}
|
||||
linkPreview={`**${resolved.text}**: ${resolved.subText}`}
|
||||
showTrademark={
|
||||
'customization' in context &&
|
||||
context.customization.trademark.enabled
|
||||
}
|
||||
targetPageId={aiSummary.pageId}
|
||||
targetSpaceId={aiSummary.spaceId}
|
||||
showTrademark
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Tooltip.Arrow className={hasAISummary ? 'fill-tint-3' : 'fill-tint-1'} />
|
||||
<Tooltip.Arrow
|
||||
className={
|
||||
typeof aiSummary !== 'undefined' ? 'fill-tint-3' : 'fill-tint-1'
|
||||
}
|
||||
/>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
@@ -0,0 +1 @@
|
||||
export * from './InlineLink';
|
||||
@@ -5,8 +5,8 @@ import { GITBOOK_INTEGRATIONS_HOST } from '@v2/lib/env';
|
||||
|
||||
import type { BlockProps } from '../Block';
|
||||
import './contentkit.css';
|
||||
import { getDataOrNull } from '@v2/lib/data';
|
||||
import { contentKitServerContext } from './contentkit';
|
||||
import { fetchSafeIntegrationUI } from './render';
|
||||
import { renderIntegrationUi } from './server-actions';
|
||||
|
||||
export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegration>) {
|
||||
@@ -16,8 +16,6 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
|
||||
throw new Error('integration block requires a content.spaceId');
|
||||
}
|
||||
|
||||
const { dataFetcher } = context.contentContext;
|
||||
|
||||
const initialInput: RenderIntegrationUI = {
|
||||
componentId: block.data.block,
|
||||
props: block.data.props,
|
||||
@@ -30,17 +28,27 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
|
||||
},
|
||||
};
|
||||
|
||||
const initialOutput = await getDataOrNull(
|
||||
dataFetcher.renderIntegrationUi({
|
||||
integrationName: block.data.integration,
|
||||
request: initialInput,
|
||||
}),
|
||||
const initialResponse = await fetchSafeIntegrationUI(context.contentContext, {
|
||||
integrationName: block.data.integration,
|
||||
request: initialInput,
|
||||
});
|
||||
|
||||
// The API can respond with a 400 error if the integration is not installed
|
||||
// and 404 if the integration is not found.
|
||||
[404, 400]
|
||||
);
|
||||
if (!initialOutput || initialOutput.type === 'complete') {
|
||||
if (initialResponse.error) {
|
||||
if (initialResponse.error.code === 404) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={tcls(style)}>
|
||||
<pre>
|
||||
Unexpected error with integration {block.data.integration}:{' '}
|
||||
{initialResponse.error.message}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const initialOutput = initialResponse.data;
|
||||
if (initialOutput.type === 'complete') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { RenderIntegrationUI } from '@gitbook/api';
|
||||
import type { GitBookBaseContext } from '@v2/lib/context';
|
||||
import { ignoreDataFetcherErrors } from '@v2/lib/data';
|
||||
|
||||
/**
|
||||
* Render an integration UI while ignoring some errors.
|
||||
*/
|
||||
export async function fetchSafeIntegrationUI(
|
||||
context: GitBookBaseContext,
|
||||
{
|
||||
integrationName,
|
||||
request,
|
||||
}: {
|
||||
integrationName: string;
|
||||
request: RenderIntegrationUI;
|
||||
}
|
||||
) {
|
||||
const output = await ignoreDataFetcherErrors(
|
||||
context.dataFetcher.renderIntegrationUi({
|
||||
integrationName,
|
||||
request,
|
||||
}),
|
||||
|
||||
// The API can respond with certain errors that are expected to happen.
|
||||
[
|
||||
404, // Integration has been uninstalled
|
||||
400, // Integration is rejecting its own request
|
||||
422, // Integration is triggering an invalid request, failing at the validation step
|
||||
502, // Integration is failing in an unexpected way
|
||||
]
|
||||
);
|
||||
|
||||
return output;
|
||||
}
|
||||
@@ -4,9 +4,9 @@ import { getV1BaseContext } from '@/lib/v1';
|
||||
import { isV2 } from '@/lib/v2';
|
||||
import type { RenderIntegrationUI } from '@gitbook/api';
|
||||
import { ContentKitOutput } from '@gitbook/react-contentkit';
|
||||
import { throwIfDataError } from '@v2/lib/data';
|
||||
import { getServerActionBaseContext } from '@v2/lib/server-actions';
|
||||
import { contentKitServerContext } from './contentkit';
|
||||
import { fetchSafeIntegrationUI } from './render';
|
||||
|
||||
/**
|
||||
* Server action to render an integration UI request from <ContentKit />.
|
||||
@@ -22,16 +22,19 @@ export async function renderIntegrationUi({
|
||||
request: RenderIntegrationUI;
|
||||
}) {
|
||||
const serverAction = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
|
||||
const output = await fetchSafeIntegrationUI(serverAction, {
|
||||
integrationName: renderContext.integrationName,
|
||||
request,
|
||||
});
|
||||
|
||||
const output = await throwIfDataError(
|
||||
serverAction.dataFetcher.renderIntegrationUi({
|
||||
integrationName: renderContext.integrationName,
|
||||
request,
|
||||
})
|
||||
);
|
||||
if (output.error) {
|
||||
return {
|
||||
error: output.error.message,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
children: <ContentKitOutput output={output} context={contentKitServerContext} />,
|
||||
output: output,
|
||||
children: <ContentKitOutput output={output.data} context={contentKitServerContext} />,
|
||||
output: output.data,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -680,9 +680,7 @@ body:has(.openapi-select-popover) {
|
||||
/* Tabs */
|
||||
.openapi-panel-header,
|
||||
.openapi-tabs-list {
|
||||
@apply flex flex-row gap-1.5 py-1.5 px-2.5 w-full overflow-x-scroll;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
@apply flex flex-row gap-1.5 py-1.5 px-2.5 w-full overflow-x-auto no-scrollbar;
|
||||
}
|
||||
|
||||
.openapi-tabs-tab {
|
||||
|
||||
@@ -4,12 +4,13 @@ import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import type { BlockProps } from './Block';
|
||||
import { Inlines } from './Inlines';
|
||||
import { getTextAlignment } from './utils';
|
||||
|
||||
export function Paragraph(props: BlockProps<DocumentBlockParagraph>) {
|
||||
const { block, style, ...contextProps } = props;
|
||||
const { block, style, ancestorBlocks, ...contextProps } = props;
|
||||
|
||||
return (
|
||||
<p className={tcls(style)}>
|
||||
<p className={tcls(style, getTextAlignment(block.data?.align, ancestorBlocks))}>
|
||||
<Inlines {...contextProps} nodes={block.nodes} ancestorInlines={[]} />
|
||||
</p>
|
||||
);
|
||||
|
||||
@@ -23,19 +23,19 @@ export async function ReusableContent(props: BlockProps<DocumentBlockReusableCon
|
||||
dataFetcher,
|
||||
});
|
||||
|
||||
if (!resolved?.reusableContent) {
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const reusableContent = resolved.reusableContent.revisionReusableContent;
|
||||
if (!reusableContent.document) {
|
||||
const { reusableContent } = resolved;
|
||||
if (!reusableContent || !reusableContent.revisionReusableContent.document) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const document = await getDataOrNull(
|
||||
dataFetcher.getDocument({
|
||||
spaceId: resolved.reusableContent.space.id,
|
||||
documentId: reusableContent.document,
|
||||
spaceId: reusableContent.space.id,
|
||||
documentId: reusableContent.revisionReusableContent.document,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -47,18 +47,20 @@ export async function ReusableContent(props: BlockProps<DocumentBlockReusableCon
|
||||
// the data fetcher with the token from the block meta and the correct
|
||||
// space and revision pointers.
|
||||
const reusableContentContext: GitBookSpaceContext =
|
||||
context.contentContext.space.id === resolved.reusableContent.space.id
|
||||
context.contentContext.space.id === reusableContent.space.id
|
||||
? context.contentContext
|
||||
: {
|
||||
...context.contentContext,
|
||||
dataFetcher,
|
||||
space: resolved.reusableContent.space,
|
||||
revisionId: resolved.reusableContent.revision,
|
||||
space: reusableContent.space,
|
||||
// When the reusable content is in a different space, we don't resolve relative links to pages
|
||||
// as this space might not be part of the current site.
|
||||
// In the future, we might expand the logic to look up the space from the list of all spaces in the site
|
||||
// and adapt the relative links to point to the correct variant.
|
||||
pages: [],
|
||||
revision: {
|
||||
...reusableContent.revision,
|
||||
pages: [], // TODO: check with Steven
|
||||
},
|
||||
shareKey: undefined,
|
||||
};
|
||||
|
||||
|
||||
@@ -7,10 +7,11 @@ import {
|
||||
import { LinkBox, LinkOverlay } from '@/components/primitives';
|
||||
import { Image } from '@/components/utils';
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { RecordColumnValue } from './RecordColumnValue';
|
||||
import type { TableRecordKV, TableViewProps } from './Table';
|
||||
import { RecordCardStyles } from './styles';
|
||||
import { getRecordValue } from './utils';
|
||||
|
||||
export async function RecordCard(
|
||||
@@ -23,18 +24,18 @@ export async function RecordCard(
|
||||
const coverFile = view.coverDefinition
|
||||
? getRecordValue<string[]>(record[1], view.coverDefinition)?.[0]
|
||||
: null;
|
||||
const cover =
|
||||
coverFile && context.contentContext
|
||||
? await resolveContentRef({ kind: 'file', file: coverFile }, context.contentContext)
|
||||
: null;
|
||||
|
||||
const targetRef = view.targetDefinition
|
||||
? (record[1].values[view.targetDefinition] as ContentRef)
|
||||
: null;
|
||||
const target =
|
||||
|
||||
const [cover, target] = await Promise.all([
|
||||
coverFile && context.contentContext
|
||||
? resolveContentRef({ kind: 'file', file: coverFile }, context.contentContext)
|
||||
: null,
|
||||
targetRef && context.contentContext
|
||||
? await resolveContentRef(targetRef, context.contentContext)
|
||||
: null;
|
||||
? resolveContentRef(targetRef, context.contentContext)
|
||||
: null,
|
||||
]);
|
||||
|
||||
const coverIsSquareOrPortrait =
|
||||
cover?.file?.dimensions &&
|
||||
@@ -143,35 +144,12 @@ export async function RecordCard(
|
||||
</div>
|
||||
);
|
||||
|
||||
const style = [
|
||||
'group',
|
||||
'grid',
|
||||
'shadow-1xs',
|
||||
'shadow-tint-9/1',
|
||||
'depth-flat:shadow-none',
|
||||
'rounded',
|
||||
'straight-corners:rounded-none',
|
||||
'circular-corners:rounded-xl',
|
||||
'dark:shadow-transparent',
|
||||
|
||||
'before:pointer-events-none',
|
||||
'before:grid-area-1-1',
|
||||
'before:transition-shadow',
|
||||
'before:w-full',
|
||||
'before:h-full',
|
||||
'before:rounded-[inherit]',
|
||||
'before:ring-1',
|
||||
'before:ring-tint-12/2',
|
||||
'before:z-10',
|
||||
'before:relative',
|
||||
] as ClassValue;
|
||||
|
||||
if (target && targetRef) {
|
||||
return (
|
||||
// We don't use `Link` directly here because we could end up in a situation where
|
||||
// a link is rendered inside a link, which is not allowed in HTML.
|
||||
// It causes an hydration error in React.
|
||||
<LinkBox href={target.href} className={tcls(style, 'hover:before:ring-tint-12/5')}>
|
||||
<LinkBox href={target.href} classNames={['RecordCardStyles']}>
|
||||
<LinkOverlay
|
||||
href={target.href}
|
||||
insights={{
|
||||
@@ -187,5 +165,5 @@ export async function RecordCard(
|
||||
);
|
||||
}
|
||||
|
||||
return <div className={tcls(style)}>{body}</div>;
|
||||
return <div className={tcls(RecordCardStyles)}>{body}</div>;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,12 @@ import { FileIcon } from '../FileIcon';
|
||||
import type { TableRecordKV } from './Table';
|
||||
import { type VerticalAlignment, getColumnAlignment } from './utils';
|
||||
|
||||
const alignmentMap: Record<'text-left' | 'text-center' | 'text-right', string> = {
|
||||
'text-left': '[&_*]:text-left text-left',
|
||||
'text-center': '[&_*]:text-center text-center',
|
||||
'text-right': '[&_*]:text-right text-right',
|
||||
};
|
||||
|
||||
/**
|
||||
* Render the value for a column in a record.
|
||||
*/
|
||||
@@ -116,13 +122,13 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
}
|
||||
|
||||
const horizontalAlignment = getColumnAlignment(definition);
|
||||
const childrenHorizontalAlignment = `[&_*]:${horizontalAlignment}`;
|
||||
const horizontalClasses = alignmentMap[horizontalAlignment];
|
||||
|
||||
return (
|
||||
<Blocks
|
||||
tag={Tag}
|
||||
document={document}
|
||||
ancestorBlocks={[]}
|
||||
ancestorBlocks={[...props.ancestorBlocks, block]}
|
||||
nodes={fragment.nodes}
|
||||
style={[
|
||||
'blocks',
|
||||
@@ -131,8 +137,7 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
'lg:space-y-3',
|
||||
'leading-normal',
|
||||
verticalAlignment,
|
||||
horizontalAlignment,
|
||||
childrenHorizontalAlignment,
|
||||
horizontalClasses,
|
||||
]}
|
||||
context={context}
|
||||
blockStyle={['w-full', 'max-w-[unset]']}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { ClassValue } from '@/lib/tailwind';
|
||||
|
||||
export const RecordCardStyles = [
|
||||
'group',
|
||||
'grid',
|
||||
'shadow-1xs',
|
||||
'shadow-tint-9/1',
|
||||
'depth-flat:shadow-none',
|
||||
'rounded',
|
||||
'straight-corners:rounded-none',
|
||||
'circular-corners:rounded-xl',
|
||||
'dark:shadow-transparent',
|
||||
|
||||
'before:pointer-events-none',
|
||||
'before:grid-area-1-1',
|
||||
'before:transition-shadow',
|
||||
'before:w-full',
|
||||
'before:h-full',
|
||||
'before:rounded-[inherit]',
|
||||
'before:ring-1',
|
||||
'before:ring-tint-12/2',
|
||||
'before:z-10',
|
||||
'before:relative',
|
||||
|
||||
'hover:before:ring-tint-12/5',
|
||||
] as ClassValue;
|
||||
@@ -1 +1,2 @@
|
||||
export * from './isBlockOffscreen';
|
||||
export * from './textAlignment';
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { ClassValue } from '@/lib/tailwind';
|
||||
import { nullIfNever } from '@/lib/typescript';
|
||||
import { type DocumentBlock, TextAlignment } from '@gitbook/api';
|
||||
|
||||
/**
|
||||
* Get the tailwind class for a text alignment.
|
||||
*/
|
||||
export function getTextAlignment(
|
||||
textAlignment: TextAlignment | undefined,
|
||||
ancestorBlocks: DocumentBlock[]
|
||||
): ClassValue {
|
||||
// Text nodes within a table will have their alignment governed by columns instead.)
|
||||
for (let i = ancestorBlocks.length - 1; i >= 0; i--) {
|
||||
const ancestor = ancestorBlocks[i];
|
||||
if (ancestor.type === 'table') {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// If not inside a table, use the normal alignment logic
|
||||
switch (textAlignment) {
|
||||
case undefined:
|
||||
case TextAlignment.Start:
|
||||
return ['text-start', 'justify-self-start'];
|
||||
case TextAlignment.Center:
|
||||
return ['text-center', 'justify-self-center'];
|
||||
case TextAlignment.End:
|
||||
return ['text-end', 'justify-self-end'];
|
||||
default:
|
||||
return nullIfNever(textAlignment);
|
||||
}
|
||||
}
|
||||
@@ -183,9 +183,8 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo
|
||||
<div
|
||||
className={tcls(
|
||||
'w-full',
|
||||
'overflow-x-scroll',
|
||||
'overflow-y-hidden',
|
||||
'hide-scroll',
|
||||
'overflow-x-auto',
|
||||
'no-scrollbar',
|
||||
'-mb-4 pb-4', // Positive padding / negative margin allows the navigation menu indicator to show in a scroll viewƒ
|
||||
!sections ? ['hidden', 'page-no-toc:flex'] : 'flex'
|
||||
)}
|
||||
@@ -202,7 +201,7 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo
|
||||
{siteSpaces.length > 1 && (
|
||||
<div
|
||||
id="variants"
|
||||
className="my-2 mr-5 page-no-toc:flex hidden grow border-tint border-r pr-5 *:grow only:mr-0 only:border-none only:pr-0 sm:max-w-64"
|
||||
className="my-2 mr-5 grow border-tint border-r pr-5 *:grow only:mr-0 only:border-none only:pr-0 sm:max-w-64"
|
||||
>
|
||||
<SpacesDropdown
|
||||
context={context}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getSiteSpaceURL } from '@/lib/sites';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import type { GitBookSiteContext } from '@v2/lib/context';
|
||||
import { DropdownChevron, DropdownMenu } from './DropdownMenu';
|
||||
import { SpacesDropdownMenuItem } from './SpacesDropdownMenuItem';
|
||||
import { SpacesDropdownMenuItems } from './SpacesDropdownMenuItem';
|
||||
|
||||
export function SpacesDropdown(props: {
|
||||
context: GitBookSiteContext;
|
||||
@@ -65,17 +65,15 @@ export function SpacesDropdown(props: {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{siteSpaces.map((otherSiteSpace, index) => (
|
||||
<SpacesDropdownMenuItem
|
||||
key={`${otherSiteSpace.id}-${index}`}
|
||||
variantSpace={{
|
||||
id: otherSiteSpace.id,
|
||||
title: otherSiteSpace.title,
|
||||
url: getSiteSpaceURL(context, otherSiteSpace),
|
||||
}}
|
||||
active={otherSiteSpace.id === siteSpace.id}
|
||||
/>
|
||||
))}
|
||||
<SpacesDropdownMenuItems
|
||||
slimSpaces={siteSpaces.map((space) => ({
|
||||
id: space.id,
|
||||
title: space.title,
|
||||
url: getSiteSpaceURL(context, space),
|
||||
isActive: space.id === siteSpace.id,
|
||||
}))}
|
||||
curPath={siteSpace.path}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,12 +6,29 @@ import { joinPath } from '@/lib/paths';
|
||||
import { useCurrentPagePath } from '../hooks';
|
||||
import { DropdownMenuItem } from './DropdownMenu';
|
||||
|
||||
function useVariantSpaceHref(variantSpaceUrl: string) {
|
||||
interface VariantSpace {
|
||||
id: Space['id'];
|
||||
title: Space['title'];
|
||||
url: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
// When switching to a different variant space, we reconstruct the URL by swapping the space path.
|
||||
function useVariantSpaceHref(variantSpaceUrl: string, currentSpacePath: string, active = false) {
|
||||
const currentPathname = useCurrentPagePath();
|
||||
|
||||
// We need to ensure that the variant space URL is not the same as the current space path.
|
||||
// If it is, we return only the variant space URL to redirect to the root of the variant space.
|
||||
// This is necessary in case the currentPathname is the same as the variantSpaceUrl,
|
||||
// otherwise we would redirect to the same space if the variant space that we are switching to is the default one.
|
||||
if (!active && currentPathname.startsWith(`${currentSpacePath}/`)) {
|
||||
return variantSpaceUrl;
|
||||
}
|
||||
|
||||
if (URL.canParse(variantSpaceUrl)) {
|
||||
const targetUrl = new URL(variantSpaceUrl);
|
||||
targetUrl.pathname = joinPath(targetUrl.pathname, currentPathname);
|
||||
|
||||
targetUrl.searchParams.set('fallback', 'true');
|
||||
|
||||
return targetUrl.toString();
|
||||
@@ -22,11 +39,12 @@ function useVariantSpaceHref(variantSpaceUrl: string) {
|
||||
}
|
||||
|
||||
export function SpacesDropdownMenuItem(props: {
|
||||
variantSpace: { id: Space['id']; title: Space['title']; url: string };
|
||||
variantSpace: VariantSpace;
|
||||
active: boolean;
|
||||
currentSpacePath: string;
|
||||
}) {
|
||||
const { variantSpace, active } = props;
|
||||
const variantHref = useVariantSpaceHref(variantSpace.url);
|
||||
const { variantSpace, active, currentSpacePath } = props;
|
||||
const variantHref = useVariantSpaceHref(variantSpace.url, currentSpacePath, active);
|
||||
|
||||
return (
|
||||
<DropdownMenuItem key={variantSpace.id} href={variantHref} active={active}>
|
||||
@@ -34,3 +52,23 @@ export function SpacesDropdownMenuItem(props: {
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export function SpacesDropdownMenuItems(props: {
|
||||
slimSpaces: VariantSpace[];
|
||||
curPath: string;
|
||||
}) {
|
||||
const { slimSpaces, curPath } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{slimSpaces.map((space) => (
|
||||
<SpacesDropdownMenuItem
|
||||
key={space.id}
|
||||
variantSpace={space}
|
||||
active={space.isActive}
|
||||
currentSpacePath={curPath}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,14 +53,14 @@ export async function PDFPage(props: {
|
||||
}) {
|
||||
const baseContext = props.context;
|
||||
const searchParams = new URLSearchParams(props.searchParams);
|
||||
const pdfParams = getPDFSearchParams(new URLSearchParams(searchParams));
|
||||
const pdfParams = getPDFSearchParams(searchParams);
|
||||
|
||||
const customization =
|
||||
'customization' in baseContext ? baseContext.customization : defaultCustomization();
|
||||
const language = getSpaceLanguage(customization);
|
||||
|
||||
// Compute the pages to render
|
||||
const { pages, total } = selectPages(baseContext.pages, pdfParams);
|
||||
const { pages, total } = selectPages(baseContext.revision.pages, pdfParams);
|
||||
const pageIds = pages.map(
|
||||
({ page }) => [page.id, getPagePDFContainerId(page)] as [string, string]
|
||||
);
|
||||
@@ -245,6 +245,7 @@ async function PDFPageDocument(props: {
|
||||
page,
|
||||
},
|
||||
getId: (id) => getPagePDFContainerId(page, id),
|
||||
shouldRenderLinkPreviews: false, // We don't want to render link previews in the PDF.
|
||||
}}
|
||||
// We consider all pages as offscreen in PDF mode
|
||||
// to ensure we can efficiently render as many pages as possible
|
||||
|
||||
@@ -4,7 +4,7 @@ import React from 'react';
|
||||
|
||||
import { getSpaceLanguage } from '@/intl/server';
|
||||
import { t } from '@/intl/translate';
|
||||
import { hasFullWidthBlock, isNodeEmpty } from '@/lib/document';
|
||||
import { hasFullWidthBlock, hasMoreThan, isNodeEmpty } from '@/lib/document';
|
||||
import type { AncestorRevisionPage } from '@/lib/pages';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { DocumentView, DocumentViewSkeleton } from '../DocumentView';
|
||||
@@ -17,6 +17,8 @@ import { PageFooterNavigation } from './PageFooterNavigation';
|
||||
import { PageHeader } from './PageHeader';
|
||||
import { PreservePageLayout } from './PreservePageLayout';
|
||||
|
||||
const LINK_PREVIEW_MAX_COUNT = 100;
|
||||
|
||||
export function PageBody(props: {
|
||||
context: GitBookSiteContext;
|
||||
page: RevisionPageDocument;
|
||||
@@ -28,6 +30,15 @@ export function PageBody(props: {
|
||||
const { customization } = context;
|
||||
|
||||
const contentFullWidth = document ? hasFullWidthBlock(document) : false;
|
||||
|
||||
// Render link previews only if there are less than LINK_PREVIEW_MAX_COUNT links in the document.
|
||||
const shouldRenderLinkPreviews = document
|
||||
? !hasMoreThan(
|
||||
document,
|
||||
(inline) => inline.object === 'inline' && inline.type === 'link',
|
||||
LINK_PREVIEW_MAX_COUNT
|
||||
)
|
||||
: false;
|
||||
const pageFullWidth = page.id === 'wtthNFMqmEQmnt5LKR0q';
|
||||
const asFullWidth = pageFullWidth || contentFullWidth;
|
||||
const language = getSpaceLanguage(customization);
|
||||
@@ -68,6 +79,7 @@ export function PageBody(props: {
|
||||
context={{
|
||||
mode: 'default',
|
||||
contentContext: context,
|
||||
shouldRenderLinkPreviews,
|
||||
}}
|
||||
/>
|
||||
</React.Suspense>
|
||||
|
||||
@@ -58,7 +58,10 @@ export async function PageBodyBlankslate(props: {
|
||||
/>
|
||||
);
|
||||
}
|
||||
const href = context.linker.toPathForPage({ pages: context.pages, page: child });
|
||||
const href = context.linker.toPathForPage({
|
||||
pages: context.revision.pages,
|
||||
page: child,
|
||||
});
|
||||
return <Card key={child.id} title={child.title} leadingIcon={icon} href={href} />;
|
||||
})
|
||||
);
|
||||
|
||||
@@ -21,8 +21,10 @@ export async function PageCover(props: {
|
||||
context: GitBookSiteContext;
|
||||
}) {
|
||||
const { as, page, cover, context } = props;
|
||||
const resolved = cover.ref ? await resolveContentRef(cover.ref, context) : null;
|
||||
const resolvedDark = cover.refDark ? await resolveContentRef(cover.refDark, context) : null;
|
||||
const [resolved, resolvedDark] = await Promise.all([
|
||||
cover.ref ? resolveContentRef(cover.ref, context) : null,
|
||||
cover.refDark ? resolveContentRef(cover.refDark, context) : null,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -17,11 +17,13 @@ export async function PageFooterNavigation(props: {
|
||||
page: RevisionPageDocument;
|
||||
}) {
|
||||
const { context, page } = props;
|
||||
const { customization, pages, linker } = context;
|
||||
const { previous, next } = resolvePrevNextPages(pages, page);
|
||||
const { customization, revision, linker } = context;
|
||||
const { previous, next } = resolvePrevNextPages(revision.pages, page);
|
||||
const language = getSpaceLanguage(customization);
|
||||
const previousHref = previous ? linker.toPathForPage({ pages, page: previous }) : '';
|
||||
const nextHref = next ? linker.toPathForPage({ pages, page: next }) : '';
|
||||
const previousHref = previous
|
||||
? linker.toPathForPage({ pages: revision.pages, page: previous })
|
||||
: '';
|
||||
const nextHref = next ? linker.toPathForPage({ pages: revision.pages, page: next }) : '';
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function PageHeader(props: {
|
||||
ancestors: AncestorRevisionPage[];
|
||||
}) {
|
||||
const { context, page, ancestors } = props;
|
||||
const { pages, linker } = context;
|
||||
const { revision, linker } = context;
|
||||
|
||||
if (!page.layout.title && !page.layout.description) {
|
||||
return null;
|
||||
@@ -36,7 +36,10 @@ export async function PageHeader(props: {
|
||||
<nav>
|
||||
<ol className={tcls('flex', 'flex-wrap', 'items-center', 'gap-2', 'text-tint')}>
|
||||
{ancestors.map((breadcrumb, index) => {
|
||||
const href = linker.toPathForPage({ pages, page: breadcrumb });
|
||||
const href = linker.toPathForPage({
|
||||
pages: revision.pages,
|
||||
page: breadcrumb,
|
||||
});
|
||||
return (
|
||||
<Fragment key={breadcrumb.id}>
|
||||
<li key={breadcrumb.id}>
|
||||
|
||||
@@ -62,15 +62,6 @@
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
/* hide scroll bar but still scroll */
|
||||
.hide-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.hide-scroll {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.linear-mask-gradient {
|
||||
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 96px, rgba(0, 0, 0, 0));
|
||||
}
|
||||
@@ -167,3 +158,111 @@ html.dark {
|
||||
html.announcement-hidden #announcement-banner {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
/* Shiki theme */
|
||||
:root {
|
||||
--shiki-color-text: theme("colors.tint.11");
|
||||
--shiki-token-punctuation: theme("colors.tint.11");
|
||||
--shiki-token-comment: theme("colors.neutral.9/.7");
|
||||
--shiki-token-link: theme("colors.primary.10");
|
||||
|
||||
--shiki-token-constant: theme("colors.warning.10");
|
||||
--shiki-token-string: theme("colors.warning.10");
|
||||
--shiki-token-string-expression: theme("colors.success.10");
|
||||
--shiki-token-keyword: theme("colors.danger.10");
|
||||
--shiki-token-parameter: theme("colors.warning.10");
|
||||
--shiki-token-function: theme("colors.primary.10");
|
||||
|
||||
--shiki-token-inserted: theme("colors.success.10");
|
||||
--shiki-token-deleted: theme("colors.danger.10");
|
||||
--shiki-token-changed: theme("colors.tint.12");
|
||||
}
|
||||
|
||||
@media (prefers-contrast: more) {
|
||||
:root {
|
||||
--shiki-color-text: theme("colors.tint.12");
|
||||
--shiki-token-punctuation: theme("colors.tint.12");
|
||||
--shiki-token-comment: theme("colors.neutral.11");
|
||||
--shiki-token-link: theme("colors.primary.11");
|
||||
|
||||
--shiki-token-constant: theme("colors.warning.11");
|
||||
--shiki-token-string: theme("colors.warning.11");
|
||||
--shiki-token-string-expression: theme("colors.success.11");
|
||||
--shiki-token-keyword: theme("colors.danger.11");
|
||||
--shiki-token-parameter: theme("colors.warning.11");
|
||||
--shiki-token-function: theme("colors.primary.11");
|
||||
|
||||
--shiki-token-inserted: theme("colors.success.11");
|
||||
--shiki-token-deleted: theme("colors.danger.11");
|
||||
--shiki-token-changed: theme("colors.tint.12");
|
||||
}
|
||||
}
|
||||
|
||||
html.dark {
|
||||
/* Override select colors to have more contrast */
|
||||
--shiki-token-comment: theme("colors.neutral.9");
|
||||
|
||||
--shiki-token-constant: theme("colors.warning.11");
|
||||
--shiki-token-string: theme("colors.warning.11");
|
||||
--shiki-token-string-expression: theme("colors.success.11");
|
||||
--shiki-token-keyword: theme("colors.danger.11");
|
||||
--shiki-token-parameter: theme("colors.warning.11");
|
||||
--shiki-token-function: theme("colors.primary.11");
|
||||
}
|
||||
|
||||
.code-monochrome {
|
||||
--shiki-token-constant: theme("colors.tint.11");
|
||||
--shiki-token-string: theme("colors.tint.12");
|
||||
--shiki-token-string-expression: theme("colors.tint.12");
|
||||
--shiki-token-keyword: theme("colors.primary.10");
|
||||
--shiki-token-parameter: theme("colors.tint.9");
|
||||
--shiki-token-function: theme("colors.primary.9");
|
||||
}
|
||||
|
||||
html.dark.code-monochrome {
|
||||
--shiki-token-constant: theme("colors.tint.11");
|
||||
--shiki-token-string: theme("colors.tint.12");
|
||||
--shiki-token-string-expression: theme("colors.tint.12");
|
||||
--shiki-token-keyword: theme("colors.primary.11");
|
||||
--shiki-token-parameter: theme("colors.tint.10");
|
||||
--shiki-token-function: theme("colors.primary.10");
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
.highlight-line {
|
||||
@apply grid [grid-template-columns:subgrid] col-span-2 relative ring-1 ring-transparent hover:ring-tint-hover hover:z-[1] rounded;
|
||||
@apply only:hover:ring-transparent;
|
||||
@apply [counter-increment:line];
|
||||
|
||||
&.highlighted {
|
||||
@apply bg-tint-active;
|
||||
@apply first:rounded-t-md *:first:mt-1;
|
||||
@apply last:rounded-b-md *:last:mb-1;
|
||||
@apply rounded-none;
|
||||
}
|
||||
|
||||
&:not(.highlighted) + .highlighted {
|
||||
@apply rounded-t-md *:mt-1;
|
||||
}
|
||||
|
||||
&.highlighted:has(+ :not(.highlighted)) {
|
||||
@apply rounded-b-md *:mb-1;
|
||||
}
|
||||
|
||||
&:not(.highlighted) + .highlighted:has(+ :not(.highlighted)) {
|
||||
@apply rounded-md;
|
||||
}
|
||||
}
|
||||
|
||||
.highlight-line-number {
|
||||
@apply text-sm text-right pr-3.5 rounded-l pl-2 sticky left-[-3px] bg-gradient-to-r from-80% from-tint-subtle contrast-more:from-tint-base theme-muted:from-tint-base [html.theme-bold.sidebar-filled_&]:from-tint-base to-transparent;
|
||||
@apply before:text-tint before:content-[counter(line)];
|
||||
|
||||
.highlight-line.highlighted > & {
|
||||
@apply before:text-tint bg-gradient-to-r from-80% from-tint-active to-transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.highlight-line-content {
|
||||
@apply ml-3 block text-sm;
|
||||
}
|
||||
|
||||
@@ -66,11 +66,7 @@ export const SearchSectionResultItem = React.forwardRef(function SearchSectionRe
|
||||
<HighlightQuery query={query} text={item.title} />
|
||||
</p>
|
||||
) : null}
|
||||
{item.body ? (
|
||||
<p className={tcls('text-sm', 'line-clamp-3', 'relative')}>
|
||||
<HighlightQuery query={query} text={item.body} />
|
||||
</p>
|
||||
) : null}
|
||||
{item.body ? highlightQueryInBody(item.body, query) : null}
|
||||
</div>
|
||||
<div
|
||||
className={tcls(
|
||||
@@ -90,3 +86,14 @@ export const SearchSectionResultItem = React.forwardRef(function SearchSectionRe
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
function highlightQueryInBody(body: string, query: string) {
|
||||
const idx = body.toLocaleLowerCase().indexOf(query.toLocaleLowerCase());
|
||||
|
||||
// Ensure the query to be highlighted is visible in the body.
|
||||
return (
|
||||
<p className={tcls('text-sm', 'line-clamp-3', 'relative')}>
|
||||
<HighlightQuery query={query} text={idx < 20 ? body : `...${body.slice(idx - 10)}`} />
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { findSiteSpaceById, getSiteStructureSections } from '@/lib/sites';
|
||||
import { filterOutNullable } from '@/lib/typescript';
|
||||
import { getV1BaseContext } from '@/lib/v1';
|
||||
import type {
|
||||
Revision,
|
||||
RevisionPage,
|
||||
SearchAIAnswer,
|
||||
SearchAIRecommendedQuestionStream,
|
||||
@@ -128,7 +129,7 @@ export async function streamAskQuestion({
|
||||
{ format: 'document' }
|
||||
);
|
||||
|
||||
const spacePromises = new Map<string, Promise<RevisionPage[]>>();
|
||||
const spacePromises = new Map<string, Promise<Revision>>();
|
||||
for await (const chunk of stream) {
|
||||
const answer = chunk.answer;
|
||||
|
||||
@@ -143,10 +144,9 @@ export async function streamAskQuestion({
|
||||
spacePromises.set(
|
||||
source.space,
|
||||
throwIfDataError(
|
||||
context.dataFetcher.getRevisionPages({
|
||||
context.dataFetcher.getRevision({
|
||||
spaceId: source.space,
|
||||
revisionId: source.revision,
|
||||
metadata: false,
|
||||
})
|
||||
)
|
||||
);
|
||||
@@ -159,8 +159,8 @@ export async function streamAskQuestion({
|
||||
// Get the pages for all spaces referenced by this answer.
|
||||
const pages = await Promise.all(
|
||||
spaces.map(async (space) => {
|
||||
const pages = await spacePromises.get(space);
|
||||
return { space, pages };
|
||||
const revision = await spacePromises.get(space);
|
||||
return { space, pages: revision?.pages };
|
||||
})
|
||||
).then((results) => {
|
||||
return results.reduce((map, result) => {
|
||||
@@ -345,6 +345,7 @@ async function transformAnswer(
|
||||
mode: 'default',
|
||||
contentContext: undefined,
|
||||
wrapBlocksInSuspense: false,
|
||||
shouldRenderLinkPreviews: false, // We don't want to render link previews in the AI answer.
|
||||
}}
|
||||
style={['space-y-5']}
|
||||
/>
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import type { CustomizationThemeMode } from '@gitbook/api';
|
||||
import type { CustomizationThemeMode, SiteExternalLinksTarget } from '@gitbook/api';
|
||||
import { ThemeProvider } from 'next-themes';
|
||||
import type React from 'react';
|
||||
import { LinkSettingsContext } from '../primitives';
|
||||
|
||||
export function ClientContexts(props: {
|
||||
nonce?: string;
|
||||
forcedTheme: CustomizationThemeMode | undefined;
|
||||
externalLinksTarget: SiteExternalLinksTarget;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { children, forcedTheme } = props;
|
||||
const { children, forcedTheme, externalLinksTarget } = props;
|
||||
|
||||
/**
|
||||
* A bug in ThemeProvider is causing the nonce to be included incorrectly
|
||||
@@ -22,7 +24,9 @@ export function ClientContexts(props: {
|
||||
|
||||
return (
|
||||
<ThemeProvider nonce={nonce} attribute="class" enableSystem forcedTheme={forcedTheme}>
|
||||
{children}
|
||||
<LinkSettingsContext.Provider value={{ externalLinksTarget }}>
|
||||
{children}
|
||||
</LinkSettingsContext.Provider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ export async function SiteLayout(props: {
|
||||
forcedTheme ??
|
||||
(customization.themes.toggeable ? undefined : customization.themes.default)
|
||||
}
|
||||
externalLinksTarget={customization.externalLinks.target}
|
||||
>
|
||||
<SpaceLayout
|
||||
context={context}
|
||||
@@ -102,30 +103,37 @@ export async function generateSiteLayoutMetadata(context: GitBookSiteContext): P
|
||||
const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null;
|
||||
|
||||
const faviconSize = 48;
|
||||
const icons = [
|
||||
{
|
||||
url: customIcon?.light
|
||||
? await getResizedImageURL(imageResizer, customIcon.light, {
|
||||
width: faviconSize,
|
||||
height: faviconSize,
|
||||
})
|
||||
: linker.toAbsoluteURL(
|
||||
linker.toPathInSpace('~gitbook/icon?size=small&theme=light')
|
||||
),
|
||||
type: 'image/png',
|
||||
media: '(prefers-color-scheme: light)',
|
||||
},
|
||||
{
|
||||
url: customIcon?.dark
|
||||
? await getResizedImageURL(imageResizer, customIcon.dark, {
|
||||
width: faviconSize,
|
||||
height: faviconSize,
|
||||
})
|
||||
: linker.toAbsoluteURL(linker.toPathInSpace('~gitbook/icon?size=small&theme=dark')),
|
||||
type: 'image/png',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
];
|
||||
const icons = await Promise.all(
|
||||
[
|
||||
{
|
||||
url: customIcon?.light
|
||||
? getResizedImageURL(imageResizer, customIcon.light, {
|
||||
width: faviconSize,
|
||||
height: faviconSize,
|
||||
})
|
||||
: linker.toAbsoluteURL(
|
||||
linker.toPathInSpace('~gitbook/icon?size=small&theme=light')
|
||||
),
|
||||
type: 'image/png',
|
||||
media: '(prefers-color-scheme: light)',
|
||||
},
|
||||
{
|
||||
url: customIcon?.dark
|
||||
? getResizedImageURL(imageResizer, customIcon.dark, {
|
||||
width: faviconSize,
|
||||
height: faviconSize,
|
||||
})
|
||||
: linker.toAbsoluteURL(
|
||||
linker.toPathInSpace('~gitbook/icon?size=small&theme=dark')
|
||||
),
|
||||
type: 'image/png',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
].map(async (icon) => ({
|
||||
...icon,
|
||||
url: await icon.url,
|
||||
}))
|
||||
);
|
||||
|
||||
return {
|
||||
title: site.title,
|
||||
@@ -134,6 +142,12 @@ export async function generateSiteLayoutMetadata(context: GitBookSiteContext): P
|
||||
icon: icons,
|
||||
apple: icons,
|
||||
},
|
||||
appleWebApp: {
|
||||
capable: true,
|
||||
title: site.title,
|
||||
statusBarStyle:
|
||||
customization.themes.default === CustomizationThemeMode.Dark ? 'black' : 'default',
|
||||
},
|
||||
robots: (await isSiteIndexable(context)) ? 'index, follow' : 'noindex, nofollow',
|
||||
};
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user