Optimize markdown parsing (#2850)

This commit is contained in:
Greg Bergé
2025-02-19 10:29:33 +01:00
committed by GitHub
parent 359bb979f8
commit 61575838f3
15 changed files with 283 additions and 80 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@gitbook/openapi-parser': patch
'@gitbook/react-openapi': patch
'gitbook': patch
---
Improve Markdown parsing
+15 -17
View File
@@ -31,7 +31,7 @@
},
"packages/gitbook": {
"name": "gitbook",
"version": "0.6.0",
"version": "0.6.1",
"dependencies": {
"@gitbook/api": "^0.93.0",
"@gitbook/cache-do": "workspace:*",
@@ -61,6 +61,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 +73,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",
},
@@ -207,7 +203,7 @@
},
"packages/react-openapi": {
"name": "@gitbook/react-openapi",
"version": "1.0.0",
"version": "1.0.1",
"dependencies": {
"@gitbook/openapi-parser": "workspace:*",
"@scalar/api-client-react": "1.0.87",
@@ -2210,7 +2206,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 +2328,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=="],
@@ -2542,7 +2538,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 +4288,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=="],
@@ -4682,6 +4676,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 +4696,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 +4752,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 +4808,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 +5610,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=="],
+2 -6
View File
@@ -45,6 +45,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 +57,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"
},
@@ -4,7 +4,7 @@ import { OpenAPIOperation } from '@gitbook/react-openapi';
import React from 'react';
import { LoadingPane } from '@/components/primitives';
import { fetchOpenAPIBlock } from '@/lib/openapi';
import { fetchOpenAPIBlock } from '@/lib/openapi/fetch';
import { tcls } from '@/lib/tailwind';
import { BlockProps } from '../Block';
@@ -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>"
`;
@@ -1,7 +1,7 @@
import { JSONDocument, ContentRef } from '@gitbook/api';
import { getNodeText } from './document';
import { fetchOpenAPIBlock } from './openapi';
import { fetchOpenAPIBlock } from './openapi/fetch';
import { ResolvedContentRef } from './references';
export interface DocumentSection {
+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,11 +1,11 @@
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';
/**
* Fetch an OpenAPI specification for an operation.
@@ -40,7 +40,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 +58,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
+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;
}
/**
@@ -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;
});
+3 -2
View File
@@ -12,8 +12,9 @@ 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']
: typeof object.description === 'string'
? object.description
: undefined;