Compare commits

..

13 Commits

Author SHA1 Message Date
Greg Bergé 404c8df073 Upgrade API client 2025-02-21 14:22:27 +01:00
Nolann B. 05e1d8cd96 Hide x-gitbook-* symbols in OpenAPI blocks (#2863) 2025-02-21 12:38:55 +01:00
Greg Bergé 9f0de74caa Add support for new OpenAPI ref (#2860) 2025-02-20 14:31:04 +01:00
spastorelli a820739bd2 Remove ununsed search api lib methods (#2855) 2025-02-20 09:46:12 +01:00
Samy Pessé 304042017c Version Packages (#2847)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-20 08:36:09 +01:00
Greg Bergé 82cd9f2979 Add support for anchor link in OpenAPI blocks (#2858) 2025-02-19 23:38:20 +01:00
Greg Bergé a3f1fea27b Display OpenAPI header description (#2857) 2025-02-19 23:34:52 +01:00
Greg Bergé bb5c6a42e7 Support multiple examples and multiple responses example (#2856) 2025-02-19 20:39:15 +01:00
Valentino Hudhra 445baaaa61 Add a new packages @gitbook/colors (#2854)
Co-authored-by: Zeno Kapitein <zenomilan@me.com>
2025-02-19 17:17:39 +01:00
Nolann B. 7419ee7dba Show additional fields in OpenAPI block (#2851) 2025-02-19 17:01:15 +01:00
Nolann B. aa6be381a4 Make OpenAPI tabs list scrollable if overflowing (#2848) 2025-02-19 11:10:11 +01:00
Greg Bergé 61575838f3 Optimize markdown parsing (#2850) 2025-02-19 10:29:33 +01:00
Samy Pessé 359bb979f8 Open external link in a new tab when embedded in an iframe (#2846) 2025-02-18 12:41:45 +00:00
52 changed files with 1112 additions and 365 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Remove unused search api method from gitbook/api/lib
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
Hide x-gitbook-\* symbols in OpenAPI blocks
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Add support for new OpenAPI ref
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
Fix ID not set when there is no operation summary
+32 -19
View File
@@ -22,6 +22,13 @@
"wrangler": "3.82.0",
},
},
"packages/colors": {
"name": "@gitbook/colors",
"version": "0.2.0",
"devDependencies": {
"typescript": "^5.5.3",
},
},
"packages/emoji-codepoints": {
"name": "@gitbook/emoji-codepoints",
"version": "0.2.0",
@@ -31,10 +38,11 @@
},
"packages/gitbook": {
"name": "gitbook",
"version": "0.6.0",
"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:*",
"@gitbook/icons": "workspace:*",
"@gitbook/openapi-parser": "workspace:*",
@@ -61,6 +69,8 @@
"mathjax": "^3.2.2",
"mdast-util-to-markdown": "^2.1.2",
"memoizee": "^0.4.15",
"micromark": "^4.0.1",
"micromark-extension-gfm": "^3.0.0",
"next": "14.2.23",
"next-themes": "^0.2.1",
"nuqs": "^2.2.3",
@@ -71,17 +81,11 @@
"react": "18.3.1",
"react-dom": "18.3.1",
"react-hotkeys-hook": "^4.4.1",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.0",
"remark-gfm": "^4.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.0",
"rison": "^0.1.1",
"server-only": "^0.0.1",
"shiki": "^1.27.2",
"tailwind-merge": "^2.2.0",
"tailwind-shades": "^1.1.2",
"unified": "^11.0.4",
"url-join": "^5.0.0",
"usehooks-ts": "^3.1.0",
},
@@ -151,7 +155,7 @@
},
"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",
@@ -207,13 +211,14 @@
},
"packages/react-openapi": {
"name": "@gitbook/react-openapi",
"version": "1.0.0",
"version": "1.0.2",
"dependencies": {
"@gitbook/openapi-parser": "workspace:*",
"@scalar/api-client-react": "1.0.87",
"@scalar/oas-utils": "^0.2.101",
"clsx": "^2.1.1",
"flatted": "^3.2.9",
"json-xml-parse": "^1.3.0",
"react-aria": "^3.37.0",
"react-aria-components": "^1.6.0",
"usehooks-ts": "^3.1.0",
@@ -598,6 +603,8 @@
"@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"],
"@gitbook/colors": ["@gitbook/colors@workspace:packages/colors"],
"@gitbook/emoji-codepoints": ["@gitbook/emoji-codepoints@workspace:packages/emoji-codepoints"],
"@gitbook/fontawesome-pro": ["@gitbook/fontawesome-pro@1.0.8", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "^6.6.0" } }, "sha512-i4PgiuGyUb52Muhc52kK3aMJIMfMkA2RbPW30tre8a6M8T6mWTfYo6gafSgjNvF1vH29zcuB8oBYnF0gO4XcHA=="],
@@ -2210,7 +2217,7 @@
"hast-util-sanitize": ["hast-util-sanitize@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.2.0", "unist-util-position": "^5.0.0" } }, "sha512-IGrgWLuip4O2nq5CugXy4GI2V8kx4sFVy5Hd4vF7AR2gxS0N9s7nEAVUyeMtZKZvzrxVsHt73XdTsno1tClIkQ=="],
"hast-util-to-html": ["hast-util-to-html@9.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg=="],
"hast-util-to-html": ["hast-util-to-html@9.0.4", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA=="],
"hast-util-to-parse5": ["hast-util-to-parse5@8.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw=="],
@@ -2332,7 +2339,7 @@
"is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="],
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
"is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="],
"is-promise": ["is-promise@2.2.2", "", {}, "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="],
@@ -2404,6 +2411,8 @@
"json-stringify-deterministic": ["json-stringify-deterministic@1.0.12", "", {}, "sha512-q3PN0lbUdv0pmurkBNdJH3pfFvOTL/Zp0lquqpvcjfKzt6Y0j49EPHAmVHCAS4Ceq/Y+PejWTzyiVpoY71+D6g=="],
"json-xml-parse": ["json-xml-parse@1.3.0", "", {}, "sha512-MVosauc/3W2wL4dd4yaJzH5oXw+HOUfptn0+d4+bFghMiJFop7MaqIwFXJNLiRnNYJNQ6L4o7B+53n5wcvoLFw=="],
"json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
"jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
@@ -2542,7 +2551,7 @@
"microdiff": ["microdiff@1.4.0", "", {}, "sha512-OBKBOa1VBznvLPb/3ljeJaENVe0fO0lnWl77lR4vhPlQD71UpjEoRV5P0KdQkcjbFlBu1Oy2mEUBMU3wxcBAGg=="],
"micromark": ["micromark@4.0.0", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ=="],
"micromark": ["micromark@4.0.1", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw=="],
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA=="],
@@ -4292,8 +4301,6 @@
"@sentry/webpack-plugin/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"@shikijs/core/hast-util-to-html": ["hast-util-to-html@9.0.4", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA=="],
"@smithy/abort-controller/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/chunked-blob-reader/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@@ -4644,6 +4651,8 @@
"gaxios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
"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=="],
@@ -4682,6 +4691,8 @@
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"mdast-util-from-markdown/micromark": ["micromark@4.0.0", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ=="],
"mdast-util-gfm/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
"mdast-util-gfm-footnote/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
@@ -4700,12 +4711,8 @@
"micro/content-type": ["content-type@1.0.4", "", {}, "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="],
"micromark/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
"minimist-options/arrify": ["arrify@1.0.1", "", {}, "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA=="],
"minimist-options/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="],
"minizlib/minipass": ["minipass@2.9.0", "", { "dependencies": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" } }, "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg=="],
"next/@swc/helpers": ["@swc/helpers@0.5.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A=="],
@@ -4760,6 +4767,8 @@
"read-yaml-file/pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="],
"rehype-stringify/hast-util-to-html": ["hast-util-to-html@9.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg=="],
"remark-stringify/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
"require-in-the-middle/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
@@ -4814,6 +4823,8 @@
"type-is/mime-types": ["mime-types@3.0.0", "", { "dependencies": { "mime-db": "^1.53.0" } }, "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w=="],
"unified/is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
"unplugin/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=="],
"update-notifier/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="],
@@ -5614,6 +5625,8 @@
"gtoken/jws/jwa": ["jwa@2.0.0", "", { "dependencies": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA=="],
"mdast-util-from-markdown/micromark/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
"raw-body/http-errors/depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="],
"raw-body/http-errors/inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
+1
View File
@@ -0,0 +1 @@
dist/
+7
View File
@@ -0,0 +1,7 @@
# @gitbook/colors
## 0.2.0
### Minor Changes
- 445baaa: Initial release
+3
View File
@@ -0,0 +1,3 @@
# `@gitbook/colors`
A set of default colors and transformation functions used throughout the GitBook Open and app.
+26
View File
@@ -0,0 +1,26 @@
{
"name": "@gitbook/colors",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"development": "./src/index.ts",
"default": "./dist/index.js"
}
},
"version": "0.2.0",
"devDependencies": {
"typescript": "^5.5.3"
},
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit",
"dev": "tsc -w"
},
"files": [
"dist",
"src",
"README.md",
"CHANGELOG.md"
]
}
+39
View File
@@ -0,0 +1,39 @@
/**
* Default primary color throughout the GitBook ecosystem.
*/
export const DEFAULT_PRIMARY_COLOR = '#346DDB';
/**
* The darkest color that exists in GitBook, used as the relative minimum of every generated color scale.
*/
export const DARK_BASE = '#1D1D1D';
/**
* The lightest color that exists in GitBook, used as the relative maximum of every generated color scale.
*/
export const LIGHT_BASE = '#FFFFFF';
/**
* Used as the basis of all UI elements that are not colored by the primary color. Neutral gray by default, overridden by site customization.
*/
export const DEFAULT_TINT_COLOR = '#787878';
/**
* Used for informational messages and neutral alerts.
*/
export const DEFAULT_HINT_INFO_COLOR = '#787878';
/**
* Used for showing important information or non-critical warnings.
*/
export const DEFAULT_HINT_WARNING_COLOR = '#FE9A00';
/**
* Used for destructive actions or raising attention to critical information.
*/
export const DEFAULT_HINT_DANGER_COLOR = '#FB2C36';
/**
* Used for showing positive actions or achievements.
*/
export const DEFAULT_HINT_SUCCESS_COLOR = '#00C950';
+2
View File
@@ -0,0 +1,2 @@
export * from './colors';
export * from './transformations';
@@ -1,3 +1,5 @@
import { DARK_BASE, LIGHT_BASE, DEFAULT_TINT_COLOR } from './colors';
type ColorShades = {
[key: string]: string;
};
@@ -6,9 +8,6 @@ type RGBColor = [number, number, number];
type OKLABColor = { L: number; A: number; B: number };
type OKLCHColor = { L: number; C: number; H: number };
export const DARK_BASE = '#1d1d1d';
export const LIGHT_BASE = '#ffffff';
export const DEFAULT_TINT_COLOR = '#787878';
const D65 = [95.047, 100.0, 108.883]; // Reference white (D65)
export enum ColorCategory {
@@ -226,7 +225,7 @@ export function colorScale(
/**
* Convert a hex color to an RGB color set.
*/
function hexToRgbArray(hex: string): RGBColor {
export function hexToRgbArray(hex: string): RGBColor {
const originalHex = hex;
let value = hex.replace('#', '');
@@ -252,7 +251,7 @@ function hexToRgbArray(hex: string): RGBColor {
/**
* Convert a RGB color set to a hex color.
*/
function rgbArrayToHex(rgb: RGBColor): string {
export function rgbArrayToHex(rgb: RGBColor): string {
return `#${rgb
.map((channel) => {
const component = channel.toString(16);
@@ -262,7 +261,7 @@ function rgbArrayToHex(rgb: RGBColor): string {
.join('')}`;
}
function getColor(percentage: number, start: RGBColor, end: RGBColor) {
export function getColor(percentage: number, start: RGBColor, end: RGBColor) {
const rgb = end.map((channel, index) => {
return Math.round(channel + percentage * (start[index] - channel));
});
@@ -271,21 +270,21 @@ function getColor(percentage: number, start: RGBColor, end: RGBColor) {
}
// Utility constants and helper functions
function rgbToLinear(rgb: RGBColor): [number, number, number] {
export function rgbToLinear(rgb: RGBColor): [number, number, number] {
return rgb.map((v) => {
const scaled = v / 255;
return scaled <= 0.04045 ? scaled / 12.92 : ((scaled + 0.055) / 1.055) ** 2.4;
}) as [number, number, number];
}
function linearToRgb(linear: [number, number, number]): RGBColor {
export function linearToRgb(linear: [number, number, number]): RGBColor {
return linear.map((v) => {
const scaled = v <= 0.0031308 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055;
return Math.round(Math.max(0, Math.min(1, scaled)) * 255);
}) as RGBColor;
}
function rgbToOklab(rgb: RGBColor): OKLABColor {
export function rgbToOklab(rgb: RGBColor): OKLABColor {
const [r, g, b] = rgbToLinear(rgb);
const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
@@ -303,7 +302,7 @@ function rgbToOklab(rgb: RGBColor): OKLABColor {
};
}
function oklabToRgb(oklab: OKLABColor): RGBColor {
export function oklabToRgb(oklab: OKLABColor): RGBColor {
const { L, A, B } = oklab;
const lRoot = L + 0.3963377774 * A + 0.2158037573 * B;
@@ -321,14 +320,14 @@ function oklabToRgb(oklab: OKLABColor): RGBColor {
return linearToRgb([r, g, b]);
}
function oklabToOklch(oklab: OKLABColor): OKLCHColor {
export function oklabToOklch(oklab: OKLABColor): OKLCHColor {
const { L, A, B } = oklab;
const C = Math.sqrt(A ** 2 + B ** 2);
const H = (Math.atan2(B, A) * 180) / Math.PI;
return { L, C, H: H < 0 ? H + 360 : H };
}
function oklchToOklab(oklch: OKLCHColor): OKLABColor {
export function oklchToOklab(oklch: OKLCHColor): OKLABColor {
const { L, C, H } = oklch;
const rad = (H * Math.PI) / 180;
return {
@@ -338,15 +337,15 @@ function oklchToOklab(oklch: OKLCHColor): OKLABColor {
};
}
function rgbToOklch(rgb: RGBColor): OKLCHColor {
export function rgbToOklch(rgb: RGBColor): OKLCHColor {
return oklabToOklch(rgbToOklab(rgb));
}
function oklchToRgb(oklch: OKLCHColor): RGBColor {
export function oklchToRgb(oklch: OKLCHColor): RGBColor {
return oklabToRgb(oklchToOklab(oklch));
}
function rgbToXyz(rgb: RGBColor): [number, number, number] {
export function rgbToXyz(rgb: RGBColor): [number, number, number] {
const [r, g, b] = rgbToLinear(rgb);
return [
(r * 0.4124564 + g * 0.3575761 + b * 0.1804375) * 100,
@@ -355,7 +354,11 @@ function rgbToXyz(rgb: RGBColor): [number, number, number] {
];
}
function xyzToLab65(xyz: [number, number, number]): { L: number; A: number; B: number } {
export function xyzToLab65(xyz: [number, number, number]): {
L: number;
A: number;
B: number;
} {
const [x, y, z] = xyz.map((v, i) => {
const scaled = v / D65[i];
return scaled > 0.008856 ? Math.cbrt(scaled) : 7.787 * scaled + 16 / 116;
@@ -368,7 +371,7 @@ function xyzToLab65(xyz: [number, number, number]): { L: number; A: number; B: n
};
}
function rgbTolab65(rgb: RGBColor): { L: number; A: number; B: number } {
export function rgbTolab65(rgb: RGBColor): { L: number; A: number; B: number } {
return xyzToLab65(rgbToXyz(rgb));
}
@@ -376,7 +379,7 @@ function rgbTolab65(rgb: RGBColor): { L: number; A: number; B: number } {
Delta Phi Star perceptual lightness contrast by Andrew Somers:
https://github.com/Myndex/deltaphistar
*/
const PHI = 0.5 + Math.sqrt(1.25);
export const PHI = 0.5 + Math.sqrt(1.25);
export function dpsContrast(a: RGBColor, b: RGBColor) {
const dps = Math.abs(rgbTolab65(a).L ** PHI - rgbTolab65(b).L ** PHI);
@@ -387,7 +390,10 @@ export function dpsContrast(a: RGBColor, b: RGBColor) {
export function colorContrast(background: string, foreground: string[] = [LIGHT_BASE, DARK_BASE]) {
const bg = hexToRgbArray(background);
const best: { color?: RGBColor; contrast: number } = { color: undefined, contrast: 0 };
const best: { color?: RGBColor; contrast: number } = {
color: undefined,
contrast: 0,
};
for (const color of foreground) {
const c = hexToRgbArray(color);
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": false,
"declaration": true,
"outDir": "dist",
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react",
"incremental": true,
"types": [
"bun-types" // add Bun global
]
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}
+17
View File
@@ -1,5 +1,22 @@
# gitbook
## 0.6.2
### Patch Changes
- 359bb97: Fix opening external links when the GitBook page is embedded in an iframe
- 6157583: Improve Markdown parsing
- 82cd9f2: Add support for anchor links in OpenAPI blocks
- Updated dependencies [445baaa]
- Updated dependencies [bb5c6a4]
- Updated dependencies [a3f1fea]
- Updated dependencies [6157583]
- Updated dependencies [7419ee7]
- Updated dependencies [82cd9f2]
- @gitbook/colors@0.2.0
- @gitbook/react-openapi@1.0.2
- @gitbook/openapi-parser@1.0.1
## 0.6.1
### Patch Changes
+5 -8
View File
@@ -1,6 +1,6 @@
{
"name": "gitbook",
"version": "0.6.1",
"version": "0.6.2",
"private": true,
"scripts": {
"dev": "env-cmd --silent -f ../../.env.local next dev",
@@ -17,8 +17,9 @@
"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:*",
"@gitbook/icons": "workspace:*",
"@gitbook/openapi-parser": "workspace:*",
@@ -45,6 +46,8 @@
"mathjax": "^3.2.2",
"mdast-util-to-markdown": "^2.1.2",
"memoizee": "^0.4.15",
"micromark": "^4.0.1",
"micromark-extension-gfm": "^3.0.0",
"next": "14.2.23",
"next-themes": "^0.2.1",
"nuqs": "^2.2.3",
@@ -55,17 +58,11 @@
"react": "18.3.1",
"react-dom": "18.3.1",
"react-hotkeys-hook": "^4.4.1",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.0",
"remark-gfm": "^4.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.0",
"rison": "^0.1.1",
"server-only": "^0.0.1",
"shiki": "^1.27.2",
"tailwind-merge": "^2.2.0",
"tailwind-shades": "^1.1.2",
"unified": "^11.0.4",
"url-join": "^5.0.0",
"usehooks-ts": "^3.1.0"
},
@@ -1,11 +1,11 @@
import { CustomizationHeaderPreset } from '@gitbook/api';
import { colorContrast } from '@gitbook/colors';
import { redirect } from 'next/navigation';
import { ImageResponse } from 'next/og';
import { NextRequest } from 'next/server';
import React from 'react';
import { googleFontsMap } from '@/fonts';
import { colorContrast } from '@/lib/colors';
import { getAbsoluteHref } from '@/lib/links';
import { filterOutNullable } from '@/lib/typescript';
import { getContentTitle } from '@/lib/utils';
@@ -1,7 +1,7 @@
import { SiteInsightsAd } from '@gitbook/api';
import { hexToRgba } from '@gitbook/colors';
import * as React from 'react';
import { hexToRgba } from '@/lib/colors';
import { getResizedImageURL } from '@/lib/images';
import { tcls } from '@/lib/tailwind';
@@ -3,12 +3,12 @@ import { Icon } from '@gitbook/icons';
import { OpenAPIOperation } from '@gitbook/react-openapi';
import React from 'react';
import { LoadingPane } from '@/components/primitives';
import { fetchOpenAPIBlock } from '@/lib/openapi';
import { resolveOpenAPIBlock } from '@/lib/openapi/fetch';
import { tcls } from '@/lib/tailwind';
import { BlockProps } from '../Block';
import { PlainCodeBlock } from '../CodeBlock';
import { Heading } from '../Heading';
import './style.css';
import './scalar.css';
@@ -27,13 +27,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>
);
@@ -54,6 +58,31 @@ async function OpenAPIBody(props: BlockProps<DocumentBlockOpenAPI>) {
plus: <Icon icon="plus" />,
},
CodeBlock: PlainCodeBlock,
renderHeading: (headingProps) => (
<Heading
document={props.document}
ancestorBlocks={props.ancestorBlocks}
isEstimatedOffscreen={props.isEstimatedOffscreen}
context={props.context}
style={headingProps.deprecated ? 'line-through' : undefined}
block={{
object: 'block',
key: `${block.key}-heading`,
meta: block.meta,
data: {},
type: 'heading-2',
nodes: [
{
key: `${block.key}-heading-text`,
object: 'text',
leaves: [
{ text: headingProps.title, object: 'leaf', marks: [] },
],
},
],
}}
/>
),
defaultInteractiveOpened: context.mode === 'print',
id: block.meta?.id,
blockKey: block.key,
@@ -62,32 +91,3 @@ async function OpenAPIBody(props: BlockProps<DocumentBlockOpenAPI>) {
/>
);
}
function OpenAPIFallback() {
return (
<div
role="status"
aria-busy
className={'openapi-block ' + tcls('flex', 'flex-1', 'flex-col', 'gap-3')}
>
<LoadingPane
tile={12}
style={['rounded-md', 'h-[47px]', '[max-width:calc(48rem-1px)]']}
/>
<LoadingPane
tile={12}
style={['rounded-md', 'h-[35px]', '[max-width:calc(48rem-1px)]']}
/>
<div className={tcls('flex', 'gap-[25px]')}>
<div className={tcls('flex', 'flex-1', 'flex-col', 'gap-3')}>
<LoadingPane tile={24} style={['rounded-md', 'aspect-[2.5/1]', 'w-full']} />
<LoadingPane tile={24} style={['rounded-md', 'aspect-[2.5/1]', 'w-full']} />
</div>
<div className={tcls('flex', 'flex-1', 'flex-col', 'gap-3')}>
<LoadingPane tile={24} style={['rounded-md', 'aspect-[4/1]', 'w-full']} />
<LoadingPane tile={24} style={['rounded-md', 'aspect-[4/1]', 'w-full']} />
</div>
</div>
</div>
);
}
@@ -1,7 +1,5 @@
/* Layout Components */
.openapi-operation {
content-visibility: auto;
contain-intrinsic-height: 600px;
@apply flex-1 flex flex-col gap-4 mb-14;
}
@@ -18,16 +16,8 @@
@apply flex flex-col items-start justify-start gap-2;
}
.openapi-summary-title {
@apply font-semibold text-xl;
}
.openapi-summary-title[data-deprecated='true'] {
@apply line-through;
}
.openapi-deprecated {
@apply py-0.5 px-1.5 min-w-[1.625rem] font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint bg-tint rounded-full text-sm leading-[calc(max(1.20em,1.25rem))] before:!content-none after:!content-none;
@apply py-0.5 px-1.5 min-w-[1.625rem] font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint bg-tint rounded text-sm leading-[calc(max(1.20em,1.25rem))] before:!content-none after:!content-none;
}
.openapi-deprecated-sunset-date {
@@ -463,7 +453,7 @@
}
.openapi-section-header-content {
@apply flex-1 text-base gap-1.5 flex w-full font-medium text-tint-strong;
@apply flex-1 overflow-hidden text-base gap-1.5 flex w-full font-medium text-tint-strong;
}
.openapi-section-header-controls {
@@ -500,11 +490,13 @@
/* Tabs */
.openapi-tabs-list {
@apply flex flex-row gap-3 py-1.5 px-2.5 border-b border-tint-subtle w-full;
@apply flex flex-row gap-1.5 py-1.5 px-2.5 border-b border-tint-subtle w-full overflow-x-scroll;
scrollbar-width: none;
-ms-overflow-style: none;
}
.openapi-tabs-tab {
@apply hover:bg-primary-hover font-mono font-normal tabular-nums hover:text-primary cursor-pointer transition-all relative text-[0.813rem] text-tint px-1 border border-transparent rounded;
@apply hover:bg-primary-hover whitespace-nowrap font-mono font-normal tabular-nums hover:text-primary cursor-pointer transition-all relative text-[0.813rem] text-tint px-1 border border-transparent rounded;
}
.openapi-tabs-tab[aria-selected='true'] {
@@ -15,31 +15,29 @@ import { ClassValue, tcls } from '@/lib/tailwind';
export function Text(props: { text: DocumentText }) {
const { text } = props;
return (
<>
{text.leaves.map((leaf, index) => {
return (
<React.Fragment key={index}>
{leaf.marks
// Sort to have code marks at the end, so that they don't interfere with other marks
.sort(
(a, b) => (a.type === 'code' ? 1 : 0) - (b.type === 'code' ? 1 : 0),
)
.reduce<React.ReactNode>((children, mark, index) => {
const Mark = MARK_STYLES[mark.type];
return text.leaves.map((leaf, index) => {
return (
<React.Fragment key={index}>
{leaf.marks
// Sort to have code marks at the end, so that they don't interfere with other marks
.sort((a, b) => (a.type === 'code' ? 1 : 0) - (b.type === 'code' ? 1 : 0))
.reduce<React.ReactNode>((children, mark, index) => {
const Mark = MARK_STYLES[mark.type];
if (!Mark) {
return children;
}
if (!Mark) {
return children;
}
// @ts-ignore
return <Mark mark={mark}>{children}</Mark>;
}, leaf.text)}
</React.Fragment>
);
})}
</>
);
return (
// @ts-ignore
<Mark key="mark" mark={mark}>
{children}
</Mark>
);
}, leaf.text)}
</React.Fragment>
);
});
}
const MARK_STYLES = {
@@ -9,18 +9,18 @@ import {
type CustomizationTint,
type SiteCustomizationSettings,
} from '@gitbook/api';
import { IconsProvider, IconStyle } from '@gitbook/icons';
import { fontNotoColorEmoji, fonts, ibmPlexMono } from '@/fonts';
import { getSpaceLanguage } from '@/intl/server';
import { getStaticFileURL } from '@/lib/assets';
import {
colorContrast,
colorScale,
type ColorScaleOptions,
DEFAULT_TINT_COLOR,
hexToRgb,
} from '@/lib/colors';
} from '@gitbook/colors';
import { IconsProvider, IconStyle } from '@gitbook/icons';
import { fontNotoColorEmoji, fonts, ibmPlexMono } from '@/fonts';
import { getSpaceLanguage } from '@/intl/server';
import { getStaticFileURL } from '@/lib/assets';
import { tcls } from '@/lib/tailwind';
import { ClientContexts } from './ClientContexts';
@@ -47,6 +47,13 @@ export const Link = React.forwardRef(function Link(
});
}
// When the page is embedded in an iframe, for security reasons other urls cannot be opened.
// In this case, we open the link in a new tab.
if (isExternal && window.self !== window.top) {
event.preventDefault();
window.open(href, '_blank');
}
domProps.onClick?.(event);
};
@@ -0,0 +1,133 @@
// Bun Snapshot v1, https://goo.gl/fbAQLP
exports[`parseMarkdown should parse a simple table 1`] = `
"<h2>Table</h2>
<table>
<thead>
<tr>
<th>a</th>
<th align="left">b</th>
<th align="right">c</th>
<th align="center">d</th>
</tr>
</thead>
</table>"
`;
exports[`parseMarkdown should parse a complex table 1`] = `
"<p>Returns information for all non-fungible tokens for an account.</p>
<h2>Ordering</h2>
<p>When considering NFTs, their order is governed by a combination of their numerical <strong>token.Id</strong> and <strong>serialnumber</strong> values, with <strong>token.id</strong> being the parent column.
A serialnumbers value governs its order within the given token.id</p>
<p>In that regard, if a user acquired a set of NFTs in the order (2-2, 2-4 1-5, 1-1, 1-3, 3-3, 3-4), the following layouts illustrate the ordering expectations for ownership listing</p>
<ol>
<li><strong>All NFTs in ASC order</strong>: 1-1, 1-3, 1-5, 2-2, 2-4, 3-3, 3-4</li>
<li><strong>All NFTs in DESC order</strong>: 3-4, 3-3, 2-4, 2-2, 1-5, 1-3, 1-1</li>
<li><strong>NFTs above 1-1 in ASC order</strong>: 1-3, 1-5, 2-2, 2-4, 3-3, 3-4</li>
<li><strong>NFTs below 3-3 in ASC order</strong>: 1-1, 1-3, 1-5, 2-2, 2-4</li>
<li><strong>NFTs between 1-3 and 3-3 inclusive in DESC order</strong>: 3-4, 3-3, 2-4, 2-2, 1-5, 1-3</li>
</ol>
<p>Note: The default order for this API is currently DESC</p>
<h2>Filtering</h2>
<p>When filtering there are some restrictions enforced to ensure correctness and scalability.</p>
<p><strong>The table below defines the restrictions and support for the NFT ownership endpoint</strong></p>
<table>
<thead>
<tr>
<th>Query Param</th>
<th>Comparison Operator</th>
<th>Support</th>
<th>Description</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>token.id</td>
<td>eq</td>
<td>Y</td>
<td>Single occurrence only.</td>
<td>?token.id=X</td>
</tr>
<tr>
<td></td>
<td>ne</td>
<td>N</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>lt(e)</td>
<td>Y</td>
<td>Single occurrence only.</td>
<td>?token.id=lte:X</td>
</tr>
<tr>
<td></td>
<td>gt(e)</td>
<td>Y</td>
<td>Single occurrence only.</td>
<td>?token.id=gte:X</td>
</tr>
<tr>
<td>serialnumber</td>
<td>eq</td>
<td>Y</td>
<td>Single occurrence only. Requires the presence of a <strong>token.id</strong> query</td>
<td>?serialnumber=Y</td>
</tr>
<tr>
<td></td>
<td>ne</td>
<td>N</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>lt(e)</td>
<td>Y</td>
<td>Single occurrence only. Requires the presence of an <strong>lte</strong> or <strong>eq</strong> <strong>token.id</strong> query</td>
<td>?token.id=lte:X&amp;serialnumber=lt:Y</td>
</tr>
<tr>
<td></td>
<td>gt(e)</td>
<td>Y</td>
<td>Single occurrence only. Requires the presence of an <strong>gte</strong> or <strong>eq</strong> <strong>token.id</strong> query</td>
<td>?token.id=gte:X&amp;serialnumber=gt:Y</td>
</tr>
<tr>
<td>spender.id</td>
<td>eq</td>
<td>Y</td>
<td></td>
<td>?spender.id=Z</td>
</tr>
<tr>
<td></td>
<td>ne</td>
<td>N</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>lt(e)</td>
<td>Y</td>
<td></td>
<td>?spender.id=lt:Z</td>
</tr>
<tr>
<td></td>
<td>gt(e)</td>
<td>Y</td>
<td></td>
<td>?spender.id=gt:Z</td>
</tr>
</tbody>
</table>
<p>Note: When searching across a range for individual NFTs a <strong>serialnumber</strong> with an additional <strong>token.id</strong> query filter must be provided.
Both filters must be a single occurrence of <strong>gt(e)</strong> or <strong>lt(e)</strong> which provide a lower and or upper boundary for search.</p>"
`;
+44 -49
View File
@@ -208,6 +208,42 @@ export const getUserById = cache({
},
});
/**
* Get the latest version of an OpenAPI spec by its slug.
*/
export const getLatestOpenAPISpecVersionContent = cache({
name: 'api.getLatestOpenApiSpecVersionContent',
tag: (organization, openAPISpec) =>
getAPICacheTag({
tag: 'openapi',
organization,
openAPISpec,
}),
get: async (organizationId: string, slug: string, options: CacheFunctionOptions) => {
try {
const apiCtx = await api();
const response = await apiCtx.client.orgs.getLatestOpenApiSpecVersionContent(
organizationId,
slug,
{
...noCacheFetchOptions,
signal: options.signal,
},
);
return cacheResponse(response, { revalidateBefore: 60 * 60 });
} catch (error) {
if (checkHasErrorCode(error, 404)) {
return {
revalidateBefore: 5,
data: null,
};
}
throw error;
}
},
});
/**
* Resolve a URL to the content to render.
*/
@@ -1066,55 +1102,6 @@ export async function getSpaceContentData(
};
}
/**
* Search content in a space.
*/
export const searchSpaceContent = cache({
name: 'api.searchSpaceContent',
tag: (spaceId) => getAPICacheTag({ tag: 'space', space: spaceId }),
getKeySuffix: getAPIContextId,
get: async (
spaceId: string,
/** The revision ID is used as a cache bust key, to avoid revalidating lot of cache entries by tags */
revisionId: string,
query: string,
options: CacheFunctionOptions,
) => {
const apiCtx = await api();
const response = await apiCtx.client.spaces.searchSpaceContent(
spaceId,
{ query },
{
...noCacheFetchOptions,
signal: options.signal,
},
);
return cacheResponse(response);
},
});
/**
* Search content accross all spaces in a parent (site or collection).
*/
export const searchParentContent = cache({
name: 'api.searchParentContent',
tag: (spaceId) => getAPICacheTag({ tag: 'space', space: spaceId }),
getKeySuffix: getAPIContextId,
get: async (parentId: string, query: string, options: CacheFunctionOptions) => {
const apiCtx = await api();
const response = await apiCtx.client.search.searchContent(
{ query },
{
...noCacheFetchOptions,
signal: options.signal,
},
);
return cacheResponse(response, {
ttl: 60 * 60,
});
},
});
/**
* Search content in a Site or specific SiteSpaces.
*/
@@ -1275,6 +1262,12 @@ export function getAPICacheTag(
| {
tag: 'site';
site: string;
}
// All data related to an OpenAPI spec
| {
tag: 'openapi';
organization: string;
openAPISpec: string;
},
): string {
switch (spec.tag) {
@@ -1298,6 +1291,8 @@ export function getAPICacheTag(
return `site:${spec.site}`;
case 'integration':
return `integration:${spec.integration}`;
case 'openapi':
return `organization:${spec.organization}:openapi:${spec.openAPISpec}`;
default:
assertNever(spec);
}
@@ -1,7 +1,7 @@
import { JSONDocument, ContentRef } from '@gitbook/api';
import { getNodeText } from './document';
import { fetchOpenAPIBlock } from './openapi';
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,
+2 -2
View File
@@ -9,7 +9,7 @@ describe('parseMarkdown', () => {
| a | b | c | d |
| - | :- | -: | :-: |`);
expect(result).toContain('<table>');
expect(result).toMatchSnapshot();
});
it('should parse a complex table', async () => {
@@ -52,6 +52,6 @@ When filtering there are some restrictions enforced to ensure correctness and sc
Note: When searching across a range for individual NFTs a **serialnumber** with an additional **token.id** query filter must be provided.
Both filters must be a single occurrence of **gt(e)** or **lt(e)** which provide a lower and or upper boundary for search.`);
expect(result).toContain('<table>');
expect(result).toMatchSnapshot();
});
});
+7 -17
View File
@@ -1,22 +1,12 @@
import rehypeSanitize from 'rehype-sanitize';
import rehypeStringify from 'rehype-stringify';
import remarkGfm from 'remark-gfm';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import { unified } from 'unified';
import { micromark } from 'micromark';
import { gfmHtml, gfm } from 'micromark-extension-gfm';
/**
* Parse markdown and output HTML.
*/
export async function parseMarkdown(markdown: string): Promise<string> {
const promise = unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkRehype)
.use(rehypeSanitize)
.use(rehypeStringify)
.process(markdown)
.then((file) => file.toString());
return promise;
export function parseMarkdown(input: string): string {
return micromark(input, {
extensions: [gfm()],
htmlExtensions: [gfmHtml()],
});
}
@@ -0,0 +1,19 @@
import { parseOpenAPI } from '@gitbook/openapi-parser';
import { describe, expect, it } from 'bun:test';
import { enrichFilesystem } from './enrich';
const spec = await Bun.file(new URL('./fixtures/multiline-spec.yaml', import.meta.url)).text();
describe('#enrichFilesystem', () => {
it('supports multiline descriptions', async () => {
const filesystem = await parseOpenAPI({
value: spec,
rootURL: null,
});
const enriched = await enrichFilesystem(filesystem);
expect(enriched[0].specification.paths['/pet'].put['x-gitbook-description-html']).toBe(
'<p>Social platform</p>',
);
});
});
@@ -0,0 +1,43 @@
import { traverse, Filesystem } from '@gitbook/openapi-parser';
import { parseMarkdown } from '../markdown';
/**
* Enrich a filesystem with HTML descriptions.
*/
export async function enrichFilesystem(filesystem: Filesystem) {
const parseMarkdownWithCache = createMarkdownParser();
return traverse(filesystem, async (node, path) => {
if (
path !== undefined &&
'description' in node &&
typeof node.description === 'string' &&
node.description
) {
const key = path[path.length - 1];
// Avoid parsing descriptions in examples.
if (key !== 'example') {
const description = node.description.trim();
if (description) {
node['x-gitbook-description-html'] = parseMarkdownWithCache(description);
}
}
}
return node;
});
}
/**
* Create a markdown parser that caches the results of parsing.
*/
const createMarkdownParser = () => (input: string) => {
const cache: Record<string, string> = {};
const existing = cache[input];
if (existing) {
return existing;
}
const result = parseMarkdown(input);
cache[input] = result;
return result;
};
@@ -1,29 +1,54 @@
import { ContentRef, DocumentBlockOpenAPI } from '@gitbook/api';
import { parseOpenAPI, OpenAPIParseError, traverse } from '@gitbook/openapi-parser';
import { parseOpenAPI, OpenAPIParseError } from '@gitbook/openapi-parser';
import { type OpenAPIOperationData, resolveOpenAPIOperation } from '@gitbook/react-openapi';
import { cache, noCacheFetchOptions, CacheFunctionOptions } from '@/lib/cache';
import { parseMarkdown } from './markdown';
import { ResolvedContentRef } from './references';
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,
@@ -40,7 +65,7 @@ export async function fetchOpenAPIBlock(
}
const fetchFilesystem = cache({
name: 'openapi.fetch.v5',
name: 'openapi.fetch.v6',
get: async (url: string, options: CacheFunctionOptions) => {
// Wrap the raw string to prevent invalid URLs from being passed to fetch.
// This can happen if the URL has whitespace, which is currently handled differently by Cloudflare's implementation of fetch:
@@ -58,38 +83,13 @@ const fetchFilesystem = cache({
const text = await response.text();
const filesystem = await parseOpenAPI({ value: text, rootURL: url });
const parseMarkdownWithCache = createMarkdownParser();
const transformedFs = await traverse(filesystem, async (node, path) => {
if ('description' in node && typeof node.description === 'string' && node.description) {
const lastKey = path && path[path.length - 1];
// Avoid parsing descriptions in examples.
if (lastKey === 'example') {
return node;
}
node['x-description-html'] = await parseMarkdownWithCache(node.description);
}
return node;
});
const richFilesystem = await enrichFilesystem(filesystem);
return {
// Cache for 4 hours
ttl: 24 * 60 * 60,
// Revalidate every 2 hours
revalidateBefore: 22 * 60 * 60,
data: transformedFs,
data: richFilesystem,
};
},
});
/**
* Create a markdown parser that caches the results of parsing.
*/
const createMarkdownParser = () => async (input: string) => {
const cache = new Map<string, Promise<string>>();
if (cache.has(input)) {
return cache.get(input) as Promise<string>;
}
const promise = parseMarkdown(input);
cache.set(input, promise);
return promise;
};
@@ -0,0 +1,41 @@
openapi: 3.0.2
info:
title: OpenAPI spec
version: 0.0.0
servers:
- url: '/api/v3'
paths:
'/pet':
put:
summary: Update an existing pet
description: |
Social platform
operationId: updatePet
requestBody:
description: Update an existent pet in the store
content:
application/json:
schema:
required:
- id
type: object
properties:
id:
type: integer
format: int64
example: 10
required: true
responses:
'200':
description: Successful operation
content:
application/json:
schema:
required:
- id
type: object
properties:
id:
type: integer
format: int64
example: 10
+26
View File
@@ -7,6 +7,7 @@ import {
SiteSpace,
Space,
} from '@gitbook/api';
import type { Filesystem } from '@gitbook/openapi-parser';
import assertNever from 'assert-never';
import React from 'react';
@@ -17,6 +18,8 @@ import {
SpaceContentPointer,
getCollection,
getDocument,
getLatestOpenAPISpecVersion,
getLatestOpenAPISpecVersionContent,
getPageDocument,
getPublishedContentSite,
getReusableContent,
@@ -50,6 +53,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 +277,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);
}
+1 -1
View File
@@ -3,7 +3,7 @@ import typography from '@tailwindcss/typography';
import type { Config } from 'tailwindcss';
import plugin from 'tailwindcss/plugin';
import { ColorCategory, hexToRgb, scale, shadesOfColor } from './src/lib/colors';
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];
+6
View File
@@ -1,5 +1,11 @@
# @gitbook/openapi-parser
## 1.0.1
### Patch Changes
- 6157583: Improve Markdown parsing
## 1.0.0
### Major Changes
+1 -1
View File
@@ -9,7 +9,7 @@
"default": "./dist/index.js"
}
},
"version": "1.0.0",
"version": "1.0.1",
"sideEffects": false,
"dependencies": {
"@scalar/openapi-parser": "^0.10.4",
+2 -2
View File
@@ -20,7 +20,7 @@ export interface OpenAPICustomSpecProperties {
/**
* Description in HTML format.
*/
'x-description-html'?: string;
'x-gitbook-description-html'?: string;
}
/**
@@ -41,7 +41,7 @@ export interface OpenAPICustomOperationProperties {
/**
* Description in HTML format.
*/
'x-description-html'?: string;
'x-gitbook-description-html'?: string;
}
/**
+12
View File
@@ -1,5 +1,17 @@
# @gitbook/react-openapi
## 1.0.2
### Patch Changes
- bb5c6a4: Support multiple response media types and examples
- a3f1fea: Fix display of OpenAPI header description
- 6157583: Improve Markdown parsing
- 7419ee7: Show additional fields in OpenAPI block
- 82cd9f2: Add support for anchor links in OpenAPI blocks
- Updated dependencies [6157583]
- @gitbook/openapi-parser@1.0.1
## 1.0.1
### Patch Changes
+2 -1
View File
@@ -8,7 +8,7 @@
"default": "./dist/index.js"
}
},
"version": "1.0.1",
"version": "1.0.2",
"sideEffects": false,
"dependencies": {
"@gitbook/openapi-parser": "workspace:*",
@@ -16,6 +16,7 @@
"@scalar/oas-utils": "^0.2.101",
"clsx": "^2.1.1",
"flatted": "^3.2.9",
"json-xml-parse": "^1.3.0",
"react-aria-components": "^1.6.0",
"react-aria": "^3.37.0",
"usehooks-ts": "^3.1.0",
@@ -58,7 +58,9 @@ export function OpenAPICodeSample(props: {
(searchParams.size ? `?${searchParams.toString()}` : ''),
method: data.method,
body: requestBodyContent
? generateMediaTypeExample(requestBodyContent[1], { onlyRequired: true })
? generateMediaTypeExample(requestBodyContent[1], {
omitEmptyAndOptionalProperties: true,
})
: undefined,
headers: {
...getSecurityHeaders(data.securities),
@@ -25,14 +25,17 @@ export function OpenAPIOperation(props: {
blockKey: context.blockKey,
};
const description = resolveDescription(operation)?.trim();
const description = resolveDescription(operation);
return (
<div className={clsx('openapi-operation', className)}>
<div className="openapi-summary" id={context.id}>
<h2 className="openapi-summary-title" data-deprecated={operation.deprecated}>
{operation.summary}
</h2>
<div className="openapi-summary" id={operation.summary ? undefined : context.id}>
{operation.summary
? context.renderHeading({
deprecated: operation.deprecated ?? false,
title: operation.summary,
})
: null}
{operation.deprecated && <div className="openapi-deprecated">Deprecated</div>}
</div>
<div className="openapi-columns">
+6 -12
View File
@@ -1,6 +1,6 @@
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
import { OpenAPISchemaProperties } from './OpenAPISchema';
import { resolveDescription } from './utils';
import { parameterToProperty, resolveDescription } from './utils';
import type { OpenAPIClientContext } from './types';
import { OpenAPIDisclosure } from './OpenAPIDisclosure';
@@ -27,13 +27,11 @@ export function OpenAPIResponse(props: {
return (
<div className="openapi-response-body">
{headers.length > 0 ? (
<OpenAPIDisclosure context={context} label={'Headers'}>
<OpenAPIDisclosure context={context} label="Headers">
<OpenAPISchemaProperties
properties={headers.map(([name, header]) => ({
propertyName: name,
schema: header.schema ?? {},
required: header.required,
}))}
properties={headers.map(([name, header]) => {
return parameterToProperty({ name, ...header });
})}
context={context}
/>
</OpenAPIDisclosure>
@@ -41,11 +39,7 @@ export function OpenAPIResponse(props: {
<div className="openapi-responsebody">
<OpenAPISchemaProperties
id={`response-${context.blockKey}`}
properties={[
{
schema: mediaType.schema ?? {},
},
]}
properties={mediaType.schema ? [{ schema: mediaType.schema }] : []}
context={context}
/>
</div>
@@ -2,9 +2,10 @@ import type { OpenAPIV3 } from '@gitbook/openapi-parser';
import { generateSchemaExample } from './generateSchemaExample';
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
import { checkIsReference, createStateKey, resolveDescription } from './utils';
import { stringifyOpenAPI } from './stringifyOpenAPI';
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.
@@ -38,84 +39,51 @@ export function OpenAPIResponseExample(props: {
return Number(a) - Number(b);
});
const examples = responses
.map(([key, value]) => {
const responseObject = value;
const mediaTypeObject = (() => {
if (!responseObject.content) {
return null;
}
const key = Object.keys(responseObject.content)[0];
return (
responseObject.content['application/json'] ??
(key ? responseObject.content[key] : null)
);
})();
const tabs = responses
.map(([key, responseObject]) => {
const description = resolveDescription(responseObject);
if (!mediaTypeObject) {
if (checkIsReference(responseObject)) {
return {
key: key,
label: key,
description: resolveDescription(responseObject),
body: <OpenAPIEmptyResponseExample />,
description,
body: (
<OpenAPIExample
example={getExampleFromReference(responseObject)}
context={context}
syntax="json"
/>
),
};
}
const example = handleUnresolvedReference(
(() => {
const { examples, example } = mediaTypeObject;
if (examples) {
const key = Object.keys(examples)[0];
if (key) {
// @TODO handle multiple examples
const firstExample = examples[key];
if (firstExample) {
return firstExample;
}
}
}
if (example) {
return { value: example };
}
const schema = mediaTypeObject.schema;
if (!schema) {
return null;
}
return { value: generateSchemaExample(schema) };
})(),
);
if (!responseObject.content || Object.keys(responseObject.content).length === 0) {
return {
key: key,
label: key,
description,
body: <OpenAPIEmptyResponseExample />,
};
}
return {
key: key,
label: key,
description: resolveDescription(responseObject),
body: example?.value ? (
<context.CodeBlock
code={
typeof example.value === 'string'
? example.value
: stringifyOpenAPI(example.value, null, 2)
}
syntax="json"
/>
) : (
<OpenAPIEmptyResponseExample />
),
body: <OpenAPIResponse context={context} content={responseObject.content} />,
};
})
.filter((val): val is { key: string; label: string; body: any; description: string } =>
Boolean(val),
);
if (examples.length === 0) {
if (tabs.length === 0) {
return null;
}
return (
<OpenAPITabs stateKey={createStateKey('response-example')} items={examples}>
<OpenAPITabs stateKey={createStateKey('response-example')} items={tabs}>
<InteractiveSection header={<OpenAPITabsList />} className="openapi-response-example">
<OpenAPITabsPanels />
</InteractiveSection>
@@ -123,6 +91,212 @@ export function OpenAPIResponseExample(props: {
);
}
function OpenAPIResponse(props: {
context: OpenAPIContextProps;
content: {
[media: string]: OpenAPIV3.MediaTypeObject;
};
}) {
const { context, content } = props;
const entries = Object.entries(content);
const firstEntry = entries[0];
if (!firstEntry) {
throw new Error('One media type is required');
}
if (entries.length === 1) {
const [mediaType, mediaTypeObject] = firstEntry;
return (
<OpenAPIResponseMediaType
context={context}
mediaType={mediaType}
mediaTypeObject={mediaTypeObject}
/>
);
}
const tabs = entries.map((entry) => {
const [mediaType, mediaTypeObject] = entry;
return {
key: mediaType,
label: mediaType,
body: (
<OpenAPIResponseMediaType
context={context}
mediaType={mediaType}
mediaTypeObject={mediaTypeObject}
/>
),
};
});
return (
<OpenAPITabs stateKey={createStateKey('response-media-types')} items={tabs}>
<InteractiveSection
header={<OpenAPITabsList />}
className="openapi-response-media-types"
>
<OpenAPITabsPanels />
</InteractiveSection>
</OpenAPITabs>
);
}
function OpenAPIResponseMediaType(props: {
mediaTypeObject: OpenAPIV3.MediaTypeObject;
mediaType: string;
context: OpenAPIContextProps;
}) {
const { mediaTypeObject, mediaType } = props;
const examples = getExamplesFromMediaTypeObject({ mediaTypeObject, mediaType });
const syntax = getSyntaxFromMediaType(mediaType);
const firstExample = examples[0];
if (!firstExample) {
return <OpenAPIEmptyResponseExample />;
}
if (examples.length === 1) {
return (
<OpenAPIExample
example={firstExample.example}
context={props.context}
syntax={syntax}
/>
);
}
const tabs = examples.map((example) => {
return {
key: example.key,
label: example.example.summary || example.key,
body: (
<OpenAPIExample
example={firstExample.example}
context={props.context}
syntax={syntax}
/>
),
};
});
return (
<OpenAPITabs stateKey={createStateKey('response-media-type-examples')} items={tabs}>
<InteractiveSection
header={<OpenAPITabsList />}
className="openapi-response-media-type-examples"
>
<OpenAPITabsPanels />
</InteractiveSection>
</OpenAPITabs>
);
}
/**
* Display an example.
*/
function OpenAPIExample(props: {
example: OpenAPIV3.ExampleObject;
context: OpenAPIContextProps;
syntax: string;
}) {
const { example, context, syntax } = props;
const code = stringifyExample({ example, xml: syntax === 'xml' });
if (code === null) {
return <OpenAPIEmptyResponseExample />;
}
return <context.CodeBlock code={code} syntax={syntax} />;
}
function stringifyExample(args: { example: OpenAPIV3.ExampleObject; xml: boolean }): string | null {
const { example, xml } = args;
if (!example.value) {
return null;
}
if (typeof example.value === 'string') {
return example.value;
}
if (xml) {
return json2xml(example.value);
}
return stringifyOpenAPI(example.value, null, 2);
}
/**
* Get the syntax from a media type.
*/
function getSyntaxFromMediaType(mediaType: string): string {
if (mediaType.includes('json')) {
return 'json';
}
if (mediaType === 'application/xml') {
return 'xml';
}
return 'text';
}
/**
* Get examples from a media type object.
*/
function getExamplesFromMediaTypeObject(args: {
mediaType: string;
mediaTypeObject: OpenAPIV3.MediaTypeObject;
}): { key: string; example: OpenAPIV3.ExampleObject }[] {
const { mediaTypeObject, mediaType } = args;
if (mediaTypeObject.examples) {
return Object.entries(mediaTypeObject.examples).map(([key, example]) => {
return {
key,
example: checkIsReference(example) ? getExampleFromReference(example) : example,
};
});
}
if (mediaTypeObject.example) {
return [{ key: 'default', example: { value: mediaTypeObject.example } }];
}
if (mediaTypeObject.schema) {
if (mediaType === 'application/xml') {
// @TODO normally we should use the name of the schema but we don't have it
// fix it when we got the reference name
const root = mediaTypeObject.schema.xml?.name ?? 'object';
return [
{
key: 'default',
example: {
value: {
[root]: generateSchemaExample(mediaTypeObject.schema, {
xml: mediaType === 'application/xml',
}),
},
},
},
];
}
return [
{
key: 'default',
example: { value: generateSchemaExample(mediaTypeObject.schema) },
},
];
}
return [];
}
/**
* Empty response example.
*/
function OpenAPIEmptyResponseExample() {
return (
<pre className="openapi-response-example-empty">
@@ -131,15 +305,9 @@ function OpenAPIEmptyResponseExample() {
);
}
function handleUnresolvedReference(
input: OpenAPIV3.ExampleObject | null,
): OpenAPIV3.ExampleObject | null {
const isReference = checkIsReference(input?.value);
if (isReference) {
// If we find a reference that wasn't resolved or needed to be resolved externally, render out the URL
return { value: input.value.$ref };
}
return input;
/**
* Generate an example from a reference object.
*/
function getExampleFromReference(ref: OpenAPIV3.ReferenceObject): OpenAPIV3.ExampleObject {
return { summary: 'Unresolved reference', value: { $ref: ref.$ref } };
}
+18 -6
View File
@@ -13,8 +13,8 @@ import { OpenAPIDisclosure } from './OpenAPIDisclosure';
type CircularRefsIds = Map<OpenAPIV3.SchemaObject, string>;
export interface OpenAPISchemaPropertyEntry {
propertyName?: string;
required?: boolean;
propertyName?: string | undefined;
required?: boolean | undefined;
schema: OpenAPIV3.SchemaObject;
}
@@ -47,7 +47,7 @@ export function OpenAPISchemaProperty(
? null
: getSchemaAlternatives(schema, new Set(circularRefs.keys()));
if ((properties && !!properties.length) || schema.type === 'object') {
if ((properties && properties.length > 0) || schema.type === 'object') {
return (
<InteractiveSection id={id} className={clsx('openapi-schema', className)}>
<OpenAPISchemaPresentation {...props} />
@@ -397,7 +397,7 @@ export function getSchemaTitle(
let type = 'any';
if (schema.enum) {
type = 'enum';
type = `${schema.type} · enum`;
// check array AND schema.items as this is sometimes null despite what the type indicates
} else if (schema.type === 'array' && !!schema.items) {
type = `${getSchemaTitle(schema.items)}[]`;
@@ -407,7 +407,7 @@ export function getSchemaTitle(
type = schema.type ?? 'object';
if (schema.format) {
type += ` ${schema.format}`;
type += ` · ${schema.format}`;
}
} else if ('anyOf' in schema) {
type = 'any of';
@@ -419,8 +419,20 @@ 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 = `nullable ${type}`;
type = `${type} | nullable`;
}
return type;
+2 -17
View File
@@ -8,7 +8,7 @@ import { OpenAPIResponses } from './OpenAPIResponses';
import { OpenAPISchemaProperties } from './OpenAPISchema';
import { OpenAPISecurities } from './OpenAPISecurities';
import type { OpenAPIClientContext, OpenAPIOperationData } from './types';
import { resolveDescription } from './utils';
import { parameterToProperty } from './utils';
/**
* Client component to render the spec for the request and response.
@@ -38,22 +38,7 @@ export function OpenAPISpec(props: { data: OpenAPIOperationData; context: OpenAP
header={group.label}
>
<OpenAPISchemaProperties
properties={group.parameters.map((parameter) => {
const description = resolveDescription(parameter);
return {
propertyName: parameter.name,
schema: {
// Description of the parameter is defined at the parameter level
// we use display it if the schema doesn't override it
description: description,
example: parameter.example,
// Deprecated can be defined at the parameter level
deprecated: parameter.deprecated,
...(parameter.schema ?? {}),
},
required: parameter.required,
};
})}
properties={group.parameters.map(parameterToProperty)}
context={context}
/>
</InteractiveSection>
@@ -0,0 +1,18 @@
// Bun Snapshot v1, https://goo.gl/fbAQLP
exports[`getUrlFromServerState indents correctly 1`] = `
"<?xml version="1.0"?>
<id>10</id>
<name>doggie</name>
<category>
<id>1</id>
<name>Dogs</name>
</category>
<photoUrls>string</photoUrls>
<tags>
<id>0</id>
<name>string</name>
</tags>
<status>available</status>
"
`;
@@ -3,18 +3,21 @@ import { getExampleFromSchema } from '@scalar/oas-utils/spec-getters';
type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue };
type ScalarGetExampleFromSchemaOptions = NonNullable<Parameters<typeof getExampleFromSchema>[1]>;
type GenerateSchemaExampleOptions = Pick<
ScalarGetExampleFromSchemaOptions,
'xml' | 'omitEmptyAndOptionalProperties' | 'mode'
>;
/**
* Generate a JSON example from a schema
*/
export function generateSchemaExample(
schema: OpenAPIV3.SchemaObject,
options: {
onlyRequired?: boolean;
} = {},
options?: GenerateSchemaExampleOptions,
): JSONValue | undefined {
return getExampleFromSchema(schema, {
emptyString: 'text',
omitEmptyAndOptionalProperties: options.onlyRequired,
variables: {
'date-time': new Date().toISOString(),
date: new Date().toISOString().split('T')[0],
@@ -28,6 +31,7 @@ export function generateSchemaExample(
byte: 'Ynl0ZXM=',
password: 'password',
},
...options,
});
}
@@ -36,9 +40,7 @@ export function generateSchemaExample(
*/
export function generateMediaTypeExample(
mediaType: OpenAPIV3.MediaTypeObject,
options: {
onlyRequired?: boolean;
} = {},
options?: GenerateSchemaExampleOptions,
): JSONValue | undefined {
if (mediaType.example) {
return mediaType.example;
@@ -0,0 +1,46 @@
import { describe, expect, it } from 'bun:test';
import { json2xml } from './json2xml';
describe('getUrlFromServerState', () => {
it('transforms JSON to xml', () => {
const xml = json2xml({
foo: 'bar',
});
expect(xml).toBe('<?xml version="1.0"?>\n<foo>bar</foo>\n');
});
it('wraps array items', () => {
const xml = json2xml({
urls: {
url: ['https://example.com', 'https://example.com'],
},
});
expect(xml).toBe(
'<?xml version="1.0"?>\n<urls>\n\t<url>https://example.com</url>\n\t<url>https://example.com</url>\n</urls>\n',
);
});
it('indents correctly', () => {
const xml = json2xml({
id: 10,
name: 'doggie',
category: {
id: 1,
name: 'Dogs',
},
photoUrls: ['string'],
tags: [
{
id: 0,
name: 'string',
},
],
status: 'available',
});
expect(xml).toMatchSnapshot();
});
});
+8
View File
@@ -0,0 +1,8 @@
import { jsXml } from 'json-xml-parse';
/**
* This function converts an object to XML.
*/
export function json2xml(data: Record<string, any>) {
return jsXml.toXmlString(data, { beautify: true });
}
@@ -9,7 +9,7 @@ async function fetchFilesystem(url: string) {
const filesystem = await parseOpenAPI({ value: text, rootURL: url });
const transformedFs = await traverse(filesystem, async (node) => {
if ('description' in node && typeof node.description === 'string' && node.description) {
node['x-description-html'] = node.description;
node['x-gitbook-description-html'] = node.description;
}
return node;
});
+13 -2
View File
@@ -1,6 +1,17 @@
/**
* Stringify an OpenAPI object. Same API as JSON.stringify.
*/
export function stringifyOpenAPI(body: unknown, transformer?: null, indent?: number): string {
return JSON.stringify(body, transformer, indent);
export function stringifyOpenAPI(body: unknown, _?: null, indent?: number): string {
return JSON.stringify(
body,
(key, value) => {
// Ignore internal keys
if (key.startsWith('x-gitbook-')) {
return undefined;
}
return value;
},
indent,
);
}
+1
View File
@@ -6,6 +6,7 @@ import type {
export interface OpenAPIContextProps extends OpenAPIClientContext {
CodeBlock: React.ComponentType<{ code: string; syntax: string }>;
renderHeading: (props: { deprecated: boolean; title: string }) => React.ReactNode;
/** Spec url for the Scalar Api Client */
specUrl: string;
+81 -5
View File
@@ -1,6 +1,8 @@
import type { AnyObject, OpenAPIV3 } from '@gitbook/openapi-parser';
import type { AnyObject, OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser';
export function checkIsReference(input: unknown): input is OpenAPIV3.ReferenceObject {
export function checkIsReference(
input: unknown,
): input is OpenAPIV3.ReferenceObject | OpenAPIV3_1.ReferenceObject {
return typeof input === 'object' && !!input && '$ref' in input;
}
@@ -12,9 +14,83 @@ export function createStateKey(key: string, scope?: string) {
* Resolve the description of an object.
*/
export function resolveDescription(object: AnyObject) {
return 'x-description-html' in object && typeof object['x-description-html'] === 'string'
? object['x-description-html']
return 'x-gitbook-description-html' in object &&
typeof object['x-gitbook-description-html'] === 'string'
? object['x-gitbook-description-html'].trim()
: typeof object.description === 'string'
? object.description
? object.description.trim()
: undefined;
}
/**
* Extract descriptions from an object.
*/
export function extractDescriptions(object: AnyObject) {
return {
description: object.description,
['x-gitbook-description-html']:
'x-gitbook-description-html' in object
? object['x-gitbook-description-html']
: undefined,
};
}
/**
* Resolve the first example from an object.
*/
export function resolveFirstExample(object: AnyObject) {
if ('examples' in object && typeof object.examples === 'object' && object.examples) {
const keys = Object.keys(object.examples);
const firstKey = keys[0];
if (firstKey && object.examples[firstKey]) {
return object.examples[firstKey];
}
}
if ('example' in object && object.example !== undefined) {
return object.example;
}
return undefined;
}
/**
* Resolve the schema of a parameter.
* Extract the description, example and deprecated from parameter.
*/
export function resolveParameterSchema(
parameter: OpenAPIV3.ParameterBaseObject,
): OpenAPIV3.SchemaObject {
const schema = checkIsReference(parameter.schema) ? undefined : parameter.schema;
return {
// Description of the parameter is defined at the parameter level
// we use display it if the schema doesn't override it
...extractDescriptions(parameter),
example: resolveFirstExample(parameter),
// Deprecated can be defined at the parameter level
deprecated: parameter.deprecated,
...schema,
};
}
/**
* Transform a parameter object to a property object.
*/
export function parameterToProperty(
parameter: OpenAPIV3.ParameterObject | OpenAPIV3.ReferenceObject | OpenAPIV3_1.ReferenceObject,
): {
propertyName: string | undefined;
schema: OpenAPIV3.SchemaObject;
required: boolean | undefined;
} {
if (checkIsReference(parameter)) {
return {
propertyName: parameter.$ref ?? 'Unknown ref',
schema: {},
required: undefined,
};
}
return {
propertyName: parameter.name,
schema: resolveParameterSchema(parameter),
required: parameter.required,
};
}