Compare commits

..

13 Commits

Author SHA1 Message Date
Nolann Biron f2766f3fdc Revert OpenAPI Spec validation 2025-02-25 16:52:06 +01:00
Greg Bergé e24206ebf0 Remove dependency to swagger2openapi and yaml (#2874) 2025-02-25 15:42:11 +01:00
Greg Bergé 3410785a3a Remove .tsbuildinfo (#2875) 2025-02-25 15:24:12 +01:00
Nolann B. b4a12d606e Fix circularRef in schema + examples OpenAPI (#2872) 2025-02-24 21:26:26 +01:00
Greg Bergé a0545545d7 Faster OpenAPI spec validation (#2871) 2025-02-24 21:21:27 +01:00
Zeno Kapitein 1f11650ca1 Bump hint icon up 1px (#2869) 2025-02-24 16:21:19 +01:00
Greg Bergé da55facf2c Support GitBook blocks in OpenAPI operation description (#2868) 2025-02-24 13:44:48 +01:00
Nolann B. dc2dbc5710 Update OpenAPI code examples to support multiple content-type (#2843) 2025-02-24 13:07:35 +01:00
Nolann B. bdd6303bcc Rearrange item types in OpenAPI blocks (#2862) 2025-02-24 11:33:47 +01:00
Zeno Kapitein 66d0fc0683 Update hint block (#2840)
Co-authored-by: Valentino Hudhra <v.hudhra@gmail.com>
2025-02-21 15:32:35 +01:00
Nolann B. 05e1d8cd96 Hide x-gitbook-* symbols in OpenAPI blocks (#2863) 2025-02-21 12:38:55 +01:00
Greg Bergé 9f0de74caa Add support for new OpenAPI ref (#2860) 2025-02-20 14:31:04 +01:00
spastorelli a820739bd2 Remove ununsed search api lib methods (#2855) 2025-02-20 09:46:12 +01:00
43 changed files with 1575 additions and 425 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Remove unused search api method from gitbook/api/lib
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
Update OpenAPI code examples to support multiple content-type
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/openapi-parser': major
---
Use Scalar parser to upgrade OpenAPI spec v2
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
Hide x-gitbook-\* symbols in OpenAPI blocks
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
Fix circularRef in schema + examples OpenAPI
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Update design for hint block: use semantic colors (info, warning, danger, success) and add alternative styling for hints with headings
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Add support for new OpenAPI ref
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
Fix ID not set when there is no operation summary
+7
View File
@@ -0,0 +1,7 @@
---
'@gitbook/openapi-parser': patch
'@gitbook/react-openapi': patch
'gitbook': patch
---
Render GitBook blocks in OpenAPI operation description
+3
View File
@@ -22,3 +22,6 @@ yarn-error.log*
# Env files
.env.local
# TypeScript
*.tsbuildinfo
+25 -77
View File
@@ -24,7 +24,7 @@
},
"packages/colors": {
"name": "@gitbook/colors",
"version": "0.1.0",
"version": "0.2.0",
"devDependencies": {
"typescript": "^5.5.3",
},
@@ -38,9 +38,9 @@
},
"packages/gitbook": {
"name": "gitbook",
"version": "0.6.1",
"version": "0.6.2",
"dependencies": {
"@gitbook/api": "^0.93.0",
"@gitbook/api": "^0.95.0",
"@gitbook/cache-do": "workspace:*",
"@gitbook/colors": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*",
@@ -155,12 +155,10 @@
},
"packages/openapi-parser": {
"name": "@gitbook/openapi-parser",
"version": "1.0.0",
"version": "1.0.1",
"dependencies": {
"@scalar/openapi-parser": "^0.10.4",
"@scalar/openapi-types": "^0.1.6",
"swagger2openapi": "^7.0.8",
"yaml": "1.10.2",
},
"devDependencies": {
"@tsconfig/node20": "^20.1.4",
@@ -211,7 +209,7 @@
},
"packages/react-openapi": {
"name": "@gitbook/react-openapi",
"version": "1.0.1",
"version": "1.0.2",
"dependencies": {
"@gitbook/openapi-parser": "workspace:*",
"@scalar/api-client-react": "1.0.87",
@@ -569,8 +567,6 @@
"@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="],
"@exodus/schemasafe": ["@exodus/schemasafe@1.3.0", "", {}, "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw=="],
"@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=="],
@@ -1683,8 +1679,6 @@
"call-bind": ["call-bind@1.0.7", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.1" } }, "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w=="],
"call-me-maybe": ["call-me-maybe@1.0.2", "", {}, "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
@@ -1725,8 +1719,6 @@
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
@@ -1923,8 +1915,6 @@
"es6-iterator": ["es6-iterator@2.0.3", "", { "dependencies": { "d": "1", "es5-ext": "^0.10.35", "es6-symbol": "^3.1.1" } }, "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g=="],
"es6-promise": ["es6-promise@3.3.1", "", {}, "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg=="],
"es6-symbol": ["es6-symbol@3.1.4", "", { "dependencies": { "d": "^1.0.2", "ext": "^1.7.0" } }, "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg=="],
"es6-weak-map": ["es6-weak-map@2.0.3", "", { "dependencies": { "d": "1", "es5-ext": "^0.10.46", "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.1" } }, "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA=="],
@@ -2053,8 +2043,6 @@
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="],
"fast-text-encoding": ["fast-text-encoding@1.0.6", "", {}, "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w=="],
"fast-uri": ["fast-uri@3.0.2", "", {}, "sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row=="],
@@ -2123,8 +2111,6 @@
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"get-intrinsic": ["get-intrinsic@1.2.4", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ=="],
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
@@ -2247,8 +2233,6 @@
"http-errors": ["http-errors@1.4.0", "", { "dependencies": { "inherits": "2.0.1", "statuses": ">= 1.2.1 < 2" } }, "sha512-oLjPqve1tuOl5aRhv8GK5eHpqP1C9fb+Ol+XTLjKfLltE44zdDbEdjPSbU7Ch5rSNsVFqZn97SrMmZLdu1/YMw=="],
"http2-client": ["http2-client@1.3.5", "", {}, "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"human-id": ["human-id@1.0.2", "", {}, "sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw=="],
@@ -2663,16 +2647,12 @@
"next-tick": ["next-tick@1.1.0", "", {}, "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="],
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"node-fetch-h2": ["node-fetch-h2@2.3.0", "", { "dependencies": { "http2-client": "^1.2.5" } }, "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg=="],
"node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="],
"node-forge": ["node-forge@1.3.1", "", {}, "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA=="],
"node-gyp-build": ["node-gyp-build@4.8.2", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw=="],
"node-readfiles": ["node-readfiles@0.2.0", "", { "dependencies": { "es6-promise": "^3.2.1" } }, "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA=="],
"node-releases": ["node-releases@2.0.18", "", {}, "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g=="],
"nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="],
@@ -2689,16 +2669,6 @@
"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=="],
"oas-kit-common": ["oas-kit-common@1.0.8", "", { "dependencies": { "fast-safe-stringify": "^2.0.7" } }, "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ=="],
"oas-linter": ["oas-linter@3.2.2", "", { "dependencies": { "@exodus/schemasafe": "^1.0.0-rc.2", "should": "^13.2.1", "yaml": "^1.10.0" } }, "sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ=="],
"oas-resolver": ["oas-resolver@2.5.6", "", { "dependencies": { "node-fetch-h2": "^2.3.0", "oas-kit-common": "^1.0.8", "reftools": "^1.1.9", "yaml": "^1.10.0", "yargs": "^17.0.1" }, "bin": { "resolve": "resolve.js" } }, "sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ=="],
"oas-schema-walker": ["oas-schema-walker@1.1.5", "", {}, "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ=="],
"oas-validator": ["oas-validator@5.0.8", "", { "dependencies": { "call-me-maybe": "^1.0.1", "oas-kit-common": "^1.0.8", "oas-linter": "^3.2.2", "oas-resolver": "^2.5.6", "oas-schema-walker": "^1.1.5", "reftools": "^1.1.9", "should": "^13.2.1", "yaml": "^1.10.0" } }, "sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="],
@@ -2931,8 +2901,6 @@
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.6", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.1", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.4", "globalthis": "^1.0.3", "which-builtin-type": "^1.1.3" } }, "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg=="],
"reftools": ["reftools@1.1.9", "", {}, "sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w=="],
"regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="],
"regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="],
@@ -2969,8 +2937,6 @@
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"require-in-the-middle": ["require-in-the-middle@7.4.0", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3", "resolve": "^1.22.8" } }, "sha512-X34iHADNbNDfr6OTStIAHWSAvvKQRYgLO6duASaVf7J2VA3lvmNYboAHOuLC2huav1IwgZJtyEcJCKVzFxOSMQ=="],
@@ -3051,18 +3017,6 @@
"shimmer": ["shimmer@1.2.1", "", {}, "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw=="],
"should": ["should@13.2.3", "", { "dependencies": { "should-equal": "^2.0.0", "should-format": "^3.0.3", "should-type": "^1.4.0", "should-type-adaptors": "^1.0.1", "should-util": "^1.0.0" } }, "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ=="],
"should-equal": ["should-equal@2.0.0", "", { "dependencies": { "should-type": "^1.4.0" } }, "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA=="],
"should-format": ["should-format@3.0.3", "", { "dependencies": { "should-type": "^1.3.0", "should-type-adaptors": "^1.0.1" } }, "sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q=="],
"should-type": ["should-type@1.4.0", "", {}, "sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ=="],
"should-type-adaptors": ["should-type-adaptors@1.1.0", "", { "dependencies": { "should-type": "^1.3.0", "should-util": "^1.0.0" } }, "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA=="],
"should-util": ["should-util@1.0.1", "", {}, "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g=="],
"side-channel": ["side-channel@1.0.6", "", { "dependencies": { "call-bind": "^1.0.7", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.4", "object-inspect": "^1.13.1" } }, "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
@@ -3157,8 +3111,6 @@
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"swagger2openapi": ["swagger2openapi@7.0.8", "", { "dependencies": { "call-me-maybe": "^1.0.1", "node-fetch": "^2.6.1", "node-fetch-h2": "^2.3.0", "node-readfiles": "^0.2.0", "oas-kit-common": "^1.0.8", "oas-resolver": "^2.5.6", "oas-schema-walker": "^1.1.5", "oas-validator": "^5.0.8", "reftools": "^1.1.9", "yaml": "^1.10.0", "yargs": "^17.0.1" }, "bin": { "swagger2openapi": "swagger2openapi.js", "oas-validate": "oas-validate.js", "boast": "boast.js" } }, "sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g=="],
"tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="],
"tailwind-merge": ["tailwind-merge@2.5.5", "", {}, "sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA=="],
@@ -3385,7 +3337,7 @@
"wrangler": ["wrangler@3.82.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.3.4", "@cloudflare/workers-shared": "0.6.0", "@esbuild-plugins/node-globals-polyfill": "^0.2.3", "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "blake3-wasm": "^2.1.5", "chokidar": "^3.5.3", "date-fns": "^4.1.0", "esbuild": "0.17.19", "itty-time": "^1.0.6", "miniflare": "3.20241018.0", "nanoid": "^3.3.3", "path-to-regexp": "^6.3.0", "resolve": "^1.22.8", "resolve.exports": "^2.0.2", "selfsigned": "^2.0.1", "source-map": "^0.6.1", "unenv": "npm:unenv-nightly@2.0.0-20241018-011344-e666fcf", "workerd": "1.20241018.1", "xxhash-wasm": "^1.0.1" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20241018.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-W4gyIkxOfqbFU6QrUvK+Ay2rwH+DcyXmIuT/1/9EG9K6jngmmon8ifVYGRTftRb2Vu7hDNLovbGfjeAXm+CEXA=="],
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
@@ -3407,15 +3359,11 @@
"y-codemirror.next": ["y-codemirror.next@0.3.5", "", { "dependencies": { "lib0": "^0.2.42" }, "peerDependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "yjs": "^13.5.6" } }, "sha512-VluNu3e5HfEXybnypnsGwKAj+fKLd4iAnR7JuX1Sfyydmn1jCBS5wwEL/uS04Ch2ib0DnMAOF6ZRR/8kK3wyGw=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
"yaml": ["yaml@2.6.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ=="],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
@@ -4147,8 +4095,6 @@
"@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
"@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@jridgewell/source-map/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
@@ -4179,6 +4125,8 @@
"@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
"@mapbox/node-pre-gyp/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"@mapbox/node-pre-gyp/tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
"@next/eslint-plugin-next/glob": ["glob@10.3.10", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.5", "minimatch": "^9.0.1", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", "path-scurry": "^1.10.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g=="],
@@ -4271,22 +4219,16 @@
"@scalar/api-client/pretty-ms": ["pretty-ms@8.0.0", "", { "dependencies": { "parse-ms": "^3.0.0" } }, "sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q=="],
"@scalar/api-client/yaml": ["yaml@2.6.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ=="],
"@scalar/components/@scalar/themes": ["@scalar/themes@0.9.48", "", { "dependencies": { "@scalar/types": "0.0.19" } }, "sha512-lsehl9KnlKgWG8BOIIaX3iIuNMLjRFZ0ctb1KsQBhOSsoTrq3c9EUJdYJiiTWzm52zdhF+RkIx7uHaX/pvgctw=="],
"@scalar/components/nanoid": ["nanoid@5.0.7", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ=="],
"@scalar/import/@scalar/openapi-parser": ["@scalar/openapi-parser@0.8.9", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-vTXrkl/hX3CG2dMe8mYutjsjfFLZOXGFJGqz6k8lUk8avFP/5eGEsd/eZe1UIcwtDwcjgJIyQ4p3pIC7jBvfuw=="],
"@scalar/import/yaml": ["yaml@2.6.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ=="],
"@scalar/oas-utils/nanoid": ["nanoid@5.0.7", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ=="],
"@scalar/oas-utils/yaml": ["yaml@2.7.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA=="],
"@scalar/openapi-parser/yaml": ["yaml@2.6.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ=="],
"@scalar/use-toasts/nanoid": ["nanoid@5.0.7", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ=="],
"@sentry/bundler-plugin-core/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
@@ -4295,6 +4237,8 @@
"@sentry/cli/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
"@sentry/cli/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"@sentry/nextjs/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="],
"@sentry/node/@opentelemetry/core": ["@opentelemetry/core@1.30.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Q/3u/K73KUjTCnFUP97ZY+pBjQ1kPEgjOfXj/bJl8zW7GbXdkw6cwuyZk6ZTXkVgCBsYRYUzx4fvYK1jxdb9MA=="],
@@ -4491,8 +4435,6 @@
"@vercel/fun/ms": ["ms@2.1.1", "", {}, "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="],
"@vercel/fun/node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="],
"@vercel/fun/semver": ["semver@7.5.4", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA=="],
"@vercel/gatsby-plugin-vercel-builder/esbuild": ["esbuild@0.14.47", "", { "optionalDependencies": { "esbuild-android-64": "0.14.47", "esbuild-android-arm64": "0.14.47", "esbuild-darwin-64": "0.14.47", "esbuild-darwin-arm64": "0.14.47", "esbuild-freebsd-64": "0.14.47", "esbuild-freebsd-arm64": "0.14.47", "esbuild-linux-32": "0.14.47", "esbuild-linux-64": "0.14.47", "esbuild-linux-arm": "0.14.47", "esbuild-linux-arm64": "0.14.47", "esbuild-linux-mips64le": "0.14.47", "esbuild-linux-ppc64le": "0.14.47", "esbuild-linux-riscv64": "0.14.47", "esbuild-linux-s390x": "0.14.47", "esbuild-netbsd-64": "0.14.47", "esbuild-openbsd-64": "0.14.47", "esbuild-sunos-64": "0.14.47", "esbuild-windows-32": "0.14.47", "esbuild-windows-64": "0.14.47", "esbuild-windows-arm64": "0.14.47" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-wI4ZiIfFxpkuxB8ju4MHrGwGLyp1+awEHAHVpx6w7a+1pmYIq8T9FGEVVwFo0iFierDoMj++Xq69GXWYn2EiwA=="],
@@ -4651,6 +4593,10 @@
"gaxios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
"gaxios/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"gitbook/@gitbook/api": ["@gitbook/api@0.95.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-9KAbt27Ile6cqAch7QEbiJHALQHojYlhsPzilgdQ5wpHgLwsrd7Smd58A3/8bWBKq4KV0vP4rh3oYhIw+LlWFw=="],
"gitbook-v2/next": ["next@15.2.0-canary.45", "", { "dependencies": { "@next/env": "15.2.0-canary.45", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.2.0-canary.45", "@next/swc-darwin-x64": "15.2.0-canary.45", "@next/swc-linux-arm64-gnu": "15.2.0-canary.45", "@next/swc-linux-arm64-musl": "15.2.0-canary.45", "@next/swc-linux-x64-gnu": "15.2.0-canary.45", "@next/swc-linux-x64-musl": "15.2.0-canary.45", "@next/swc-win32-arm64-msvc": "15.2.0-canary.45", "@next/swc-win32-x64-msvc": "15.2.0-canary.45", "sharp": "^0.33.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-UsneTQn9tntbiAaXpvoXhhsTBb58Q2XIs2Dfka+qWA8motBz0ZvW297YHLxhdur4xN0IJvknnZKl5Bs7wAGlOg=="],
"glob/minimatch": ["minimatch@10.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ=="],
@@ -4703,8 +4649,6 @@
"meow/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
"meow/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
"micro/arg": ["arg@4.1.0", "", {}, "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg=="],
"micro/content-type": ["content-type@1.0.4", "", {}, "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="],
@@ -4735,8 +4679,6 @@
"postcss-load-config/lilconfig": ["lilconfig@3.1.2", "", {}, "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow=="],
"postcss-load-config/yaml": ["yaml@2.6.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ=="],
"psi/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="],
"pump/end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="],
@@ -4837,6 +4779,12 @@
"wrangler/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
"wrap-ansi/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"write-file-atomic/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"youch/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
@@ -5313,8 +5261,6 @@
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
"@manypkg/find-root/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
"@mapbox/node-pre-gyp/tar/chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
@@ -5671,6 +5617,8 @@
"wrangler/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"@aws-sdk/client-dynamodb/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
"@aws-sdk/client-dynamodb/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.734.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.734.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", "@aws-sdk/middleware-recursion-detection": "3.734.0", "@aws-sdk/middleware-user-agent": "3.734.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/types": "3.734.0", "@aws-sdk/util-endpoints": "3.734.0", "@aws-sdk/util-user-agent-browser": "3.734.0", "@aws-sdk/util-user-agent-node": "3.734.0", "@smithy/config-resolver": "^4.0.1", "@smithy/core": "^3.1.1", "@smithy/fetch-http-handler": "^5.0.1", "@smithy/hash-node": "^4.0.1", "@smithy/invalid-dependency": "^4.0.1", "@smithy/middleware-content-length": "^4.0.1", "@smithy/middleware-endpoint": "^4.0.2", "@smithy/middleware-retry": "^4.0.3", "@smithy/middleware-serde": "^4.0.1", "@smithy/middleware-stack": "^4.0.1", "@smithy/node-config-provider": "^4.0.1", "@smithy/node-http-handler": "^4.0.2", "@smithy/protocol-http": "^5.0.1", "@smithy/smithy-client": "^4.1.2", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.3", "@smithy/util-defaults-mode-node": "^4.0.3", "@smithy/util-endpoints": "^3.0.1", "@smithy/util-middleware": "^4.0.1", "@smithy/util-retry": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-oerepp0mut9VlgTwnG5Ds/lb0C0b2/rQ+hL/rF6q+HGKPfGsCuPvFx1GtwGKCXd49ase88/jVgrhcA9OQbz3kg=="],
+5 -1
View File
@@ -14,7 +14,7 @@ import {
CustomizationThemeMode,
SiteCustomizationSettings,
} from '@gitbook/api';
import { test, expect, Page, BrowserContext } from '@playwright/test';
import { BrowserContext, expect, Page, test } from '@playwright/test';
import deepMerge from 'deepmerge';
import rison from 'rison';
import { DeepPartial } from 'ts-essentials';
@@ -222,6 +222,10 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
styling: {
theme: newTheme,
primaryColor: { light: '#346DDB', dark: '#346DDB' },
infoColor: { light: '#787878', dark: '#787878' },
warningColor: { light: '#FE9A00', dark: '#FE9A00' },
dangerColor: { light: '#FB2C36', dark: '#FB2C36' },
successColor: { light: '#00C950', dark: '#00C950' },
corners: CustomizationCorners.Rounded,
font: CustomizationFont.Inter,
background: CustomizationBackground.Plain,
+1 -1
View File
@@ -17,7 +17,7 @@
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math"
},
"dependencies": {
"@gitbook/api": "^0.93.0",
"@gitbook/api": "^0.95.0",
"@gitbook/cache-do": "workspace:*",
"@gitbook/colors": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*",
@@ -244,8 +244,8 @@ async function PDFPageDocument(props: {
{document ? (
<DocumentView
document={document}
style={'mt-6 space-y-6'}
blockStyle={['max-w-full']}
style="mt-6 space-y-6"
blockStyle="max-w-full"
context={{
mode: 'print',
content: {
@@ -1,9 +1,9 @@
import { DocumentBlockHeading } from '@gitbook/api';
import type { DocumentBlockHeading } from '@gitbook/api';
import { Icon } from '@gitbook/icons';
import { tcls } from '@/lib/tailwind';
import { BlockProps } from './Block';
import type { BlockProps } from './Block';
import { Inlines } from './Inlines';
import { getBlockTextStyle } from './spacing';
@@ -24,6 +24,7 @@ export function Heading(props: BlockProps<DocumentBlockHeading>) {
>
<div
className={tcls(
'hash',
'grid',
'grid-area-1-1',
'relative',
@@ -41,20 +42,14 @@ export function Heading(props: BlockProps<DocumentBlockHeading>) {
<a
href={`#${id}`}
aria-label="Direct link to heading"
className={tcls(
'inline-flex',
'h-full',
'items-start',
'dark:shadow-none',
'dark:ring-0',
textStyle.lineHeight,
)}
className={tcls('inline-flex', 'h-full', 'items-start', textStyle.lineHeight)}
>
<Icon
icon="hashtag"
className={tcls(
'w-3.5',
'h-[1lh]',
'h-[1em]',
'mt-0.5',
'transition-colors',
'text-transparent',
'group-hover:text-tint-subtle',
@@ -1,10 +1,10 @@
import { DocumentBlockHint } from '@gitbook/api';
import { Icon, IconName } from '@gitbook/icons';
import type { DocumentBlockHeading, DocumentBlockHint } from '@gitbook/api';
import { Icon, type IconName } from '@gitbook/icons';
import React from 'react';
import { ClassValue, tcls } from '@/lib/tailwind';
import { type ClassValue, tcls } from '@/lib/tailwind';
import { BlockProps } from './Block';
import { Block, type BlockProps } from './Block';
import { Blocks } from './Blocks';
import { getBlockTextStyle } from './spacing';
@@ -13,41 +13,74 @@ export function Hint(props: BlockProps<DocumentBlockHint>) {
const hintStyle = HINT_STYLES[block.data.style] ?? HINT_STYLES.info;
const firstLine = getBlockTextStyle(block.nodes[0]);
const firstNode = block.nodes[0];
const hasHeading = ['heading-1', 'heading-2', 'heading-3'].includes(block.nodes[0].type);
return (
<div
className={tcls(
'hint',
'p-4',
'transition-colors',
'rounded-md',
hasHeading ? 'rounded-l' : null,
'straight-corners:rounded-none',
hintStyle.style,
'overflow-hidden',
hasHeading ? ['border-l-2', hintStyle.containerWithHeader] : hintStyle.container,
'text-sm',
'grid',
'grid-cols-[auto_1fr]',
hasHeading ? 'grid-rows-[auto_auto]' : '',
style,
)}
>
<div className={tcls('flex', 'flex-row')}>
<div
className={tcls(
'py-3',
'pl-3',
'-mt-px', // Bump icon up 1px for optical alignment with heading
hasHeading ? hintStyle.header : null,
hintStyle.iconColor,
)}
>
<Icon
icon={hintStyle.icon}
className={tcls(
'size-5',
'mr-4',
'mt-0.5',
firstLine.lineHeight,
hintStyle.iconColor,
)}
/>
<Blocks
{...contextProps}
ancestorBlocks={[...ancestorBlocks, block]}
nodes={block.nodes}
blockStyle={tcls(
hintStyle.bodyColor,
// render hash icon on the other side of the heading
'flip-heading-hash',
)}
style={['flex-1', 'space-y-4', '[&_.hint]:border', '[&_pre]:border']}
className={tcls('size-[1.2em]', 'mt-0.5', firstLine.lineHeight)}
/>
</div>
{hasHeading ? (
<Block
style={tcls(
'text-[1em] *:mt-0 p-3 flip-heading-hash',
hasHeading ? hintStyle.header : null,
)}
ancestorBlocks={[...ancestorBlocks, block]}
{...contextProps}
block={firstNode}
/>
) : null}
<Blocks
{...contextProps}
ancestorBlocks={[...ancestorBlocks, block]}
nodes={hasHeading ? block.nodes.slice(1) : block.nodes}
blockStyle={tcls(
hintStyle.body,
// render hash icon on the other side of the heading
'flip-heading-hash',
)}
style={[
'p-3',
'empty:p-0',
'-row-end-1',
'-col-end-1',
'space-y-4',
'[&_.hint]:border',
'[&_pre]:border',
'[&_pre]:border-neutral',
]}
/>
</div>
);
}
@@ -55,77 +88,69 @@ export function Hint(props: BlockProps<DocumentBlockHint>) {
const HINT_STYLES: {
[style in DocumentBlockHint['data']['style']]: {
icon: IconName;
iconColor: ClassValue;
bodyColor: ClassValue;
style: ClassValue;
iconColor?: ClassValue;
body?: ClassValue;
header?: ClassValue;
container?: ClassValue;
containerWithHeader?: ClassValue;
};
} = {
info: {
icon: 'circle-info',
iconColor: ['text-primary'],
bodyColor: ['[&_a]:text-primary', '[&_a:hover]:text-primary-strong'],
style: [
'bg-tint',
'print-mode:!bg-tint',
'theme-muted:bg-tint-base',
'theme-bold-tint:bg-tint-base',
'theme-gradient:bg-tint-12/1',
'border-tint',
'[&_.can-override-bg]:bg-tint-active',
'[&_.can-override-text]:text-tint-strong',
iconColor: 'text-info-subtle contrast-more:text-info',
header: 'bg-info-active',
body: [
'text-neutral-strong',
'[&_.can-override-bg]:bg-neutral-active',
'[&_.can-override-text]:text-neutral-strong',
],
container:
'bg-neutral theme-muted:bg-neutral-base theme-bold-tint:bg-neutral-base theme-gradient:bg-neutral-12/1 border-neutral',
containerWithHeader: 'border-info-solid bg-neutral-subtle theme-gradient:bg-neutral-12/1',
},
warning: {
icon: 'circle-exclamation',
iconColor: ['text-amber-500', 'dark:text-orange-400'], // Darker shades of orange-* mismatch with lighter shades, so in light mode we use amber text on top of orange bg.
bodyColor: [
'text-orange-950',
'dark:text-orange-50',
'[&_a]:text-orange-800',
'[&_a:hover]:text-orange-900',
'dark:[&_a]:text-orange-400',
'dark:[&_a:hover]:text-orange-300',
'[&_.can-override-bg]:bg-orange-500/3',
'[&_.can-override-text]:text-orange-800',
'dark:[&_.can-override-text]:text-orange-400',
'decoration-orange-800/6',
'dark:decoration-orange-400/6',
iconColor: 'text-warning-subtle contrast-more:text-warning',
header: 'bg-warning-active',
body: [
'text-neutral-strong',
'[&_a]:text-warning',
'[&_a:hover]:text-warning-strong',
'[&_.can-override-bg]:bg-warning-active',
'[&_.can-override-text]:text-warning-strong',
'decoration-warning/6',
],
style: ['bg-orange-500/2', 'border-orange-500/4'],
container: 'bg-warning border-warning',
containerWithHeader: 'border-warning-solid bg-warning-subtle',
},
danger: {
icon: 'triangle-exclamation',
iconColor: ['text-red-500', 'dark:text-red-400'],
bodyColor: [
'text-red-950',
'dark:text-red-50',
'[&_a]:text-red-800',
'[&_a:hover]:text-red-900',
'dark:[&_a]:text-red-400',
'dark:[&_a:hover]:text-red-300',
'[&_.can-override-bg]:bg-red-500/3',
'[&_.can-override-text]:text-red-400',
'decoration-red-800/6',
'dark:decoration-red-400/6',
iconColor: 'text-danger-subtle contrast-more:text-danger',
header: 'bg-danger-active',
body: [
'text-neutral-strong',
'[&_a]:text-danger',
'[&_a:hover]:text-danger-strong',
'[&_.can-override-bg]:bg-danger-active',
'[&_.can-override-text]:text-danger-strong',
'decoration-danger/6',
],
style: ['bg-red-500/2', 'border-red-500/4'],
container: 'bg-danger border-danger',
containerWithHeader: 'border-danger-solid bg-danger-subtle',
},
success: {
icon: 'circle-check',
iconColor: ['text-green-500', 'dark:text-green-400'],
bodyColor: [
'text-green-950',
'dark:text-green-50',
'[&_a]:text-green-800',
'[&_a:hover]:text-green-900',
'dark:[&_a]:text-green-400',
'dark:[&_a:hover]:text-green-300',
'[&_.can-override-bg]:bg-green-500/3',
'[&_.can-override-text]:text-green-800',
'dark:[&_.can-override-text]:text-green-400',
'decoration-green-800/6',
'dark:decoration-green-400/6',
iconColor: 'text-success-subtle contrast-more:text-success',
header: 'bg-success-active',
body: [
'text-neutral-strong',
'[&_a]:text-success',
'[&_a:hover]:text-success-strong',
'[&_.can-override-bg]:bg-success-active',
'[&_.can-override-text]:text-success-strong',
'decoration-success/6',
],
style: ['bg-green-500/2', 'border-green-500/4'],
container: 'bg-success border-success',
containerWithHeader: 'border-success-solid bg-success-subtle',
},
};
@@ -1,13 +1,14 @@
import { DocumentBlockOpenAPI } from '@gitbook/api';
import { DocumentBlockOpenAPI, JSONDocument } from '@gitbook/api';
import { Icon } from '@gitbook/icons';
import { OpenAPIOperation } from '@gitbook/react-openapi';
import React from 'react';
import { fetchOpenAPIBlock } from '@/lib/openapi/fetch';
import { resolveOpenAPIBlock } from '@/lib/openapi/fetch';
import { tcls } from '@/lib/tailwind';
import { BlockProps } from '../Block';
import { PlainCodeBlock } from '../CodeBlock';
import { DocumentView } from '../DocumentView';
import { Heading } from '../Heading';
import './style.css';
@@ -27,13 +28,17 @@ export async function OpenAPI(props: BlockProps<DocumentBlockOpenAPI>) {
async function OpenAPIBody(props: BlockProps<DocumentBlockOpenAPI>) {
const { block, context } = props;
const { data, specUrl, error } = await fetchOpenAPIBlock(block, context.resolveContentRef);
const { data, specUrl, error } = await resolveOpenAPIBlock({
block,
context: { resolveContentRef: context.resolveContentRef },
});
if (error) {
return (
<div className={tcls('hidden')}>
<div className="hidden">
<p>
Error with {error.rootURL}: {error.message}
Error with {specUrl}: {error.message}
</p>
</div>
);
@@ -53,7 +58,15 @@ async function OpenAPIBody(props: BlockProps<DocumentBlockOpenAPI>) {
chevronRight: <Icon icon="chevron-right" />,
plus: <Icon icon="plus" />,
},
CodeBlock: PlainCodeBlock,
renderCodeBlock: (codeProps) => <PlainCodeBlock {...codeProps} />,
renderDocument: (documentProps) => (
<DocumentView
document={documentProps.document as JSONDocument}
context={props.context}
style="space-y-6"
blockStyle="max-w-full"
/>
),
renderHeading: (headingProps) => (
<Heading
document={props.document}
@@ -21,7 +21,7 @@
}
.openapi-deprecated-sunset-date {
@apply font-semibold font-mono;
@apply font-semibold font-mono truncate;
}
.openapi-description.openapi-markdown {
@@ -181,7 +181,7 @@
.openapi-schema-name {
/* To make double click on the property name select only the name,
we disable selection on the parent and re-enable it on the children. */
@apply select-none flex gap-2.5 items-baseline text-sm;
@apply select-none flex gap-x-2.5 items-baseline text-sm flex-wrap;
}
.openapi-schema-name .openapi-deprecated {
@@ -571,11 +571,15 @@
/* Disclosure */
.openapi-disclosure-trigger {
@apply transition-all duration-300 hover:text-tint-strong rounded-2xl border border-tint-subtle px-2.5 py-1 text-[0.813rem] text-tint flex flex-row items-center gap-1.5 -outline-offset-1;
@apply transition-all truncate duration-300 max-w-full hover:text-tint-strong rounded-2xl border border-tint-subtle px-2.5 py-1 text-[0.813rem] text-tint flex flex-row items-center gap-1.5 -outline-offset-1;
}
.openapi-disclosure-trigger span {
@apply truncate;
}
.openapi-disclosure svg {
@apply size-3 transition-transform duration-300;
@apply size-3 shrink-0 transition-transform duration-300;
}
.openapi-disclosure-trigger[aria-expanded='true'] svg {
@@ -14,6 +14,10 @@ import {
colorScale,
type ColorScaleOptions,
DEFAULT_TINT_COLOR,
DEFAULT_HINT_INFO_COLOR,
DEFAULT_HINT_SUCCESS_COLOR,
DEFAULT_HINT_DANGER_COLOR,
DEFAULT_HINT_WARNING_COLOR,
hexToRgb,
} from '@gitbook/colors';
import { IconsProvider, IconStyle } from '@gitbook/icons';
@@ -42,6 +46,7 @@ export async function CustomizationRootLayout(props: {
const tintColor = getTintColor(customization);
const mixColor = getTintMixColor(customization.styling.primaryColor, tintColor);
const sidebarStyles = getSidebarStyles(customization);
const { infoColor, successColor, warningColor, dangerColor } = getSemanticColors(customization);
return (
<html
@@ -85,6 +90,11 @@ export async function CustomizationRootLayout(props: {
)
};
--header-link: ${hexToRgb(customization.header.linkColor?.light ?? colorContrast(tintColor?.light ?? customization.styling.primaryColor.light))};
${generateColorVariable('info', infoColor.light)}
${generateColorVariable('warning', warningColor.light)}
${generateColorVariable('danger', dangerColor.light)}
${generateColorVariable('success', successColor.light)}
}
.dark {
@@ -94,6 +104,11 @@ export async function CustomizationRootLayout(props: {
--header-background: ${hexToRgb(customization.header.backgroundColor?.dark ?? tintColor?.dark ?? customization.styling.primaryColor.dark)};
--header-link: ${hexToRgb(customization.header.linkColor?.dark ?? colorContrast(tintColor?.dark ?? customization.styling.primaryColor.dark))};
${generateColorVariable('info', infoColor.dark, { darkMode: true })}
${generateColorVariable('warning', warningColor.dark, { darkMode: true })}
${generateColorVariable('danger', dangerColor.dark, { darkMode: true })}
${generateColorVariable('success', successColor.dark, { darkMode: true })}
}
`}</style>
</head>
@@ -198,6 +213,45 @@ function getSidebarStyles(
};
}
/**
* Get the semnatic color customization settings.
* If it is a space customization, it will return the default styles.
*/
function getSemanticColors(
customization: CustomizationSettings | SiteCustomizationSettings,
): Pick<
SiteCustomizationSettings['styling'],
'infoColor' | 'successColor' | 'warningColor' | 'dangerColor'
> {
if ('infoColor' in customization.styling) {
return {
infoColor: customization.styling.infoColor,
successColor: customization.styling.successColor,
warningColor: customization.styling.warningColor,
dangerColor: customization.styling.dangerColor,
};
}
return {
infoColor: {
light: DEFAULT_HINT_INFO_COLOR,
dark: DEFAULT_HINT_INFO_COLOR,
},
successColor: {
light: DEFAULT_HINT_SUCCESS_COLOR,
dark: DEFAULT_HINT_SUCCESS_COLOR,
},
warningColor: {
light: DEFAULT_HINT_WARNING_COLOR,
dark: DEFAULT_HINT_WARNING_COLOR,
},
dangerColor: {
light: DEFAULT_HINT_DANGER_COLOR,
dark: DEFAULT_HINT_DANGER_COLOR,
},
};
}
type ColorInput = string;
function generateColorVariable(
name: string,
+44 -49
View File
@@ -208,6 +208,42 @@ export const getUserById = cache({
},
});
/**
* Get the latest version of an OpenAPI spec by its slug.
*/
export const getLatestOpenAPISpecVersionContent = cache({
name: 'api.getLatestOpenApiSpecVersionContent',
tag: (organization, openAPISpec) =>
getAPICacheTag({
tag: 'openapi',
organization,
openAPISpec,
}),
get: async (organizationId: string, slug: string, options: CacheFunctionOptions) => {
try {
const apiCtx = await api();
const response = await apiCtx.client.orgs.getLatestOpenApiSpecVersionContent(
organizationId,
slug,
{
...noCacheFetchOptions,
signal: options.signal,
},
);
return cacheResponse(response, { revalidateBefore: 60 * 60 });
} catch (error) {
if (checkHasErrorCode(error, 404)) {
return {
revalidateBefore: 5,
data: null,
};
}
throw error;
}
},
});
/**
* Resolve a URL to the content to render.
*/
@@ -1066,55 +1102,6 @@ export async function getSpaceContentData(
};
}
/**
* Search content in a space.
*/
export const searchSpaceContent = cache({
name: 'api.searchSpaceContent',
tag: (spaceId) => getAPICacheTag({ tag: 'space', space: spaceId }),
getKeySuffix: getAPIContextId,
get: async (
spaceId: string,
/** The revision ID is used as a cache bust key, to avoid revalidating lot of cache entries by tags */
revisionId: string,
query: string,
options: CacheFunctionOptions,
) => {
const apiCtx = await api();
const response = await apiCtx.client.spaces.searchSpaceContent(
spaceId,
{ query },
{
...noCacheFetchOptions,
signal: options.signal,
},
);
return cacheResponse(response);
},
});
/**
* Search content accross all spaces in a parent (site or collection).
*/
export const searchParentContent = cache({
name: 'api.searchParentContent',
tag: (spaceId) => getAPICacheTag({ tag: 'space', space: spaceId }),
getKeySuffix: getAPIContextId,
get: async (parentId: string, query: string, options: CacheFunctionOptions) => {
const apiCtx = await api();
const response = await apiCtx.client.search.searchContent(
{ query },
{
...noCacheFetchOptions,
signal: options.signal,
},
);
return cacheResponse(response, {
ttl: 60 * 60,
});
},
});
/**
* Search content in a Site or specific SiteSpaces.
*/
@@ -1275,6 +1262,12 @@ export function getAPICacheTag(
| {
tag: 'site';
site: string;
}
// All data related to an OpenAPI spec
| {
tag: 'openapi';
organization: string;
openAPISpec: string;
},
): string {
switch (spec.tag) {
@@ -1298,6 +1291,8 @@ export function getAPICacheTag(
return `site:${spec.site}`;
case 'integration':
return `integration:${spec.integration}`;
case 'openapi':
return `organization:${spec.organization}:openapi:${spec.openAPISpec}`;
default:
assertNever(spec);
}
@@ -1,7 +1,7 @@
import { JSONDocument, ContentRef } from '@gitbook/api';
import { getNodeText } from './document';
import { fetchOpenAPIBlock } from './openapi/fetch';
import { resolveOpenAPIBlock } from './openapi/fetch';
import { ResolvedContentRef } from './references';
export interface DocumentSection {
@@ -38,7 +38,10 @@ export async function getDocumentSections(
}
if (block.type === 'swagger' && block.meta?.id) {
const { data: operation } = await fetchOpenAPIBlock(block, resolveContentRef);
const { data: operation } = await resolveOpenAPIBlock({
block,
context: { resolveContentRef },
});
if (operation) {
sections.push({
id: block.meta.id,
+35 -10
View File
@@ -7,23 +7,48 @@ import { cache, noCacheFetchOptions, CacheFunctionOptions } from '@/lib/cache';
import { enrichFilesystem } from './enrich';
import { ResolvedContentRef } from '../references';
const weakmap = new WeakMap<DocumentBlockOpenAPI, ResolveOpenAPIBlockResult>();
/**
* Fetch an OpenAPI specification for an operation.
* Cache the result of resolving an OpenAPI block.
* It is important because the resolve is called in sections and in the block itself.
*/
export async function fetchOpenAPIBlock(
block: DocumentBlockOpenAPI,
resolveContentRef: (ref: ContentRef) => Promise<ResolvedContentRef | null>,
): Promise<
| { data: OpenAPIOperationData | null; specUrl: string | null; error?: undefined }
export function resolveOpenAPIBlock(args: ResolveOpenAPIBlockArgs): ResolveOpenAPIBlockResult {
if (weakmap.has(args.block)) {
return weakmap.get(args.block)!;
}
const result = baseResolveOpenAPIBlock(args);
weakmap.set(args.block, result);
return result;
}
type ResolveOpenAPIBlockArgs = {
block: DocumentBlockOpenAPI;
context: { resolveContentRef: (ref: ContentRef) => Promise<ResolvedContentRef | null> };
};
type ResolveOpenAPIBlockResult = Promise<
| { error?: undefined; data: OpenAPIOperationData | null; specUrl: string | null }
| { error: OpenAPIParseError; data?: undefined; specUrl?: undefined }
> {
const resolved = block.data.ref ? await resolveContentRef(block.data.ref) : null;
if (!resolved || !block.data.path || !block.data.method) {
>;
/**
* Resolve OpenAPI block.
*/
async function baseResolveOpenAPIBlock(args: ResolveOpenAPIBlockArgs): ResolveOpenAPIBlockResult {
const { context, block } = args;
if (!block.data.path || !block.data.method) {
return { data: null, specUrl: null };
}
const resolved = block.data.ref ? await context.resolveContentRef(block.data.ref) : null;
if (!resolved) {
return { data: null, specUrl: null };
}
try {
const filesystem = await fetchFilesystem(resolved.href);
const filesystem = resolved.openAPIFilesystem ?? (await fetchFilesystem(resolved.href));
const data = await resolveOpenAPIOperation(filesystem, {
path: block.data.path,
method: block.data.method,
+25
View File
@@ -7,6 +7,7 @@ import {
SiteSpace,
Space,
} from '@gitbook/api';
import type { Filesystem } from '@gitbook/openapi-parser';
import assertNever from 'assert-never';
import React from 'react';
@@ -17,6 +18,7 @@ import {
SpaceContentPointer,
getCollection,
getDocument,
getLatestOpenAPISpecVersionContent,
getPageDocument,
getPublishedContentSite,
getReusableContent,
@@ -50,6 +52,8 @@ export interface ResolvedContentRef {
file?: RevisionFile;
/** Resolved reusable content, if the ref points to reusable content on a revision. */
reusableContent?: RevisionReusableContent;
/** Resolve OpenAPI spec filesystem. */
openAPIFilesystem?: Filesystem;
}
export interface ContentRefContext extends PageHrefContext {
@@ -272,6 +276,27 @@ export async function resolveContentRef(
};
}
case 'openapi': {
if (!siteContext) {
return null;
}
const { organizationId } = siteContext;
const openAPISpecVersionContent = await getLatestOpenAPISpecVersionContent(
organizationId,
contentRef.spec,
);
if (!openAPISpecVersionContent) {
return null;
}
return {
href: openAPISpecVersionContent.url,
text: contentRef.spec,
active: false,
openAPIFilesystem: openAPISpecVersionContent.filesystem as Filesystem,
};
}
default:
assertNever(contentRef);
}
+97 -3
View File
@@ -8,6 +8,8 @@ import { ColorCategory, hexToRgb, scale, shadesOfColor } from '@gitbook/colors';
export const shades = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];
export const opacities = [0, 4, 8, 12, 16, 24, 40, 64, 72, 88, 96, 100];
export const semanticColors = ['info', 'warning', 'danger', 'success'];
/**
* Generate a Tailwind color shades from a variable.
*/
@@ -100,6 +102,18 @@ const config: Config = {
'header-background': 'rgb(var(--header-background))',
'header-link': 'rgb(var(--header-link))',
// Add each semantic color
...Object.fromEntries(
semanticColors.map((color) => [color, generateVarShades(color)]),
),
...Object.fromEntries(
semanticColors.map((color) => [
`contrast-${color}`,
generateVarShades(`contrast-${color}`),
]),
),
yellow: generateShades('#f4e28d'),
teal: generateShades('#3f89a1'),
pomegranate: generateShades('#f25b3a'),
@@ -127,6 +141,17 @@ const config: Config = {
ColorCategory.components,
ColorCategory.accents,
]),
// Semantic colors
...Object.fromEntries(
semanticColors.map((color) => [
color,
generateVarShades(color, [
ColorCategory.backgrounds,
ColorCategory.components,
ColorCategory.accents,
]),
]),
),
},
gradientColorStops: {
primary: generateVarShades('primary', [
@@ -144,26 +169,71 @@ const config: Config = {
ColorCategory.components,
ColorCategory.accents,
]),
// Semantic colors
...Object.fromEntries(
semanticColors.map((color) => [
color,
generateVarShades(color, [
ColorCategory.backgrounds,
ColorCategory.components,
ColorCategory.accents,
]),
]),
),
},
borderColor: {
primary: generateVarShades('primary', [ColorCategory.borders]),
tint: generateVarShades('tint', [ColorCategory.borders]),
neutral: generateVarShades('neutral', [ColorCategory.borders]),
primary: generateVarShades('primary', [
ColorCategory.borders,
ColorCategory.accents,
]),
tint: generateVarShades('tint', [ColorCategory.borders, ColorCategory.accents]),
neutral: generateVarShades('neutral', [
ColorCategory.borders,
ColorCategory.accents,
]),
// Semantic colors
...Object.fromEntries(
semanticColors.map((color) => [
color,
generateVarShades(color, [ColorCategory.borders, ColorCategory.accents]),
]),
),
},
ringColor: {
primary: generateVarShades('primary', [ColorCategory.borders]),
tint: generateVarShades('tint', [ColorCategory.borders]),
neutral: generateVarShades('neutral', [ColorCategory.borders]),
// Semantic colors
...Object.fromEntries(
semanticColors.map((color) => [
color,
generateVarShades(color, [ColorCategory.borders]),
]),
),
},
outlineColor: {
primary: generateVarShades('primary', [ColorCategory.borders]),
tint: generateVarShades('tint', [ColorCategory.borders]),
neutral: generateVarShades('neutral', [ColorCategory.borders]),
// Semantic colors
...Object.fromEntries(
semanticColors.map((color) => [
color,
generateVarShades(color, [ColorCategory.borders]),
]),
),
},
boxShadowColor: {
primary: generateVarShades('primary', [ColorCategory.borders]),
tint: generateVarShades('tint', [ColorCategory.borders]),
neutral: generateVarShades('neutral', [ColorCategory.borders]),
// Semantic colors
...Object.fromEntries(
semanticColors.map((color) => [
color,
generateVarShades(color, [ColorCategory.borders]),
]),
),
},
textColor: {
primary: generateVarShades('primary', [ColorCategory.text]),
@@ -181,6 +251,18 @@ const config: Config = {
ColorCategory.backgrounds,
ColorCategory.accents,
]),
...Object.fromEntries(
semanticColors.flatMap((color) => [
[color, generateVarShades(color, [ColorCategory.text])],
[
`contrast-${color}`,
generateVarShades(`contrast-${color}`, [
ColorCategory.backgrounds,
ColorCategory.accents,
]),
],
]),
),
},
textDecorationColor: {
primary: generateVarShades('primary', [ColorCategory.text]),
@@ -198,6 +280,18 @@ const config: Config = {
ColorCategory.backgrounds,
ColorCategory.accents,
]),
...Object.fromEntries(
semanticColors.flatMap((color) => [
[color, generateVarShades(color, [ColorCategory.text])],
[
`contrast-${color}`,
generateVarShades(`contrast-${color}`, [
ColorCategory.backgrounds,
ColorCategory.accents,
]),
],
]),
),
},
animation: {
present: 'present .5s ease-out both',
+1 -3
View File
@@ -13,9 +13,7 @@
"sideEffects": false,
"dependencies": {
"@scalar/openapi-parser": "^0.10.4",
"@scalar/openapi-types": "^0.1.6",
"swagger2openapi": "^7.0.8",
"yaml": "1.10.2"
"@scalar/openapi-types": "^0.1.6"
},
"devDependencies": {
"@tsconfig/strictest": "^2.0.5",
+5
View File
@@ -42,6 +42,11 @@ export interface OpenAPICustomOperationProperties {
* Description in HTML format.
*/
'x-gitbook-description-html'?: string;
/**
* Description in Document format.
*/
'x-gitbook-description-document'?: object;
}
/**
+1 -1
View File
@@ -11,6 +11,6 @@ describe('#convertOpenAPIV2ToOpenAPIV3', () => {
});
// Ensure the structure returned is not recursive (not dereferenced).
JSON.stringify(schema);
expect(schema[0]!.specification.openapi).toBe('3.0.0');
expect(schema[0]!.specification.openapi).toBe('3.1.1');
});
});
+13 -50
View File
@@ -1,10 +1,8 @@
import YAML from 'yaml';
import swagger2openapi, { type ConvertOutputOptions } from 'swagger2openapi';
import { OpenAPIParseError } from './error';
import { parseOpenAPIV3 } from './v3';
import type { AnyApiDefinitionFormat } from '@scalar/openapi-parser';
import type { Filesystem, OpenAPIV3xDocument } from './types';
import { upgrade } from '@scalar/openapi-parser';
/**
* Convert a Swagger 2.0 schema to an OpenAPI 3.0 schema.
@@ -20,57 +18,22 @@ export async function convertOpenAPIV2ToOpenAPIV3(input: {
rootURL: string | null;
}): Promise<Filesystem<OpenAPIV3xDocument>> {
const { value, rootURL } = input;
// In this case we want the raw value to be able to convert it.
const schema = typeof value === 'string' ? rawParseOpenAPI({ value, rootURL }) : value;
try {
// @ts-expect-error Types are incompatible between the two libraries
const convertResult = (await swagger2openapi.convertObj(schema, {
resolve: false,
resolveInternal: false,
laxDefaults: true,
laxurls: true,
lint: false,
prevalidate: false,
anchors: true,
patch: true,
})) as ConvertOutputOptions;
return parseOpenAPIV3({ rootURL, value: convertResult.openapi });
} catch (error) {
if (error instanceof Error && error.name === 'S2OError') {
throw new OpenAPIParseError('Failed to convert Swagger 2.0 to OpenAPI 3.0', {
code: 'v2-conversion',
rootURL,
cause: error,
});
} else {
throw error;
}
}
}
/**
* Parse the config file from a raw string.
* Useful to get the raw object from a file.
*/
function rawParseOpenAPI(input: { value: string; rootURL: string | null }): unknown {
const { value, rootURL } = input;
// Try with JSON
try {
return JSON.parse(value);
} catch (jsonError) {
const upgradeResult = (() => {
try {
// Try with YAML
return YAML.parse(value);
} catch (yamlError) {
if (yamlError instanceof Error && yamlError.name.startsWith('YAML')) {
throw new OpenAPIParseError('Failed to parse YAML: ' + yamlError.message, {
code: 'yaml-parse',
return upgrade(value);
} catch (error) {
if (error instanceof Error) {
throw new OpenAPIParseError('Failed to convert Swagger 2.0 to OpenAPI 3.1.1', {
code: 'v2-conversion',
rootURL,
cause: error,
});
}
throw yamlError;
throw error;
}
}
})();
return parseOpenAPIV3({ ...input, rootURL, value: upgradeResult.specification });
}
File diff suppressed because one or more lines are too long
@@ -57,11 +57,7 @@ export function OpenAPICodeSample(props: {
data.path +
(searchParams.size ? `?${searchParams.toString()}` : ''),
method: data.method,
body: requestBodyContent
? generateMediaTypeExample(requestBodyContent[1], {
omitEmptyAndOptionalProperties: true,
})
: undefined,
body: requestBodyContent ? generateMediaTypeExample(requestBodyContent[1]) : undefined,
headers: {
...getSecurityHeaders(data.securities),
...headersObject,
@@ -76,7 +72,10 @@ export function OpenAPICodeSample(props: {
const autoCodeSamples = codeSampleGenerators.map((generator) => ({
key: `default-${generator.id}`,
label: generator.label,
body: <context.CodeBlock code={generator.generate(input)} syntax={generator.syntax} />,
body: context.renderCodeBlock({
code: generator.generate(input),
syntax: generator.syntax,
}),
}));
// Use custom samples if defined
@@ -99,7 +98,10 @@ export function OpenAPICodeSample(props: {
.map((sample) => ({
key: `redocly-${sample.lang}`,
label: sample.label,
body: <context.CodeBlock code={sample.source} syntax={sample.lang} />,
body: context.renderCodeBlock({
code: sample.source,
syntax: sample.lang,
}),
}));
}
});
@@ -7,6 +7,7 @@ import { OpenAPISpec } from './OpenAPISpec';
import type { OpenAPIClientContext, OpenAPIContextProps, OpenAPIOperationData } from './types';
import { OpenAPIPath } from './OpenAPIPath';
import { resolveDescription } from './utils';
import { OpenAPICustomOperationProperties, OpenAPIV3 } from '@gitbook/openapi-parser';
/**
* Display an interactive OpenAPI operation.
@@ -25,11 +26,9 @@ export function OpenAPIOperation(props: {
blockKey: context.blockKey,
};
const description = resolveDescription(operation);
return (
<div className={clsx('openapi-operation', className)}>
<div className="openapi-summary">
<div className="openapi-summary" id={operation.summary ? undefined : context.id}>
{operation.summary
? context.renderHeading({
deprecated: operation.deprecated ?? false,
@@ -49,11 +48,7 @@ export function OpenAPIOperation(props: {
{`.`}
</div>
) : null}
{description ? (
<div className="openapi-intro">
<Markdown className="openapi-description" source={description} />
</div>
) : null}
<OpenAPIOperationDescription operation={operation} context={context} />
<OpenAPIPath data={data} context={context} />
<OpenAPISpec data={data} context={clientContext} />
</div>
@@ -67,3 +62,30 @@ export function OpenAPIOperation(props: {
</div>
);
}
function OpenAPIOperationDescription(props: {
operation: OpenAPIV3.OperationObject<OpenAPICustomOperationProperties>;
context: OpenAPIContextProps;
}) {
const { operation } = props;
if (operation['x-gitbook-description-document']) {
return (
<div className="openapi-intro">
{props.context.renderDocument({
document: operation['x-gitbook-description-document'],
})}
</div>
);
}
const description = resolveDescription(operation);
if (!description) {
return null;
}
return (
<div className="openapi-intro">
<Markdown className="openapi-description" source={description} />
</div>
);
}
@@ -5,6 +5,7 @@ import { checkIsReference, createStateKey, resolveDescription } from './utils';
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
import { InteractiveSection } from './InteractiveSection';
import { json2xml } from './json2xml';
import { stringifyOpenAPI } from './stringifyOpenAPI';
/**
* Display an example of the response content.
@@ -208,7 +209,7 @@ function OpenAPIExample(props: {
return <OpenAPIEmptyResponseExample />;
}
return <context.CodeBlock code={code} syntax={syntax} />;
return context.renderCodeBlock({ code, syntax });
}
function stringifyExample(args: { example: OpenAPIV3.ExampleObject; xml: boolean }): string | null {
@@ -226,7 +227,7 @@ function stringifyExample(args: { example: OpenAPIV3.ExampleObject; xml: boolean
return json2xml(example.value);
}
return JSON.stringify(example.value, null, 2);
return stringifyOpenAPI(example.value, null, 2);
}
/**
+78 -67
View File
@@ -47,23 +47,6 @@ export function OpenAPISchemaProperty(
? null
: getSchemaAlternatives(schema, new Set(circularRefs.keys()));
if ((properties && properties.length > 0) || schema.type === 'object') {
return (
<InteractiveSection id={id} className={clsx('openapi-schema', className)}>
<OpenAPISchemaPresentation {...props} />
{properties && properties.length > 0 ? (
<OpenAPIDisclosure context={context}>
<OpenAPISchemaProperties
properties={properties}
circularRefs={circularRefs}
context={context}
/>
</OpenAPIDisclosure>
) : null}
</InteractiveSection>
);
}
if (alternatives?.[0]?.length) {
return (
<InteractiveSection id={id} className={clsx('openapi-schema', className)}>
@@ -76,6 +59,29 @@ export function OpenAPISchemaProperty(
context={context}
/>
))}
{parentCircularRef ? (
<OpenAPISchemaCircularRef id={parentCircularRef} schema={schema} />
) : null}
</InteractiveSection>
);
}
if ((properties && properties.length > 0) || schema.type === 'object') {
return (
<InteractiveSection id={id} className={clsx('openapi-schema', className)}>
<OpenAPISchemaPresentation {...props} />
{properties && properties.length > 0 ? (
<OpenAPIDisclosure context={context} label={getDisclosureLabel(schema)}>
<OpenAPISchemaProperties
properties={properties}
circularRefs={circularRefs}
context={context}
/>
</OpenAPIDisclosure>
) : null}
{parentCircularRef ? (
<OpenAPISchemaCircularRef id={parentCircularRef} schema={schema} />
) : null}
</InteractiveSection>
);
}
@@ -166,16 +172,24 @@ function OpenAPISchemaAlternative(props: {
const { schema, circularRefs, context } = props;
const id = useId();
const subProperties = getSchemaProperties(schema);
const description = resolveDescription(schema);
return (
<OpenAPIDisclosure context={context}>
<OpenAPISchemaProperties
id={id}
properties={subProperties ?? [{ schema }]}
circularRefs={subProperties ? new Map(circularRefs).set(schema, id) : circularRefs}
context={context}
/>
</OpenAPIDisclosure>
<>
{description ? (
<Markdown source={description} className="openapi-schema-description" />
) : null}
<OpenAPIDisclosure context={context} label={getDisclosureLabel(schema)}>
<OpenAPISchemaProperties
id={id}
properties={subProperties ?? [{ schema }]}
circularRefs={
subProperties ? new Map(circularRefs).set(schema, id) : circularRefs
}
context={context}
/>
</OpenAPIDisclosure>
</>
);
}
@@ -219,7 +233,7 @@ export function OpenAPISchemaPresentation(props: OpenAPISchemaPropertyEntry) {
const shouldDisplayExample = (schema: OpenAPIV3.SchemaObject): boolean => {
return (
typeof schema.example === 'string' ||
(typeof schema.example === 'string' && !!schema.example) ||
typeof schema.example === 'number' ||
typeof schema.example === 'boolean' ||
(Array.isArray(schema.example) && schema.example.length > 0) ||
@@ -234,10 +248,10 @@ export function OpenAPISchemaPresentation(props: OpenAPISchemaPropertyEntry) {
return (
<div className="openapi-schema-presentation">
<OpenAPISchemaName
schema={schema}
type={getSchemaTitle(schema)}
propertyName={propertyName}
required={required}
deprecated={schema.deprecated}
/>
{schema['x-deprecated-sunset'] ? (
<div className="openapi-deprecated-sunset openapi-schema-description openapi-markdown">
@@ -252,12 +266,7 @@ export function OpenAPISchemaPresentation(props: OpenAPISchemaPropertyEntry) {
) : null}
{shouldDisplayExample(schema) ? (
<div className="openapi-schema-example">
Example:{' '}
<code>
{typeof schema.example === 'string'
? schema.example
: stringifyOpenAPI(schema.example)}
</code>
Example: <code>{formatExample(schema.example)}</code>
</div>
) : null}
{schema.pattern ? (
@@ -276,17 +285,6 @@ export function OpenAPISchemaPresentation(props: OpenAPISchemaPropertyEntry) {
* Get the sub-properties of a schema.
*/
function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISchemaPropertyEntry[] {
if (schema.allOf) {
return schema.allOf.reduce((acc, subSchema) => {
const properties = getSchemaProperties(subSchema) ?? [
{
schema: subSchema,
},
];
return [...acc, ...properties];
}, [] as OpenAPISchemaPropertyEntry[]);
}
// check array AND schema.items as this is sometimes null despite what the type indicates
if (schema.type === 'array' && !!schema.items) {
const items = schema.items;
@@ -295,6 +293,11 @@ function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISche
return itemProperties;
}
// If the items are a primitive type, we don't need to display them
if (['string', 'number', 'boolean', 'integer'].includes(items.type) && !items.enum) {
return null;
}
return [
{
propertyName: 'items',
@@ -351,8 +354,7 @@ export function getSchemaAlternatives(
}
if (schema.allOf) {
// allOf is managed in `getSchemaProperties`
return null;
return [flattenAlternatives('allOf', schema.allOf, downAncestors), schema.discriminator];
}
return null;
@@ -378,11 +380,6 @@ export function getSchemaTitle(
/** If the title is inferred in a oneOf with discriminator, we can use it to optimize the title */
discriminator?: OpenAPIV3.DiscriminatorObject,
): string {
if (schema.title) {
// If the schema has a title, use it
return schema.title;
}
// Try using the discriminator
if (discriminator?.propertyName && schema.properties) {
const discriminatorProperty = schema.properties[discriminator.propertyName];
@@ -419,21 +416,35 @@ export function getSchemaTitle(
type = 'not';
}
if (schema.minimum || schema.minLength) {
type += ` · min: ${schema.minimum || schema.minLength}`;
}
if (schema.maximum || schema.maxLength) {
type += ` · max: ${schema.maximum || schema.maxLength}`;
}
if (schema.default) {
type += ` · default: ${schema.default}`;
}
if (schema.nullable) {
type = `${type} | nullable`;
}
return type;
}
function getDisclosureLabel(schema: OpenAPIV3.SchemaObject): string | undefined {
if (schema.type === 'array' && !!schema.items) {
if (schema.items.oneOf) {
return 'available items';
}
// Fallback to "child attributes" for enums and objects
if (schema.items.enum || schema.items.type === 'object') {
return;
}
return schema.items.title ?? schema.title ?? getSchemaTitle(schema.items);
}
return schema.title;
}
function formatExample(example: any): string {
if (typeof example === 'string') {
return example
.replace(/\n/g, ' ') // Replace newlines with spaces
.replace(/\s+/g, ' ') // Collapse multiple spaces/newlines into a single space
.replace(/([\{\}:,])\s+/g, '$1 ') // Ensure a space after {, }, :, and ,
.replace(/\s+([\{\}:,])/g, ' $1') // Ensure a space before {, }, :, and ,
.trim();
}
return stringifyOpenAPI(example);
}
@@ -1,8 +1,10 @@
import { OpenAPIV3 } from '@gitbook/openapi-parser';
interface OpenAPISchemaNameProps {
schema?: OpenAPIV3.SchemaObject;
propertyName?: string | JSX.Element;
required?: boolean;
type?: string;
deprecated?: boolean;
}
/**
@@ -10,18 +12,48 @@ interface OpenAPISchemaNameProps {
* It includes the property name, type, required and deprecated status.
*/
export function OpenAPISchemaName(props: OpenAPISchemaNameProps): JSX.Element {
const { type, propertyName, required, deprecated } = props;
const { schema, type, propertyName, required } = props;
const additionalItems = schema && getAdditionalItems(schema);
return (
<div className="openapi-schema-name">
{propertyName ? (
<span data-deprecated={deprecated} className="openapi-schema-propertyname">
<span data-deprecated={schema?.deprecated} className="openapi-schema-propertyname">
{propertyName}
</span>
) : null}
{type ? <span className="openapi-schema-type">{type}</span> : null}
<span>
{type ? <span className="openapi-schema-type">{type}</span> : null}
{additionalItems ? (
<span className="openapi-schema-type">{additionalItems}</span>
) : null}
</span>
{required ? <span className="openapi-schema-required">required</span> : null}
{deprecated ? <span className="openapi-deprecated">Deprecated</span> : null}
{schema?.deprecated ? <span className="openapi-deprecated">Deprecated</span> : null}
</div>
);
}
function getAdditionalItems(schema: OpenAPIV3.SchemaObject): string {
let additionalItems = '';
if (schema.minimum || schema.minLength) {
additionalItems += ` · min: ${schema.minimum || schema.minLength}`;
}
if (schema.maximum || schema.maxLength) {
additionalItems += ` · max: ${schema.maximum || schema.maxLength}`;
}
// If the schema has a default value, we display it
if (typeof schema.default !== 'undefined') {
additionalItems += ` · default: ${schema.default}`;
}
if (schema.nullable) {
additionalItems = ` | nullable`;
}
return additionalItems;
}
+8 -2
View File
@@ -74,11 +74,17 @@ export function OpenAPITabs(
const tabFromState = syncedTabs.get(stateKey);
if (!items.some((item) => item.key === tabFromState?.key)) {
return;
return setSelectedTab(defaultTab);
}
if (tabFromState && tabFromState?.key !== selectedTab?.key) {
setSelectedTab(tabFromState);
const tabFromItems = items.find((item) => item.key === tabFromState.key);
if (!tabFromItems) {
return;
}
setSelectedTab(tabFromItems);
}
}
}, [isVisible, stateKey, syncedTabs, selectedTabKey]);
+594 -2
View File
@@ -1,5 +1,5 @@
import { it, expect } from 'bun:test';
import { parseHostAndPath } from './code-samples';
import { it, expect, describe } from 'bun:test';
import { codeSampleGenerators, CodeSampleInput, parseHostAndPath } from './code-samples';
it('should parse host and path on url strings properly', () => {
const testUrls = [
@@ -49,3 +49,595 @@ it('should parse host and path on url strings properly', () => {
},
]);
});
describe('curL code sample generator', () => {
const generator = codeSampleGenerators.find((g) => g.id === 'curl');
expect(generator).toBeDefined();
it('should format application/x-www-form-urlencoded body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: {
key: 'value',
},
};
const output = generator!.generate(input);
expect(output).toBe(
"curl -L \\\n --url 'https://example.com/path' \\\n --header 'Content-Type: application/x-www-form-urlencoded' \\\n --data 'key=value'",
);
});
it('should format application/json body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'application/json',
},
body: {
key: 'value',
},
};
const output = generator!.generate(input);
expect(output).toBe(
"curl -L \\\n --url 'https://example.com/path' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"key\": \"value\"\n }'",
);
});
it('should format application/xml body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'application/xml',
},
body: '<key>value</key>',
};
const output = generator!.generate(input);
expect(output).toBe(
"curl -L \\\n --url 'https://example.com/path' \\\n --header 'Content-Type: application/xml' \\\n --data-binary $'<key>value</key>'",
);
});
it('should format application/graphql body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'application/graphql',
},
body: '{ key }',
};
const output = generator!.generate(input);
expect(output).toBe(
"curl -L \\\n --url 'https://example.com/path' \\\n --header 'Content-Type: application/json' \\\n --data '\"{ key }\"'",
);
});
it('should format text/csv body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'text/csv',
},
body: 'key,value',
};
const output = generator!.generate(input);
expect(output).toBe(
"curl -L \\\n --url 'https://example.com/path' \\\n --header 'Content-Type: text/csv' \\\n --data-binary $'key,value'",
);
});
it('should format application/pdf body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'application/pdf',
},
body: 'file',
};
const output = generator!.generate(input);
expect(output).toBe(
"curl -L \\\n --url 'https://example.com/path' \\\n --header 'Content-Type: application/pdf' \\\n --data-binary '@file'",
);
});
it('should format text/plain body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'text/plain',
},
body: 'value',
};
const output = generator!.generate(input);
expect(output).toBe(
"curl -L \\\n --url 'https://example.com/path' \\\n --header 'Content-Type: text/plain' \\\n --data 'value'",
);
});
it('should format multipart/form-data body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'multipart/form-data',
},
body: {
key: 'value',
},
};
const output = generator!.generate(input);
expect(output).toBe(
"curl -L \\\n --url 'https://example.com/path' \\\n --header 'Content-Type: multipart/form-data' \\\n --form 'key=value'",
);
});
});
describe('javascript code sample generator', () => {
const generator = codeSampleGenerators.find((g) => g.id === 'javascript');
expect(generator).toBeDefined();
it('should format application/x-www-form-urlencoded body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: {
key: 'value',
},
};
const output = generator!.generate(input);
expect(output).toBe(
'const params = new URLSearchParams();\n\nparams.append("key", "value");\n\nconst response = await fetch(\'https://example.com/path\', {\n method: \'GET\',\n headers: {\n "Content-Type": "application/x-www-form-urlencoded"\n },\n body: params.toString()\n});\n\nconst data = await response.json();',
);
});
it('should format application/json body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'application/json',
},
body: {
key: 'value',
},
};
const output = generator!.generate(input);
expect(output).toBe(
'const response = await fetch(\'https://example.com/path\', {\n method: \'GET\',\n headers: {\n "Content-Type": "application/json"\n },\n body: JSON.stringify({\n "key": "value"\n })\n});\n\nconst data = await response.json();',
);
});
it('should format application/xml body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'application/xml',
},
body: '<key>value</key>',
};
const output = generator!.generate(input);
expect(output).toBe(
'const xml = `\n <key>value</key>`;\n\nconst response = await fetch(\'https://example.com/path\', {\n method: \'GET\',\n headers: {\n "Content-Type": "application/xml"\n },\n body: xml\n});\n\nconst data = await response.json();',
);
});
it('should format application/graphql body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'application/graphql',
},
body: '{ key }',
};
const output = generator!.generate(input);
expect(output).toBe(
'const query = `\n { key }`;\n\nconst response = await fetch(\'https://example.com/path\', {\n method: \'GET\',\n headers: {\n "Content-Type": "application/graphql"\n },\n body: JSON.stringify(query)\n});\n\nconst data = await response.json();',
);
});
it('should format text/csv body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'text/csv',
},
body: 'key,value',
};
const output = generator!.generate(input);
expect(output).toBe(
'const csv = `\n key,value`;\n\nconst response = await fetch(\'https://example.com/path\', {\n method: \'GET\',\n headers: {\n "Content-Type": "text/csv"\n },\n body: csv\n});\n\nconst data = await response.json();',
);
});
it('should format application/pdf body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'application/pdf',
},
body: 'file',
};
const output = generator!.generate(input);
expect(output).toBe(
'const formData = new FormData();\n\nformData.append("file", "file");\n\nconst response = await fetch(\'https://example.com/path\', {\n method: \'GET\',\n headers: {\n "Content-Type": "application/pdf"\n },\n body: formData\n});\n\nconst data = await response.json();',
);
});
it('should format text/plain body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'text/plain',
},
body: 'value',
};
const output = generator!.generate(input);
expect(output).toBe(
'const response = await fetch(\'https://example.com/path\', {\n method: \'GET\',\n headers: {\n "Content-Type": "text/plain"\n },\n body: "value"\n});\n\nconst data = await response.json();',
);
});
it('should format multipart/form-data body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'multipart/form-data',
},
body: {
key: 'value',
},
};
const output = generator!.generate(input);
expect(output).toBe(
'const formData = new FormData();\n\nformData.append("key", "value");\n\nconst response = await fetch(\'https://example.com/path\', {\n method: \'GET\',\n headers: {\n "Content-Type": "multipart/form-data"\n },\n body: formData\n});\n\nconst data = await response.json();',
);
});
});
describe('python code sample generator', () => {
const generator = codeSampleGenerators.find((g) => g.id === 'python');
expect(generator).toBeDefined();
it('should format application/x-www-form-urlencoded body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: {
key: 'value',
},
};
const output = generator!.generate(input);
expect(output).toBe(
'import requests\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"application/x-www-form-urlencoded"},\n data={"key":"value"}\n)\n\ndata = response.json()',
);
});
it('should format application/json body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'application/json',
},
body: {
key: 'value',
},
};
const output = generator!.generate(input);
expect(output).toBe(
'import requests\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"application/json"},\n data={"key":"value"}\n)\n\ndata = response.json()',
);
});
it('should format application/xml body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'application/xml',
},
body: '<key>value</key>',
};
const output = generator!.generate(input);
expect(output).toBe(
'import requests\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"application/xml"},\n data="<key>value</key>"\n)\n\ndata = response.json()',
);
});
it('should format application/graphql body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'application/graphql',
},
body: '{ key }',
};
const output = generator!.generate(input);
expect(output).toBe(
'import requests\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"application/graphql"},\n data="{ key }"\n)\n\ndata = response.json()',
);
});
it('should format text/csv body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'text/csv',
},
body: 'key,value',
};
const output = generator!.generate(input);
expect(output).toBe(
'import requests\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"text/csv"},\n data="key,value"\n)\n\ndata = response.json()',
);
});
it('should format application/pdf body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'application/pdf',
},
body: 'file',
};
const output = generator!.generate(input);
expect(output).toBe(
'import requests\n\nfiles = {\n "file": "file",\n}\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"application/pdf"},\n files=files\n)\n\ndata = response.json()',
);
});
it('should format text/plain body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'text/plain',
},
body: 'value',
};
const output = generator!.generate(input);
expect(output).toBe(
'import requests\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"text/plain"},\n data="value"\n)\n\ndata = response.json()',
);
});
it('should format multipart/form-data body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'multipart/form-data',
},
body: {
key: 'value',
},
};
const output = generator!.generate(input);
expect(output).toBe(
'import requests\n\nfiles = {\n "key": "value",\n}\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"multipart/form-data"},\n files=files\n)\n\ndata = response.json()',
);
});
});
describe('http code sample generator', () => {
const generator = codeSampleGenerators.find((g) => g.id === 'http');
expect(generator).toBeDefined();
it('should format application/x-www-form-urlencoded body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: {
key: 'value',
},
};
const output = generator!.generate(input);
expect(output).toBe(
'GET /path HTTP/1.1\nHost: example.com\nContent-Type: application/x-www-form-urlencoded\nContent-Length: 15\nAccept: */*\n\n"key=value"',
);
});
it('should format application/json body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'application/json',
},
body: {
key: 'value',
},
};
const output = generator!.generate(input);
expect(output).toBe(
'GET /path HTTP/1.1\nHost: example.com\nContent-Type: application/json\nContent-Length: 15\nAccept: */*\n\n{\n "key": "value"\n}',
);
});
it('should format application/xml body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'application/xml',
},
body: '<key>value</key>',
};
const output = generator!.generate(input);
expect(output).toBe(
'GET /path HTTP/1.1\nHost: example.com\nContent-Type: application/xml\nContent-Length: 18\nAccept: */*\n\n"<key>value</key>"',
);
});
it('should format application/graphql body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'application/graphql',
},
body: '{ key }',
};
const output = generator!.generate(input);
expect(output).toBe(
'GET /path HTTP/1.1\nHost: example.com\nContent-Type: application/graphql\nContent-Length: 9\nAccept: */*\n\n"{ key }"',
);
});
it('should format text/csv body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'text/csv',
},
body: 'key,value',
};
const output = generator!.generate(input);
expect(output).toBe(
'GET /path HTTP/1.1\nHost: example.com\nContent-Type: text/csv\nContent-Length: 11\nAccept: */*\n\n"key,value"',
);
});
it('should format application/pdf body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'application/pdf',
},
body: 'file',
};
const output = generator!.generate(input);
expect(output).toBe(
'GET /path HTTP/1.1\nHost: example.com\nContent-Type: application/pdf\nContent-Length: 6\nAccept: */*\n\n"file"',
);
});
it('should format text/plain body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'text/plain',
},
body: 'value',
};
const output = generator!.generate(input);
expect(output).toBe(
'GET /path HTTP/1.1\nHost: example.com\nContent-Type: text/plain\nContent-Length: 7\nAccept: */*\n\n"value"',
);
});
it('should format multipart/form-data body properly', () => {
const input: CodeSampleInput = {
method: 'GET',
url: 'https://example.com/path',
headers: {
'Content-Type': 'multipart/form-data',
},
body: {
key: 'value',
},
};
const output = generator!.generate(input);
expect(output).toBe(
'GET /path HTTP/1.1\nHost: example.com\nContent-Type: multipart/form-data\nContent-Length: 15\nAccept: */*\n\n{\n "key": "value"\n}',
);
});
});
+231 -10
View File
@@ -1,3 +1,13 @@
import {
isFormData,
isPDF,
isFormUrlEncoded,
isText,
isXML,
isCSV,
isGraphQL,
isPlainObject,
} from './contentTypeChecks';
import { stringifyOpenAPI } from './stringifyOpenAPI';
export interface CodeSampleInput {
@@ -30,14 +40,27 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [
lines.push(`--url '${url}'`);
if (headers) {
if (body) {
const bodyContent = BodyGenerators.getCurlBody(body, headers);
if (bodyContent) {
body = bodyContent.body;
headers = bodyContent.headers;
}
}
if (headers && Object.keys(headers).length > 0) {
Object.entries(headers).forEach(([key, value]) => {
lines.push(`--header '${key}: ${value}'`);
});
}
if (body && Object.keys(body).length > 0) {
lines.push(`--data '${stringifyOpenAPI(body)}'`);
if (body) {
if (Array.isArray(body)) {
lines.push(...body);
} else {
lines.push(body);
}
}
return lines.map((line, index) => (index > 0 ? indent(line, 2) : line)).join(separator);
@@ -50,18 +73,29 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [
generate: ({ method, url, headers, body }) => {
let code = '';
if (body) {
const lines = BodyGenerators.getJavaScriptBody(body, headers);
if (lines) {
// add the generated code to the top
code += lines.code;
body = lines.body;
headers = lines.headers;
}
}
code += `const response = await fetch('${url}', {
method: '${method.toUpperCase()}',\n`;
if (headers) {
if (headers && Object.keys(headers).length > 0) {
code += indent(`headers: ${stringifyOpenAPI(headers, null, 2)},\n`, 4);
}
if (body) {
code += indent(`body: JSON.stringify(${stringifyOpenAPI(body, null, 2)}),\n`, 4);
code += indent(`body: ${body}\n`, 4);
}
code += `});\n`;
code += `});\n\n`;
code += `const data = await response.json();`;
return code;
@@ -73,15 +107,34 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [
syntax: 'python',
generate: ({ method, url, headers, body }) => {
let code = 'import requests\n\n';
if (body) {
const lines = BodyGenerators.getPythonBody(body, headers);
// add the generated code to the top
if (lines) {
code += lines.code;
body = lines.body;
headers = lines.headers;
}
}
code += `response = requests.${method.toLowerCase()}(\n`;
code += indent(`"${url}",\n`, 4);
if (headers) {
if (headers && Object.keys(headers).length > 0) {
code += indent(`headers=${stringifyOpenAPI(headers)},\n`, 4);
}
if (body) {
code += indent(`json=${stringifyOpenAPI(body)}\n`, 4);
if (body === 'files') {
code += indent(`files=${body}\n`, 4);
} else {
code += indent(`data=${stringifyOpenAPI(body)}\n`, 4);
}
}
code += ')\n';
code += ')\n\n';
code += `data = response.json()`;
return code;
},
@@ -99,6 +152,12 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [
// handle unicode chars with a text encoder
const encoder = new TextEncoder();
const bodyString = BodyGenerators.getHTTPBody(body, headers);
if (bodyString) {
body = bodyString;
}
headers = {
...headers,
'Content-Length': encoder.encode(bodyContent).length.toString(),
@@ -117,7 +176,7 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [
.join('\n') + '\n'
: '';
const bodyString = body ? `\n${stringifyOpenAPI(body, null, 2)}` : '';
const bodyString = body ? `\n${body}` : '';
const httpRequest = `${method.toUpperCase()} ${decodeURI(path)} HTTP/1.1
Host: ${host}
@@ -157,3 +216,165 @@ export function parseHostAndPath(url: string) {
return { host, path };
}
}
// Body Generators
const BodyGenerators = {
getCurlBody(body: any, headers?: Record<string, string>) {
if (!body || !headers) return undefined;
// Copy headers to avoid mutating the original object
const headersCopy = { ...headers };
const contentType: string = headersCopy['Content-Type'] || '';
if (isFormData(contentType)) {
body = isPlainObject(body)
? Object.entries(body).map(([key, value]) => `--form '${key}=${String(value)}'`)
: `--form 'file=@${body}'`;
} else if (isFormUrlEncoded(contentType)) {
body = isPlainObject(body)
? `--data '${Object.entries(body)
.map(([key, value]) => `${key}=${String(value)}`)
.join('&')}'`
: String(body);
} else if (isText(contentType)) {
body = `--data '${String(body).replace(/"/g, '')}'`;
} else if (isXML(contentType) || isCSV(contentType)) {
// We use --data-binary to avoid cURL converting newlines to \r\n
body = `--data-binary $'${stringifyOpenAPI(body).replace(/"/g, '')}'`;
} else if (isGraphQL(contentType)) {
body = `--data '${stringifyOpenAPI(body)}'`;
// Set Content-Type to application/json for GraphQL, recommended by GraphQL spec
headersCopy['Content-Type'] = 'application/json';
} else if (isPDF(contentType)) {
// We use --data-binary to avoid cURL converting newlines to \r\n
body = `--data-binary '@${String(body)}'`;
} else {
body = `--data '${stringifyOpenAPI(body, null, 2)}'`;
}
return {
body,
headers: headersCopy,
};
},
getJavaScriptBody: (body: any, headers?: Record<string, string>) => {
if (!body || !headers) return;
let code = '';
// Copy headers to avoid mutating the original object
const headersCopy = { ...headers };
const contentType: string = headersCopy['Content-Type'] || '';
// Use FormData for file uploads
if (isFormData(contentType)) {
code += 'const formData = new FormData();\n\n';
if (isPlainObject(body)) {
Object.entries(body).forEach(([key, value]) => {
code += `formData.append("${key}", "${String(value)}");\n`;
});
} else if (typeof body === 'string') {
code += `formData.append("file", "${body}");\n`;
}
code += '\n';
body = 'formData';
} else if (isFormUrlEncoded(contentType)) {
// Use URLSearchParams for form-urlencoded data
code += 'const params = new URLSearchParams();\n\n';
if (isPlainObject(body)) {
Object.entries(body).forEach(([key, value]) => {
code += `params.append("${key}", "${String(value)}");\n`;
});
}
code += '\n';
body = 'params.toString()';
} else if (isGraphQL(contentType)) {
if (isPlainObject(body)) {
Object.entries(body).forEach(([key, value]) => {
code += `const ${key} = \`\n${indent(String(value), 4)}\`;\n\n`;
});
body = `JSON.stringify({ ${Object.keys(body).join(', ')} })`;
// Set Content-Type to application/json for GraphQL, recommended by GraphQL spec
headersCopy['Content-Type'] = 'application/json';
} else {
code += `const query = \`\n${indent(String(body), 4)}\`;\n\n`;
body = 'JSON.stringify(query)';
}
} else if (isCSV(contentType)) {
code += 'const csv = `\n';
code += indent(String(body), 4);
code += '`;\n\n';
body = 'csv';
} else if (isPDF(contentType)) {
// Use FormData to upload PDF files
code += 'const formData = new FormData();\n\n';
code += `formData.append("file", "${body}");\n\n`;
body = 'formData';
} else if (isXML(contentType)) {
code += 'const xml = `\n';
code += indent(String(body), 4);
code += '`;\n\n';
body = 'xml';
} else if (isText(contentType)) {
body = stringifyOpenAPI(body, null, 2);
} else {
body = `JSON.stringify(${stringifyOpenAPI(body, null, 2)})`;
}
return { body, code, headers: headersCopy };
},
getPythonBody: (body: any, headers?: Record<string, string>) => {
if (!body || !headers) return;
let code = '';
const contentType: string = headers['Content-Type'] || '';
if (isFormData(contentType)) {
code += 'files = {\n';
if (isPlainObject(body)) {
Object.entries(body).forEach(([key, value]) => {
code += indent(`"${key}": "${String(value)}",`, 4) + '\n';
});
}
code += '}\n\n';
body = 'files';
}
if (isPDF(contentType)) {
code += 'files = {\n';
code += indent(`"file": "${body}",`, 4) + '\n';
code += '}\n\n';
body = 'files';
}
return { body, code, headers };
},
getHTTPBody: (body: any, headers?: Record<string, string>) => {
if (!body || !headers) return undefined;
const contentType: string = headers['Content-Type'] || '';
const typeHandlers = {
pdf: () => `${stringifyOpenAPI(body, null, 2)}`,
formUrlEncoded: () => {
const encoded = isPlainObject(body)
? Object.entries(body)
.map(([key, value]) => `${key}=${String(value)}`)
.join('&')
: String(body);
return `"${encoded}"`;
},
text: () => `"${String(body)}"`,
xmlOrCsv: () => `"${stringifyOpenAPI(body).replace(/"/g, '')}"`,
default: () => `${stringifyOpenAPI(body, null, 2)}`,
};
if (isPDF(contentType)) return typeHandlers.pdf();
if (isFormUrlEncoded(contentType)) return typeHandlers.formUrlEncoded();
if (isText(contentType)) return typeHandlers.text();
if (isXML(contentType) || isCSV(contentType)) {
return typeHandlers.xmlOrCsv();
}
return typeHandlers.default();
},
};
@@ -0,0 +1,35 @@
export function isJSON(contentType?: string): boolean {
return contentType?.toLowerCase().includes('application/json') || false;
}
export function isXML(contentType?: string): boolean {
return contentType?.toLowerCase().includes('application/xml') || false;
}
export function isGraphQL(contentType?: string): boolean {
return contentType?.toLowerCase().includes('application/graphql') || false;
}
export function isCSV(contentType?: string): boolean {
return contentType?.toLowerCase().includes('text/csv') || false;
}
export function isPDF(contentType?: string): boolean {
return contentType?.toLowerCase().includes('application/pdf') || false;
}
export function isText(contentType?: string): boolean {
return contentType?.toLowerCase().includes('text/plain') || false;
}
export function isFormUrlEncoded(contentType?: string): boolean {
return contentType?.toLowerCase().includes('application/x-www-form-urlencoded') || false;
}
export function isFormData(contentType?: string): boolean {
return !!contentType && contentType.toLowerCase().includes('multipart/form-data');
}
export function isPlainObject(value: unknown): boolean {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
@@ -16,23 +16,27 @@ export function generateSchemaExample(
schema: OpenAPIV3.SchemaObject,
options?: GenerateSchemaExampleOptions,
): JSONValue | undefined {
return getExampleFromSchema(schema, {
emptyString: 'text',
variables: {
'date-time': new Date().toISOString(),
date: new Date().toISOString().split('T')[0],
email: 'name@gmail.com',
hostname: 'example.com',
ipv4: '0.0.0.0',
ipv6: '2001:0db8:85a3:0000:0000:8a2e:0370:7334',
uri: 'https://example.com',
uuid: '123e4567-e89b-12d3-a456-426614174000',
binary: 'binary',
byte: 'Ynl0ZXM=',
password: 'password',
return getExampleFromSchema(
schema,
{
emptyString: 'text',
variables: {
'date-time': new Date().toISOString(),
date: new Date().toISOString().split('T')[0],
email: 'name@gmail.com',
hostname: 'example.com',
ipv4: '0.0.0.0',
ipv6: '2001:0db8:85a3:0000:0000:8a2e:0370:7334',
uri: 'https://example.com',
uuid: '123e4567-e89b-12d3-a456-426614174000',
binary: 'binary',
byte: 'Ynl0ZXM=',
password: 'password',
},
...options,
},
...options,
});
3, // Max depth for circular references
);
}
/**
+13 -2
View File
@@ -1,6 +1,17 @@
/**
* Stringify an OpenAPI object. Same API as JSON.stringify.
*/
export function stringifyOpenAPI(body: unknown, transformer?: null, indent?: number): string {
return JSON.stringify(body, transformer, indent);
export function stringifyOpenAPI(body: unknown, _?: null, indent?: number): string {
return JSON.stringify(
body,
(key, value) => {
// Ignore internal keys
if (key.startsWith('x-gitbook-')) {
return undefined;
}
return value;
},
indent,
);
}
+11 -1
View File
@@ -5,8 +5,18 @@ import type {
} from '@gitbook/openapi-parser';
export interface OpenAPIContextProps extends OpenAPIClientContext {
CodeBlock: React.ComponentType<{ code: string; syntax: string }>;
/**
* Render a code block.
*/
renderCodeBlock: (props: { code: string; syntax: string }) => React.ReactNode;
/**
* Render the heading of the operation.
*/
renderHeading: (props: { deprecated: boolean; title: string }) => React.ReactNode;
/**
* Render the document of the operation.
*/
renderDocument: (props: { document: object }) => React.ReactNode;
/** Spec url for the Scalar Api Client */
specUrl: string;
+5 -1
View File
@@ -13,7 +13,11 @@ export function createStateKey(key: string, scope?: string) {
/**
* Resolve the description of an object.
*/
export function resolveDescription(object: AnyObject) {
export function resolveDescription(object: OpenAPIV3.SchemaObject | AnyObject) {
if ('items' in object && object.items) {
return resolveDescription(object.items);
}
return 'x-gitbook-description-html' in object &&
typeof object['x-gitbook-description-html'] === 'string'
? object['x-gitbook-description-html'].trim()
File diff suppressed because one or more lines are too long