Compare commits

..

16 Commits

Author SHA1 Message Date
Nicolas Dorseuil 6edf3b78f0 use dataFetcher itself as the key for the weakmap 2025-06-12 14:24:11 +02:00
Samy Pessé fecdbe8dde Support dynamic expressions (#3295) 2025-06-12 12:25:28 +02:00
Nolann B. 9316ccd1f4 Update Models page styling (#3307) 2025-06-12 10:57:37 +02:00
Samy Pessé c3b620e975 Preserve current variant when navigating between sections (#3306) 2025-06-12 09:30:08 +02:00
Nolann B. 7a00880e5b Improve support for OAuth2 security type (#3304) 2025-06-11 15:34:52 +02:00
Zeno Kapitein b6b597564d Reverse order of feedback smileys (#3300) 2025-06-11 14:44:07 +02:00
spastorelli d410381957 Add docs.testgitbook.com to ADAPTIVE_CONTENT_HOSTS list (#3303) 2025-06-11 14:25:05 +02:00
conico974 902c3c6c1a apply customization for dynamic site context in v2 (#3302)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-11 14:05:30 +02:00
Zeno Kapitein 4fb2a4a6d7 Rework full-width layout, add support for full-width page option (#3293)
Co-authored-by: conico974 <nicodorseuil@yahoo.fr>
2025-06-11 14:03:45 +02:00
conico974 2d64c78787 return null for 404 cached page in the incremental cache (#3296)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-11 13:47:34 +02:00
Samy Pessé fbfcca5dae Fix rendering of font in ogimage (#3299) 2025-06-11 13:41:26 +02:00
Greg Bergé 664ae8bff6 Make heading from stepper visible in outline (#3301) 2025-06-11 13:40:59 +02:00
Samy Pessé fa3d2aae08 Fix inline expression crashing pages (#3297) 2025-06-11 12:27:24 +02:00
Zeno Kapitein e7a591d9f6 Fix border being added to cards (#3294) 2025-06-10 17:25:29 +00:00
conico974 2860beacdc revert to use withoutConcurrentExecution (#3292)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-10 19:05:07 +02:00
Zeno Kapitein 231167d3e1 Make icons for page groups more contrasting (#3286) 2025-06-10 17:48:06 +02:00
92 changed files with 2011 additions and 1130 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook-v2": patch
---
Add docs.testgitbook.com to ADAPTIVE_CONTENT_HOSTS list
+6
View File
@@ -0,0 +1,6 @@
---
'@gitbook/react-openapi': patch
'gitbook': patch
---
Improve support for OAuth2 security type
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Update Models page styling
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": minor
---
Best effort at preserving current variant when navigating between sections by matching the pathname against site spaces in the new section.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": minor
---
Rework full-width layout, add support for full-width page option
+5
View File
@@ -0,0 +1,5 @@
---
"@gitbook/fonts": minor
---
Initial version of the package
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Make icons for page groups more contrasting
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix border being added to cards
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook-v2": patch
---
apply customization for dynamic context
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Reverse order of feedback smileys
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix ogimage using incorrect Google Font depending on language.
+4 -4
View File
@@ -159,7 +159,7 @@ jobs:
runs-on: ubuntu-latest
name: Visual Testing v1
needs: deploy-v1-cloudflare
timeout-minutes: 8
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -220,7 +220,7 @@ jobs:
runs-on: ubuntu-latest
name: Visual Testing Customers v1
needs: deploy-v1-cloudflare
timeout-minutes: 8
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -240,7 +240,7 @@ jobs:
runs-on: ubuntu-latest
name: Visual Testing Customers v2
needs: deploy-v2-vercel
timeout-minutes: 8
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -261,7 +261,7 @@ jobs:
runs-on: ubuntu-latest
name: Visual Testing Customers v2 (Cloudflare)
needs: deploy-v2-cloudflare
timeout-minutes: 8
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
+92 -8
View File
@@ -26,7 +26,7 @@
"name": "@gitbook/cache-tags",
"version": "0.3.1",
"dependencies": {
"@gitbook/api": "^0.119.0",
"@gitbook/api": "catalog:",
"assert-never": "^1.2.1",
},
"devDependencies": {
@@ -47,15 +47,27 @@
"emoji-assets": "^8.0.0",
},
},
"packages/fonts": {
"name": "@gitbook/fonts",
"version": "0.0.0",
"dependencies": {
"@gitbook/api": "catalog:",
},
"devDependencies": {
"google-font-metadata": "^6.0.3",
"typescript": "^5.5.3",
},
},
"packages/gitbook": {
"name": "gitbook",
"version": "0.12.0",
"dependencies": {
"@gitbook/api": "^0.119.0",
"@gitbook/api": "catalog:",
"@gitbook/cache-do": "workspace:*",
"@gitbook/cache-tags": "workspace:*",
"@gitbook/colors": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*",
"@gitbook/fonts": "workspace:*",
"@gitbook/icons": "workspace:*",
"@gitbook/openapi-parser": "workspace:*",
"@gitbook/react-contentkit": "workspace:*",
@@ -150,7 +162,7 @@
"name": "gitbook-v2",
"version": "0.3.0",
"dependencies": {
"@gitbook/api": "^0.119.0",
"@gitbook/api": "catalog:",
"@gitbook/cache-tags": "workspace:*",
"@opennextjs/cloudflare": "1.2.1",
"@sindresorhus/fnv1a": "^3.1.0",
@@ -209,7 +221,7 @@
"name": "@gitbook/react-contentkit",
"version": "0.7.0",
"dependencies": {
"@gitbook/api": "^0.119.0",
"@gitbook/api": "catalog:",
"@gitbook/icons": "workspace:*",
"classnames": "^2.5.1",
},
@@ -267,10 +279,12 @@
},
"overrides": {
"@codemirror/state": "6.4.1",
"@gitbook/api": "^0.119.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
},
"catalog": {
"@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=="],
@@ -604,6 +618,8 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg=="],
"@evan/concurrency": ["@evan/concurrency@0.0.3", "", {}, "sha512-vjhkm2nrXoM39G4aP/U4CC5vFv/ZlMRSjbTII0N65J9R0EpgjdGswnHKS1KSjfGAp/9zKSNaojBgi0SxKnGapw=="],
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
"@floating-ui/core": ["@floating-ui/core@1.6.8", "", { "dependencies": { "@floating-ui/utils": "^0.2.8" } }, "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA=="],
@@ -632,7 +648,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.119.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-l2gfJ5+1HT3Sj1OQCh72lUOGU9LV9WbDjgKPjxIFz+NgOhSest1bJ3JhmHP66Xd4ETpmm0YQqnq4y18wKRMjBQ=="],
"@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"],
@@ -644,6 +660,8 @@
"@gitbook/fontawesome-pro": ["@gitbook/fontawesome-pro@1.0.8", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "^6.6.0" } }, "sha512-i4PgiuGyUb52Muhc52kK3aMJIMfMkA2RbPW30tre8a6M8T6mWTfYo6gafSgjNvF1vH29zcuB8oBYnF0gO4XcHA=="],
"@gitbook/fonts": ["@gitbook/fonts@workspace:packages/fonts"],
"@gitbook/icons": ["@gitbook/icons@workspace:packages/icons"],
"@gitbook/openapi-parser": ["@gitbook/openapi-parser@workspace:packages/openapi-parser"],
@@ -798,6 +816,22 @@
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@octokit/auth-token": ["@octokit/auth-token@5.1.2", "", {}, "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw=="],
"@octokit/core": ["@octokit/core@6.1.5", "", { "dependencies": { "@octokit/auth-token": "^5.0.0", "@octokit/graphql": "^8.2.2", "@octokit/request": "^9.2.3", "@octokit/request-error": "^6.1.8", "@octokit/types": "^14.0.0", "before-after-hook": "^3.0.2", "universal-user-agent": "^7.0.0" } }, "sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg=="],
"@octokit/endpoint": ["@octokit/endpoint@10.1.4", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA=="],
"@octokit/graphql": ["@octokit/graphql@8.2.2", "", { "dependencies": { "@octokit/request": "^9.2.3", "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA=="],
"@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
"@octokit/request": ["@octokit/request@9.2.3", "", { "dependencies": { "@octokit/endpoint": "^10.1.4", "@octokit/request-error": "^6.1.8", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^2.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w=="],
"@octokit/request-error": ["@octokit/request-error@6.1.8", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ=="],
"@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
"@opennextjs/aws": ["@opennextjs/aws@3.6.5", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "cookie": "^1.0.2", "esbuild": "0.25.4", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-wni+CWlRCyWfhNfekQBBPPkrDDnaGdZLN9hMybKI0wKOKTO+zhPOqR65Eh3V0pzWAi84Sureb5mdMuLwCxAAcw=="],
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.2.1", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.6.5", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.19.1" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-cOco+nHwlo/PLB1bThF8IIvaS8PgAT9MEI5ZttFO/qt6spgvr2lUaPkpjgSIQmI3sBIEG2cLUykvQ2nbbZEcVw=="],
@@ -1470,6 +1504,8 @@
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"before-after-hook": ["before-after-hook@3.0.2", "", {}, "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A=="],
"better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="],
"bignumber.js": ["bignumber.js@9.1.2", "", {}, "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug=="],
@@ -1482,6 +1518,8 @@
"body-parser": ["body-parser@2.0.2", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "3.1.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.5.2", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "^3.0.0", "type-is": "~1.6.18" } }, "sha512-SNMk0OONlQ01uk8EPeiBvTW7W4ovpL5b1O3t1sjpPgfxOQ6BqQJ6XjxinDPR79Z6HdcD5zBBwr5ssiTlgdNztQ=="],
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
"bowser": ["bowser@2.11.0", "", {}, "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="],
"boxen": ["boxen@4.2.0", "", { "dependencies": { "ansi-align": "^3.0.0", "camelcase": "^5.3.1", "chalk": "^3.0.0", "cli-boxes": "^2.2.0", "string-width": "^4.1.0", "term-size": "^2.1.0", "type-fest": "^0.8.1", "widest-line": "^3.1.0" } }, "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ=="],
@@ -1506,6 +1544,8 @@
"bytes": ["bytes@3.1.0", "", {}, "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="],
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
"cacheable": ["cacheable@1.8.9", "", { "dependencies": { "hookified": "^1.7.1", "keyv": "^5.3.1" } }, "sha512-FicwAUyWnrtnd4QqYAoRlNs44/a1jTL7XDKqm5gJ90wz1DQPlC7U2Rd1Tydpv+E7WAr4sQHuw8Q8M3nZMAyecQ=="],
"cacheable-request": ["cacheable-request@6.1.0", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^3.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^4.1.0", "responselike": "^1.0.2" } }, "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg=="],
@@ -1602,10 +1642,16 @@
"css-functions-list": ["css-functions-list@3.2.3", "", {}, "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA=="],
"css-select": ["css-select@5.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg=="],
"css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
"css-what": ["css-what@6.1.0", "", {}, "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="],
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"cssom": ["cssom@0.5.0", "", {}, "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"cva": ["cva@1.0.0-beta.2", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "typescript": ">= 4.5.5 < 6" }, "optionalPeers": ["typescript"] }, "sha512-dqcOFe247I5pKxfuzqfq3seLL5iMYsTgo40Uw7+pKZAntPgFtR7Tmy59P5IVIq/XgB0NQWoIvYDt9TwHkuK8Cg=="],
@@ -1662,6 +1708,14 @@
"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=="],
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
"dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="],
"dotenv": ["dotenv@16.4.7", "", {}, "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="],
@@ -1694,7 +1748,7 @@
"enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="],
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"env-cmd": ["env-cmd@10.1.0", "", { "dependencies": { "commander": "^4.0.0", "cross-spawn": "^7.0.0" }, "bin": { "env-cmd": "bin/env-cmd.js" } }, "sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA=="],
@@ -1800,6 +1854,8 @@
"external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="],
"fast-content-type-parse": ["fast-content-type-parse@2.0.1", "", {}, "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
@@ -1904,6 +1960,8 @@
"google-auth-library": ["google-auth-library@5.10.1", "", { "dependencies": { "arrify": "^2.0.0", "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "fast-text-encoding": "^1.0.0", "gaxios": "^2.1.0", "gcp-metadata": "^3.4.0", "gtoken": "^4.1.0", "jws": "^4.0.0", "lru-cache": "^5.0.0" } }, "sha512-rOlaok5vlpV9rSiUu5EpR0vVpc+PhN62oF4RyX/6++DG1VsaulAFEMlDYBLjJDDPI6OcNOCGAKy9UVB/3NIDXg=="],
"google-font-metadata": ["google-font-metadata@6.0.3", "", { "dependencies": { "@evan/concurrency": "^0.0.3", "@octokit/core": "^6.1.2", "cac": "^6.7.14", "consola": "^3.3.3", "deepmerge": "^4.3.1", "json-stringify-pretty-compact": "^4.0.0", "linkedom": "^0.18.6", "pathe": "^1.1.2", "picocolors": "^1.1.1", "playwright": "^1.49.1", "stylis": "^4.3.4", "zod": "^3.24.1" }, "bin": { "gfm": "dist/cli.mjs" } }, "sha512-62+QB+nmBKppHrxwvILZkV/EvKctEAdzkKBIJY26TtwZkUjeEqmAQnE5uHvtMTdB8odqGpQu5LAKPqqUYHI9wA=="],
"google-p12-pem": ["google-p12-pem@2.0.5", "", { "dependencies": { "node-forge": "^0.10.0" }, "bin": { "gp12-pem": "build/src/bin/gp12-pem.js" } }, "sha512-7RLkxwSsMsYh9wQ5Vb2zRtkAHvqPvfoMGag+nugl1noYO7gf0844Yr9TIFA5NEBMAeVt2Z+Imu7CQMp3oNatzQ=="],
"googleapis": ["googleapis@47.0.0", "", { "dependencies": { "google-auth-library": "^5.6.1", "googleapis-common": "^3.2.0" } }, "sha512-+Fnjgcc3Na/rk57dwxqW1V0HJXJFjnt3aqFlckULqAqsPkmex/AyJJe6MSlXHC37ZmlXEb9ZtPGUp5ZzRDXpHg=="],
@@ -1980,12 +2038,16 @@
"hosted-git-info": ["hosted-git-info@2.8.9", "", {}, "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="],
"html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="],
"html-tags": ["html-tags@3.3.1", "", {}, "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ=="],
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
"html-whitespace-sensitive-tag-names": ["html-whitespace-sensitive-tag-names@3.0.1", "", {}, "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA=="],
"htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="],
"http-cache-semantics": ["http-cache-semantics@4.1.1", "", {}, "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="],
"http-errors": ["http-errors@1.4.0", "", { "dependencies": { "inherits": "2.0.1", "statuses": ">= 1.2.1 < 2" } }, "sha512-oLjPqve1tuOl5aRhv8GK5eHpqP1C9fb+Ol+XTLjKfLltE44zdDbEdjPSbU7Ch5rSNsVFqZn97SrMmZLdu1/YMw=="],
@@ -2096,6 +2158,8 @@
"json-stringify-deterministic": ["json-stringify-deterministic@1.0.12", "", {}, "sha512-q3PN0lbUdv0pmurkBNdJH3pfFvOTL/Zp0lquqpvcjfKzt6Y0j49EPHAmVHCAS4Ceq/Y+PejWTzyiVpoY71+D6g=="],
"json-stringify-pretty-compact": ["json-stringify-pretty-compact@4.0.0", "", {}, "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q=="],
"json-xml-parse": ["json-xml-parse@1.3.0", "", {}, "sha512-MVosauc/3W2wL4dd4yaJzH5oXw+HOUfptn0+d4+bFghMiJFop7MaqIwFXJNLiRnNYJNQ6L4o7B+53n5wcvoLFw=="],
"jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": { "jsondiffpatch": "bin/jsondiffpatch.js" } }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="],
@@ -2134,6 +2198,8 @@
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"linkedom": ["linkedom@0.18.11", "", { "dependencies": { "css-select": "^5.1.0", "cssom": "^0.5.0", "html-escaper": "^3.0.3", "htmlparser2": "^10.0.0", "uhyphen": "^0.2.0" } }, "sha512-K03GU3FUlnhBAP0jPb7tN7YJl7LbjZx30Z8h6wgLXusnKF7+BEZvfEbdkN/lO9LfFzxN3S0ZAriDuJ/13dIsLA=="],
"locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
"lodash.castarray": ["lodash.castarray@4.4.0", "", {}, "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q=="],
@@ -2364,6 +2430,8 @@
"npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
"nuqs": ["nuqs@2.2.3", "", { "dependencies": { "mitt": "^3.0.1" }, "peerDependencies": { "@remix-run/react": ">=2", "next": ">=14.2.0", "react": ">=18.2.0 || ^19.0.0-0", "react-router-dom": ">=6" }, "optionalPeers": ["@remix-run/react", "next", "react-router-dom"] }, "sha512-nMCcUW06KSqEXA0xp+LiRqDpIE59BVYbjZLe0HUisJAlswfihHYSsAjYTzV0lcE1thfh8uh+LqUHGdQ8qq8rfA=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
@@ -2454,7 +2522,7 @@
"path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
"pcre-to-regexp": ["pcre-to-regexp@1.1.0", "", {}, "sha512-KF9XxmUQJ2DIlMj3TqNqY1AWvyvTuIuq11CuuekxyaYMiFuMKGgQrePYMX5bXKLhLG3sDI4CsGAYHPaT7VV7+g=="],
@@ -2722,6 +2790,8 @@
"stylelint": ["stylelint@16.16.0", "", { "dependencies": { "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "@csstools/media-query-list-parser": "^4.0.2", "@csstools/selector-specificity": "^5.0.0", "@dual-bundle/import-meta-resolve": "^4.1.0", "balanced-match": "^2.0.0", "colord": "^2.9.3", "cosmiconfig": "^9.0.0", "css-functions-list": "^3.2.3", "css-tree": "^3.1.0", "debug": "^4.3.7", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", "file-entry-cache": "^10.0.7", "global-modules": "^2.0.0", "globby": "^11.1.0", "globjoin": "^0.1.4", "html-tags": "^3.3.1", "ignore": "^7.0.3", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", "known-css-properties": "^0.35.0", "mathml-tag-names": "^2.1.3", "meow": "^13.2.0", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.5.3", "postcss-resolve-nested-selector": "^0.1.6", "postcss-safe-parser": "^7.0.1", "postcss-selector-parser": "^7.1.0", "postcss-value-parser": "^4.2.0", "resolve-from": "^5.0.0", "string-width": "^4.2.3", "supports-hyperlinks": "^3.2.0", "svg-tags": "^1.0.0", "table": "^6.9.0", "write-file-atomic": "^5.0.1" }, "bin": { "stylelint": "bin/stylelint.mjs" } }, "sha512-40X5UOb/0CEFnZVEHyN260HlSSUxPES+arrUphOumGWgXERHfwCD0kNBVILgQSij8iliYVwlc0V7M5bcLP9vPg=="],
"stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="],
"sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
@@ -2826,6 +2896,8 @@
"ufo": ["ufo@1.5.4", "", {}, "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ=="],
"uhyphen": ["uhyphen@0.2.0", "", {}, "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA=="],
"uid-promise": ["uid-promise@1.0.0", "", {}, "sha512-R8375j0qwXyIu/7R0tjdF06/sElHqbmdmWC9M2qQHpEVbvE4I5+38KJI7LUUmQMp7NVq4tKHiBMkT0NFM453Ig=="],
"undici": ["undici@5.28.4", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g=="],
@@ -2852,6 +2924,8 @@
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="],
"universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="],
"universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
@@ -4014,6 +4088,8 @@
"@vercel/static-config/ajv": ["ajv@8.6.3", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw=="],
"@vue/compiler-core/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"@vue/compiler-sfc/postcss": ["postcss@8.4.47", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.0", "source-map-js": "^1.2.1" } }, "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ=="],
"@vueuse/integrations/@vueuse/core": ["@vueuse/core@11.2.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.20", "@vueuse/metadata": "11.2.0", "@vueuse/shared": "11.2.0", "vue-demi": ">=0.14.10" } }, "sha512-JIUwRcOqOWzcdu1dGlfW04kaJhW3EXnnjJJfLTtddJanymTL7lF1C0+dVVZ/siLfc73mWn+cGP1PE1PKPruRSA=="],
@@ -4062,6 +4138,8 @@
"decamelize-keys/map-obj": ["map-obj@1.0.1", "", {}, "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg=="],
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"edge-runtime/async-listen": ["async-listen@3.0.1", "", {}, "sha512-cWMaNwUJnf37C/S5TfCkk/15MwbPRwVYALA2jtjkbHjCmAPiDXyNJy2q3p1KAZzDLHAWyarUWSujUoHR4pEgrA=="],
"edge-runtime/picocolors": ["picocolors@1.0.0", "", {}, "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="],
@@ -4110,6 +4188,8 @@
"google-auth-library/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"google-font-metadata/picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"googleapis-common/uuid": ["uuid@7.0.3", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg=="],
"got/get-stream": ["get-stream@4.1.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w=="],
@@ -4178,6 +4258,8 @@
"package-json/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"parse5/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"path-match/path-to-regexp": ["path-to-regexp@1.9.0", "", { "dependencies": { "isarray": "0.0.1" } }, "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g=="],
"path-scurry/lru-cache": ["lru-cache@11.0.2", "", {}, "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA=="],
@@ -4270,6 +4352,8 @@
"ts-node/arg": ["arg@4.1.0", "", {}, "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg=="],
"unenv/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"update-notifier/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="],
"url-parse-lax/prepend-http": ["prepend-http@2.0.0", "", {}, "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA=="],
+7 -3
View File
@@ -7,10 +7,9 @@
"turbo": "^2.5.0",
"vercel": "^39.3.0"
},
"packageManager": "bun@1.2.11",
"packageManager": "bun@1.2.15",
"overrides": {
"@codemirror/state": "6.4.1",
"@gitbook/api": "^0.119.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
@@ -34,7 +33,12 @@
"download:env": "op read op://gitbook-x-dev/gitbook-open/.env.local >> .env.local",
"clean": "turbo run clean"
},
"workspaces": ["packages/*"],
"workspaces": {
"packages": ["packages/*"],
"catalog": {
"@gitbook/api": "^0.120.0"
}
},
"patchedDependencies": {
"decode-named-character-reference@1.0.2": "patches/decode-named-character-reference@1.0.2.patch",
"@vercel/next@4.4.2": "patches/@vercel%2Fnext@4.4.2.patch"
+1 -1
View File
@@ -10,7 +10,7 @@
},
"version": "0.3.1",
"dependencies": {
"@gitbook/api": "^0.119.0",
"@gitbook/api": "catalog:",
"assert-never": "^1.2.1"
},
"devDependencies": {
+2
View File
@@ -0,0 +1,2 @@
dist/
src/data/*.json
+3
View File
@@ -0,0 +1,3 @@
# `@gitbook/fonts`
Utilities to lookup default fonts supported by GitBook.
+91
View File
@@ -0,0 +1,91 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { APIv2 } from 'google-font-metadata';
import { CustomizationDefaultFont } from '@gitbook/api';
import type { FontDefinitions } from '../src/types';
const googleFontsMap: { [fontName in CustomizationDefaultFont]: string } = {
[CustomizationDefaultFont.Inter]: 'inter',
[CustomizationDefaultFont.FiraSans]: 'fira-sans-extra-condensed',
[CustomizationDefaultFont.IBMPlexSerif]: 'ibm-plex-serif',
[CustomizationDefaultFont.Lato]: 'lato',
[CustomizationDefaultFont.Merriweather]: 'merriweather',
[CustomizationDefaultFont.NotoSans]: 'noto-sans',
[CustomizationDefaultFont.OpenSans]: 'open-sans',
[CustomizationDefaultFont.Overpass]: 'overpass',
[CustomizationDefaultFont.Poppins]: 'poppins',
[CustomizationDefaultFont.Raleway]: 'raleway',
[CustomizationDefaultFont.Roboto]: 'roboto',
[CustomizationDefaultFont.RobotoSlab]: 'roboto-slab',
[CustomizationDefaultFont.SourceSansPro]: 'source-sans-3',
[CustomizationDefaultFont.Ubuntu]: 'ubuntu',
[CustomizationDefaultFont.ABCFavorit]: 'inter',
};
/**
* Scripts to generate the list of all icons.
*/
async function main() {
// @ts-expect-error - we build the object
const output: FontDefinitions = {};
for (const font of Object.values(CustomizationDefaultFont)) {
const googleFontName = googleFontsMap[font];
const fontMetadata = APIv2[googleFontName.toLowerCase()];
if (!fontMetadata) {
throw new Error(`Font ${googleFontName} not found`);
}
output[font] = {
font: googleFontName,
unicodeRange: fontMetadata.unicodeRange,
variants: {
'400': {},
'700': {},
},
};
Object.keys(output[font].variants).forEach((weight) => {
const variants = fontMetadata.variants[weight];
const normalVariant = variants.normal;
if (!normalVariant) {
throw new Error(`Font ${googleFontName} has no normal variant`);
}
output[font].variants[weight] = {};
Object.entries(normalVariant).forEach(([script, url]) => {
output[font].variants[weight][script] = url.url.woff;
});
});
}
await writeDataFile('fonts', JSON.stringify(output, null, 2));
}
/**
* We write both in dist and src as the build process might have happen already
* and tsc doesn't copy the files.
*/
async function writeDataFile(name, content) {
const srcData = path.resolve(__dirname, '../src/data');
const distData = path.resolve(__dirname, '../dist/data');
// Ensure the directories exists
await Promise.all([
fs.mkdir(srcData, { recursive: true }),
fs.mkdir(distData, { recursive: true }),
]);
await Promise.all([
fs.writeFile(path.resolve(srcData, `${name}.json`), content),
fs.writeFile(path.resolve(distData, `${name}.json`), content),
]);
}
main().catch((error) => {
console.error(`Error generating icons list: ${error}`);
process.exit(1);
});
+31
View File
@@ -0,0 +1,31 @@
{
"name": "@gitbook/fonts",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"development": "./src/index.ts",
"default": "./dist/index.js"
}
},
"version": "0.0.0",
"dependencies": {
"@gitbook/api": "catalog:"
},
"devDependencies": {
"google-font-metadata": "^6.0.3",
"typescript": "^5.5.3"
},
"scripts": {
"generate": "bun ./bin/generate.js",
"build": "tsc --project tsconfig.build.json",
"typecheck": "tsc --noEmit",
"dev": "tsc -w",
"clean": "rm -rf ./dist && rm -rf ./src/data",
"unit": "bun test"
},
"files": ["dist", "src", "bin", "README.md", "CHANGELOG.md"],
"engines": {
"node": ">=20.0.0"
}
}
@@ -0,0 +1,57 @@
// Bun Snapshot v1, https://goo.gl/fbAQLP
exports[`getDefaultFont should return correct object for Latin text 1`] = `
{
"font": "Inter",
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
}
`;
exports[`getDefaultFont should return correct object for Cyrillic text 1`] = `
{
"font": "Inter",
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZthjp-Ek-_0ewmM.woff",
}
`;
exports[`getDefaultFont should return correct object for Greek text 1`] = `
{
"font": "Inter",
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZxhjp-Ek-_0ewmM.woff",
}
`;
exports[`getDefaultFont should handle mixed script text 1`] = `
{
"font": "Inter",
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZthjp-Ek-_0ewmM.woff",
}
`;
exports[`getDefaultFont should handle different font weights: regular 1`] = `
{
"font": "Inter",
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
}
`;
exports[`getDefaultFont should handle different font weights: bold 1`] = `
{
"font": "Inter",
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuFuYAZ9hjp-Ek-_0ew.woff",
}
`;
exports[`getDefaultFont should handle different fonts: inter 1`] = `
{
"font": "Inter",
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
}
`;
exports[`getDefaultFont should handle different fonts: roboto 1`] = `
{
"font": "Roboto",
"url": "https://fonts.gstatic.com/s/roboto/v32/KFOmCnqEu92Fr1Mu4mxMKTU1Kg.woff",
}
`;
+5
View File
@@ -0,0 +1,5 @@
import type { FontDefinitions } from './types';
import rawFonts from './data/fonts.json' with { type: 'json' };
export const fonts: FontDefinitions = rawFonts;
+119
View File
@@ -0,0 +1,119 @@
import { describe, expect, it } from 'bun:test';
import { CustomizationDefaultFont } from '@gitbook/api';
import { getDefaultFont } from './getDefaultFont';
describe('getDefaultFont', () => {
it('should return null for invalid font', () => {
const result = getDefaultFont({
font: 'invalid-font' as CustomizationDefaultFont,
text: 'Hello',
weight: 400,
});
expect(result).toBeNull();
});
it('should return null for invalid weight', () => {
const result = getDefaultFont({
font: CustomizationDefaultFont.Inter,
text: 'Hello',
weight: 999 as any,
});
expect(result).toBeNull();
});
it('should return null for text not supported by any script', () => {
const result = getDefaultFont({
font: CustomizationDefaultFont.Inter,
text: '😀', // Emoji not supported by Inter
weight: 400,
});
expect(result).toBeNull();
});
it('should return correct object for Latin text', () => {
const result = getDefaultFont({
font: CustomizationDefaultFont.Inter,
text: 'Hello World',
weight: 400,
});
expect(result).not.toBeNull();
expect(result?.font).toBe(CustomizationDefaultFont.Inter);
expect(result).toMatchSnapshot();
});
it('should return correct object for Cyrillic text', () => {
const result = getDefaultFont({
font: CustomizationDefaultFont.Inter,
text: 'Привет мир',
weight: 400,
});
expect(result).not.toBeNull();
expect(result?.font).toBe(CustomizationDefaultFont.Inter);
expect(result).toMatchSnapshot();
});
it('should return correct object for Greek text', () => {
const result = getDefaultFont({
font: CustomizationDefaultFont.Inter,
text: 'Γεια σας',
weight: 400,
});
expect(result).not.toBeNull();
expect(result?.font).toBe(CustomizationDefaultFont.Inter);
expect(result).toMatchSnapshot();
});
it('should handle mixed script text', () => {
const result = getDefaultFont({
font: CustomizationDefaultFont.Inter,
text: 'Hello Привет',
weight: 400,
});
expect(result).not.toBeNull();
expect(result?.font).toBe(CustomizationDefaultFont.Inter);
expect(result).toMatchSnapshot();
});
it('should handle different font weights', () => {
const regular = getDefaultFont({
font: CustomizationDefaultFont.Inter,
text: 'Hello',
weight: 400,
});
const bold = getDefaultFont({
font: CustomizationDefaultFont.Inter,
text: 'Hello',
weight: 700,
});
expect(regular).not.toBeNull();
expect(bold).not.toBeNull();
expect(regular).toMatchSnapshot('regular');
expect(bold).toMatchSnapshot('bold');
});
it('should handle empty string', () => {
const result = getDefaultFont({
font: CustomizationDefaultFont.Inter,
text: '',
weight: 400,
});
expect(result).toBeNull();
});
it('should handle different fonts', () => {
const inter = getDefaultFont({
font: CustomizationDefaultFont.Inter,
text: 'Hello',
weight: 400,
});
const roboto = getDefaultFont({
font: CustomizationDefaultFont.Roboto,
text: 'Hello',
weight: 400,
});
expect(inter).not.toBeNull();
expect(roboto).not.toBeNull();
expect(inter).toMatchSnapshot('inter');
expect(roboto).toMatchSnapshot('roboto');
});
});
+112
View File
@@ -0,0 +1,112 @@
import type { CustomizationDefaultFont } from '@gitbook/api';
import { fonts } from './fonts';
import type { FontWeight } from './types';
/**
* Get the URL to load a font for a text.
*/
export function getDefaultFont(input: {
/**
* GitBook font to use.
*/
font: CustomizationDefaultFont;
/**
* Text to display with the font.
*/
text: string;
/**
* Font weight to use.
*/
weight: FontWeight;
}): { font: string; url: string } | null {
if (!input.text.trim()) {
return null;
}
const fontDefinition = fonts[input.font];
if (!fontDefinition) {
return null;
}
const variant = fontDefinition.variants[`${input.weight}`];
if (!variant) {
return null;
}
const script = getBestUnicodeRange(input.text, fontDefinition.unicodeRange);
if (!script) {
return null;
}
return variant[script]
? {
font: input.font,
url: variant[script],
}
: null;
}
/**
* Determine which named @font-face unicode-range covers
* the greatest share of the characters in `text`.
*
* @param text The text you want to inspect.
* @param ranges An object whose keys are range names and whose
* values are CSS-style comma-separated unicode-range
* declarations (e.g. "U+0370-03FF,U+1F00-1FFF").
* @returns The key of the best-matching range, or `null`
* when nothing matches at all.
*/
function getBestUnicodeRange(text: string, ranges: Record<string, string>): string | null {
// ---------- helper: parse "U+XXXX" or "U+XXXX-YYYY" ----------
const parseOne = (token: string): [number, number] | null => {
token = token.trim().toUpperCase();
if (!token.startsWith('U+')) return null;
const body = token.slice(2); // drop "U+"
const [startHex, endHex] = body.split('-');
const start = Number.parseInt(startHex, 16);
const end = endHex ? Number.parseInt(endHex, 16) : start;
if (Number.isNaN(start) || Number.isNaN(end) || end < start) return null;
return [start, end];
};
// ---------- helper: build lookup table ----------
const parsed: Record<string, [number, number][]> = {};
for (const [label, list] of Object.entries(ranges)) {
parsed[label] = list
.split(',')
.map(parseOne)
.filter((x): x is [number, number] => x !== null);
}
// ---------- tally code-point hits ----------
const hits: Record<string, number> = Object.fromEntries(Object.keys(parsed).map((k) => [k, 0]));
for (let i = 0; i < text.length; ) {
const cp = text.codePointAt(i)!;
i += cp > 0xffff ? 2 : 1; // advance by 1 UTF-16 code-unit (or 2 for surrogates)
for (const [label, rangesArr] of Object.entries(parsed)) {
if (rangesArr.some(([lo, hi]) => cp >= lo && cp <= hi)) {
hits[label]++;
}
}
}
// ---------- choose the "best" ----------
let winner: string | null = null;
let maxCount = 0;
for (const [label, count] of Object.entries(hits)) {
if (count > maxCount) {
maxCount = count;
winner = label;
}
}
return maxCount > 0 ? winner : null;
}
+2
View File
@@ -0,0 +1,2 @@
export * from './getDefaultFont';
export * from './types';
+17
View File
@@ -0,0 +1,17 @@
import type { CustomizationDefaultFont } from '@gitbook/api';
export type FontWeight = 400 | 700;
export type FontDefinition = {
font: string;
unicodeRange: {
[script: string]: string;
};
variants: {
[weight in string]: {
[script: string]: string;
};
};
};
export type FontDefinitions = { [fontName in CustomizationDefaultFont]: FontDefinition };
+4
View File
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "src/**/*.test.ts"]
}
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"lib": ["es2023"],
"module": "ESNext",
"target": "es2022",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowJs": true,
"noEmit": false,
"declaration": true,
"outDir": "dist",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"types": [
"bun-types" // add Bun global
]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
+9
View File
@@ -0,0 +1,9 @@
{
"extends": ["//"],
"tasks": {
"generate": {
"inputs": ["bin/**/*", "package.json"],
"outputs": ["src/data/*.json", "dist/data/*.json"]
}
}
}
@@ -49,17 +49,20 @@ class GitbookIncrementalCache implements IncrementalCache {
const localCacheEntry = await localCache.match(this.getCacheUrlKey(cacheKey));
if (localCacheEntry) {
span.setAttribute('cacheHit', 'local');
return localCacheEntry.json();
const result = (await localCacheEntry.json()) as WithLastModified<
CacheValue<CacheType>
>;
return this.returnNullOn404(result);
}
const r2Object = await r2.get(cacheKey);
if (!r2Object) return null;
span.setAttribute('cacheHit', 'r2');
return {
return this.returnNullOn404({
value: await r2Object.json(),
lastModified: r2Object.uploaded.getTime(),
};
});
} catch (e) {
console.error('Failed to get from cache', e);
return null;
@@ -68,6 +71,18 @@ class GitbookIncrementalCache implements IncrementalCache {
);
}
//TODO: This is a workaround to handle 404 responses in the cache.
// It should be handled by OpenNext cache interception directly. This should be removed once OpenNext cache interception is fixed.
returnNullOn404<CacheType extends CacheEntryType = 'cache'>(
cacheEntry: WithLastModified<CacheValue<CacheType>> | null
): WithLastModified<CacheValue<CacheType>> | null {
if (!cacheEntry?.value) return null;
if ('meta' in cacheEntry.value && cacheEntry.value.meta?.status === 404) {
return null;
}
return cacheEntry;
}
async set<CacheType extends CacheEntryType = 'cache'>(
key: string,
value: CacheValue<CacheType>,
@@ -1,101 +1,46 @@
import { trace } from '@/lib/tracing';
import type { DurableObjectLocationHint } from '@cloudflare/workers-types';
import { generateShardId } from '@opennextjs/aws/core/routing/queue.js';
import type { NextModeTagCache } from '@opennextjs/aws/types/overrides.js';
import { getCloudflareContext } from '@opennextjs/cloudflare';
import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache';
import { softTagFilter } from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter';
export const DEFAULT_REGION = 'enam';
//TODO: We may not need all these regions.
export const AVAILABLE_REGIONS = [
'enam',
'weur',
'apac',
'sam',
'afr',
'oc',
] as DurableObjectLocationHint[];
const originalTagCache = doShardedTagCache({
baseShardSize: 12,
regionalCache: true,
// We can probably increase this value even further
regionalCacheTtlSec: 60,
shardReplication: {
numberOfSoftReplicas: 2,
numberOfHardReplicas: 1,
regionalReplication: {
defaultRegion: 'enam',
},
},
});
/**
* - `change-request` are only used by preview urls. One DO per region and space
* - `internal` are used for the release tag and integrations which does not depend on the user
* - `global` are used for url and site tag
* - `space` are only used the space tag (i.e. when a CR is merged)
* - `organization` are used for translation and openAPI. One DO per region and organization
*/
type TagType = 'change-request' | 'internal' | 'global' | 'space' | 'organization';
interface DOIdOptions {
baseShardId: string;
numberOfReplicas: number;
shardType: TagType;
replicaId?: number;
region?: DurableObjectLocationHint;
tag: string;
}
interface CacheTagKeyOptions {
doId: DOId;
tags: string[];
}
export class DOId {
options;
shardId;
replicaId;
region;
constructor(options: DOIdOptions) {
this.options = options;
const { baseShardId, shardType, numberOfReplicas, replicaId, region } = options;
this.shardId = `tag-${shardType};${baseShardId}`;
this.replicaId = replicaId ?? this.generateRandomNumberBetween(1, numberOfReplicas);
this.region = region;
}
generateRandomNumberBetween(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
get key() {
// For those 2 type of tags, we likely don't need any kind of replication or even sharding
// We may reconsider it for `change-request` at some point
if (this.options.shardType === 'organization') {
const organization = this.options.tag.split(':')[1];
return `${this.shardId};region-${this.region};org:${organization}`;
export default {
name: 'GitbookTagCache',
mode: 'nextMode',
getLastRevalidated: async (tags: string[]) => {
const tagsToCheck = tags.filter(softTagFilter);
if (tagsToCheck.length === 0) {
// If we reach here, it probably means that there is an issue that we'll need to address.
console.warn(
'getLastRevalidated - No valid tags to check for last revalidation, original tags:',
tags
);
return 0; // If no tags to check, return 0
}
if (this.options.shardType === 'change-request') {
const space = this.options.tag.split(':')[1];
return `${this.shardId};region-${this.region};space:${space}`;
}
return `${this.shardId};replica-${this.replicaId}${this.region ? `;region-${this.region}` : ''}`;
}
}
class GitbookTagCache implements NextModeTagCache {
localCache: Cache | undefined;
maxWriteRetries = 3;
constructor(
private config: {
numberOfShards: Record<TagType, number>;
numberOfReplicas: Record<TagType, number>;
ttl: Record<TagType, number>;
}
) {}
name = 'GitbookTagCache';
mode = 'nextMode' as const;
// This one is only used for soft tags, no need to do anything for us
async getLastRevalidated(_tags: string[]) {
return 0;
}
getDurableObjectStub(doId: DOId) {
const durableObject = getCloudflareContext().env.NEXT_TAG_CACHE_DO_SHARDED;
if (!durableObject) throw new Error('No durable object binding for cache revalidation');
const id = durableObject.idFromName(doId.key);
return durableObject.get(id, { locationHint: doId.region });
}
async hasBeenRevalidated(tags: string[], lastModified?: number) {
return trace(
{
operation: 'gitbookTagCacheGetLastRevalidated',
name: tagsToCheck.join(', '),
},
async () => {
return await originalTagCache.getLastRevalidated(tagsToCheck);
}
);
},
hasBeenRevalidated: async (tags: string[], lastModified?: number) => {
const tagsToCheck = tags.filter(softTagFilter);
if (tagsToCheck.length === 0) {
// If we reach here, it probably means that there is an issue that we'll need to address.
@@ -111,38 +56,12 @@ class GitbookTagCache implements NextModeTagCache {
name: tagsToCheck.join(', '),
},
async () => {
const shardedTagGroups = this.groupTagsByDO({ tags });
const shardedTagRevalidationOutcomes = await Promise.all(
shardedTagGroups.map(async ({ doId, tags }) => {
const cachedValue = await this.getFromRegionalCache({
doId,
tags,
});
if (cachedValue) {
return (await cachedValue.text()) === 'true';
}
const stub = this.getDurableObjectStub(doId);
const _hasBeenRevalidated = await stub.hasBeenRevalidated(
tags,
lastModified
);
//TODO: Do we want to cache the result if it has been revalidated ?
// If we do so, we risk causing cache MISS even though it has been revalidated elsewhere
// On the other hand revalidating a tag that is used in a lot of places will cause a lot of requests
if (!_hasBeenRevalidated) {
getCloudflareContext().ctx.waitUntil(
this.putToRegionalCache({ doId, tags }, _hasBeenRevalidated)
);
}
return _hasBeenRevalidated;
})
);
return shardedTagRevalidationOutcomes.some((result) => result);
const result = await originalTagCache.hasBeenRevalidated(tagsToCheck, lastModified);
return result;
}
);
}
async writeTags(tags: string[]) {
},
writeTags: async (tags: string[]) => {
return trace(
{
operation: 'gitbookTagCacheWriteTags',
@@ -155,250 +74,8 @@ class GitbookTagCache implements NextModeTagCache {
return; // If no tags to write, exit early
}
// Write only the filtered tags
const shardedTagGroups = this.groupTagsByDO({
tags: tagsToWrite,
});
// We want to use the same revalidation time for all tags
const currentTime = Date.now();
await Promise.all(
shardedTagGroups.map(async ({ doId, tags }) => {
await this.performWriteTagsWithRetry(doId, tags, currentTime);
})
);
await originalTagCache.writeTags(tagsToWrite);
}
);
}
async performWriteTagsWithRetry(
doId: DOId,
tags: string[],
lastModified: number,
retryNumber = 0
) {
try {
const stub = this.getDurableObjectStub(doId);
await stub.writeTags(tags, lastModified);
// Depending on the shards and the tags, deleting from the regional cache will not work for every tag
// We also need to delete both cache
await Promise.all([this.deleteRegionalCache({ doId, tags })]);
} catch (e) {
if (retryNumber >= this.maxWriteRetries) {
console.error('Error while writing tags, too many retries', e);
// Do we want to throw an error here ?
await getCloudflareContext().env.NEXT_TAG_CACHE_DO_SHARDED_DLQ?.send({
failingShardId: doId.key,
failingTags: tags,
lastModified,
});
return;
}
await this.performWriteTagsWithRetry(doId, tags, lastModified, retryNumber + 1);
}
}
/**
* Same tags are guaranteed to be in the same shard
* @param tags
* @returns An array of DO ids and tags
*/
groupTagsByDO({ tags }: { tags: string[] }) {
const crTags = this.generateDOIdArray({ tags, shardType: 'change-request' });
const spaceTags = this.generateDOIdArray({ tags, shardType: 'space' });
const internalTags = this.generateDOIdArray({ tags, shardType: 'internal' });
const globalTags = this.generateDOIdArray({ tags, shardType: 'global' });
const orgTags = this.generateDOIdArray({ tags, shardType: 'organization' });
const tagIdCollection = [
...crTags,
...spaceTags,
...internalTags,
...globalTags,
...orgTags,
];
// We then group the tags by DO id
const tagsByDOId = new Map<string, { doId: DOId; tags: string[] }>();
for (const { doId, tag } of tagIdCollection) {
const doIdString = doId.key;
const tagsArray = tagsByDOId.get(doIdString)?.tags ?? [];
tagsArray.push(tag);
tagsByDOId.set(doIdString, {
// We override the doId here, but it should be the same for all tags
doId,
tags: tagsArray,
});
}
const result = Array.from(tagsByDOId.values());
return result;
}
/**
* Generates a list of DO ids for the shards and replicas
* @param tags The tags to generate shards for
* @param shardType The type of the shard (change-request, internal, global, space)
* @returns An array of TagCacheDOId and tag
*/
generateDOIdArray({
tags,
shardType,
}: {
tags: string[];
shardType: TagType;
}) {
const numReplicas = this.config.numberOfReplicas[shardType];
const replicaIndexes = Array.from({ length: numReplicas }, (_, i) => i + 1);
const regionalReplicas = replicaIndexes.flatMap((replicaId) => {
return tags
.filter((tag) => this.getTagType(tag) === shardType)
.map((tag) => {
return {
doId: new DOId({
baseShardId: generateShardId(
tag,
this.config.numberOfShards[shardType],
'shard'
),
numberOfReplicas: numReplicas,
shardType,
replicaId,
tag,
}),
tag,
};
});
});
// If we have regional replication enabled, we need to further duplicate the shards in all the regions
const regionalReplicasInAllRegions = regionalReplicas.flatMap(({ doId, tag }) => {
return AVAILABLE_REGIONS.map((region) => {
return {
doId: new DOId({
baseShardId: doId.options.baseShardId,
numberOfReplicas: numReplicas,
shardType,
replicaId: doId.replicaId,
region,
tag,
}),
tag,
};
});
});
return regionalReplicasInAllRegions;
}
getTagType(tag: string): TagType {
if (tag.includes('change-request:')) {
return 'change-request';
}
if (tag.startsWith('release:') || tag.startsWith('integration:')) {
// Maybe use platform instead of internal ??
return 'internal';
}
if (tag.startsWith('site:') || tag.startsWith('url:')) {
// TODO: find a better name here.
// BTW do we want to append something like the organization to these cache tag
return 'global';
}
if (tag.startsWith('organization:')) {
return 'organization';
}
return 'space';
}
/**
*
* @returns The closest region to the user, if not found we default to "enam"
*/
getClosestRegion() {
const continent = getCloudflareContext().cf?.continent;
if (!continent) return 'enam';
switch (continent) {
case 'AF':
return 'afr';
case 'AS':
return 'apac';
case 'EU':
return 'weur';
case 'NA':
return 'enam';
case 'OC':
return 'oc';
case 'SA':
return 'sam';
default:
return 'enam';
}
}
// Cache API
async getCacheInstance() {
if (!this.localCache) {
this.localCache = await caches.open('sharded-do-tag-cache');
}
return this.localCache;
}
getCacheUrlKey(opts: CacheTagKeyOptions) {
const { doId, tags } = opts;
return `http://local.cache/shard/${doId.shardId}?tags=${encodeURIComponent(tags.join(';'))}`;
}
async getFromRegionalCache(opts: CacheTagKeyOptions) {
try {
const cache = await this.getCacheInstance();
if (!cache) return;
return cache.match(this.getCacheUrlKey(opts));
} catch (e) {
console.error('Error while fetching from regional cache', e);
}
}
async putToRegionalCache(optsKey: CacheTagKeyOptions, value: boolean) {
const cache = await this.getCacheInstance();
if (!cache) return;
const tags = optsKey.tags;
await cache.put(
this.getCacheUrlKey(optsKey),
new Response(`${value}`, {
headers: {
'cache-control': `max-age=${this.config.ttl[optsKey.doId.options.shardType] ?? 5}`,
...(tags.length > 0
? {
'cache-tag': tags.join(','),
}
: {}),
},
})
);
}
async deleteRegionalCache(optsKey: CacheTagKeyOptions) {
// We never want to crash because of the cache
try {
const cache = await this.getCacheInstance();
if (!cache) return;
await cache.delete(this.getCacheUrlKey(optsKey));
} catch (e) {
console.warn(e);
}
}
}
export default new GitbookTagCache({
numberOfShards: {
'change-request': 1,
global: 10,
internal: 10,
space: 10,
organization: 1,
},
numberOfReplicas: {
'change-request': 1,
global: 2,
internal: 2,
space: 1,
organization: 1,
},
ttl: {
'change-request': 60,
global: 3600,
internal: 86400,
space: 3600,
organization: 86400,
},
});
} satisfies NextModeTagCache;
+1 -1
View File
@@ -3,7 +3,7 @@
"version": "0.3.0",
"private": true,
"dependencies": {
"@gitbook/api": "^0.119.0",
"@gitbook/api": "catalog:",
"@gitbook/cache-tags": "workspace:*",
"@opennextjs/cloudflare": "1.2.1",
"@sindresorhus/fnv1a": "^3.1.0",
+3
View File
@@ -1,4 +1,5 @@
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 { jwtDecode } from 'jwt-decode';
@@ -67,6 +68,8 @@ export async function getDynamicSiteContext(params: RouteLayoutParams) {
siteURLData
);
context.customization = await getDynamicCustomizationSettings(context.customization);
return {
context,
visitorAuthClaims: getVisitorAuthClaims(siteURLData),
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,7 +1,7 @@
export * from './api';
export * from './types';
export * from './pages';
export * from './urls';
export * from './errors';
export * from './lookup';
export * from './visitor';
export * from './pages';
@@ -0,0 +1,89 @@
import type { GitBookDataFetcher } from './types';
/**
* Wrap a function by preventing concurrent executions of the same function.
* With a logic to work per-request in Cloudflare Workers.
*/
export function withoutConcurrentExecution<ArgsType extends any[], ReturnType>(
dataFetcherObject: GitBookDataFetcher,
wrapped: (key: string, ...args: ArgsType) => Promise<ReturnType>
): (cacheKey: string, ...args: ArgsType) => Promise<ReturnType> {
const globalPromiseCache = new WeakMap<object, Map<string, Promise<ReturnType>>>();
return (key: string, ...args: ArgsType) => {
const globalContext = dataFetcherObject;
/**
* Cache storage that is scoped to the current request when executed in Cloudflare Workers,
* to avoid "Cannot perform I/O on behalf of a different request" errors.
*/
const promiseCache =
globalPromiseCache.get(globalContext) ?? new Map<string, Promise<ReturnType>>();
globalPromiseCache.set(globalContext, promiseCache);
const concurrent = promiseCache.get(key);
if (concurrent) {
return concurrent;
}
const promise = (async () => {
try {
const result = await wrapped(key, ...args);
return result;
} finally {
promiseCache.delete(key);
}
})();
promiseCache.set(key, promise);
return promise;
};
}
/**
* Wrap a function by passing it a cache key that is computed from the function arguments.
*/
export function withCacheKey<ArgsType extends any[], ReturnType>(
wrapped: (cacheKey: string, ...args: ArgsType) => Promise<ReturnType>
): (...args: ArgsType) => Promise<ReturnType> {
return (...args: ArgsType) => {
const cacheKey = getCacheKey(args);
return wrapped(cacheKey, ...args);
};
}
/**
* Compute a cache key from the function arguments.
*/
function getCacheKey(args: any[]) {
return JSON.stringify(deepSortValue(args));
}
function deepSortValue(value: unknown): unknown {
if (
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean' ||
value === null ||
value === undefined
) {
return value;
}
if (Array.isArray(value)) {
return value.map(deepSortValue);
}
if (value && typeof value === 'object') {
return Object.entries(value)
.map(([key, subValue]) => {
return [key, deepSortValue(subValue)] as const;
})
.sort((a, b) => {
return a[0].localeCompare(b[0]);
});
}
return value;
}
+44 -4
View File
@@ -1,15 +1,30 @@
import type { JSONDocument, RevisionPageDocument, Space } from '@gitbook/api';
import { waitUntil } from '@/lib/waitUntil';
import type { JSONDocument, RevisionPageDocument } from '@gitbook/api';
import type { GitBookSiteContext, GitBookSpaceContext } from '../context';
import { getDataOrNull } from './errors';
import type { GitBookDataFetcher } from './types';
/**
* Get the document for a page.
*/
export async function getPageDocument(
dataFetcher: GitBookDataFetcher,
space: Space,
context: GitBookSpaceContext | GitBookSiteContext,
page: RevisionPageDocument
): Promise<JSONDocument | null> {
const { dataFetcher, space } = context;
if (
'site' in context &&
(context.site.id === 'site_JOVzv' || context.site.id === 'site_IxAYj')
) {
return getDataOrNull(
dataFetcher.getRevisionPageDocument({
spaceId: space.id,
revisionId: space.revision,
pageId: page.id,
})
);
}
if (page.documentId) {
return getDataOrNull(
dataFetcher.getDocument({ spaceId: space.id, documentId: page.documentId })
@@ -26,5 +41,30 @@ export async function getPageDocument(
);
}
// Pre-fetch the document to start filling the cache before we migrate to this API.
if (isInPercentRollout(space.id, 10)) {
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;
}
@@ -106,6 +106,15 @@ export interface GitBookDataFetcher {
pageId: string;
}): Promise<DataFetcherResponse<string>>;
/**
* Get the document of a page by its path.
*/
getRevisionPageDocument(params: {
spaceId: string;
revisionId: string;
pageId: string;
}): Promise<DataFetcherResponse<api.JSONDocument>>;
/**
* Get a document by its space ID and document ID.
*/
+2
View File
@@ -41,6 +41,8 @@ const ADAPTIVE_CONTENT_HOSTS = [
'docs.gitbook.com',
'adaptive-docs.gitbook-staging.com',
'enriched-content-playground.gitbook-staging.io',
'docs.testgitbook.com',
'launchdarkly-site.gitbook.education',
];
export async function middleware(request: NextRequest) {
+5 -5
View File
@@ -204,11 +204,11 @@ const testCases: TestsCase[] = [
contentBaseURL: 'https://sosovalue-white-paper.gitbook.io',
tests: [{ name: 'Home', url: '/' }],
},
{
name: 'docs.revrobotics.com',
contentBaseURL: 'https://docs.revrobotics.com',
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
},
// {
// name: 'docs.revrobotics.com',
// contentBaseURL: 'https://docs.revrobotics.com',
// tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
// },
{
name: 'chartschool.stockcharts.com',
contentBaseURL: 'https://chartschool.stockcharts.com',
+3 -2
View File
@@ -10,18 +10,19 @@
"typecheck": "tsc --noEmit",
"e2e": "playwright test e2e/internal.spec.ts",
"e2e-customers": "playwright test e2e/customers.spec.ts",
"unit": "bun test {src,packages}",
"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",
"copy:icons": "gitbook-icons ./public/~gitbook/static/icons",
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math"
},
"dependencies": {
"@gitbook/api": "^0.119.0",
"@gitbook/api": "catalog:",
"@gitbook/cache-do": "workspace:*",
"@gitbook/cache-tags": "workspace:*",
"@gitbook/colors": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*",
"@gitbook/icons": "workspace:*",
"@gitbook/fonts": "workspace:*",
"@gitbook/openapi-parser": "workspace:*",
"@gitbook/react-contentkit": "workspace:*",
"@gitbook/react-math": "workspace:*",
@@ -10,6 +10,7 @@ import {
} from '@/components/primitives';
import type { ClassValue } from '@/lib/tailwind';
import { nullIfNever } from '@/lib/typescript';
import { BlockContentRef } from './BlockContentRef';
import { CodeBlock } from './CodeBlock';
import { Columns } from './Columns';
@@ -45,13 +46,6 @@ export interface BlockProps<Block extends DocumentBlock> extends DocumentContext
style?: ClassValue;
}
/**
* Alternative to `assertNever` that returns `null` instead of throwing an error.
*/
function nullIfNever(_value: never): null {
return null;
}
export function Block<T extends DocumentBlock>(props: BlockProps<T>) {
const { block, style, isEstimatedOffscreen, context } = props;
@@ -70,8 +70,8 @@ export function UnwrappedBlocks<TBlock extends DocumentBlock>(props: UnwrappedBl
style={[
'mx-auto w-full decoration-primary/6',
node.data && 'fullWidth' in node.data && node.data.fullWidth
? 'max-w-screen-xl'
: 'max-w-3xl',
? 'max-w-screen-2xl'
: 'page-full-width:ml-0 max-w-3xl',
blockStyle,
]}
isEstimatedOffscreen={isOffscreen}
@@ -19,7 +19,7 @@ export function Columns(props: BlockProps<DocumentBlockColumns>) {
ancestorBlocks={[...ancestorBlocks, block, columnBlock]}
context={context}
blockStyle="flip-heading-hash"
style="w-full space-y-4"
style="w-full space-y-4 *:max-w-full"
/>
</Column>
);
@@ -7,5 +7,5 @@ import type { BlockProps } from './Block';
export function Divider(props: BlockProps<DocumentBlockDivider>) {
const { style } = props;
return <hr className={tcls(style, 'border-tint-subtle')} />;
return <hr className={tcls(style, 'page-full-width:max-w-full border-tint-subtle')} />;
}
@@ -1,16 +1,6 @@
import type {
DocumentInline,
DocumentInlineAnnotation,
DocumentInlineButton,
DocumentInlineEmoji,
DocumentInlineImage,
DocumentInlineLink,
DocumentInlineMath,
DocumentInlineMention,
JSONDocument,
} from '@gitbook/api';
import assertNever from 'assert-never';
import type { DocumentInline, JSONDocument } from '@gitbook/api';
import { nullIfNever } from '@/lib/typescript';
import { Annotation } from './Annotation/Annotation';
import type { DocumentContextProps } from './DocumentView';
import { Emoji } from './Emoji';
@@ -39,16 +29,7 @@ export interface InlineProps<T extends DocumentInline> extends DocumentContextPr
children?: React.ReactNode;
}
export function Inline<
T extends
| DocumentInlineImage
| DocumentInlineAnnotation
| DocumentInlineEmoji
| DocumentInlineLink
| DocumentInlineMath
| DocumentInlineMention
| DocumentInlineButton,
>(props: InlineProps<T>) {
export function Inline<T extends DocumentInline>(props: InlineProps<T>) {
const { inline, ...contextProps } = props;
switch (inline.type) {
@@ -66,7 +47,11 @@ export function Inline<
return <InlineImage {...contextProps} inline={inline} />;
case 'button':
return <InlineButton {...contextProps} inline={inline} />;
case 'expression':
// The GitBook API should take care of evaluating expressions.
// We should never need to render them.
return null;
default:
assertNever(inline);
return nullIfNever(inline);
}
}
@@ -12,7 +12,8 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
const resolved = context.contentContext
? await resolveContentRef(inline.data.ref, context.contentContext, {
resolveAnchorText: true,
// 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;
@@ -6,7 +6,7 @@
}
.openapi-schemas {
@apply flex flex-col mb-14 flex-1;
@apply flex flex-col mb-14 gap-0 flex-1;
}
.openapi-schemas-title {
@@ -310,6 +310,26 @@
@apply py-2 border-b border-tint-subtle max-w-full flex-1;
}
.openapi-securities-oauth-flows {
@apply flex flex-col gap-2 divide-y divide-tint-subtle;
}
.openapi-securities-oauth-content {
@apply prose *:!prose-sm *:text-tint;
}
.openapi-securities-oauth-content.openapi-markdown code {
@apply text-xs;
}
.openapi-securities-oauth-content ul {
@apply !my-0;
}
.openapi-securities-url {
@apply ml-0.5 px-0.5 rounded hover:bg-tint transition-colors;
}
.openapi-securities-body {
@apply flex flex-col gap-2;
}
@@ -728,8 +748,12 @@ body:has(.openapi-select-popover) {
}
/* Disclosure */
.openapi-schemas-disclosure {
@apply border-t border-x last:border-b border-tint-subtle !ring-0 first:!rounded-t-xl last:!rounded-b-xl !rounded-none;
}
.openapi-schemas-disclosure > .openapi-disclosure-trigger {
@apply flex items-center font-mono transition-all text-tint-strong !text-sm hover:bg-tint-subtle relative flex-1 gap-2.5 p-3 truncate -outline-offset-1;
@apply flex items-center font-mono transition-all font-normal text-tint-strong !text-sm hover:bg-tint-subtle relative flex-1 gap-2.5 p-5 truncate -outline-offset-1;
}
.openapi-schemas-disclosure > .openapi-disclosure-trigger,
@@ -773,7 +797,6 @@ body:has(.openapi-select-popover) {
@apply bg-tint-subtle overflow-hidden;
}
.openapi-disclosure:has(> .openapi-disclosure-trigger:hover),
.openapi-disclosure[data-expanded="true"] {
@apply ring-1 shadow-sm;
}
@@ -36,12 +36,16 @@ export function Footer(props: { context: GitBookSiteContext }) {
<div className={tcls(CONTAINER_STYLE, 'px-4', 'py-8', 'lg:py-12', 'mx-auto')}>
<div
className={tcls(
'mx-auto grid max-w-3xl justify-between gap-12 lg:max-w-none',
'lg:!max-w-none mx-auto grid max-w-3xl site-full-width:max-w-screen-2xl justify-between gap-12',
'grid-cols-[auto_auto]',
'lg:grid-cols-[18rem_minmax(auto,_48rem)_auto]',
'xl:grid-cols-[18rem_minmax(auto,_48rem)_14rem]',
'site-full-width:lg:grid-cols-[18rem_minmax(auto,_80rem)_auto]',
'site-full-width:xl:grid-cols-[18rem_minmax(auto,_80rem)_14rem]',
'page-no-toc:lg:grid-cols-[minmax(auto,_48rem)_auto]',
'page-no-toc:xl:grid-cols-[14rem_minmax(auto,_48rem)_14rem]'
'page-no-toc:xl:grid-cols-[14rem_minmax(auto,_48rem)_14rem]',
'[body:has(.site-full-width,.page-no-toc)_&]:lg:grid-cols-[minmax(auto,_90rem)_auto]',
'[body:has(.site-full-width,.page-no-toc)_&]:xl:grid-cols-[14rem_minmax(auto,_90rem)_14rem]'
)}
>
{
@@ -102,7 +106,7 @@ export function Footer(props: { context: GitBookSiteContext }) {
'col-span-2 page-has-toc:lg:col-span-1 page-has-toc:lg:col-start-2 page-no-toc:xl:col-span-1 page-no-toc:xl:col-start-2'
)}
>
<div className="mx-auto flex max-w-3xl flex-col gap-10 sm:flex-row sm:gap-6">
<div className="mx-auto flex max-w-3xl site-full-width:max-w-screen-2xl flex-col gap-10 sm:flex-row sm:gap-6">
{partition(customization.footer.groups, FOOTER_COLUMNS).map(
(column, columnIndex) => (
<div
@@ -104,11 +104,9 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo
'lg:basis-40',
'md:max-w-[40%]',
'lg:max-w-lg',
'lg:ml-[max(calc((100%-18rem-48rem-3rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem) - margin (3rem)
'lg:ml-[max(calc((100%-18rem-48rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem)
'xl:ml-[max(calc((100%-18rem-48rem-14rem-3rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem) - outline (14rem) - margin (3rem)
'page-no-toc:lg:ml-[max(calc((100%-18rem-48rem-18rem-3rem)/2),0rem)]',
'page-full-width:lg:ml-[max(calc((100%-18rem-103rem-3rem)/2),1.5rem)]',
'page-full-width:2xl:ml-[max(calc((100%-18rem-96rem-14rem+3rem)/2),1.5rem)]',
'md:mr-auto',
'order-last',
'md:order-[unset]',
@@ -195,7 +193,6 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo
<div
className={tcls(
CONTAINER_STYLE,
'page-default-width:max-w-[unset]',
'grow',
'flex',
'items-end',
@@ -1,8 +1,7 @@
import type { SiteSpace } from '@gitbook/api';
import { getSiteSpaceURL } from '@/lib/sites';
import { tcls } from '@/lib/tailwind';
import { joinPath } from '@/lib/paths';
import type { GitBookSiteContext } from '@v2/lib/context';
import { DropdownChevron, DropdownMenu } from './DropdownMenu';
import { SpacesDropdownMenuItem } from './SpacesDropdownMenuItem';
@@ -14,7 +13,6 @@ export function SpacesDropdown(props: {
className?: string;
}) {
const { context, siteSpace, siteSpaces, className } = props;
const { linker } = context;
return (
<DropdownMenu
@@ -73,9 +71,7 @@ export function SpacesDropdown(props: {
variantSpace={{
id: otherSiteSpace.id,
title: otherSiteSpace.title,
url: otherSiteSpace.urls.published
? linker.toLinkForContent(otherSiteSpace.urls.published)
: getFallbackSiteSpaceURL(otherSiteSpace, context),
url: getSiteSpaceURL(context, otherSiteSpace),
}}
active={otherSiteSpace.id === siteSpace.id}
/>
@@ -83,14 +79,3 @@ export function SpacesDropdown(props: {
</DropdownMenu>
);
}
/**
* When the site is not published yet, `urls.published` is not available.
* To ensure navigation works in preview, we compute a relative URL from the siteSpace path.
*/
function getFallbackSiteSpaceURL(siteSpace: SiteSpace, context: GitBookSiteContext) {
const { linker, sections } = context;
return linker.toPathInSite(
sections?.current ? joinPath(sections.current.path, siteSpace.path) : siteSpace.path
);
}
@@ -9,7 +9,6 @@ import {
} from '@gitbook/api';
import { Icon } from '@gitbook/icons';
import type { GitBookSiteContext, GitBookSpaceContext } from '@v2/lib/context';
import { getPageDocument } from '@v2/lib/data';
import type { GitBookLinker } from '@v2/lib/links';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
@@ -29,6 +28,7 @@ import { PageControlButtons } from './PageControlButtons';
import { PrintButton } from './PrintButton';
import './pdf.css';
import { sanitizeGitBookAppURL } from '@/lib/app';
import { getPageDocument } from '@v2/lib/data';
const DEFAULT_LIMIT = 100;
@@ -224,8 +224,7 @@ async function PDFPageDocument(props: {
context: GitBookSpaceContext;
}) {
const { page, context } = props;
const { space } = context;
const document = await getPageDocument(context.dataFetcher, space, page);
const document = await getPageDocument(context, page);
return (
<PrintPage id={getPagePDFContainerId(page)}>
@@ -48,11 +48,9 @@ export function PageAside(props: {
'group/aside',
'hidden',
'xl:flex',
// 'page-no-toc:lg:flex',
'flex-col',
'basis-56',
// 'page-no-toc:basis-40',
// 'page-no-toc:xl:basis-56',
'xl:ml-12',
'grow-0',
'shrink-0',
'break-anywhere', // To prevent long words in headings from breaking the layout
@@ -27,7 +27,9 @@ export function PageBody(props: {
const { page, context, ancestors, document, withPageFeedback } = props;
const { customization } = context;
const asFullWidth = document ? hasFullWidthBlock(document) : false;
const contentFullWidth = document ? hasFullWidthBlock(document) : false;
const pageFullWidth = page.id === 'wtthNFMqmEQmnt5LKR0q';
const asFullWidth = pageFullWidth || contentFullWidth;
const language = getSpaceLanguage(customization);
const updatedAt = page.updatedAt ?? page.createdAt;
@@ -36,15 +38,11 @@ export function PageBody(props: {
<main
className={tcls(
'relative min-w-0 flex-1',
'py-8 lg:px-12',
'mx-auto max-w-screen-2xl py-8',
// Allow words to break if they are too long.
'break-anywhere',
// When in api page mode without the aside, we align with the border of the main content
'page-api-block:xl:max-2xl:pr-0',
// Max size to ensure one column in api is aligned with rest of content (2 x 3xl) + (gap-3 + 2) * px-12
'page-api-block:mx-auto page-api-block:max-w-screen-2xl',
// page.layout.tableOfContents ? null : 'xl:ml-56',
asFullWidth ? 'page-full-width' : 'page-default-width',
pageFullWidth ? 'page-full-width 2xl:px-8' : 'page-default-width',
asFullWidth ? 'site-full-width' : 'site-default-width',
page.layout.tableOfContents ? 'page-has-toc' : 'page-no-toc'
)}
>
@@ -81,7 +79,7 @@ export function PageBody(props: {
<PageFooterNavigation context={context} page={page} />
) : null}
<div className="mx-auto mt-6 page-api-block:ml-0 flex max-w-3xl flex-row flex-wrap items-center gap-4 text-tint contrast-more:text-tint-strong">
<div className="mx-auto mt-6 page-api-block:ml-0 flex max-w-3xl page-full-width:max-w-screen-2xl flex-row flex-wrap items-center gap-4 text-tint contrast-more:text-tint-strong">
{updatedAt ? (
<p className="mr-auto text-sm">
{t(language, 'page_last_modified', <DateRelative value={updatedAt} />)}
@@ -34,14 +34,20 @@ export async function PageCover(props: {
? [
'sm:-mx-6',
'md:-mx-8',
'-lg:mr-8',
'lg:ml-0',
'lg:-mr-8',
'lg:-ml-12',
!page.layout.tableOfContents &&
context.customization.header.preset !== 'none'
? 'xl:-ml-64'
? 'xl:-ml-[19rem]'
: null,
]
: ['sm:mx-auto', 'max-w-3xl', 'sm:rounded-md', 'mb-8']
: [
'sm:mx-auto',
'max-w-3xl ',
'page-full-width:max-w-screen-2xl',
'sm:rounded-md',
'mb-8',
]
)}
>
<Image
@@ -32,8 +32,8 @@ export async function PageFooterNavigation(props: {
'mt-6',
'gap-2',
'max-w-3xl',
'page-full-width:max-w-screen-2xl',
'mx-auto',
'page-api-block:ml-0',
'text-tint'
)}
>
@@ -23,7 +23,14 @@ export async function PageHeader(props: {
return (
<header
className={tcls('max-w-3xl', 'mx-auto', 'mb-6', 'space-y-3', 'page-api-block:ml-0')}
className={tcls(
'max-w-3xl',
'page-full-width:max-w-screen-2xl',
'mx-auto',
'mb-6',
'space-y-3',
'page-api-block:ml-0'
)}
>
{ancestors.length > 0 && (
<nav>
@@ -3,12 +3,12 @@ import * as React from 'react';
/**
* This component preserves the layout of the page while loading a new one.
* This approach is needed as page layout (full width block) is done using CSS (`body:has(.page-full-width)`),
* This approach is needed as page layout (full width block) is done using CSS (`body:has(.full-width)`),
* which becomes false while transitioning between the 2 page states:
*
* 1. Page 1 with full width block: `body:has(.page-full-width)` is true
* 2. Loading skeleton while transitioning to page 2: `body:has(.page-full-width)` is false
* 3. Page 2 with full width block: `body:has(.page-full-width)` is true
* 1. Page 1 with full width block: `body:has(.site-full-width)` is true
* 2. Loading skeleton while transitioning to page 2: `body:has(.site-full-width)` is false
* 3. Page 2 with full width block: `body:has(.site-full-width)` is true
*
* This component ensures that the layout is preserved while transitioning between the 2 page states (in step 2).
*/
@@ -24,9 +24,9 @@ export function PreservePageLayout(props: { asFullWidth: boolean }) {
}
if (asFullWidth) {
header.classList.add('page-full-width');
header.classList.add('site-full-width');
} else {
header.classList.remove('page-full-width');
header.classList.remove('site-full-width');
}
}, [asFullWidth]);
@@ -64,10 +64,10 @@ export function PageFeedbackForm(props: {
<div className="rounded-full border border-tint-subtle bg-tint-base contrast-more:border-tint-12">
<div className="flex">
<RatingButton
rating={PageFeedbackRating.Bad}
label={tString(languages, 'was_this_helpful_negative')}
onClick={() => onSubmitRating(PageFeedbackRating.Bad)}
active={rating === PageFeedbackRating.Bad}
rating={PageFeedbackRating.Good}
label={tString(languages, 'was_this_helpful_positive')}
onClick={() => onSubmitRating(PageFeedbackRating.Good)}
active={rating === PageFeedbackRating.Good}
disabled={rating !== undefined}
/>
<RatingButton
@@ -78,10 +78,10 @@ export function PageFeedbackForm(props: {
disabled={rating !== undefined}
/>
<RatingButton
rating={PageFeedbackRating.Good}
label={tString(languages, 'was_this_helpful_positive')}
onClick={() => onSubmitRating(PageFeedbackRating.Good)}
active={rating === PageFeedbackRating.Good}
rating={PageFeedbackRating.Bad}
label={tString(languages, 'was_this_helpful_negative')}
onClick={() => onSubmitRating(PageFeedbackRating.Bad)}
active={rating === PageFeedbackRating.Bad}
disabled={rating !== undefined}
/>
</div>
@@ -143,10 +143,6 @@
margin-right: 0;
width: calc(100% - var(--scrollbar-width));
}
body:has(.page-full-width) .scroll-nojump {
margin-left: 0;
width: 100%;
}
}
.elevate-link {
@@ -62,7 +62,7 @@ export async function SitePage(props: SitePageProps) {
const withSections = Boolean(sections && sections.list.length > 0);
const headerOffset = { sectionsHeader: withSections, topHeader: withTopHeader };
const document = await getPageDocument(context.dataFetcher, context.space, page);
const document = await getPageDocument(context, page);
return (
<PageContextProvider pageId={page.id} spaceId={context.space.id} title={page.title}>
@@ -19,7 +19,7 @@ export function SitePageSkeleton() {
'lg:items-start'
)}
>
<div className={tcls('flex-1', 'max-w-3xl', 'mx-auto', 'page-full-width:mx-0')}>
<div className={tcls('flex-1', 'max-w-3xl', 'mx-auto', 'site-full-width:mx-0')}>
<SkeletonHeading style={tcls('mb-8')} />
<SkeletonParagraph style={tcls('mb-4')} />
</div>
@@ -1,3 +1,4 @@
import { getSectionURL, getSiteSpaceURL } from '@/lib/sites';
import type { SiteSection, SiteSectionGroup } from '@gitbook/api';
import type { GitBookSiteContext, SiteSections } from '@v2/lib/context';
@@ -46,15 +47,35 @@ export function encodeClientSiteSections(context: GitBookSiteContext, sections:
}
function encodeSection(context: GitBookSiteContext, section: SiteSection) {
const { linker } = context;
return {
id: section.id,
title: section.title,
description: section.description,
icon: section.icon,
object: section.object,
url: section.urls.published
? linker.toLinkForContent(section.urls.published)
: linker.toPathInSite(section.path),
url: findBestTargetURL(context, section),
};
}
/**
* Find the best default site space to navigate to for a givent section:
* 1. If we are on the default, continue on the default.
* 2. If a site space has the same path as the current one, return it.
* 3. Otherwise, return the default one.
*/
function findBestTargetURL(context: GitBookSiteContext, section: SiteSection) {
const { siteSpace: currentSiteSpace } = context;
if (section.siteSpaces.length === 1 || currentSiteSpace.default) {
return getSectionURL(context, section);
}
const bestMatch = section.siteSpaces.find(
(siteSpace) => siteSpace.path === currentSiteSpace.path
);
if (bestMatch) {
return getSiteSpaceURL(context, bestMatch);
}
return getSectionURL(context, section);
}
@@ -73,6 +73,7 @@ export function SpaceLayout(props: {
'flex-col',
'lg:flex-row',
CONTAINER_STYLE,
'site-full-width:max-w-full',
// Ensure the footer is display below the viewport even if the content is not enough
withFooter && 'min-h-[calc(100vh-64px)]',
@@ -13,7 +13,7 @@ export function TOCPageIcon({ page }: { page: RevisionPage }) {
page={page}
style={tcls(
'text-base',
'text-tint-strong/6',
'[.toclink_&]:text-tint-strong/6',
'group-aria-current-page/toclink:text-primary-subtle',
'contrast-more:group-aria-current-page/toclink:text-primary',
@@ -35,6 +35,7 @@ export function TableOfContents(props: {
'relative',
'z-[1]',
'lg:sticky',
'lg:mr-12',
// Server-side static positioning
'lg:top-0',
@@ -67,7 +67,7 @@ function LinkItem(
insights={insights}
aria-current={isActive ? 'page' : undefined}
className={tcls(
'group/toclink relative transition-colors',
'group/toclink toclink relative transition-colors',
'flex flex-row justify-between',
'circular-corners:rounded-2xl rounded-md straight-corners:rounded-none p-1.5 pl-3',
'text-balance font-normal text-sm text-tint-strong/7 hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong',
+1 -1
View File
@@ -14,7 +14,7 @@ export const CONTAINER_STYLE: ClassValue = [
'md:px-8',
'max-w-screen-2xl',
'mx-auto',
'page-full-width:max-w-full',
// 'site-full-width:max-w-full',
];
/**
@@ -106,10 +106,7 @@ export const LinkOverlay = React.forwardRef(function LinkOverlay(
<Link
ref={ref}
{...domProps}
className={tcls(
'link-overlay static before:absolute before:top-0 before:left-0 before:z-10 before:h-full before:w-full',
className
)}
className={tcls('link-overlay absolute inset-0 z-10', className)}
>
{children}
</Link>
+33
View File
@@ -493,6 +493,39 @@ export const getRevisionPageByPath = cache({
},
});
/**
* Get a document from a page by its ID
*/
export const getRevisionPageDocument = cache({
name: 'api.getRevisionPageDocument.v1',
tag: (spaceId, revisionId) =>
getCacheTag({ tag: 'revision', space: spaceId, revision: revisionId }),
tagImmutable: true,
getKeySuffix: getAPIContextId,
get: async (
spaceId: string,
revisionId: string,
pageId: string,
options: CacheFunctionOptions
) => {
const apiCtx = await api();
const response = await apiCtx.client.spaces.getPageDocumentInRevisionById(
spaceId,
revisionId,
pageId,
{
evaluated: true,
},
{
...noCacheFetchOptions,
signal: options.signal,
}
);
return cacheResponse(response, cacheTtl_7days);
},
});
/**
* Resolve a file by its ID.
* It should not be used directly, use `getRevisionFile` instead.
+89 -45
View File
@@ -1,4 +1,4 @@
import type { JSONDocument } from '@gitbook/api';
import type { DocumentBlock, JSONDocument } from '@gitbook/api';
import type { GitBookAnyContext } from '@v2/lib/context';
import { getNodeText } from './document';
@@ -19,58 +19,102 @@ export interface DocumentSection {
export async function getDocumentSections(
context: GitBookAnyContext,
document: JSONDocument
): Promise<DocumentSection[]> {
return getSectionsFromNodes(document.nodes, context);
}
/**
* Extract a list of sections from a list of nodes.
*/
async function getSectionsFromNodes(
nodes: DocumentBlock[],
context: GitBookAnyContext
): Promise<DocumentSection[]> {
const sections: DocumentSection[] = [];
let depth = 0;
for (const block of document.nodes) {
if ((block.type === 'heading-1' || block.type === 'heading-2') && block.meta?.id) {
if (block.type === 'heading-1') {
for (const block of nodes) {
switch (block.type) {
case 'heading-1': {
const id = block.meta?.id;
if (!id) {
continue;
}
depth = 1;
}
const title = getNodeText(block);
const id = block.meta.id;
sections.push({
id,
title,
depth: block.type === 'heading-1' ? 1 : depth > 0 ? 2 : 1,
});
}
if ((block.type === 'swagger' || block.type === 'openapi-operation') && block.meta?.id) {
const { data: operation } = await resolveOpenAPIOperationBlock({
block,
context,
});
if (operation) {
const title = getNodeText(block);
sections.push({
id: block.meta.id,
tag: operation.method.toUpperCase(),
title: operation.operation.summary || operation.path,
depth: 1,
deprecated: operation.operation.deprecated,
});
}
}
if (
block.type === 'openapi-schemas' &&
!block.data.grouped &&
block.meta?.id &&
block.data.schemas.length === 1
) {
const { data } = await resolveOpenAPISchemasBlock({
block,
context,
});
const schema = data?.schemas[0];
if (schema) {
sections.push({
id: block.meta.id,
title: `The ${schema.name} object`,
id,
title,
depth: 1,
});
continue;
}
case 'heading-2': {
const id = block.meta?.id;
if (!id) {
continue;
}
const title = getNodeText(block);
sections.push({
id,
title,
depth: depth > 0 ? 2 : 1,
});
continue;
}
case 'stepper': {
const stepNodes = await Promise.all(
block.nodes.map(async (step) => getSectionsFromNodes(step.nodes, context))
);
for (const stepSections of stepNodes) {
sections.push(...stepSections);
}
continue;
}
case 'swagger':
case 'openapi-operation': {
const id = block.meta?.id;
if (!id) {
continue;
}
const { data: operation } = await resolveOpenAPIOperationBlock({
block,
context,
});
if (operation) {
sections.push({
id,
tag: operation.method.toUpperCase(),
title: operation.operation.summary || operation.path,
depth: 1,
deprecated: operation.operation.deprecated,
});
}
continue;
}
case 'openapi-schemas': {
const id = block.meta?.id;
if (!id) {
continue;
}
if (block.data.grouped || block.data.schemas.length !== 1) {
// Skip grouped schemas, they are not sections
continue;
}
const { data } = await resolveOpenAPISchemasBlock({
block,
context,
});
const schema = data?.schemas[0];
if (schema) {
sections.push({
id,
title: `The ${schema.name} object`,
depth: 1,
});
}
continue;
}
}
}
+2 -2
View File
@@ -147,7 +147,7 @@ export async function resolveContentRef(
// Compute the text to display for the link
if (anchor) {
text = `#${anchor}`;
text = page.title;
ancestors.push({
label: page.title,
icon: <PageIcon page={page} style={iconStyle} />,
@@ -155,7 +155,7 @@ export async function resolveContentRef(
});
if (resolveAnchorText) {
const document = await getPageDocument(dataFetcher, space, page);
const document = await getPageDocument(context, page);
if (document) {
const block = getBlockById(document, anchor);
if (block) {
+30
View File
@@ -1,4 +1,6 @@
import type { SiteSection, SiteSectionGroup, SiteSpace, SiteStructure } from '@gitbook/api';
import type { GitBookSiteContext } from '@v2/lib/context';
import { joinPath } from './paths';
/**
* Get all sections from a site structure.
@@ -64,6 +66,34 @@ export function findSiteSpaceById(siteStructure: SiteStructure, spaceId: string)
return null;
}
/**
* Get the URL to navigate to for a section.
* When the site is not published yet, `urls.published` is not available.
* To ensure navigation works in preview, we compute a relative URL from the siteSection path.
*/
export function getSectionURL(context: GitBookSiteContext, section: SiteSection) {
const { linker } = context;
return section.urls.published
? linker.toLinkForContent(section.urls.published)
: linker.toPathInSite(section.path);
}
/**
* Get the URL to navigate to for a site space.
* When the site is not published yet, `urls.published` is not available.
* To ensure navigation works in preview, we compute a relative URL from the siteSpace path.
*/
export function getSiteSpaceURL(context: GitBookSiteContext, siteSpace: SiteSpace) {
const { linker, sections } = context;
if (siteSpace.urls.published) {
return linker.toLinkForContent(siteSpace.urls.published);
}
return linker.toPathInSite(
sections?.current ? joinPath(sections.current.path, siteSpace.path) : siteSpace.path
);
}
function findSiteSpaceByIdInSections(sections: SiteSection[], spaceId: string): SiteSpace | null {
for (const section of sections) {
const siteSpace =
+7
View File
@@ -4,3 +4,10 @@
export function filterOutNullable<T>(value: T): value is NonNullable<T> {
return !!value;
}
/**
* Alternative to `assertNever` that returns `null` instead of throwing an error.
*/
export function nullIfNever(_value: never): null {
return null;
}
+13
View File
@@ -25,6 +25,7 @@ import {
getRevision,
getRevisionFile,
getRevisionPageByPath,
getRevisionPageDocument,
getRevisionPages,
getSiteRedirectBySource,
getSpace,
@@ -241,6 +242,18 @@ function getDataFetcherV1(apiTokenOverride?: string): GitBookDataFetcher {
);
},
getRevisionPageDocument(params) {
return withAPI(() =>
wrapDataFetcherError(async () => {
return getRevisionPageDocument(
params.spaceId,
params.revisionId,
params.pageId
);
})
);
},
getRevisionPageByPath(params) {
return withAPI(() =>
wrapDataFetcherError(async () => {
+10
View File
@@ -1,4 +1,6 @@
import type { ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types';
import { getCloudflareContext as getCloudflareContextV2 } from '@v2/lib/data/cloudflare';
import { isV2 } from './v2';
let pendings: Array<Promise<unknown>> = [];
@@ -47,6 +49,14 @@ export async function waitUntil(promise: Promise<unknown>) {
return;
}
if (isV2()) {
const context = getCloudflareContextV2();
if (context) {
context.ctx.waitUntil(promise);
return;
}
}
const cloudflareContext = await getGlobalContext();
if ('waitUntil' in cloudflareContext) {
cloudflareContext.waitUntil(promise);
+28 -55
View File
@@ -1,5 +1,6 @@
import { CustomizationDefaultFont, CustomizationHeaderPreset } from '@gitbook/api';
import { colorContrast } from '@gitbook/colors';
import { type FontWeight, getDefaultFont } from '@gitbook/fonts';
import { redirect } from 'next/navigation';
import { ImageResponse } from 'next/og';
@@ -11,24 +12,6 @@ import { getCacheTag } from '@gitbook/cache-tags';
import type { GitBookSiteContext } from '@v2/lib/context';
import { getResizedImageURL } from '@v2/lib/images';
const googleFontsMap: { [fontName in CustomizationDefaultFont]: string } = {
[CustomizationDefaultFont.Inter]: 'Inter',
[CustomizationDefaultFont.FiraSans]: 'Fira Sans Extra Condensed',
[CustomizationDefaultFont.IBMPlexSerif]: 'IBM Plex Serif',
[CustomizationDefaultFont.Lato]: 'Lato',
[CustomizationDefaultFont.Merriweather]: 'Merriweather',
[CustomizationDefaultFont.NotoSans]: 'Noto Sans',
[CustomizationDefaultFont.OpenSans]: 'Open Sans',
[CustomizationDefaultFont.Overpass]: 'Overpass',
[CustomizationDefaultFont.Poppins]: 'Poppins',
[CustomizationDefaultFont.Raleway]: 'Raleway',
[CustomizationDefaultFont.Roboto]: 'Roboto',
[CustomizationDefaultFont.RobotoSlab]: 'Roboto Slab',
[CustomizationDefaultFont.SourceSansPro]: 'Source Sans 3',
[CustomizationDefaultFont.Ubuntu]: 'Ubuntu',
[CustomizationDefaultFont.ABCFavorit]: 'Inter',
};
/**
* Render the OpenGraph image for a site content.
*/
@@ -65,19 +48,15 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
const { fontFamily, fonts } = await (async () => {
// google fonts
if (typeof customization.styling.font === 'string') {
const fontFamily = googleFontsMap[customization.styling.font] ?? 'Inter';
const fontFamily = customization.styling.font ?? CustomizationDefaultFont.Inter;
const regularText = pageDescription;
const boldText = `${contentTitle}${pageTitle}`;
const fonts = (
await Promise.all([
getWithCache(`google-font:${fontFamily}:400`, () =>
loadGoogleFont({ fontFamily, text: regularText, weight: 400 })
),
getWithCache(`google-font:${fontFamily}:700`, () =>
loadGoogleFont({ fontFamily, text: boldText, weight: 700 })
),
loadGoogleFont({ font: fontFamily, text: regularText, weight: 400 }),
loadGoogleFont({ font: fontFamily, text: boldText, weight: 700 }),
])
).filter(filterOutNullable);
@@ -260,37 +239,31 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
);
}
async function loadGoogleFont(input: { fontFamily: string; text: string; weight: 400 | 700 }) {
const { fontFamily, text, weight } = input;
async function loadGoogleFont(input: {
font: CustomizationDefaultFont;
text: string;
weight: FontWeight;
}) {
const lookup = getDefaultFont({
font: input.font,
text: input.text,
weight: input.weight,
});
if (!text.trim()) {
return null;
}
const url = new URL('https://fonts.googleapis.com/css2');
url.searchParams.set('family', `${fontFamily}:wght@${weight}`);
url.searchParams.set('text', text);
const result = await fetch(url.href);
if (!result.ok) {
return null;
}
const css = await result.text();
const resource = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/);
const resourceUrl = resource ? resource[1] : null;
if (resourceUrl) {
const response = await fetch(resourceUrl);
if (response.ok) {
const data = await response.arrayBuffer();
return {
name: fontFamily,
data,
style: 'normal' as const,
weight,
};
}
// If we found a font file, load it
if (lookup) {
return getWithCache(`google-font-files:${lookup.url}`, async () => {
const response = await fetch(lookup.url);
if (response.ok) {
const data = await response.arrayBuffer();
return {
name: lookup.font,
data,
style: 'normal' as const,
weight: input.weight,
};
}
});
}
// If for some reason we can't load the font, we'll just use the default one
+9 -1
View File
@@ -447,6 +447,13 @@ const config: Config = {
},
},
opacity: opacity(),
screens: {
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
'2xl': '1536px',
},
},
plugins: [
plugin(({ addVariant }) => {
@@ -514,8 +521,9 @@ const config: Config = {
/**
* Variant when the page contains a block that will be rendered in full-width mode.
*/
addVariant('site-full-width', 'body:has(.site-full-width) &');
addVariant('site-default-width', 'body:has(.site-default-width) &');
addVariant('page-full-width', 'body:has(.page-full-width) &');
addVariant('page-default-width', 'body:has(.page-default-width) &');
/**
* Variant when the page is configured to hide the table of content.
+8
View File
@@ -0,0 +1,8 @@
import { mock } from 'bun:test';
/**
* Mock the `server-only` module to avoid errors when running tests as it doesn't work well in Bun
*/
mock.module('server-only', () => {
return {};
});
+2 -2
View File
@@ -32,7 +32,7 @@
},
"scripts": {
"generate": "bun ./bin/gen-list.js",
"build": "tsc",
"build": "tsc --project tsconfig.build.json",
"typecheck": "tsc --noEmit",
"dev": "tsc -w",
"clean": "rm -rf ./dist && rm -rf ./src/data",
@@ -41,7 +41,7 @@
"bin": {
"gitbook-icons": "./bin/gitbook-icons.js"
},
"files": ["dist", "src", "bin", "data", "README.md", "CHANGELOG.md"],
"files": ["dist", "src", "bin", "README.md", "CHANGELOG.md"],
"engines": {
"node": ">=20.0.0"
}
+4
View File
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "src/**/*.test.ts"]
}
+1 -1
View File
@@ -10,7 +10,7 @@
},
"dependencies": {
"classnames": "^2.5.1",
"@gitbook/api": "^0.119.0",
"@gitbook/api": "catalog:",
"@gitbook/icons": "workspace:*"
},
"peerDependencies": {
@@ -27,12 +27,14 @@ export function OpenAPISchemaName(props: OpenAPISchemaNameProps) {
{propertyName}
</span>
) : null}
<span>
{type ? <span className="openapi-schema-type">{type}</span> : null}
{additionalItems ? (
<span className="openapi-schema-type">{additionalItems}</span>
) : null}
</span>
{type || additionalItems ? (
<span>
{type ? <span className="openapi-schema-type">{type}</span> : null}
{additionalItems ? (
<span className="openapi-schema-type">{additionalItems}</span>
) : null}
</span>
) : null}
{schema?.readOnly ? (
<span className="openapi-schema-readonly">
{t(context.translation, 'read_only')}
@@ -1,5 +1,7 @@
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
import { InteractiveSection } from './InteractiveSection';
import { Markdown } from './Markdown';
import { OpenAPICopyButton } from './OpenAPICopyButton';
import { OpenAPISchemaName } from './OpenAPISchemaName';
import type { OpenAPIClientContext } from './context';
import { t } from './translate';
@@ -105,13 +107,7 @@ function getLabelForType(security: OpenAPISecurityWithRequired, context: OpenAPI
/>
);
case 'oauth2':
return (
<OpenAPISchemaName
context={context}
propertyName="OAuth2"
required={security.required}
/>
);
return <OpenAPISchemaOAuth2Flows context={context} security={security} />;
case 'openIdConnect':
return (
<OpenAPISchemaName
@@ -125,3 +121,111 @@ function getLabelForType(security: OpenAPISecurityWithRequired, context: OpenAPI
return security.type;
}
}
function OpenAPISchemaOAuth2Flows(props: {
context: OpenAPIClientContext;
security: OpenAPIV3.OAuth2SecurityScheme & { required?: boolean };
}) {
const { context, security } = props;
const flows = Object.entries(security.flows ?? {});
return (
<div className="openapi-securities-oauth-flows">
{flows.map(([name, flow], index) => (
<OpenAPISchemaOAuth2Item
key={index}
flow={flow}
name={name}
context={context}
security={security}
/>
))}
</div>
);
}
function OpenAPISchemaOAuth2Item(props: {
flow: NonNullable<OpenAPIV3.OAuth2SecurityScheme['flows']>[keyof NonNullable<
OpenAPIV3.OAuth2SecurityScheme['flows']
>];
name: string;
context: OpenAPIClientContext;
security: OpenAPIV3.OAuth2SecurityScheme & { required?: boolean };
}) {
const { flow, context, security, name } = props;
if (!flow) {
return null;
}
const scopes = Object.entries(flow?.scopes ?? {});
return (
<div>
<OpenAPISchemaName
context={context}
propertyName="OAuth2"
type={name}
required={security.required}
/>
<div className="openapi-securities-oauth-content openapi-markdown">
{security.description ? <Markdown source={security.description} /> : null}
{'authorizationUrl' in flow && flow.authorizationUrl ? (
<span>
Authorization URL:{' '}
<OpenAPICopyButton
value={flow.authorizationUrl}
context={context}
className="openapi-securities-url"
withTooltip
>
{flow.authorizationUrl}
</OpenAPICopyButton>
</span>
) : null}
{'tokenUrl' in flow && flow.tokenUrl ? (
<span>
Token URL:{' '}
<OpenAPICopyButton
value={flow.tokenUrl}
context={context}
className="openapi-securities-url"
withTooltip
>
{flow.tokenUrl}
</OpenAPICopyButton>
</span>
) : null}
{'refreshUrl' in flow && flow.refreshUrl ? (
<span>
Refresh URL:{' '}
<OpenAPICopyButton
value={flow.refreshUrl}
context={context}
className="openapi-securities-url"
withTooltip
>
{flow.refreshUrl}
</OpenAPICopyButton>
</span>
) : null}
{scopes.length ? (
<div>
{t(context.translation, 'available_scopes')}:{' '}
<ul>
{scopes.map(([key, value]) => (
<li key={key}>
<OpenAPICopyButton value={key} context={context} withTooltip>
<code>{key}</code>
</OpenAPICopyButton>
: {value}
</li>
))}
</ul>
</div>
) : null}
</div>
</div>
);
}
@@ -35,6 +35,7 @@ export const de = {
show: 'Zeige ${1}',
hide: 'Verstecke ${1}',
available_items: 'Verfügbare Elemente',
available_scopes: 'Verfügbare scopes',
properties: 'Eigenschaften',
or: 'oder',
and: 'und',
@@ -35,6 +35,7 @@ export const en = {
show: 'Show ${1}',
hide: 'Hide ${1}',
available_items: 'Available items',
available_scopes: 'Available scopes',
possible_values: 'Possible values',
properties: 'Properties',
or: 'or',
@@ -35,6 +35,7 @@ export const es = {
show: 'Mostrar ${1}',
hide: 'Ocultar ${1}',
available_items: 'Elementos disponibles',
available_scopes: 'Scopes disponibles',
properties: 'Propiedades',
or: 'o',
and: 'y',
@@ -35,6 +35,7 @@ export const fr = {
show: 'Afficher ${1}',
hide: 'Masquer ${1}',
available_items: 'Éléments disponibles',
available_scopes: 'Scopes disponibles',
properties: 'Propriétés',
or: 'ou',
and: 'et',
@@ -35,6 +35,7 @@ export const ja = {
show: '${1}を表示',
hide: '${1}を非表示',
available_items: '利用可能なアイテム',
available_scopes: '利用可能なスコープ',
properties: 'プロパティ',
or: 'または',
and: 'かつ',
@@ -35,6 +35,7 @@ export const nl = {
show: 'Toon ${1}',
hide: 'Verberg ${1}',
available_items: 'Beschikbare items',
available_scopes: 'Beschikbare scopes',
properties: 'Eigenschappen',
or: 'of',
and: 'en',
@@ -35,6 +35,7 @@ export const no = {
show: 'Vis ${1}',
hide: 'Skjul ${1}',
available_items: 'Tilgjengelige elementer',
available_scopes: 'Tilgjengelige scopes',
properties: 'Egenskaper',
or: 'eller',
and: 'og',
@@ -35,6 +35,7 @@ export const pt_br = {
show: 'Mostrar ${1}',
hide: 'Ocultar ${1}',
available_items: 'Itens disponíveis',
available_scopes: 'Scopes disponíveis',
properties: 'Propriedades',
or: 'ou',
and: 'e',
@@ -35,6 +35,7 @@ export const zh = {
show: '显示${1}',
hide: '隐藏${1}',
available_items: '可用项',
available_scopes: '可用范围',
properties: '属性',
or: '或',
and: '和',