mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-14 14:45:17 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f2766f3fdc | |||
| e24206ebf0 | |||
| 3410785a3a | |||
| b4a12d606e | |||
| a0545545d7 | |||
| 1f11650ca1 | |||
| da55facf2c | |||
| dc2dbc5710 | |||
| bdd6303bcc | |||
| 66d0fc0683 | |||
| 05e1d8cd96 | |||
| 9f0de74caa | |||
| a820739bd2 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Remove unused search api method from gitbook/api/lib
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Update OpenAPI code examples to support multiple content-type
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/openapi-parser': major
|
||||
---
|
||||
|
||||
Use Scalar parser to upgrade OpenAPI spec v2
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Hide x-gitbook-\* symbols in OpenAPI blocks
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Fix circularRef in schema + examples OpenAPI
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Add support for new OpenAPI ref
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Fix ID not set when there is no operation summary
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@gitbook/openapi-parser': patch
|
||||
'@gitbook/react-openapi': patch
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Render GitBook blocks in OpenAPI operation description
|
||||
@@ -22,3 +22,6 @@ yarn-error.log*
|
||||
|
||||
# Env files
|
||||
.env.local
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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}',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user