Compare commits

..

9 Commits

Author SHA1 Message Date
Nicolas Dorseuil 44ae7afc37 resolve more things at the beginning 2025-06-16 16:32:42 +02:00
Nicolas Dorseuil b2859600bc Merge remote-tracking branch 'origin/main' into gbo/prefetch-data-rnd-7317 2025-06-16 15:04:46 +02:00
Nicolas Dorseuil aabdd3f4d7 fix v1 2025-06-16 11:48:57 +02:00
Nicolas Dorseuil a2fc076acb fix getContentRef 2025-06-16 10:54:01 +02:00
Nicolas Dorseuil 82ed911cd8 Refactor content reference resolution to use context.getContentRef method across components 2025-06-16 10:12:40 +02:00
Nicolas Dorseuil 7b4908c8fd Refactor content reference handling to use getContentRef method across components 2025-06-16 09:47:21 +02:00
Nicolas Dorseuil de19ada4fa move to prefetch 2025-06-14 12:01:02 +02:00
Nicolas Dorseuil 47c80991d3 try using prefetched data 2025-06-14 11:49:16 +02:00
Nicolas Dorseuil 9e582ed9f0 basic dirty implementation of prefetching 2025-06-14 11:49:16 +02:00
138 changed files with 1807 additions and 2162 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Hide scrollbar on sections
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": minor
---
Add support for inline icons.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix crash when integration script fails to render block.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Show scrollbars
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": minor
---
Add support for icons in buttons.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": minor
---
Fix rendering of ogimage with SVG logos.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix crash when integration is triggering invalid requests.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook-v2": patch
---
Optimize the fetch of revision files by using only the getRevision cache.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix CodeBlock layout
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Scroll to active TOC when clicking a link
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": minor
---
Add support for text alignment for headings and paragraphs.
-5
View File
@@ -1,5 +0,0 @@
---
"@gitbook/react-contentkit": patch
---
Add basic error handling when transitioning between states.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Don't crash ogimage generation on RTL text, as a workaround until we can support it.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": minor
---
Add support for site customization option to change how external links open.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix rendering of ogimage when logo or icon are AVIF images.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix ogimage generation failing with some JPEG images.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix page group not expanded by default
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook-v2": patch
---
Don't cache unexpected API errors for more than a few minutes.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook-v2": patch
---
Fix an issue where PDF export URLs were not keeping their query params.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Consistently show variant selector in section bar if site has sections
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Clicking an active TOC item toggles its descendants
-6
View File
@@ -1,6 +0,0 @@
---
"gitbook": patch
"gitbook-v2": patch
---
Fix InlineLinkTooltip having a negative impact on performance, especially on larger pages.
-6
View File
@@ -1,6 +0,0 @@
---
'@gitbook/openapi-parser': patch
'@gitbook/react-openapi': patch
---
Optional label in OpenAPI x-codeSamples
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Add metadata for adding site to Apple devices home
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix crash during rendering of ogimage for VA sites with default icon.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Allow to zoom images on mobile if relevant
-3
View File
@@ -1,3 +0,0 @@
# 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
+21 -77
View File
@@ -85,10 +85,8 @@
"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",
@@ -257,7 +255,7 @@
"version": "1.3.0",
"dependencies": {
"@gitbook/openapi-parser": "workspace:*",
"@scalar/api-client-react": "^1.3.16",
"@scalar/api-client-react": "^1.2.19",
"@scalar/oas-utils": "^0.2.130",
"clsx": "^2.1.1",
"flatted": "^3.2.9",
@@ -286,7 +284,7 @@
"react-dom": "^19.0.0",
},
"catalog": {
"@gitbook/api": "^0.123.0",
"@gitbook/api": "^0.120.0",
},
"packages": {
"@ai-sdk/provider": ["@ai-sdk/provider@1.1.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew=="],
@@ -651,7 +649,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.123.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-BszoIk4H/wWb0jnGSJPSSXSAO8tV4B4/LHwl5ygfERHqk9rOGpM/U7+yOlmbX/tnm30vE+MQ3QQ5i8hN8wVk1w=="],
"@gitbook/api": ["@gitbook/api@0.120.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-FiRmPiSBwobMxmNjd14QkkOdM95BAPLDDRShgpS9Vsd8lHjNMyZfrJKVJTsJUuFcgYoi4cqNw9yu/TiUBUgv3g=="],
"@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"],
@@ -841,8 +839,6 @@
"@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=="],
@@ -1127,21 +1123,19 @@
"@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.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": ["@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-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/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/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/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/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/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/draggable": ["@scalar/draggable@0.2.0", "", { "dependencies": { "vue": "^3.5.12" } }, "sha512-UetHRB5Bqo5egVYlS21roWBcICmyk8CKh2htsidO+bFGAjl2e7Te+rY0borhNrMclr0xezHlPuLpEs1dvgLS2g=="],
"@scalar/draggable": ["@scalar/draggable@0.1.11", "", { "dependencies": { "vue": "^3.5.12" } }, "sha512-EQW9N1+mDORhsbjdtCI3XDvmUKsuKw1uf6r3kT1Mm2zQKT+rWwA0ChsAkEN6OG62C0YumMuXpH71h1seAWptxw=="],
"@scalar/helpers": ["@scalar/helpers@0.0.4", "", {}, "sha512-AkWdiU8zhztb4bJ0U2LHgCtJs+kwWB5UFXebfy9QCLwCP0guhhFR2UJtRCU3QZ8xmZnI0AX0i7BjrxU/MN1roQ=="],
"@scalar/icons": ["@scalar/icons@0.1.3", "", { "dependencies": { "vue": "^3.5.12" } }, "sha512-Bl46u7WsJ7NYjW1Fva7SMvw9c/92pGBP8B68tvDc+QevQ04DVNxw6+ny1NU/PnLtpuu1rUpPdtSCAkV1OdQGZQ=="],
"@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/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/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=="],
@@ -1151,21 +1145,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.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/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/snippetz": ["@scalar/snippetz@0.3.1", "", { "dependencies": { "stringify-object": "^5.0.0" } }, "sha512-r5tjYhHxSgyxEPbzIp21xUVR3oy6hjrG4B4ozQWR8qvzpST+Am1KZI+dS9l69PKzfQ0YvQROOvfCMIh3JAFTFQ=="],
"@scalar/snippetz": ["@scalar/snippetz@0.2.19", "", { "dependencies": { "stringify-object": "^5.0.0" } }, "sha512-fxC5mL3AZWiXAM21sMe1QU1/mu5KceN8ZmzFaP3xmdK26o/MkPKSLGVWW7w6OQkZi5hNloLHXXQiaI235qomEg=="],
"@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.2.3", "", { "dependencies": { "@scalar/openapi-types": "0.3.3", "nanoid": "^5.1.5", "zod": "3.24.1" } }, "sha512-K1/Vp5xaQ8TEGDYwHg88OBuQWJRWrE3ToZ0Z2LJwskcY1Eg+uDiRQXKF4QPngZzu5rrWgRErAq4yte4Y7omqgA=="],
"@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/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-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-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-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-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-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-tooltip": ["@scalar/use-tooltip@1.1.0", "", { "dependencies": { "tippy.js": "^6.3.7", "vue": "^3.5.12" } }, "sha512-KJConD/JDyGP8GPGpD+TXA6FEcKT9bmHQb/JyBmME+tJoJGHGtNcGy7kcezFakaKCqfKyY7cgPzL1ctUaGIRag=="],
"@scalar/use-tooltip": ["@scalar/use-tooltip@1.0.6", "", { "dependencies": { "tippy.js": "^6.3.7", "vue": "^3.5.12" } }, "sha512-f0gadIaUnILfi9qYAk7g+fNTsvLGXnam8oOUTxovavC1ocYuGTEykdz3g2MTqnAqRS8OkAB64h9mHf0FBfg6mg=="],
"@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=="],
@@ -1325,8 +1319,6 @@
"@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=="],
@@ -1715,8 +1707,6 @@
"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=="],
@@ -2077,8 +2067,6 @@
"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=="],
@@ -3875,45 +3863,19 @@
"@rollup/pluginutils/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"@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/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="],
"@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/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/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/openapi-types": ["@scalar/openapi-types@0.3.3", "", { "dependencies": { "zod": "3.24.1" } }, "sha512-mr3OvbCyvuzSDgv9gR9D3pJAsl82BRNBp1P8ldgRgKF4Wiswfa7nQeKibpX0kHSGXLgiyvxWJULUFwgdWMXNNA=="],
"@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="],
"@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=="],
"@scalar/use-toasts/nanoid": ["nanoid@5.1.2", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-b+CiXQCNMUGe0Ri64S9SXFcP9hogjAJ2Rd6GdVxhPLRm7mhGaM7VgOvCAJ1ZshfHbqVDI3uqTI5C8/GaKuLI7g=="],
"@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=="],
@@ -4971,24 +4933,6 @@
"@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
View File
@@ -36,7 +36,7 @@
"workspaces": {
"packages": ["packages/*"],
"catalog": {
"@gitbook/api": "^0.123.0"
"@gitbook/api": "^0.120.0"
}
},
"patchedDependencies": {
@@ -122,11 +122,6 @@
]
},
"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,11 +98,7 @@
},
"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,14 +183,7 @@ class GitbookIncrementalCache implements IncrementalCache {
}
// Utility function to generate keys for R2/Cache API
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]);
}
getR2Key(key: string, cacheType: CacheEntryType = 'cache'): string {
const hash = createHash('sha256').update(key).digest('hex');
return `${DEFAULT_PREFIX}/${cacheType === 'cache' ? process.env?.NEXT_BUILD_ID : 'dataCache'}/${hash}.${cacheType}`.replace(
/\/+/g,
@@ -3,7 +3,7 @@ import {
generateSitePageMetadata,
generateSitePageViewport,
} from '@/components/SitePage';
import { type RouteParams, getDynamicSiteContext, getPagePathFromParams } from '@v2/app/utils';
import { type RouteParams, getDynamicSiteContext } from '@v2/app/utils';
import type { Metadata, Viewport } from 'next';
type PageProps = {
@@ -14,9 +14,8 @@ type PageProps = {
export default async function Page(props: PageProps) {
const params = await props.params;
const { context } = await getDynamicSiteContext(params);
const pathname = getPagePathFromParams(params);
return <SitePage context={context} pageParams={{ pathname }} />;
return <SitePage context={context} pageParams={params} />;
}
export async function generateViewport(props: PageProps): Promise<Viewport> {
@@ -27,10 +26,9 @@ export async function generateViewport(props: PageProps): Promise<Viewport> {
export async function generateMetadata(props: PageProps): Promise<Metadata> {
const params = await props.params;
const { context } = await getDynamicSiteContext(params);
const pathname = getPagePathFromParams(params);
return generateSitePageMetadata({
context,
pageParams: { pathname },
pageParams: params,
});
}
@@ -1,9 +1,5 @@
import { CustomizationRootLayout } from '@/components/RootLayout';
import {
SiteLayout,
generateSiteLayoutMetadata,
generateSiteLayoutViewport,
} from '@/components/SiteLayout';
import { SiteLayout, generateSiteLayoutViewport } from '@/components/SiteLayout';
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
import { GITBOOK_DISABLE_TRACKING } from '@v2/lib/env';
import { getThemeFromMiddleware } from '@v2/lib/middleware';
@@ -38,7 +34,7 @@ export async function generateViewport({ params }: SiteDynamicLayoutProps) {
return generateSiteLayoutViewport(context);
}
export async function generateMetadata({ params }: SiteDynamicLayoutProps) {
const { context } = await getDynamicSiteContext(await params);
return generateSiteLayoutMetadata(context);
}
// export async function generateMetadata({ params }: SiteDynamicLayoutProps) {
// const { context } = await getDynamicSiteContext(await params);
// return generateSiteLayoutMetadata(context);
// }
@@ -3,7 +3,8 @@ import {
generateSitePageMetadata,
generateSitePageViewport,
} from '@/components/SitePage';
import { type RouteParams, getPagePathFromParams, getStaticSiteContext } from '@v2/app/utils';
import type { RouteParams } from '@v2/app/utils';
import { getPrefetchedDataFromLayoutParams } from '@v2/lib/data/prefetch';
import type { Metadata, Viewport } from 'next';
@@ -15,24 +16,24 @@ type PageProps = {
export default async function Page(props: PageProps) {
const params = await props.params;
const { context } = await getStaticSiteContext(params);
const pathname = getPagePathFromParams(params);
const { staticSiteContext } = getPrefetchedDataFromLayoutParams(params);
const { context } = await staticSiteContext;
return <SitePage context={context} pageParams={{ pathname }} />;
return <SitePage context={context} pageParams={params} />;
}
export async function generateViewport(props: PageProps): Promise<Viewport> {
const { context } = await getStaticSiteContext(await props.params);
const params = await props.params;
const { context } = await getPrefetchedDataFromLayoutParams(params).staticSiteContext;
return generateSitePageViewport(context);
}
export async function generateMetadata(props: PageProps): Promise<Metadata> {
const params = await props.params;
const { context } = await getStaticSiteContext(params);
const pathname = getPagePathFromParams(params);
const { context } = await getPrefetchedDataFromLayoutParams(params).staticSiteContext;
return generateSitePageMetadata({
context,
pageParams: { pathname },
pageParams: params,
});
}
@@ -5,6 +5,7 @@ import {
generateSiteLayoutViewport,
} from '@/components/SiteLayout';
import { type RouteLayoutParams, getStaticSiteContext } from '@v2/app/utils';
import { getPrefetchedDataFromLayoutParams } from '@v2/lib/data/prefetch';
import { GITBOOK_DISABLE_TRACKING } from '@v2/lib/env';
interface SiteStaticLayoutProps {
@@ -36,6 +37,6 @@ export async function generateViewport({ params }: SiteStaticLayoutProps) {
}
export async function generateMetadata({ params }: SiteStaticLayoutProps) {
const { context } = await getStaticSiteContext(await params);
return generateSiteLayoutMetadata(context);
const prefetchedData = getPrefetchedDataFromLayoutParams(await params);
return generateSiteLayoutMetadata(prefetchedData);
}
+19 -1
View File
@@ -1,7 +1,13 @@
import { getVisitorAuthClaims, getVisitorAuthClaimsFromToken } from '@/lib/adaptive';
import { getDynamicCustomizationSettings } from '@/lib/customization';
import type { SiteAPIToken } from '@gitbook/api';
import { type SiteURLData, fetchSiteContextByURLLookup, getBaseContext } from '@v2/lib/context';
import {
type GitBookSiteContext,
type SiteURLData,
fetchSiteContextByURLLookup,
getBaseContext,
} from '@v2/lib/context';
import { getResizedImageURL } from '@v2/lib/images';
import { jwtDecode } from 'jwt-decode';
import { forbidden } from 'next/navigation';
import rison from 'rison';
@@ -105,3 +111,15 @@ function getSiteURLDataFromParams(params: RouteLayoutParams): SiteURLData {
const decoded = decodeURIComponent(params.siteData);
return rison.decode(decoded);
}
export const getIcon = async (context: GitBookSiteContext, theme: 'light' | 'dark') => {
const { linker, imageResizer, customization } = context;
const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null;
const faviconSize = 48;
return customIcon?.[theme]
? await getResizedImageURL(imageResizer, customIcon[theme], {
width: faviconSize,
height: faviconSize,
})
: linker.toAbsoluteURL(linker.toPathInSpace('~gitbook/icon?size=small&theme=light'));
};
+14 -7
View File
@@ -2,7 +2,7 @@ import { getSiteStructureSections } from '@/lib/sites';
import type {
ChangeRequest,
PublishedSiteContent,
Revision,
RevisionPage,
RevisionPageDocument,
Site,
SiteCustomizationSettings,
@@ -84,8 +84,11 @@ export type GitBookSpaceContext = GitBookBaseContext & {
space: Space;
changeRequest: ChangeRequest | null;
/** Revision of the space. */
revision: Revision;
/** ID of the current revision. */
revisionId: string;
/** Pages of the space. */
pages: RevisionPage[];
/** Share key of the space. */
shareKey: string | undefined;
@@ -348,17 +351,20 @@ export async function fetchSpaceContextByIds(
const revisionId = ids.revision ?? changeRequest?.revision ?? space.revision;
const revision = await getDataOrNull(
dataFetcher.getRevision({
const pages = await getDataOrNull(
dataFetcher.getRevisionPages({
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 (!revision) {
if (!pages) {
notFound();
}
@@ -366,8 +372,9 @@ export async function fetchSpaceContextByIds(
...baseContext,
organizationId: space.organization,
space,
revision,
pages,
changeRequest,
revisionId,
shareKey: ids.shareKey,
};
}
+202 -89
View File
@@ -9,7 +9,7 @@ import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-t
import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env';
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache';
import { cache } from '../cache';
import { DataFetcherError, wrapCacheDataFetcherError } from './errors';
import { DataFetcherError, wrapDataFetcherError } from './errors';
import type { GitBookDataFetcher } from './types';
interface DataFetcherInput {
@@ -73,6 +73,25 @@ export function createDataFetcher(
getRevision(input, {
spaceId: params.spaceId,
revisionId: params.revisionId,
metadata: params.metadata,
})
);
},
getRevisionPages(params) {
return trace('getRevisionPages', () =>
getRevisionPages(input, {
spaceId: params.spaceId,
revisionId: params.revisionId,
metadata: params.metadata,
})
);
},
getRevisionFile(params) {
return trace('getRevisionFile', () =>
getRevisionFile(input, {
spaceId: params.spaceId,
revisionId: params.revisionId,
fileId: params.fileId,
})
);
},
@@ -103,6 +122,15 @@ export function createDataFetcher(
})
);
},
getReusableContent(params) {
return trace('getReusableContent', () =>
getReusableContent(input, {
spaceId: params.spaceId,
revisionId: params.revisionId,
reusableContentId: params.reusableContentId,
})
);
},
getLatestOpenAPISpecVersionContent(params) {
return trace('getLatestOpenAPISpecVersionContent', () =>
getLatestOpenAPISpecVersionContent(input, {
@@ -178,8 +206,8 @@ export function createDataFetcher(
const getUserById = cache(async (input: DataFetcherInput, params: { userId: string }) => {
'use cache';
return wrapCacheDataFetcherError(async () => {
return trace(`getUserById(${params.userId})`, async () => {
return trace(`getUserById(${params.userId})`, async () => {
return wrapDataFetcherError(async () => {
const api = apiClient(input);
const res = await api.users.getUserById(params.userId, {
...noCacheFetchOptions,
@@ -201,8 +229,8 @@ const getSpace = cache(
})
);
return wrapCacheDataFetcherError(async () => {
return trace(`getSpace(${params.spaceId}, ${params.shareKey})`, async () => {
return trace(`getSpace(${params.spaceId}, ${params.shareKey})`, async () => {
return wrapDataFetcherError(async () => {
const api = apiClient(input);
const res = await api.spaces.getSpaceById(
params.spaceId,
@@ -232,38 +260,38 @@ const getChangeRequest = cache(
})
);
return wrapCacheDataFetcherError(async () => {
return trace(
`getChangeRequest(${params.spaceId}, ${params.changeRequestId})`,
async () => {
const api = apiClient(input);
const res = await api.spaces.getChangeRequestById(
params.spaceId,
params.changeRequestId,
{
...noCacheFetchOptions,
}
);
cacheTag(...getCacheTagsFromResponse(res));
cacheLife('minutes');
return res.data;
}
);
return trace(`getChangeRequest(${params.spaceId}, ${params.changeRequestId})`, async () => {
return wrapDataFetcherError(async () => {
const api = apiClient(input);
const res = await api.spaces.getChangeRequestById(
params.spaceId,
params.changeRequestId,
{
...noCacheFetchOptions,
}
);
cacheTag(...getCacheTagsFromResponse(res));
cacheLife('minutes');
return res.data;
});
});
}
);
const getRevision = cache(
async (input: DataFetcherInput, params: { spaceId: string; revisionId: string }) => {
async (
input: DataFetcherInput,
params: { spaceId: string; revisionId: string; metadata: boolean }
) => {
'use cache';
return wrapCacheDataFetcherError(async () => {
return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => {
return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => {
return wrapDataFetcherError(async () => {
const api = apiClient(input);
const res = await api.spaces.getRevisionById(
params.spaceId,
params.revisionId,
{
metadata: true,
metadata: params.metadata,
},
{
...noCacheFetchOptions,
@@ -277,16 +305,72 @@ const getRevision = cache(
}
);
const getRevisionPages = cache(
async (
input: DataFetcherInput,
params: { spaceId: string; revisionId: string; metadata: boolean }
) => {
'use cache';
return trace(`getRevisionPages(${params.spaceId}, ${params.revisionId})`, async () => {
return wrapDataFetcherError(async () => {
const api = apiClient(input);
const res = await api.spaces.listPagesInRevisionById(
params.spaceId,
params.revisionId,
{
metadata: params.metadata,
},
{
...noCacheFetchOptions,
}
);
cacheTag(...getCacheTagsFromResponse(res));
cacheLife('max');
return res.data.pages;
});
});
}
);
const getRevisionFile = cache(
async (
input: DataFetcherInput,
params: { spaceId: string; revisionId: string; fileId: string }
) => {
'use cache';
return trace(
`getRevisionFile(${params.spaceId}, ${params.revisionId}, ${params.fileId})`,
async () => {
return wrapDataFetcherError(async () => {
const api = apiClient(input);
const res = await api.spaces.getFileInRevisionById(
params.spaceId,
params.revisionId,
params.fileId,
{},
{
...noCacheFetchOptions,
}
);
cacheTag(...getCacheTagsFromResponse(res));
cacheLife('max');
return res.data;
});
}
);
}
);
const getRevisionPageMarkdown = cache(
async (
input: DataFetcherInput,
params: { spaceId: string; revisionId: string; pageId: string }
) => {
'use cache';
return wrapCacheDataFetcherError(async () => {
return trace(
`getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
async () => {
return trace(
`getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
async () => {
return wrapDataFetcherError(async () => {
const api = apiClient(input);
const res = await api.spaces.getPageInRevisionById(
params.spaceId,
@@ -307,9 +391,9 @@ const getRevisionPageMarkdown = cache(
throw new DataFetcherError('Page is not a document', 404);
}
return res.data.markdown;
}
);
});
});
}
);
}
);
@@ -319,10 +403,10 @@ const getRevisionPageDocument = cache(
params: { spaceId: string; revisionId: string; pageId: string }
) => {
'use cache';
return wrapCacheDataFetcherError(async () => {
return trace(
`getRevisionPageDocument(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
async () => {
return trace(
`getRevisionPageDocument(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
async () => {
return wrapDataFetcherError(async () => {
const api = apiClient(input);
const res = await api.spaces.getPageDocumentInRevisionById(
params.spaceId,
@@ -340,9 +424,9 @@ const getRevisionPageDocument = cache(
cacheLife('max');
return res.data;
}
);
});
});
}
);
}
);
@@ -352,11 +436,11 @@ const getRevisionPageByPath = cache(
params: { spaceId: string; revisionId: string; path: string }
) => {
'use cache';
return wrapCacheDataFetcherError(async () => {
return trace(
`getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`,
async () => {
const encodedPath = encodeURIComponent(params.path);
return trace(
`getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`,
async () => {
const encodedPath = encodeURIComponent(params.path);
return wrapDataFetcherError(async () => {
const api = apiClient(input);
const res = await api.spaces.getPageInRevisionByPath(
params.spaceId,
@@ -370,17 +454,17 @@ const getRevisionPageByPath = cache(
cacheTag(...getCacheTagsFromResponse(res));
cacheLife('max');
return res.data;
}
);
});
});
}
);
}
);
const getDocument = cache(
async (input: DataFetcherInput, params: { spaceId: string; documentId: string }) => {
'use cache';
return wrapCacheDataFetcherError(async () => {
return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => {
return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => {
return wrapDataFetcherError(async () => {
const api = apiClient(input);
const res = await api.spaces.getDocumentById(
params.spaceId,
@@ -419,10 +503,10 @@ const getComputedDocument = cache(
)
);
return wrapCacheDataFetcherError(async () => {
return trace(
`getComputedDocument(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`,
async () => {
return trace(
`getComputedDocument(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`,
async () => {
return wrapDataFetcherError(async () => {
const api = apiClient(input);
const res = await api.spaces.getComputedDocument(
params.spaceId,
@@ -438,9 +522,38 @@ const getComputedDocument = cache(
cacheTag(...getCacheTagsFromResponse(res));
cacheLife('max');
return res.data;
}
);
});
});
}
);
}
);
const getReusableContent = cache(
async (
input: DataFetcherInput,
params: { spaceId: string; revisionId: string; reusableContentId: string }
) => {
'use cache';
return trace(
`getReusableContent(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`,
async () => {
return wrapDataFetcherError(async () => {
const api = apiClient(input);
const res = await api.spaces.getReusableContentInRevisionById(
params.spaceId,
params.revisionId,
params.reusableContentId,
{},
{
...noCacheFetchOptions,
}
);
cacheTag(...getCacheTagsFromResponse(res));
cacheLife('max');
return res.data;
});
}
);
}
);
@@ -455,10 +568,10 @@ const getLatestOpenAPISpecVersionContent = cache(
})
);
return wrapCacheDataFetcherError(async () => {
return trace(
`getLatestOpenAPISpecVersionContent(${params.organizationId}, ${params.slug})`,
async () => {
return trace(
`getLatestOpenAPISpecVersionContent(${params.organizationId}, ${params.slug})`,
async () => {
return wrapDataFetcherError(async () => {
const api = apiClient(input);
const res = await api.orgs.getLatestOpenApiSpecVersionContent(
params.organizationId,
@@ -470,9 +583,9 @@ const getLatestOpenAPISpecVersionContent = cache(
cacheTag(...getCacheTagsFromResponse(res));
cacheLife('max');
return res.data;
}
);
});
});
}
);
}
);
@@ -489,10 +602,10 @@ const getPublishedContentSite = cache(
})
);
return wrapCacheDataFetcherError(async () => {
return trace(
`getPublishedContentSite(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`,
async () => {
return trace(
`getPublishedContentSite(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`,
async () => {
return wrapDataFetcherError(async () => {
const api = apiClient(input);
const res = await api.orgs.getPublishedContentSite(
params.organizationId,
@@ -507,9 +620,9 @@ const getPublishedContentSite = cache(
cacheTag(...getCacheTagsFromResponse(res));
cacheLife('days');
return res.data;
}
);
});
});
}
);
}
);
@@ -531,10 +644,10 @@ const getSiteRedirectBySource = cache(
})
);
return wrapCacheDataFetcherError(async () => {
return trace(
`getSiteRedirectBySource(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`,
async () => {
return trace(
`getSiteRedirectBySource(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`,
async () => {
return wrapDataFetcherError(async () => {
const api = apiClient(input);
const res = await api.orgs.getSiteRedirectBySource(
params.organizationId,
@@ -550,9 +663,9 @@ const getSiteRedirectBySource = cache(
cacheTag(...getCacheTagsFromResponse(res));
cacheLife('days');
return res.data;
}
);
});
});
}
);
}
);
@@ -566,8 +679,8 @@ const getEmbedByUrl = cache(
})
);
return wrapCacheDataFetcherError(async () => {
return trace(`getEmbedByUrl(${params.spaceId}, ${params.url})`, async () => {
return trace(`getEmbedByUrl(${params.spaceId}, ${params.url})`, async () => {
return wrapDataFetcherError(async () => {
const api = apiClient(input);
const res = await api.spaces.getEmbedByUrlInSpace(
params.spaceId,
@@ -599,10 +712,10 @@ const searchSiteContent = cache(
})
);
return wrapCacheDataFetcherError(async () => {
return trace(
`searchSiteContent(${params.organizationId}, ${params.siteId}, ${params.query})`,
async () => {
return trace(
`searchSiteContent(${params.organizationId}, ${params.siteId}, ${params.query})`,
async () => {
return wrapDataFetcherError(async () => {
const { organizationId, siteId, query, scope } = params;
const api = apiClient(input);
const res = await api.orgs.searchSiteContent(
@@ -620,9 +733,9 @@ const searchSiteContent = cache(
cacheTag(...getCacheTagsFromResponse(res));
cacheLife('hours');
return res.data.items;
}
);
});
});
}
);
}
);
@@ -639,8 +752,8 @@ const renderIntegrationUi = cache(
})
);
return wrapCacheDataFetcherError(async () => {
return trace(`renderIntegrationUi(${params.integrationName})`, async () => {
return trace(`renderIntegrationUi(${params.integrationName})`, async () => {
return wrapDataFetcherError(async () => {
const api = apiClient(input);
const res = await api.integrations.renderIntegrationUiWithPost(
params.integrationName,
+5 -46
View File
@@ -1,5 +1,4 @@
import { GitBookAPIError } from '@gitbook/api';
import { unstable_cacheLife as cacheLife } from 'next/cache';
import type { DataFetcherErrorData, DataFetcherResponse } from './types';
export class DataFetcherError extends Error {
@@ -48,7 +47,11 @@ export function getDataOrNull<T>(
return response.then((result) => getDataOrNull(result, ignoreErrors));
}
return ignoreDataFetcherErrors(response, ignoreErrors).data ?? null;
if (response.error) {
if (ignoreErrors.includes(response.error.code)) return null;
throw new DataFetcherError(response.error.message, response.error.code);
}
return response.data;
}
/**
@@ -90,50 +93,6 @@ 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,4 +5,3 @@ export * from './errors';
export * from './lookup';
export * from './visitor';
export * from './pages';
export * from './revisions';
+33 -2
View File
@@ -1,4 +1,4 @@
import { isV2 } from '@/lib/v2';
import { waitUntil } from '@/lib/waitUntil';
import type { JSONDocument, RevisionPageDocument } from '@gitbook/api';
import type { GitBookSiteContext, GitBookSpaceContext } from '../context';
import { getDataOrNull } from './errors';
@@ -12,7 +12,10 @@ export async function getPageDocument(
): Promise<JSONDocument | null> {
const { dataFetcher, space } = context;
if (isV2()) {
if (
'site' in context &&
(context.site.id === 'site_JOVzv' || context.site.id === 'site_IxAYj')
) {
return getDataOrNull(
dataFetcher.getRevisionPageDocument({
spaceId: space.id,
@@ -38,5 +41,33 @@ 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,220 @@
import { type PagePathParams, fetchPageData } from '@/components/SitePage';
import type { VisitorAuthClaims } from '@/lib/adaptive';
import type { AncestorRevisionPage } from '@/lib/pages';
import {
type ResolveContentRefOptions,
type ResolvedContentRef,
resolveContentRef,
} from '@/lib/references';
import type { ContentRef, JSONDocument, RevisionPageDocument } from '@gitbook/api';
import {
type RouteLayoutParams,
type RouteParams,
getIcon,
getPagePathFromParams,
getStaticSiteContext,
} from '@v2/app/utils';
import { identify } from 'object-identity';
import { cache } from '../cache';
import type { GitBookSiteContext } from '../context';
import { getPageDocument } from './pages';
export interface PrefetchedLayoutData {
staticSiteContext: Promise<{
context: GitBookSiteContext;
visitorAuthClaims: VisitorAuthClaims;
}>;
icons: Promise<
{
url: string;
type: string;
media: string;
}[]
>;
}
export interface PrefetchedPageData {
pageData: Promise<{
context: GitBookSiteContext & {
page?: RevisionPageDocument;
};
pageTarget?: {
page: RevisionPageDocument;
ancestors: AncestorRevisionPage[];
};
}>;
document: Promise<JSONDocument | null>;
getPrefetchedRef: (
ref?: ContentRef,
options?: ResolveContentRefOptions
) => Promise<ResolvedContentRef | null>;
}
const cachedInitialDate = cache(() => Date.now());
/**
* Fetches the page data matching the requested pathname and fallback to root page when page is not found.
*/
export async function getPageDataWithFallback(args: {
context: GitBookSiteContext;
pagePathParams: PagePathParams;
}) {
const { context: baseContext, pagePathParams } = args;
const { context, pageTarget } = await fetchPageData(baseContext, pagePathParams);
return {
context: {
...context,
page: pageTarget?.page,
},
pageTarget,
};
}
export async function getIcons(context: GitBookSiteContext): Promise<
{
url: string;
type: string;
media: string;
}[]
> {
return Promise.all([getIcon(context, 'light'), getIcon(context, 'dark')]).then((urls) => [
{
url: urls[0],
type: 'image/png',
media: '(prefers-color-scheme: light)',
},
{
url: urls[1],
type: 'image/png',
media: '(prefers-color-scheme: dark)',
},
]);
}
export const getPrefetchedDataFromLayoutParams = cache(
(params: RouteLayoutParams): PrefetchedLayoutData => {
const startingDate = cachedInitialDate();
const staticSiteContext = getStaticSiteContext(params).finally(() => {
console.log(`Finished fetching static site context in ${Date.now() - startingDate}ms`);
});
const icons = staticSiteContext
.then(({ context }) => getIcons(context))
.finally(() => {
console.log(`Finished fetching icons in ${Date.now() - startingDate}ms`);
});
return {
staticSiteContext,
icons,
};
}
);
export const prefetchedDocumentRef = (
document: JSONDocument | null,
context: GitBookSiteContext
) => {
const fetched = new Map<string, Promise<ResolvedContentRef | null>>();
if (!document) return fetched;
const traverseNodes = (nodes: any[]): void => {
for (const node of nodes) {
// We try prefetching as many references as possible.
if (node.data?.ref) {
fetched.set(identify(node.data.ref), resolveContentRef(node.data.ref, context));
}
// Handle prefetching of references for cards
if (node.data?.view && node.data.view.type === 'cards') {
const view = node.data.view;
const records = Object.entries(node.data.records || {});
records.forEach(async (record: [string, any]) => {
const coverFile = view.coverDefinition
? record[1].values[view.coverDefinition]?.[0]
: null;
const targetRef = view.targetDefinition
? (record[1].values[view.targetDefinition] as ContentRef)
: null;
if (targetRef) {
fetched.set(identify(targetRef), resolveContentRef(targetRef, context));
}
if (coverFile) {
const fileRef = {
kind: 'file' as const,
file: coverFile,
};
fetched.set(identify(fileRef), resolveContentRef(fileRef, context));
}
});
}
if (node.nodes && Array.isArray(node.nodes)) {
traverseNodes(node.nodes);
}
if (node.fragments && Array.isArray(node.fragments)) {
traverseNodes(node.fragments);
}
}
};
if (document.nodes && Array.isArray(document.nodes)) {
traverseNodes(document.nodes);
}
return fetched;
};
export const getPrefetchedDataFromPageParams = cache((params: RouteParams): PrefetchedPageData => {
const startingDate = cachedInitialDate();
const { staticSiteContext } = getPrefetchedDataFromLayoutParams(params);
const pathname = getPagePathFromParams(params);
const pageData = staticSiteContext
.then(({ context }) =>
getPageDataWithFallback({
context,
pagePathParams: {
pathname,
},
})
)
.finally(() => {
console.log(`Finished fetching page data in ${Date.now() - startingDate}ms`);
});
const document = pageData
.then(({ context, pageTarget }) => {
if (!pageTarget?.page) {
return null;
}
return getPageDocument(context, pageTarget?.page);
})
.finally(() => {
console.log(`Finished fetching document in ${Date.now() - startingDate}ms`);
});
const prefetchedRef = Promise.all([staticSiteContext, document])
.then(([{ context }, document]) => {
// Prefetch the references in the document
return prefetchedDocumentRef(document, context);
})
.finally(() => {
console.log(`Finished prefetching references in ${Date.now() - startingDate}ms`);
});
const getContentRef = async (
ref?: ContentRef,
options?: ResolveContentRefOptions
): Promise<ResolvedContentRef | null> => {
if (!ref) {
return null;
}
if (options) {
const { context } = await staticSiteContext;
return resolveContentRef(ref, context, options);
}
return prefetchedRef.then((prefetched) => prefetched.get(identify(ref)) ?? null);
};
return {
pageData,
document,
getPrefetchedRef: getContentRef,
};
});
@@ -1,36 +0,0 @@
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;
}
+28
View File
@@ -67,8 +67,27 @@ 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.
*/
@@ -113,6 +132,15 @@ 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' | 'png';
format?: 'webp' | 'avif' | 'json' | 'jpeg';
fit?: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad';
width?: number;
height?: number;
+4 -6
View File
@@ -35,9 +35,8 @@ describe('toPathInSpace', () => {
expect(variantInSection.toPathInSpace('some/path/')).toBe('/section/variant/some/path');
});
it('should not add an unnecessary trailing slash', () => {
// The index page should not be an empty path
expect(root.toPathInSpace('')).toBe('/');
it('should not add a trailing slash', () => {
expect(root.toPathInSpace('')).toBe('');
expect(variantInSection.toPathInSpace('')).toBe('/section/variant');
});
});
@@ -53,9 +52,8 @@ describe('toPathInSite', () => {
expect(siteGitBookIO.toPathInSite('some/path/')).toBe('/sitename/some/path');
});
it('should not add an unnecessary trailing slash', () => {
// The index page should not be an empty path
expect(root.toPathInSite('')).toBe('/');
it('should not add a trailing slash', () => {
expect(root.toPathInSite('')).toBe('');
expect(siteGitBookIO.toPathInSite('')).toBe('/sitename');
});
});
+1 -2
View File
@@ -128,8 +128,7 @@ export function createLinker(
function joinPaths(prefix: string, path: string): string {
const prefixPath = prefix.endsWith('/') ? prefix : `${prefix}/`;
const suffixPath = path.startsWith('/') ? path.slice(1) : path;
const pathWithoutTrailingSlash = removeTrailingSlash(prefixPath + suffixPath);
return pathWithoutTrailingSlash === '' ? '/' : pathWithoutTrailingSlash;
return removeTrailingSlash(prefixPath + suffixPath);
}
function removeTrailingSlash(path: string): string {
-35
View File
@@ -43,12 +43,6 @@ 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) {
@@ -62,25 +56,6 @@ 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.
*/
@@ -107,14 +82,6 @@ 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
//
@@ -292,8 +259,6 @@ 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,
+5 -5
View File
@@ -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',
-10
View File
@@ -603,21 +603,11 @@ 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',
-170
View File
@@ -1,170 +0,0 @@
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);
},
});
});
});
+1 -5
View File
@@ -16,7 +16,6 @@ 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';
@@ -318,9 +317,6 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
aiSearch: {
enabled: true,
},
externalLinks: {
target: SiteExternalLinksTarget.Self,
},
advancedCustomization: {
enabled: true,
},
@@ -350,7 +346,7 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
/**
* Wait for all icons present on the page to be loaded.
*/
export async function waitForIcons(page: Page) {
async function waitForIcons(page: Page) {
await page.waitForFunction(() => {
const urlStates: Record<
string,
+2 -4
View File
@@ -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/pdf.spec.ts",
"e2e": "playwright test e2e/internal.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,9 +81,7 @@
"usehooks-ts": "^3.1.0",
"zod": "^3.24.2",
"zod-to-json-schema": "^3.24.5",
"zustand": "^5.0.3",
"image-size": "^2.0.2",
"direction": "^2.0.1"
"zustand": "^5.0.3"
},
"devDependencies": {
"@argos-ci/playwright": "^5.0.3",
@@ -11,6 +11,7 @@ import { getVisitorAuthClaims } from '@/lib/adaptive';
import { getSiteContentPointer } from '@/lib/pointer';
import { shouldTrackEvents } from '@/lib/tracking';
import { fetchV1ContextForSitePointer } from '@/lib/v1';
import { getIcons } from '@v2/lib/data/prefetch';
export const runtime = 'edge';
export const dynamic = 'force-dynamic';
@@ -44,7 +45,14 @@ export async function generateViewport(): Promise<Viewport> {
export async function generateMetadata(): Promise<Metadata> {
const context = await fetchLayoutData();
return generateSiteLayoutMetadata(context);
const icons = getIcons(context);
return generateSiteLayoutMetadata({
staticSiteContext: Promise.resolve({
context,
visitorAuthClaims: {},
}),
icons,
});
}
async function fetchLayoutData() {
@@ -4,6 +4,7 @@ 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';
@@ -58,7 +59,7 @@ export async function AdminToolbar(props: AdminToolbarProps) {
return <ChangeRequestToolbar context={context} />;
}
if (context.revision.id !== context.space.revision) {
if (context.revisionId !== context.space.revision) {
return <RevisionToolbar context={context} />;
}
@@ -105,7 +106,15 @@ async function ChangeRequestToolbar(props: { context: GitBookSiteContext }) {
async function RevisionToolbar(props: { context: GitBookSiteContext }) {
const { context } = props;
const { revision } = context;
const { space, revisionId } = context;
const revision = await throwIfDataError(
context.dataFetcher.getRevision({
spaceId: space.id,
revisionId,
metadata: true,
})
);
return (
<ToolbarLayout>
@@ -7,8 +7,7 @@ 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 } from '../primitives';
import { LinkStyles } from '../primitives/styles';
import { Link, linkStyles } from '../primitives';
import { ANNOUNCEMENT_CSS_CLASS, ANNOUNCEMENT_STORAGE_KEY } from './constants';
/**
@@ -59,7 +58,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,19 +1,17 @@
import { type DocumentBlockContentRef, SiteInsightsLinkPosition } from '@gitbook/api';
import { Card } from '@/components/primitives';
import { type ResolvedContentRef, resolveContentRef } from '@/lib/references';
import type { ResolvedContentRef } from '@/lib/references';
import type { BlockProps } from './Block';
export async function BlockContentRef(props: BlockProps<DocumentBlockContentRef>) {
const { block, context, style } = props;
const resolved = context.contentContext
? await resolveContentRef(block.data.ref, context.contentContext, {
resolveAnchorText: true,
iconStyle: ['text-xl', 'text-tint'],
})
: null;
const resolved = await context.getContentRef(block.data.ref, {
resolveAnchorText: true,
iconStyle: ['text-xl', 'text-tint'],
});
if (!resolved) {
return null;
@@ -0,0 +1,37 @@
.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,6 +9,9 @@ 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;
@@ -47,7 +50,7 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
/>
<pre
className={tcls(
'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',
'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',
'rounded-md straight-corners:rounded-sm shadow-sm',
title && 'rounded-ss-none'
)}
@@ -55,7 +58,7 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
<code
id={id}
className={tcls(
'inline-grid min-w-full grid-cols-[auto_1fr] [count-reset:line] print:whitespace-pre-wrap',
'inline-grid min-w-full grid-cols-[auto_1fr] p-2 [count-reset:line] print:whitespace-pre-wrap',
withWrap && 'whitespace-pre-wrap'
)}
>
@@ -48,6 +48,7 @@ export function PlainCodeBlock(props: { code: string; syntax: string }) {
document={document}
context={{
mode: 'default',
getContentRef: async () => null, // No content references needed for plain code block
}}
block={block}
ancestorBlocks={[]}
@@ -12,7 +12,6 @@ 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 = {
@@ -264,28 +263,16 @@ function getPlainCodeBlockLine(
if (node.object === 'text') {
content += cleanupLine(node.leaves.map((leaf) => leaf.text).join(''));
} else {
switch (node.type) {
case 'annotation': {
const start = index + content.length;
content += getPlainCodeBlockLine(node, index + content.length, inlines);
const end = index + content.length;
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,
});
}
break;
}
case 'expression': {
break;
}
default: {
nullIfNever(node);
break;
}
if (inlines) {
inlines.push({
inline: node,
start,
end,
});
}
}
}
@@ -0,0 +1,67 @@
: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");
}
@@ -1,7 +1,8 @@
import type { ClassValue } from '@/lib/tailwind';
import type { JSONDocument } from '@gitbook/api';
import type { ContentRef, JSONDocument } from '@gitbook/api';
import type { GitBookAnyContext } from '@v2/lib/context';
import type { ResolveContentRefOptions, ResolvedContentRef } from '@/lib/references';
import { BlockSkeleton } from './Block';
import { Blocks } from './Blocks';
@@ -29,13 +30,10 @@ export interface DocumentContext {
*/
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;
getContentRef: (
ref: ContentRef,
options?: ResolveContentRefOptions
) => Promise<ResolvedContentRef | null>;
}
export interface DocumentContextProps {
@@ -1,7 +1,5 @@
import type { DocumentBlockDrawing } from '@gitbook/api';
import { resolveContentRef } from '@/lib/references';
import { Image } from '../utils';
import type { BlockProps } from './Block';
import { Caption } from './Caption';
@@ -9,11 +7,12 @@ import { imageBlockSizes } from './Images';
export async function Drawing(props: BlockProps<DocumentBlockDrawing>) {
const { block, context } = props;
if (!block.data.ref) {
return null;
}
const resolved = await context.getContentRef(block.data.ref);
const resolved =
block.data.ref && context.contentContext
? await resolveContentRef(block.data.ref, context.contentContext)
: null;
if (!resolved) {
return null;
}
@@ -1,7 +1,6 @@
import { type DocumentBlockFile, SiteInsightsLinkPosition } from '@gitbook/api';
import { getSimplifiedContentType } from '@/lib/files';
import { resolveContentRef } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
import { Link } from '../primitives';
@@ -12,9 +11,8 @@ import { FileIcon } from './FileIcon';
export async function File(props: BlockProps<DocumentBlockFile>) {
const { block, context } = props;
const contentRef = context.contentContext
? await resolveContentRef(block.data.ref, context.contentContext)
: null;
const contentRef = await context.getContentRef(block.data.ref);
const file = contentRef?.file;
if (!file) {
@@ -6,10 +6,9 @@ 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, ancestorBlocks, ...rest } = props;
const { block, style, context, ...rest } = props;
const textStyle = getBlockTextStyle(block);
@@ -43,7 +42,7 @@ export function Heading(props: BlockProps<DocumentBlockHeading>) {
'grid-area-1-1',
'z-[1]',
'justify-self-start',
getTextAlignment(block.data.align, ancestorBlocks),
'text-left',
textStyle.lineHeight,
textStyle.marginTop
)}
@@ -1,7 +1,6 @@
import type { DocumentBlockImage, DocumentBlockImages, JSONDocument, Length } from '@gitbook/api';
import { Image, type ImageResponsiveSize } from '@/components/utils';
import { resolveContentRef } from '@/lib/references';
import { type ClassValue, tcls } from '@/lib/tailwind';
import type { BlockProps } from './Block';
@@ -66,10 +65,8 @@ async function ImageBlock(props: {
const { block, context, isEstimatedOffscreen } = props;
const [src, darkSrc] = await Promise.all([
context.contentContext ? resolveContentRef(block.data.ref, context.contentContext) : null,
block.data.refDark && context.contentContext
? resolveContentRef(block.data.refDark, context.contentContext)
: null,
context.getContentRef(block.data.ref),
block.data.refDark ? context.getContentRef(block.data.refDark) : null,
]);
if (!src) {
@@ -5,7 +5,6 @@ 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';
@@ -48,8 +47,6 @@ 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,6 +1,4 @@
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';
@@ -11,7 +9,7 @@ export async function InlineButton(props: InlineProps<api.DocumentInlineButton>)
throw new Error('InlineButton requires a contentContext');
}
const resolved = await resolveContentRef(inline.data.ref, context.contentContext);
const resolved = await context.getContentRef(inline.data.ref);
if (!resolved) {
return null;
@@ -26,7 +24,6 @@ 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: {
@@ -1,10 +0,0 @@
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]" />;
}
@@ -2,7 +2,7 @@ import type { DocumentInlineImage } from '@gitbook/api';
import type { GitBookBaseContext } from '@v2/lib/context';
import assertNever from 'assert-never';
import { type ResolvedContentRef, resolveContentRef } from '@/lib/references';
import type { ResolvedContentRef } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
import { Image } from '../utils';
@@ -13,10 +13,8 @@ export async function InlineImage(props: InlineProps<DocumentInlineImage>) {
const { size = 'original' } = inline.data;
const [src, darkSrc] = await Promise.all([
context.contentContext ? resolveContentRef(inline.data.ref, context.contentContext) : null,
inline.data.refDark && context.contentContext
? resolveContentRef(inline.data.refDark, context.contentContext)
: null,
context.getContentRef(inline.data.ref),
inline.data.refDark ? context.getContentRef(inline.data.refDark) : null,
]);
if (!src) {
@@ -0,0 +1,54 @@
import { type DocumentInlineLink, SiteInsightsLinkPosition } from '@gitbook/api';
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 = await context.getContentRef(inline.data.ref);
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>
);
}
@@ -1,153 +0,0 @@
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>
);
}
@@ -1,73 +0,0 @@
'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
);
}
@@ -1 +0,0 @@
export * from './InlineLink';
@@ -1,27 +1,55 @@
'use client';
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';
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';
import { Button, StyledLink } from '../../primitives';
import { AIPageLinkSummary } from '../Adaptive/AIPageLinkSummary';
import { Button, StyledLink } from '../primitives';
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;
export async function InlineLinkTooltip(props: {
inline: DocumentInlineLink;
context: GitBookAnyContext;
children: React.ReactNode;
resolved: ResolvedContentRef;
}) {
const { isSamePage, isExternal, aiSummary, openInNewTabLabel, target, breadcrumbs, children } =
props;
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');
return (
<Tooltip.Provider delayDuration={200}>
@@ -72,15 +100,15 @@ export function InlineLinkTooltipImpl(props: {
isExternal && 'text-sm [overflow-wrap:anywhere]'
)}
>
{target.icon ? (
{resolved.icon ? (
<div className="mt-1 text-tint-subtle empty:hidden">
{target.icon}
{resolved.icon}
</div>
) : null}
<h5 className="font-semibold">{target.text}</h5>
<h5 className="font-semibold">{resolved.text}</h5>
</div>
</div>
{!isSamePage && target.href ? (
{!isSamePage && resolved.href ? (
<Button
className={tcls(
'-mx-2 -my-2 ml-auto',
@@ -89,35 +117,40 @@ export function InlineLinkTooltipImpl(props: {
: null
)}
variant="blank"
href={target.href}
href={resolved.href}
target="_blank"
label={openInNewTabLabel}
label={tString(language, 'open_in_new_tab')}
size="small"
icon="arrow-up-right-from-square"
iconOnly={true}
/>
) : null}
</div>
{target.subText ? (
<p className="mt-1 text-sm text-tint">{target.subText}</p>
{resolved.subText ? (
<p className="mt-1 text-sm text-tint">{resolved.subText}</p>
) : null}
</div>
{aiSummary ? (
{hasAISummary && 'page' in context && 'page' in inline.data.ref ? (
<div className="border-tint-subtle border-t bg-tint p-4">
<AIPageLinkSummary
targetPageId={aiSummary.pageId}
targetSpaceId={aiSummary.spaceId}
showTrademark
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
}
/>
</div>
) : null}
</div>
<Tooltip.Arrow
className={
typeof aiSummary !== 'undefined' ? 'fill-tint-3' : 'fill-tint-1'
}
/>
<Tooltip.Arrow className={hasAISummary ? 'fill-tint-3' : 'fill-tint-1'} />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
@@ -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,6 +16,8 @@ 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,
@@ -28,27 +30,17 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
},
};
const initialResponse = await fetchSafeIntegrationUI(context.contentContext, {
integrationName: block.data.integration,
request: initialInput,
});
const initialOutput = await getDataOrNull(
dataFetcher.renderIntegrationUi({
integrationName: block.data.integration,
request: initialInput,
}),
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') {
// 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') {
return null;
}
@@ -1,34 +0,0 @@
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,19 +22,16 @@ export async function renderIntegrationUi({
request: RenderIntegrationUI;
}) {
const serverAction = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
const output = await fetchSafeIntegrationUI(serverAction, {
integrationName: renderContext.integrationName,
request,
});
if (output.error) {
return {
error: output.error.message,
};
}
const output = await throwIfDataError(
serverAction.dataFetcher.renderIntegrationUi({
integrationName: renderContext.integrationName,
request,
})
);
return {
children: <ContentKitOutput output={output.data} context={contentKitServerContext} />,
output: output.data,
children: <ContentKitOutput output={output} context={contentKitServerContext} />,
output: output,
};
}
@@ -1,18 +1,15 @@
import { type DocumentInlineMention, SiteInsightsLinkPosition } from '@gitbook/api';
import { StyledLink } from '@/components/primitives';
import { resolveContentRef } from '@/lib/references';
import type { InlineProps } from './Inline';
export async function Mention(props: InlineProps<DocumentInlineMention>) {
const { inline, context } = props;
const resolved = context.contentContext
? await resolveContentRef(inline.data.ref, context.contentContext, {
resolveAnchorText: true,
})
: null;
const resolved = await context.getContentRef(inline.data.ref, {
resolveAnchorText: true,
});
if (!resolved) {
return null;
@@ -680,7 +680,9 @@ 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-auto no-scrollbar;
@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;
}
.openapi-tabs-tab {
@@ -4,13 +4,12 @@ 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, ancestorBlocks, ...contextProps } = props;
const { block, style, ...contextProps } = props;
return (
<p className={tcls(style, getTextAlignment(block.data?.align, ancestorBlocks))}>
<p className={tcls(style)}>
<Inlines {...contextProps} nodes={block.nodes} ancestorInlines={[]} />
</p>
);
@@ -18,24 +18,25 @@ export async function ReusableContent(props: BlockProps<DocumentBlockReusableCon
? context.contentContext.dataFetcher.withToken({ apiToken: block.meta.token })
: context.contentContext.dataFetcher;
// TODO: prefetch the reusable content as well
const resolved = await resolveContentRef(block.data.ref, {
...context.contentContext,
dataFetcher,
});
if (!resolved) {
if (!resolved?.reusableContent) {
return null;
}
const { reusableContent } = resolved;
if (!reusableContent || !reusableContent.revisionReusableContent.document) {
const reusableContent = resolved.reusableContent.revisionReusableContent;
if (!reusableContent.document) {
return null;
}
const document = await getDataOrNull(
dataFetcher.getDocument({
spaceId: reusableContent.space.id,
documentId: reusableContent.revisionReusableContent.document,
spaceId: resolved.reusableContent.space.id,
documentId: reusableContent.document,
})
);
@@ -47,20 +48,18 @@ 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 === reusableContent.space.id
context.contentContext.space.id === resolved.reusableContent.space.id
? context.contentContext
: {
...context.contentContext,
dataFetcher,
space: reusableContent.space,
space: resolved.reusableContent.space,
revisionId: resolved.reusableContent.revision,
// 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.
revision: {
...reusableContent.revision,
pages: [], // TODO: check with Steven
},
pages: [],
shareKey: undefined,
};
@@ -6,12 +6,10 @@ import {
import { LinkBox, LinkOverlay } from '@/components/primitives';
import { Image } from '@/components/utils';
import { resolveContentRef } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
import { type ClassValue, 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(
@@ -29,12 +27,8 @@ export async function RecordCard(
: null;
const [cover, target] = await Promise.all([
coverFile && context.contentContext
? resolveContentRef({ kind: 'file', file: coverFile }, context.contentContext)
: null,
targetRef && context.contentContext
? resolveContentRef(targetRef, context.contentContext)
: null,
coverFile ? context.getContentRef({ kind: 'file', file: coverFile }) : null,
targetRef && context.getContentRef(targetRef),
]);
const coverIsSquareOrPortrait =
@@ -144,12 +138,35 @@ 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} classNames={['RecordCardStyles']}>
<LinkBox href={target.href} className={tcls(style, 'hover:before:ring-tint-12/5')}>
<LinkOverlay
href={target.href}
insights={{
@@ -165,5 +182,5 @@ export async function RecordCard(
);
}
return <div className={tcls(RecordCardStyles)}>{body}</div>;
return <div className={tcls(style)}>{body}</div>;
}
@@ -12,7 +12,6 @@ import { StyledLink } from '@/components/primitives';
import { Image } from '@/components/utils';
import { getNodeFragmentByName } from '@/lib/document';
import { getSimplifiedContentType } from '@/lib/files';
import { resolveContentRef } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
import { filterOutNullable } from '@/lib/typescript';
@@ -22,12 +21,6 @@ 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.
*/
@@ -122,13 +115,13 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
}
const horizontalAlignment = getColumnAlignment(definition);
const horizontalClasses = alignmentMap[horizontalAlignment];
const childrenHorizontalAlignment = `[&_*]:${horizontalAlignment}`;
return (
<Blocks
tag={Tag}
document={document}
ancestorBlocks={[...props.ancestorBlocks, block]}
ancestorBlocks={[]}
nodes={fragment.nodes}
style={[
'blocks',
@@ -137,7 +130,8 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
'lg:space-y-3',
'leading-normal',
verticalAlignment,
horizontalClasses,
horizontalAlignment,
childrenHorizontalAlignment,
]}
context={context}
blockStyle={['w-full', 'max-w-[unset]']}
@@ -150,15 +144,10 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
case 'files': {
const files = await Promise.all(
(value as string[]).map((fileId) =>
context.contentContext
? resolveContentRef(
{
kind: 'file',
file: fileId,
},
context.contentContext
)
: null
context.getContentRef({
kind: 'file',
file: fileId,
})
)
);
@@ -222,13 +211,12 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
}
case 'content-ref': {
const contentRef = value ? (value as ContentRef) : null;
const resolved =
contentRef && context.contentContext
? await resolveContentRef(contentRef, context.contentContext, {
resolveAnchorText: true,
iconStyle: ['mr-2', 'text-tint-subtle'],
})
: null;
const resolved = contentRef
? await context.getContentRef(contentRef, {
resolveAnchorText: true,
iconStyle: ['mr-2', 'text-tint-subtle'],
})
: null;
return (
<Tag
className={tcls('text-base', 'text-balance', 'flex', 'items-center')}
@@ -263,9 +251,7 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
kind: 'user',
user: userId,
};
const resolved = context.contentContext
? await resolveContentRef(contentRef, context.contentContext)
: null;
const resolved = await context.getContentRef(contentRef);
if (!resolved) {
return null;
}
@@ -1,26 +0,0 @@
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,2 +1 @@
export * from './isBlockOffscreen';
export * from './textAlignment';
@@ -1,32 +0,0 @@
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);
}
}
@@ -34,6 +34,7 @@ export function FooterLinksGroup(props: {
async function FooterLink(props: { link: CustomizationContentLink; context: GitBookAnyContext }) {
const { link, context } = props;
// TODO: prefetch content ref outside of the main document
const resolved = await resolveContentRef(link.to, context);
if (!resolved) {
@@ -183,8 +183,9 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo
<div
className={tcls(
'w-full',
'overflow-x-auto',
'no-scrollbar',
'overflow-x-scroll',
'overflow-y-hidden',
'hide-scroll',
'-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'
)}
@@ -201,7 +202,7 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo
{siteSpaces.length > 1 && (
<div
id="variants"
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"
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"
>
<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 { SpacesDropdownMenuItems } from './SpacesDropdownMenuItem';
import { SpacesDropdownMenuItem } from './SpacesDropdownMenuItem';
export function SpacesDropdown(props: {
context: GitBookSiteContext;
@@ -65,15 +65,17 @@ export function SpacesDropdown(props: {
</div>
}
>
<SpacesDropdownMenuItems
slimSpaces={siteSpaces.map((space) => ({
id: space.id,
title: space.title,
url: getSiteSpaceURL(context, space),
isActive: space.id === siteSpace.id,
}))}
curPath={siteSpace.path}
/>
{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}
/>
))}
</DropdownMenu>
);
}
@@ -6,29 +6,12 @@ import { joinPath } from '@/lib/paths';
import { useCurrentPagePath } from '../hooks';
import { DropdownMenuItem } from './DropdownMenu';
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) {
function useVariantSpaceHref(variantSpaceUrl: string) {
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();
@@ -39,12 +22,11 @@ function useVariantSpaceHref(variantSpaceUrl: string, currentSpacePath: string,
}
export function SpacesDropdownMenuItem(props: {
variantSpace: VariantSpace;
variantSpace: { id: Space['id']; title: Space['title']; url: string };
active: boolean;
currentSpacePath: string;
}) {
const { variantSpace, active, currentSpacePath } = props;
const variantHref = useVariantSpaceHref(variantSpace.url, currentSpacePath, active);
const { variantSpace, active } = props;
const variantHref = useVariantSpaceHref(variantSpace.url);
return (
<DropdownMenuItem key={variantSpace.id} href={variantHref} active={active}>
@@ -52,23 +34,3 @@ 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}
/>
))}
</>
);
}
@@ -28,6 +28,7 @@ import { PageControlButtons } from './PageControlButtons';
import { PrintButton } from './PrintButton';
import './pdf.css';
import { sanitizeGitBookAppURL } from '@/lib/app';
import { resolveContentRef } from '@/lib/references';
import { getPageDocument } from '@v2/lib/data';
const DEFAULT_LIMIT = 100;
@@ -53,14 +54,14 @@ export async function PDFPage(props: {
}) {
const baseContext = props.context;
const searchParams = new URLSearchParams(props.searchParams);
const pdfParams = getPDFSearchParams(searchParams);
const pdfParams = getPDFSearchParams(new URLSearchParams(searchParams));
const customization =
'customization' in baseContext ? baseContext.customization : defaultCustomization();
const language = getSpaceLanguage(customization);
// Compute the pages to render
const { pages, total } = selectPages(baseContext.revision.pages, pdfParams);
const { pages, total } = selectPages(baseContext.pages, pdfParams);
const pageIds = pages.map(
({ page }) => [page.id, getPagePDFContainerId(page)] as [string, string]
);
@@ -244,8 +245,9 @@ async function PDFPageDocument(props: {
...context,
page,
},
//TODO: Use prefetchedRef to avoid fetching the same content multiple times
getContentRef: (ref, options) => resolveContentRef(ref, context, options),
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
@@ -1,11 +1,12 @@
import type { JSONDocument, RevisionPageDocument } from '@gitbook/api';
import type { ContentRef, JSONDocument, RevisionPageDocument } from '@gitbook/api';
import type { GitBookSiteContext } from '@v2/lib/context';
import React from 'react';
import { getSpaceLanguage } from '@/intl/server';
import { t } from '@/intl/translate';
import { hasFullWidthBlock, hasMoreThan, isNodeEmpty } from '@/lib/document';
import { hasFullWidthBlock, isNodeEmpty } from '@/lib/document';
import type { AncestorRevisionPage } from '@/lib/pages';
import type { ResolveContentRefOptions, ResolvedContentRef } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
import { DocumentView, DocumentViewSkeleton } from '../DocumentView';
import { TrackPageViewEvent } from '../Insights';
@@ -17,28 +18,21 @@ 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;
ancestors: AncestorRevisionPage[];
document: JSONDocument | null;
getContentRef: (
ref: ContentRef,
options?: ResolveContentRefOptions
) => Promise<ResolvedContentRef | null>;
withPageFeedback: boolean;
}) {
const { page, context, ancestors, document, withPageFeedback } = 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);
@@ -79,7 +73,7 @@ export function PageBody(props: {
context={{
mode: 'default',
contentContext: context,
shouldRenderLinkPreviews,
getContentRef: props.getContentRef,
}}
/>
</React.Suspense>
@@ -58,10 +58,7 @@ export async function PageBodyBlankslate(props: {
/>
);
}
const href = context.linker.toPathForPage({
pages: context.revision.pages,
page: child,
});
const href = context.linker.toPathForPage({ pages: context.pages, page: child });
return <Card key={child.id} title={child.title} leadingIcon={icon} href={href} />;
})
);
@@ -17,13 +17,11 @@ export async function PageFooterNavigation(props: {
page: RevisionPageDocument;
}) {
const { context, page } = props;
const { customization, revision, linker } = context;
const { previous, next } = resolvePrevNextPages(revision.pages, page);
const { customization, pages, linker } = context;
const { previous, next } = resolvePrevNextPages(pages, page);
const language = getSpaceLanguage(customization);
const previousHref = previous
? linker.toPathForPage({ pages: revision.pages, page: previous })
: '';
const nextHref = next ? linker.toPathForPage({ pages: revision.pages, page: next }) : '';
const previousHref = previous ? linker.toPathForPage({ pages, page: previous }) : '';
const nextHref = next ? linker.toPathForPage({ pages, page: next }) : '';
return (
<div
@@ -15,7 +15,7 @@ export async function PageHeader(props: {
ancestors: AncestorRevisionPage[];
}) {
const { context, page, ancestors } = props;
const { revision, linker } = context;
const { pages, linker } = context;
if (!page.layout.title && !page.layout.description) {
return null;
@@ -36,10 +36,7 @@ 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: revision.pages,
page: breadcrumb,
});
const href = linker.toPathForPage({ pages, page: breadcrumb });
return (
<Fragment key={breadcrumb.id}>
<li key={breadcrumb.id}>
@@ -62,6 +62,15 @@
}
@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));
}
@@ -158,111 +167,3 @@ 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;
}
@@ -5,7 +5,6 @@ import { findSiteSpaceById, getSiteStructureSections } from '@/lib/sites';
import { filterOutNullable } from '@/lib/typescript';
import { getV1BaseContext } from '@/lib/v1';
import type {
Revision,
RevisionPage,
SearchAIAnswer,
SearchAIRecommendedQuestionStream,
@@ -21,6 +20,7 @@ import { createStreamableValue } from 'ai/rsc';
import type * as React from 'react';
import { joinPathWithBaseURL } from '@/lib/paths';
import { resolveContentRef } from '@/lib/references';
import { isV2 } from '@/lib/v2';
import type { IconName } from '@gitbook/icons';
import { throwIfDataError } from '@v2/lib/data';
@@ -129,7 +129,7 @@ export async function streamAskQuestion({
{ format: 'document' }
);
const spacePromises = new Map<string, Promise<Revision>>();
const spacePromises = new Map<string, Promise<RevisionPage[]>>();
for await (const chunk of stream) {
const answer = chunk.answer;
@@ -144,9 +144,10 @@ export async function streamAskQuestion({
spacePromises.set(
source.space,
throwIfDataError(
context.dataFetcher.getRevision({
context.dataFetcher.getRevisionPages({
spaceId: source.space,
revisionId: source.revision,
metadata: false,
})
)
);
@@ -159,8 +160,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 revision = await spacePromises.get(space);
return { space, pages: revision?.pages };
const pages = await spacePromises.get(space);
return { space, pages };
})
).then((results) => {
return results.reduce((map, result) => {
@@ -345,7 +346,9 @@ async function transformAnswer(
mode: 'default',
contentContext: undefined,
wrapBlocksInSuspense: false,
shouldRenderLinkPreviews: false, // We don't want to render link previews in the AI answer.
// TODO: Use prefetched content references
getContentRef: async (ref, options) =>
resolveContentRef(ref, context, options),
}}
style={['space-y-5']}
/>

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