Merge remote-tracking branch 'source/master' into akumarsingh/BDEVEXP-1200

# Conflicts:
#	package.json
#	src/components/ApiInfo/ApiInfo.tsx
#	src/components/SearchBox/styled.elements.tsx
#	src/services/RedocNormalizedOptions.ts
#	src/theme.ts
This commit is contained in:
akumarsingh 2020-01-27 14:44:31 -05:00
commit a460e38299
88 changed files with 6211 additions and 6924 deletions

View File

@ -36,7 +36,7 @@ $ yarn install # or npm
# dev-server, watch and auto reload playground # dev-server, watch and auto reload playground
$ yarn start $ yarn start
# start playground app in production environement # start playground app in production environment
$ yarn start:prod $ yarn start:prod
# runt tslint # runt tslint
@ -84,11 +84,11 @@ There are some other scripts available in the `scripts` section of the `package.
- **`src`**: contains the source code. The codebase is written in Typescript. CSS styles are managed with [Styled components](https://www.styled-components.com/). State is managed by [MobX](https://github.com/mobxjs/mobx) - **`src`**: contains the source code. The codebase is written in Typescript. CSS styles are managed with [Styled components](https://www.styled-components.com/). State is managed by [MobX](https://github.com/mobxjs/mobx)
- **`src/common-elements`**: containts common Styled elements or components used in multiple places - **`src/common-elements`**: contains common Styled elements or components used in multiple places
- **`src/components`**: contains main visual components - **`src/components`**: contains main visual components
- **`src/services`**: contains different services used by ReDoc including MobX stores - **`src/services`**: contains different services used by ReDoc including MobX stores
- **`src/services/models`**: contains classes for OpenAPI entities (e.g. Response, Operations, etc) - **`src/services/models`**: contains classes for OpenAPI entities (e.g. Response, Operations, etc)
- **`src/types`**: contains extra typescript typings including OpenAPI doc typings - **`src/types`**: contains extra typescript typings including OpenAPI doc typings
- **`src/utils`**: utility functions - **`src/utils`**: utility functions
- **`src/styled-components.ts`**: - reexprots styled-components with proper typescript annotations using theme - **`src/styled-components.ts`**: - reexports styled-components with proper typescript annotations using theme
- **`src/theme.ts`**: - default theme (colors, fonts, etc) used by all the components - **`src/theme.ts`**: - default theme (colors, fonts, etc) used by all the components

21
.github/workflows/unit-tests.yml vendored Normal file
View File

@ -0,0 +1,21 @@
name: Unit Tests
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: 10.x
- name: yarn install, build, and test
run: |
npm install -g yarn
yarn install
yarn bundle
yarn test

View File

@ -1,7 +1,10 @@
language: node_js language: node_js
node_js: node_js:
- '8' - '10'
cache: yarn cache:
yarn: true
directories:
- "~/.cache"
env: env:
global: global:
- GH_REF: github.com/Redocly/redoc.git - GH_REF: github.com/Redocly/redoc.git
@ -11,9 +14,12 @@ env:
- secure: apiavCfCQngL9Een1m7MIXMf3bqO3rY4YY59TMBl/yFKi80CEsHPHhgVUkl6hC+aM5PeBt/vgjh37rHMX31j/pcSZ4Z8SO/4Bwr36iHfhSxSEuAQog8P07qWqH7wYYWGIVmF682stgl0fYF+GN92sx/6edFVzsWVECf2G7imtICKSTbhKGm3Dhn2JwGnhD7eyfgZ33omgiaswumdu0xABoXDfqSZR+16fC4Ap5rhv3fXO9ndvRNy1STn376nT+my6e86UrQL4aS/S+HNHgIe1BUs+5cOp6Jgw6t0ie7phY0EAiECsRxy9K4e3Dctv9m6+Wma4+vy65MS0zGyrqey6oyV4l827sCOjrD1qcqc9bX6FlMSouVoNfE4ZjINNAbgigTaiLSoDSPcf5I5smkkM2ezzFOMSZwZxNdaNL2LKb97vc8m/ZUkv0sKZyT7oqVL7aJweEivsSHj5l2KR8Z7XrVB1y2eI6GvyTSa/d+CL4dSRzjh8+IRN047YBrdTKD5IkdT0upfoBu14WPUfFmLKxX+iMCslXRWb6kwojhrWNYmZvL65KRAzJ6+eIPDG/W5QUOpYyYT77bLlBQjVo6NmVvl9v3HMECq9CHH0ivKFBGPiKMOx7cJkTax3FuyznOW2WCXB9kTb5Zk9toaiNlSp9L6ll/h2Eyxa6n6sWUgmmM= - secure: apiavCfCQngL9Een1m7MIXMf3bqO3rY4YY59TMBl/yFKi80CEsHPHhgVUkl6hC+aM5PeBt/vgjh37rHMX31j/pcSZ4Z8SO/4Bwr36iHfhSxSEuAQog8P07qWqH7wYYWGIVmF682stgl0fYF+GN92sx/6edFVzsWVECf2G7imtICKSTbhKGm3Dhn2JwGnhD7eyfgZ33omgiaswumdu0xABoXDfqSZR+16fC4Ap5rhv3fXO9ndvRNy1STn376nT+my6e86UrQL4aS/S+HNHgIe1BUs+5cOp6Jgw6t0ie7phY0EAiECsRxy9K4e3Dctv9m6+Wma4+vy65MS0zGyrqey6oyV4l827sCOjrD1qcqc9bX6FlMSouVoNfE4ZjINNAbgigTaiLSoDSPcf5I5smkkM2ezzFOMSZwZxNdaNL2LKb97vc8m/ZUkv0sKZyT7oqVL7aJweEivsSHj5l2KR8Z7XrVB1y2eI6GvyTSa/d+CL4dSRzjh8+IRN047YBrdTKD5IkdT0upfoBu14WPUfFmLKxX+iMCslXRWb6kwojhrWNYmZvL65KRAzJ6+eIPDG/W5QUOpYyYT77bLlBQjVo6NmVvl9v3HMECq9CHH0ivKFBGPiKMOx7cJkTax3FuyznOW2WCXB9kTb5Zk9toaiNlSp9L6ll/h2Eyxa6n6sWUgmmM=
- secure: vVRg9BKGBwF2MbXQnEccFL+XW0/7RaBmge9k7jbGYScBwkP3XjnQ/Xaj0cvTz2CM2EqXsbpwfvr4Jo+enW/E3MGy5RiEzv5hUe/jIFRR0gfAFbZxSTvg5xiFhTDffqQk0fncO4jXu+wPO5lZ2CMRWzyXz3i1MZhjMcAgoDr1+TRss/EGXLNHxr2RM88tpUW0fV2prIRoyGqhCgnYZtrm7hmr41Ej+itg1MqZLml/Rjkt3KsNgI+z0O5Qn3QSAO8GtPZqeftQxAjevOmxZGcssxY8EJvqbjAujr4y51WncXpEmCRPSY2J9R5+fkgZurqwnJapbQpjwKYemok3ps7EHg2gWkAlmPdQO4LKpbffGkM/o5b+8+HdIuQZugsSWQD9hUSftTAFLcfA1isi7V2lHE1m8bX/vk9zIyDdcPSwIaFe9y+w3PexwFmTjPLq+nia/UY2kARFZMEIFAJby6gkA70DcAJ50QOM86InJu5DSzGbIssgTGAXCn0TPPyGveaurVLw8x61j3yh8LDF46gUHey3rqv6WjpCM9h/vg7X/gq5ve/5Q2KHscUKfs/sA53Mt7qPeqRZY1QCaaRjzqJO/ZraHqWWeKmPKaWhPGR0kYEnkvB+K9GZ+HNSWCltjCO4SJ1xeEl7CRqQxAwdiMATF5SKqyiC+bn5oc35mFgbRF8= - secure: vVRg9BKGBwF2MbXQnEccFL+XW0/7RaBmge9k7jbGYScBwkP3XjnQ/Xaj0cvTz2CM2EqXsbpwfvr4Jo+enW/E3MGy5RiEzv5hUe/jIFRR0gfAFbZxSTvg5xiFhTDffqQk0fncO4jXu+wPO5lZ2CMRWzyXz3i1MZhjMcAgoDr1+TRss/EGXLNHxr2RM88tpUW0fV2prIRoyGqhCgnYZtrm7hmr41Ej+itg1MqZLml/Rjkt3KsNgI+z0O5Qn3QSAO8GtPZqeftQxAjevOmxZGcssxY8EJvqbjAujr4y51WncXpEmCRPSY2J9R5+fkgZurqwnJapbQpjwKYemok3ps7EHg2gWkAlmPdQO4LKpbffGkM/o5b+8+HdIuQZugsSWQD9hUSftTAFLcfA1isi7V2lHE1m8bX/vk9zIyDdcPSwIaFe9y+w3PexwFmTjPLq+nia/UY2kARFZMEIFAJby6gkA70DcAJ50QOM86InJu5DSzGbIssgTGAXCn0TPPyGveaurVLw8x61j3yh8LDF46gUHey3rqv6WjpCM9h/vg7X/gq5ve/5Q2KHscUKfs/sA53Mt7qPeqRZY1QCaaRjzqJO/ZraHqWWeKmPKaWhPGR0kYEnkvB+K9GZ+HNSWCltjCO4SJ1xeEl7CRqQxAwdiMATF5SKqyiC+bn5oc35mFgbRF8=
- secure: ela1tn4wkJQZ8O4iv+4pIZi5cebxeCStVF1tEUe6qa6WWgJYVXmS2tEv3QQ36NUBFrP58Y6yl10XguPnvj/2BCqcZI4FUBHh3BfiBoUtXxDCVKI5LtlniNiOFGUwfzEeYka8T51zFlcUXSCCaxHkRZbmBsIzeJ39UwTi5fy0qwLv9GgL0czhwm8I8sZ8gyWdGmqpXNFEsb9JP4ZA3mw2qpWkGpGAqQPD9XSCkU3LmX1/ltwsBMAgGYKLLo7vU8d5KV2c8L1Gnxfl6BvfmqUD/dsas/1rnk08rU2nez5ekuQa2tJRkDLOv8bqvrGRLjHSUa3yPuisC6SsDGSU7/3DcozZyYsz7WQ6WI8tYabyjqqeJTF1N8a5T3IbZaZNV1J4JHOO9Cb/y7gIg4edANg6tbe7MzZpdEPRBnw6OkdTdirpNsWQ/jnfpY1hn6mraQZz/q8yaz3W21NjbBJhVnvfh5gWLKQ3YAAziCBhmmrThFhUu0czz+G920MuFo477TBcxvlrE7CaNJ0Q6yYkDehEPOv3jvEs1QVHPwuRrlaLTbBhrlTICKZ58gdX30O8N4i0Xgp/v6qrC03bplnMQc8E/uC61wcVLJixnlZVp8FODpUvPjsxVFkpuNSOIAaiqcERmoiPXx05Epzmr78hjU5rYCx/1MmVoeB4gs9YO+4guD4= - secure: ela1tn4wkJQZ8O4iv+4pIZi5cebxeCStVF1tEUe6qa6WWgJYVXmS2tEv3QQ36NUBFrP58Y6yl10XguPnvj/2BCqcZI4FUBHh3BfiBoUtXxDCVKI5LtlniNiOFGUwfzEeYka8T51zFlcUXSCCaxHkRZbmBsIzeJ39UwTi5fy0qwLv9GgL0czhwm8I8sZ8gyWdGmqpXNFEsb9JP4ZA3mw2qpWkGpGAqQPD9XSCkU3LmX1/ltwsBMAgGYKLLo7vU8d5KV2c8L1Gnxfl6BvfmqUD/dsas/1rnk08rU2nez5ekuQa2tJRkDLOv8bqvrGRLjHSUa3yPuisC6SsDGSU7/3DcozZyYsz7WQ6WI8tYabyjqqeJTF1N8a5T3IbZaZNV1J4JHOO9Cb/y7gIg4edANg6tbe7MzZpdEPRBnw6OkdTdirpNsWQ/jnfpY1hn6mraQZz/q8yaz3W21NjbBJhVnvfh5gWLKQ3YAAziCBhmmrThFhUu0czz+G920MuFo477TBcxvlrE7CaNJ0Q6yYkDehEPOv3jvEs1QVHPwuRrlaLTbBhrlTICKZ58gdX30O8N4i0Xgp/v6qrC03bplnMQc8E/uC61wcVLJixnlZVp8FODpUvPjsxVFkpuNSOIAaiqcERmoiPXx05Epzmr78hjU5rYCx/1MmVoeB4gs9YO+4guD4=
- secure: SEqTg6WoGPPpcWzJ03ZfcSBb3nZ2Mdhug0ec2PszuzYO3libCb9usiqi+jils9z6qyXsL6ecz8HYazDGOUepnubhIpI5otLgfn9XiapjMT06Bj//AjbKpH7eu3TJSpJMzoRHZrKIE1y9ZKIBqKwl9Xs7ko+1oa+MLhrLuxXkoi0JqRB5UzkQtJRDoxVNjysnLQn+hsfnm+yuqPHZd2+Loy++q//WHuf9bwJrlkXn2ICYQIX5oQGlxNO6ui+OZklb0YknvyO5GdQeoKaHYru3MMKKCIS6I7AG9wLmPs5Ou3T0Ia0Xx4/7xazs0rH4NCVpIceSYc3v6evR37pp8MsFTC3BzjL1V3slTnmitC1KSNM8ndGRUg1nsCBkJysnR3HpX6SHuCH+UzOuMxEjwiPdSRnzJPEbTHa1HqMfTkTJMbm4zhp7W4/ozX4TtjUB0ql6NoQE2n0Z3aYgR2C78TmzaPQun8EgredWnCID1FedyexaNcw4HyZ2rXlcvG3rBzSwLHH5PePT9skyqy6KtIaL0MlAP556ilgUeyCZfCNdTmzCvPDZuqaeLRezWDdsKnRfTkxIW80QWlmZ6sW0hynJV5JN2Oghk9Tr+QzgV4ZF68FHwoU9YXCTyX4w5iTYq/GjvfTBqB3VSGPOz3PwU7r47tmaYzPj+I44zqktgxyuxDo= - secure: Pc86j2/rgCPAEWcjzPbbVFkL2SwNt4CpeXP69zedMi9RomQblMJa9R0wbAT6H5VCnPky2cpmxkzjFWOc91N9crzceg6aOoWkPr6pTTnTv+EL70i6+XSrAFpkgRjszprxnU1Bz+GcYEjP/zR8479fx8ooSl7MwHOaP7XiQyaBQAbY1CPlmpT+b4Ut7Fm5QnD90/NgPjbKkngl0kVUfNFdFOSfJ3QWLyFCUUSQ4DlxccJOTIaOH/n8u9Nz4NTuuHE3XeRuEsuj2SJutJnFBUYwsvugrdPvKWiubkewJfylp6EwABzByENsg6XxW9SIq80lMc3Oi7ld9L2lAgpj+8/42olnbMzH0F0rw/p1ccPAdVwQVV6YFaqCzivK5A5aX5LmGKwJ6SR9k1PgcWP6sKKMIsIEObbyM88Tke3QkreEz+cLg/3jjko7Vpb0tbqh8BtbpWV+exL4rX3r2C5Mb1Es1W597hN5LSczWYFgw0ZETpfbVZg6Ri1iZks0wpsT/E+c0q2scUaBVrdTZseHxUPB7mPDlXL1l9/i4sOxPyBHZtJRAzeTT/fOXfj4vuD+ihspXzoRRLaQbizlb8FpyPA47XdmBDpXi3OBiaIFLwvybEn7qM7rqvWxdz6vvCZv0t/AN3t3Qvh2vHKCshHecaa8NoJQHWrdFMHeecYHyeoujZ8=
addons: addons:
chrome: stable chrome: stable
apt:
packages:
- libgconf-2-4
before_script: npm run bundle before_script: npm run bundle
script: npm test && ([ "${TRAVIS_PULL_REQUEST}" = "false" ] && npm run e2e-ci || npm script: npm test && ([ "${TRAVIS_PULL_REQUEST}" = "false" ] && npm run e2e-ci || npm
run e2e) run e2e)
@ -27,9 +33,6 @@ deploy:
api_key: "$NPM_TOKEN" api_key: "$NPM_TOKEN"
on: on:
tags: true tags: true
- provider: script
skip_cleanup: true
script: cd cli && yarn install && yarn ci-publish || true
- provider: script - provider: script
skip_cleanup: true skip_cleanup: true
script: yarn deploy:demo script: yarn deploy:demo

View File

@ -1,3 +1,203 @@
# [2.0.0-rc.22](https://github.com/Redocly/redoc/compare/v2.0.0-rc.21...v2.0.0-rc.22) (2020-01-15)
### Bug Fixes
* do not process oneOf if inherited from parent with discriminator ([5248415](https://github.com/Redocly/redoc/commit/52484157912d908daea8255d0b7d684b33258d7a))
### Features
* add HTTP syntax highlighting ([#1157](https://github.com/Redocly/redoc/issues/1157)) ([27a4af7](https://github.com/Redocly/redoc/commit/27a4af707686d56280753473b4294ee4af096534))
# [2.0.0-rc.21](https://github.com/Redocly/redoc/compare/v2.0.0-rc.20...v2.0.0-rc.21) (2020-01-10)
### Bug Fixes
* empty servers behaviour per OAS spec ([ed1db0c](https://github.com/Redocly/redoc/commit/ed1db0c9027087ae0ae923e390e3e1d638a647ae)), closes [#1151](https://github.com/Redocly/redoc/issues/1151)
* fix duplicated content in tags when using md headings ([a260c84](https://github.com/Redocly/redoc/commit/a260c8414c34a259a70a20ebcd20ecbb06c3d250)), closes [#1150](https://github.com/Redocly/redoc/issues/1150) [#1152](https://github.com/Redocly/redoc/issues/1152)
* use mobile menu background color value from theme ([#1144](https://github.com/Redocly/redoc/issues/1144)) ([41a9b3c](https://github.com/Redocly/redoc/commit/41a9b3c18228d236d182d3c15c9abc35ae72a0d5))
# [2.0.0-rc.20](https://github.com/Redocly/redoc/compare/v2.0.0-rc.19...v2.0.0-rc.20) (2019-12-13)
### Bug Fixes
* fix missing parameters ([942d782](https://github.com/Redocly/redoc/commit/942d782b5a8d08767a7538741b75587cf1e67f44)), closes [#1142](https://github.com/Redocly/redoc/issues/1142)
# [2.0.0-rc.19](https://github.com/Redocly/redoc/compare/v2.0.0-rc.18...v2.0.0-rc.19) (2019-12-13)
### Bug Fixes
* change the title of "Security Scheme Type" to match "HTTP Authorization Scheme" ([#1126](https://github.com/Redocly/redoc/issues/1126)) ([289c8e6](https://github.com/Redocly/redoc/commit/289c8e6ae1ff00371f86d3f2646607c64bc30050))
* do not URI-encode parameter values for better readability ([6aeb0bf](https://github.com/Redocly/redoc/commit/6aeb0bf68df3f03f2ca1317f8b5787545bd363f1)), closes [#1138](https://github.com/Redocly/redoc/issues/1138)
* fix sortByRequired (stabilise sort) ([#1136](https://github.com/Redocly/redoc/issues/1136)) ([d92434d](https://github.com/Redocly/redoc/commit/d92434d11b08e8b0f6be5453ec69aa1d0e0df79f)), closes [#1104](https://github.com/Redocly/redoc/issues/1104) [#1121](https://github.com/Redocly/redoc/issues/1121) [#1061](https://github.com/Redocly/redoc/issues/1061)
* h2 padding on mobile ([7ed1a7e](https://github.com/Redocly/redoc/commit/7ed1a7ef0e7978a0dfb40afcc72c3362466f9624)), closes [#1118](https://github.com/Redocly/redoc/issues/1118)
* python comment stripped in headings ([4a25aae](https://github.com/Redocly/redoc/commit/4a25aaef69fad814836392ea7e41eb32c182a261)), closes [#1116](https://github.com/Redocly/redoc/issues/1116)
* remove hardcoded fontFamily for oneOf labels ([094ce91](https://github.com/Redocly/redoc/commit/094ce914e3f9cfe567b39db4ea88208014d8b686)), closes [#1120](https://github.com/Redocly/redoc/issues/1120)
* search-box use theme ([1bf490c](https://github.com/Redocly/redoc/commit/1bf490c05b343d262f8819bf1ddc433e070be1b9))
* support discriminator mapping 1-n ([6e390f9](https://github.com/Redocly/redoc/commit/6e390f9c7909da0b5d1d6fc571ab4ad92e715d6e)), closes [#1111](https://github.com/Redocly/redoc/issues/1111)
* wrap json examples in code tag ([#1064](https://github.com/Redocly/redoc/issues/1064)) ([dc5430e](https://github.com/Redocly/redoc/commit/dc5430e53def780a81612d269cc3aea3f8785eea))
### Features
* display `multipleOf` constrains ([#1065](https://github.com/Redocly/redoc/issues/1065)) ([3e90133](https://github.com/Redocly/redoc/commit/3e901336643b988ae45ae86c485005b8865e6e04))
* enable menuToggle by default ([5d81abe](https://github.com/Redocly/redoc/commit/5d81abeb28c1e4f2826e41424c10163834c37e45))
* new option hideSchemaTitles ([11cc4c4](https://github.com/Redocly/redoc/commit/11cc4c4c3e04a7e5bf3a9ebba20d10fa882a49e5))
* new option payloadSampleIdx ([eaaa99d](https://github.com/Redocly/redoc/commit/eaaa99d68e2392273e8d9c0173db3b546e035d5f))
* **cli:** Fallback on the spec's title before falling back on… ([#1073](https://github.com/Redocly/redoc/issues/1073)) ([e01eea4](https://github.com/Redocly/redoc/commit/e01eea445c93d74b66533c860d76bb3aff4d6df2))
# [2.0.0-rc.18](https://github.com/Redocly/redoc/compare/v2.0.0-rc.17...v2.0.0-rc.18) (2019-10-16)
### Bug Fixes
* add oneOf buttons vertical space when wrapped to new line ([cd9fd61](https://github.com/Redocly/redoc/commit/cd9fd61))
* improve mime-type dropdown font ([ce885f8](https://github.com/Redocly/redoc/commit/ce885f8))
# [2.0.0-rc.17](https://github.com/Redocly/redoc/compare/v2.0.0-rc.16...v2.0.0-rc.17) (2019-10-16)
### Bug Fixes
* active menu item scroll into view ([0a01e9a](https://github.com/Redocly/redoc/commit/0a01e9a))
* changed several components style font-family to monospace ([#1063](https://github.com/Redocly/redoc/issues/1063)) ([0c20e64](https://github.com/Redocly/redoc/commit/0c20e64)), closes [#909](https://github.com/Redocly/redoc/issues/909)
* no quotes for default values in header fields. ([#1059](https://github.com/Redocly/redoc/issues/1059)) ([b5af71d](https://github.com/Redocly/redoc/commit/b5af71d))
* types over-pluralization ([#1057](https://github.com/Redocly/redoc/issues/1057)) ([4494f80](https://github.com/Redocly/redoc/commit/4494f80)), closes [#1053](https://github.com/Redocly/redoc/issues/1053)
### Features
* added support for file paths as --options cli argument ([#1049](https://github.com/Redocly/redoc/issues/1049)) ([4adb927](https://github.com/Redocly/redoc/commit/4adb927))
# [2.0.0-rc.16](https://github.com/Redocly/redoc/compare/v2.0.0-rc.15...v2.0.0-rc.16) (2019-09-30)
### Bug Fixes
* fix scrollYOffset when SSR ([d09c1c1](https://github.com/Redocly/redoc/commit/d09c1c1))
# [2.0.0-rc.15](https://github.com/Redocly/redoc/compare/v2.0.0-rc.14...v2.0.0-rc.15) (2019-09-30)
### Bug Fixes
* auth section appears twice ([5aa7784](https://github.com/Redocly/redoc/commit/5aa7784)), closes [#818](https://github.com/Redocly/redoc/issues/818)
* clicking on group title breaks first tag ([4649683](https://github.com/Redocly/redoc/commit/4649683)), closes [#1034](https://github.com/Redocly/redoc/issues/1034)
* do not crash on empty scopes ([e787d9e](https://github.com/Redocly/redoc/commit/e787d9e)), closes [#1044](https://github.com/Redocly/redoc/issues/1044)
* false-positive recursive detection with allOf at the same level ([faa74d6](https://github.com/Redocly/redoc/commit/faa74d6))
* fix scrollYOffset when SSR ([21258a5](https://github.com/Redocly/redoc/commit/21258a5))
* left menu item before group is not highlighted ([67e2a8f](https://github.com/Redocly/redoc/commit/67e2a8f)), closes [#1033](https://github.com/Redocly/redoc/issues/1033)
* remove excessive whitespace between md sections on small screens ([e318fb3](https://github.com/Redocly/redoc/commit/e318fb3)), closes [#874](https://github.com/Redocly/redoc/issues/874)
* use url-template dependency ([#1008](https://github.com/Redocly/redoc/issues/1008)) ([32a464a](https://github.com/Redocly/redoc/commit/32a464a)), closes [#1007](https://github.com/Redocly/redoc/issues/1007)
### Features
* **cli:** added support for JSON string value for --options CLI argument ([#1047](https://github.com/Redocly/redoc/issues/1047)) ([2a28130](https://github.com/Redocly/redoc/commit/2a28130)), closes [#797](https://github.com/Redocly/redoc/issues/797)
* **cli:** add `disableGoogleFont` parameter to cli ([#1045](https://github.com/Redocly/redoc/issues/1045)) ([aceb343](https://github.com/Redocly/redoc/commit/aceb343))
* new option expandDefaultServerVariables ([#1014](https://github.com/Redocly/redoc/issues/1014)) ([0360dce](https://github.com/Redocly/redoc/commit/0360dce))
# [2.0.0-rc.14](https://github.com/Redocly/redoc/compare/v2.0.0-rc.13...v2.0.0-rc.14) (2019-08-07)
### Bug Fixes
* fix escaping JSON string values ([58cb20d](https://github.com/Redocly/redoc/commit/58cb20d)), closes [#999](https://github.com/Redocly/redoc/issues/999)
* revert expanding default server variables ([7849f7f](https://github.com/Redocly/redoc/commit/7849f7f))
# [2.0.0-rc.13](https://github.com/Redocly/redoc/compare/v2.0.0-rc.12...v2.0.0-rc.13) (2019-08-01)
### Bug Fixes
* enum list doesn't wrap ([bfbb0c1](https://github.com/Redocly/redoc/commit/bfbb0c1)), closes [#993](https://github.com/Redocly/redoc/issues/993)
* incorrect serialization of some parameter samples ([aba45db](https://github.com/Redocly/redoc/commit/aba45db)), closes [#992](https://github.com/Redocly/redoc/issues/992)
* support json serialization for parameter examples ([1367380](https://github.com/Redocly/redoc/commit/1367380)), closes [#934](https://github.com/Redocly/redoc/issues/934)
* unify accordion icons for responses section ([2afc2e4](https://github.com/Redocly/redoc/commit/2afc2e4)), closes [#975](https://github.com/Redocly/redoc/issues/975)
* update to core.js 3 ([9e3375d](https://github.com/Redocly/redoc/commit/9e3375d)), closes [#997](https://github.com/Redocly/redoc/issues/997)
# [2.0.0-rc.12](https://github.com/Redocly/redoc/compare/v2.0.0-rc.11...v2.0.0-rc.12) (2019-07-30)
### Bug Fixes
* rename ObjectDescription to SchemaDefinition as discussed ([4496622](https://github.com/Redocly/redoc/commit/4496622))
# [2.0.0-rc.11](https://github.com/Redocly/redoc/compare/v2.0.0-rc.10...v2.0.0-rc.11) (2019-07-30)
### Bug Fixes
* do not add extra slashes to pattern ([70d1ee9](https://github.com/Redocly/redoc/commit/70d1ee9)), closes [#983](https://github.com/Redocly/redoc/issues/983)
* dropdown fixes related to object description ([0504ad4](https://github.com/Redocly/redoc/commit/0504ad4))
* incorrect serialization of parameter sample with hyphen ([f7dd658](https://github.com/Redocly/redoc/commit/f7dd658))
* redoc-cli: Add missing content type header on compressed responses of `/` path
### Features
* menu items from tags + md extension for Schema Definition ([#681](https://github.com/Redocly/redoc/pull/681))
* new option `menuToggle` - fold active MenuItem if clicked ([#963](https://github.com/Redocly/redoc/issues/963))
* Add option for skipping quotes in enums `enumSkipQuotes` ([#968](https://github.com/Redocly/redoc/issues/968)) ([afc7e36](https://github.com/Redocly/redoc/commit/afc7e36))
* add `sampleCollapseLevel` option ([#937](https://github.com/Redocly/redoc/issues/937)) ([d3f1c16](https://github.com/Redocly/redoc/commit/d3f1c16))
# [2.0.0-rc.10](https://github.com/Redocly/redoc/compare/v2.0.0-rc.9...v2.0.0-rc.10) (2019-07-08)
### Bug Fixes
* broken headings with single quote ([51d3b9b](https://github.com/Redocly/redoc/commit/51d3b9b)), closes [#955](https://github.com/Redocly/redoc/issues/955)
* fix fields table overflow if deeply nested with long title ([12b7057](https://github.com/Redocly/redoc/commit/12b7057))
* hide empty example when it is not defined ([4bd499f](https://github.com/Redocly/redoc/commit/4bd499f))
* markdown in examples descriptions + minor ui tweaks ([f52d9e8](https://github.com/Redocly/redoc/commit/f52d9e8))
* organize response examples in dropdown and display description ([995e557](https://github.com/Redocly/redoc/commit/995e557))
# [2.0.0-rc.9](https://github.com/Redocly/redoc/compare/v2.0.0-rc.8-1...v2.0.0-rc.9) (2019-06-27)
### Bug Fixes
* fix regression double slashes added to full URL display ([f29a4fe](https://github.com/Redocly/redoc/commit/f29a4fe))
* IE11, add missing Object.assign polyfill ([888f04e](https://github.com/Redocly/redoc/commit/888f04e))
* serialize parameter example values according to the spec ([#917](https://github.com/Redocly/redoc/issues/917)) ([3939286](https://github.com/Redocly/redoc/commit/3939286))
* styled-component style error in tabs ([#946](https://github.com/Redocly/redoc/issues/946)) ([c488bbf](https://github.com/Redocly/redoc/commit/c488bbf))
### Features
* add x-additionalPropertiesName ([#622](https://github.com/Redocly/redoc/issues/622)) ([#944](https://github.com/Redocly/redoc/issues/944)) ([0eb1e66](https://github.com/Redocly/redoc/commit/0eb1e66))
# [2.0.0-rc.8-1](https://github.com/Rebilly/ReDoc/compare/v2.0.0-rc.8...v2.0.0-rc.8-1) (2019-05-13) # [2.0.0-rc.8-1](https://github.com/Rebilly/ReDoc/compare/v2.0.0-rc.8...v2.0.0-rc.8-1) (2019-05-13)
@ -31,7 +231,7 @@
### Bug Fixes ### Bug Fixes
* broken schema talbes with long enums ([3a74b74](https://github.com/Rebilly/ReDoc/commit/3a74b74)) * broken schema tables with long enums ([3a74b74](https://github.com/Rebilly/ReDoc/commit/3a74b74))
* deep linking sometimes not working when sent over messengers ([2491d97](https://github.com/Rebilly/ReDoc/commit/2491d97)) * deep linking sometimes not working when sent over messengers ([2491d97](https://github.com/Rebilly/ReDoc/commit/2491d97))
@ -49,7 +249,7 @@
* IE11 add missing fetch and URL polyfills ([d2ce1bd](https://github.com/Rebilly/ReDoc/commit/d2ce1bd)), closes [#875](https://github.com/Rebilly/ReDoc/issues/875) * IE11 add missing fetch and URL polyfills ([d2ce1bd](https://github.com/Rebilly/ReDoc/commit/d2ce1bd)), closes [#875](https://github.com/Rebilly/ReDoc/issues/875)
* ignore empty x-tagGroups array ([#869](https://github.com/Rebilly/ReDoc/issues/869)) ([4366a0d](https://github.com/Rebilly/ReDoc/commit/4366a0d)) * ignore empty x-tagGroups array ([#869](https://github.com/Rebilly/ReDoc/issues/869)) ([4366a0d](https://github.com/Rebilly/ReDoc/commit/4366a0d))
* incorrect detected schema title for deeply inherited schemas ([7d7b4e3](https://github.com/Rebilly/ReDoc/commit/7d7b4e3)) * incorrect detected schema title for deeply inherited schemas ([7d7b4e3](https://github.com/Rebilly/ReDoc/commit/7d7b4e3))
* pluralize arrray of types ([fdcac30](https://github.com/Rebilly/ReDoc/commit/fdcac30)) * pluralize array of types ([fdcac30](https://github.com/Rebilly/ReDoc/commit/fdcac30))
* remove huge space after Authentication section ([548fae3](https://github.com/Rebilly/ReDoc/commit/548fae3)), closes [#872](https://github.com/Rebilly/ReDoc/issues/872) * remove huge space after Authentication section ([548fae3](https://github.com/Rebilly/ReDoc/commit/548fae3)), closes [#872](https://github.com/Rebilly/ReDoc/issues/872)
* remove query string from server URL ([#895](https://github.com/Rebilly/ReDoc/issues/895)) ([64453ff](https://github.com/Rebilly/ReDoc/commit/64453ff)) * remove query string from server URL ([#895](https://github.com/Rebilly/ReDoc/issues/895)) ([64453ff](https://github.com/Rebilly/ReDoc/commit/64453ff))
* remove tabs top margin ([5c187f3](https://github.com/Rebilly/ReDoc/commit/5c187f3)) * remove tabs top margin ([5c187f3](https://github.com/Rebilly/ReDoc/commit/5c187f3))
@ -133,7 +333,7 @@
* improve scrolling performance in Chrome with non-wrapped json examples ([a69c402](https://github.com/Rebilly/ReDoc/commit/a69c402)) * improve scrolling performance in Chrome with non-wrapped json examples ([a69c402](https://github.com/Rebilly/ReDoc/commit/a69c402))
* nested oneOf button spacing ([3673720](https://github.com/Rebilly/ReDoc/commit/3673720)), closes [#719](https://github.com/Rebilly/ReDoc/issues/719) * nested oneOf button spacing ([3673720](https://github.com/Rebilly/ReDoc/commit/3673720)), closes [#719](https://github.com/Rebilly/ReDoc/issues/719)
* onLoaded callback not run on spec error ([e77df0c](https://github.com/Rebilly/ReDoc/commit/e77df0c)), closes [#690](https://github.com/Rebilly/ReDoc/issues/690) * onLoaded callback not run on spec error ([e77df0c](https://github.com/Rebilly/ReDoc/commit/e77df0c)), closes [#690](https://github.com/Rebilly/ReDoc/issues/690)
* theme improvments by [@stasiukanya](https://github.com/stasiukanya) ([e2d0cd5](https://github.com/Rebilly/ReDoc/commit/e2d0cd5)) * theme improvements by [@stasiukanya](https://github.com/stasiukanya) ([e2d0cd5](https://github.com/Rebilly/ReDoc/commit/e2d0cd5))
* **cli:** old peer dependency issue with styled-components ([#699](https://github.com/Rebilly/ReDoc/issues/699)) ([9e2853c](https://github.com/Rebilly/ReDoc/commit/9e2853c)) * **cli:** old peer dependency issue with styled-components ([#699](https://github.com/Rebilly/ReDoc/issues/699)) ([9e2853c](https://github.com/Rebilly/ReDoc/commit/9e2853c))
@ -205,7 +405,7 @@
### Bug Fixes ### Bug Fixes
* addd indent to array schema internals ([865f3ce](https://github.com/Rebilly/ReDoc/commit/865f3ce)) * add indent to array schema internals ([865f3ce](https://github.com/Rebilly/ReDoc/commit/865f3ce))
* fix oneOf/anyOf titles ([39b930d](https://github.com/Rebilly/ReDoc/commit/39b930d)), closes [#618](https://github.com/Rebilly/ReDoc/issues/618) [#621](https://github.com/Rebilly/ReDoc/issues/621) * fix oneOf/anyOf titles ([39b930d](https://github.com/Rebilly/ReDoc/commit/39b930d)), closes [#618](https://github.com/Rebilly/ReDoc/issues/618) [#621](https://github.com/Rebilly/ReDoc/issues/621)
@ -251,7 +451,7 @@
### Bug Fixes ### Bug Fixes
* add some spacing between operation description and parameters ([597688e](https://github.com/Rebilly/ReDoc/commit/597688e)) * add some spacing between operation description and parameters ([597688e](https://github.com/Rebilly/ReDoc/commit/597688e))
* description is not rendered if doesn't containt markdown headings ([90ed717](https://github.com/Rebilly/ReDoc/commit/90ed717)), closes [#591](https://github.com/Rebilly/ReDoc/issues/591) * description is not rendered if doesn't contain markdown headings ([90ed717](https://github.com/Rebilly/ReDoc/commit/90ed717)), closes [#591](https://github.com/Rebilly/ReDoc/issues/591)
* download button downloads index.html instead of spec with CLI ([334f904](https://github.com/Rebilly/ReDoc/commit/334f904)), closes [#594](https://github.com/Rebilly/ReDoc/issues/594) * download button downloads index.html instead of spec with CLI ([334f904](https://github.com/Rebilly/ReDoc/commit/334f904)), closes [#594](https://github.com/Rebilly/ReDoc/issues/594)
* fix Authentication section is not rendered ([2ecc8bc](https://github.com/Rebilly/ReDoc/commit/2ecc8bc)), closes [#590](https://github.com/Rebilly/ReDoc/issues/590) * fix Authentication section is not rendered ([2ecc8bc](https://github.com/Rebilly/ReDoc/commit/2ecc8bc)), closes [#590](https://github.com/Rebilly/ReDoc/issues/590)
* fix linebreaks in multiparagraph field descriptions ([8fb9cd6](https://github.com/Rebilly/ReDoc/commit/8fb9cd6)) * fix linebreaks in multiparagraph field descriptions ([8fb9cd6](https://github.com/Rebilly/ReDoc/commit/8fb9cd6))
@ -562,7 +762,7 @@
### Bug Fixes ### Bug Fixes
* Path parameters are not correctly overriden ([c406dc5](https://github.com/Rebilly/ReDoc/commit/c406dc5)), closes [#400](https://github.com/Rebilly/ReDoc/issues/400) * Path parameters are not correctly overridden ([c406dc5](https://github.com/Rebilly/ReDoc/commit/c406dc5)), closes [#400](https://github.com/Rebilly/ReDoc/issues/400)
* Use parentNode instead of parentElement to fix IE11 crash ([e8adb60](https://github.com/Rebilly/ReDoc/commit/e8adb60)), closes [#406](https://github.com/Rebilly/ReDoc/issues/406) * Use parentNode instead of parentElement to fix IE11 crash ([e8adb60](https://github.com/Rebilly/ReDoc/commit/e8adb60)), closes [#406](https://github.com/Rebilly/ReDoc/issues/406)
@ -724,7 +924,7 @@
* do not ignore path level parameters ([14f8408](https://github.com/Rebilly/Redoc/commit/14f8408)) * do not ignore path level parameters ([14f8408](https://github.com/Rebilly/Redoc/commit/14f8408))
* improve rendering of types ([17da7b7](https://github.com/Rebilly/Redoc/commit/17da7b7)) * improve rendering of types ([17da7b7](https://github.com/Rebilly/Redoc/commit/17da7b7))
* move title propagation to the correct place ([0b0bc99](https://github.com/Rebilly/Redoc/commit/0b0bc99)) * move title propagation to the correct place ([0b0bc99](https://github.com/Rebilly/Redoc/commit/0b0bc99))
* owerwrite text-align to left ([bfee3ed](https://github.com/Rebilly/Redoc/commit/bfee3ed)) * overwrite text-align to left ([bfee3ed](https://github.com/Rebilly/Redoc/commit/bfee3ed))
### Features ### Features
@ -786,7 +986,7 @@ Complete rewrite also means that this rewrite may introduce issues, but they sho
### Deprecations ### Deprecations
- Fonts are not loaded by ReDoc so you should load them. Default fonts can be loaded as bellow: - Fonts are not loaded by ReDoc so you should load them. Default fonts can be loaded as below:
```html ```html
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
@ -1155,7 +1355,7 @@ closes [#321](https://github.com/Rebilly/ReDoc/issues/321)
### Bug fixes ### Bug fixes
* Update webpack to the latest beta ([#143](https://github.com/Rebilly/ReDoc/issues/143)) * Update webpack to the latest beta ([#143](https://github.com/Rebilly/ReDoc/issues/143))
* Fix read-only fields appear in request samples ([#142](https://github.com/Rebilly/ReDoc/issues/142)) * Fix read-only fields appear in request samples ([#142](https://github.com/Rebilly/ReDoc/issues/142))
* A few more minor UI improvemnts * A few more minor UI improvements
### Features/Improvements ### Features/Improvements
* Major performance optimization with new option `lazy-rendering` * Major performance optimization with new option `lazy-rendering`

View File

@ -138,7 +138,7 @@ For npm:
Install peer dependencies required by ReDoc if you don't have them installed already: Install peer dependencies required by ReDoc if you don't have them installed already:
npm i react react-dom mobx@^4.2.0 styled-components npm i react react-dom mobx@^4.2.0 styled-components core-js
Import `RedocStandalone` component from 'redoc' module: Import `RedocStandalone` component from 'redoc' module:
@ -213,7 +213,7 @@ ReDoc makes use of the following [vendor extensions](https://swagger.io/specific
* [`x-traitTag`](docs/redoc-vendor-extensions.md#x-traitTag) - useful for handling out common things like Pagination, Rate-Limits, etc * [`x-traitTag`](docs/redoc-vendor-extensions.md#x-traitTag) - useful for handling out common things like Pagination, Rate-Limits, etc
* [`x-code-samples`](docs/redoc-vendor-extensions.md#x-code-samples) - specify operation code samples * [`x-code-samples`](docs/redoc-vendor-extensions.md#x-code-samples) - specify operation code samples
* [`x-examples`](docs/redoc-vendor-extensions.md#x-examples) - specify JSON example for requests * [`x-examples`](docs/redoc-vendor-extensions.md#x-examples) - specify JSON example for requests
* [`x-nullable`](docs/redoc-vendor-extensions.md#nullable) - mark schema param as a nullable * [`x-nullable`](docs/redoc-vendor-extensions.md#x-nullable) - mark schema param as a nullable
* [`x-displayName`](docs/redoc-vendor-extensions.md#x-displayname) - specify human-friendly names for the menu categories * [`x-displayName`](docs/redoc-vendor-extensions.md#x-displayname) - specify human-friendly names for the menu categories
* [`x-tagGroups`](docs/redoc-vendor-extensions.md#x-tagGroups) - group tags by categories in the side menu * [`x-tagGroups`](docs/redoc-vendor-extensions.md#x-tagGroups) - group tags by categories in the side menu
* [`x-servers`](docs/redoc-vendor-extensions.md#x-servers) - ability to specify different servers for API (backported from OpenAPI 3.0) * [`x-servers`](docs/redoc-vendor-extensions.md#x-servers) - ability to specify different servers for API (backported from OpenAPI 3.0)
@ -223,27 +223,32 @@ ReDoc makes use of the following [vendor extensions](https://swagger.io/specific
### `<redoc>` options object ### `<redoc>` options object
You can use all of the following options with standalone version on <redoc> tag by kebab-casing them, e.g. `scrollYOffset` becomes `scroll-y-offset` and `expandResponses` becomes `expand-responses`. You can use all of the following options with standalone version on <redoc> tag by kebab-casing them, e.g. `scrollYOffset` becomes `scroll-y-offset` and `expandResponses` becomes `expand-responses`.
* `untrustedSpec` - if set, the spec is considered untrusted and all HTML/markdown is sanitized to prevent XSS. **Disabled by default** for performance reasons. **Enable this option if you work with untrusted user data!** * `disableSearch` - disable search indexing and search box.
* `expandDefaultServerVariables` - enable expanding default server variables, default `false`.
* `expandResponses` - specify which responses to expand by default by response codes. Values should be passed as comma-separated list without spaces e.g. `expandResponses="200,201"`. Special value `"all"` expands all responses by default. Be careful: this option can slow-down documentation rendering time.
* `hideDownloadButton` - do not show "Download" spec button. **THIS DOESN'T MAKE YOUR SPEC PRIVATE**, it just hides the button.
* `hideHostname` - if set, the protocol and hostname is not shown in the operation definition.
* `hideLoading` - do not show loading animation. Useful for small docs.
* `hideSingleRequestSampleTab` - do not show the request sample tab for requests with only one sample.
* `jsonSampleExpandLevel` - set the default expand level for JSON payload samples (responses and request body). Special value 'all' expands all levels. The default value is `2`.
* `lazyRendering` - _Not implemented yet_ ~~if set, enables lazy rendering mode in ReDoc. This mode is useful for APIs with big number of operations (e.g. > 50). In this mode ReDoc shows initial screen ASAP and then renders the rest operations asynchronously while showing progress bar on the top. Check out the [demo](\\redocly.github.io/redoc) for the example.~~
* `menuToggle` - if true clicking second time on expanded menu item will collapse it, default `false`.
* `nativeScrollbars` - use native scrollbar for sidemenu instead of perfect-scroll (scrolling performance optimization for big specs).
* `noAutoAuth` - do not inject Authentication section automatically.
* `onlyRequiredInSamples` - shows only required fields in request samples.
* `pathInMiddlePanel` - show path link and HTTP verb in the middle panel instead of the right one.
* `requiredPropsFirst` - show required properties first ordered in the same order as in `required` array.
* `scrollYOffset` - If set, specifies a vertical scroll-offset. This is often useful when there are fixed positioned elements at the top of the page, such as navbars, headers etc; * `scrollYOffset` - If set, specifies a vertical scroll-offset. This is often useful when there are fixed positioned elements at the top of the page, such as navbars, headers etc;
`scrollYOffset` can be specified in various ways: `scrollYOffset` can be specified in various ways:
* **number**: A fixed number of pixels to be used as offset; * **number**: A fixed number of pixels to be used as offset.
* **selector**: selector of the element to be used for specifying the offset. The distance from the top of the page to the element's bottom will be used as offset; * **selector**: selector of the element to be used for specifying the offset. The distance from the top of the page to the element's bottom will be used as offset.
* **function**: A getter function. Must return a number representing the offset (in pixels); * **function**: A getter function. Must return a number representing the offset (in pixels).
* `showExtensions` - show vendor extensions ("x-" fields). Extensions used by ReDoc are ignored. Can be boolean or an array of `string` with names of extensions to display.
* `sortPropsAlphabetically` - sort properties alphabetically.
* `suppressWarnings` - if set, warnings are not rendered at the top of documentation (they still are logged to the console). * `suppressWarnings` - if set, warnings are not rendered at the top of documentation (they still are logged to the console).
* `lazyRendering` - _Not implemented yet_ ~~if set, enables lazy rendering mode in ReDoc. This mode is useful for APIs with big number of operations (e.g. > 50). In this mode ReDoc shows initial screen ASAP and then renders the rest operations asynchronously while showing progress bar on the top. Check out the [demo](\\redocly.github.io/redoc) for the example.~~ * `payloadSampleIdx` - if set, payload sample will be inserted at this index or last. Indexes start from 0.
* `hideHostname` - if set, the protocol and hostname is not shown in the operation definition. * `theme` - ReDoc theme. Not documented yet. For details check source code: [theme.ts](https://github.com/Redocly/redoc/blob/master/src/theme.ts).
* `expandResponses` - specify which responses to expand by default by response codes. Values should be passed as comma-separated list without spaces e.g. `expandResponses="200,201"`. Special value `"all"` expands all responses by default. Be careful: this option can slow-down documentation rendering time. * `untrustedSpec` - if set, the spec is considered untrusted and all HTML/markdown is sanitized to prevent XSS. **Disabled by default** for performance reasons. **Enable this option if you work with untrusted user data!**
* `requiredPropsFirst` - show required properties first ordered in the same order as in `required` array.
* `sortPropsAlphabetically` - sort properties alphabetically
* `showExtensions` - show vendor extensions ("x-" fields). Extensions used by ReDoc are ignored. Can be boolean or an array of `string` with names of extensions to display
* `noAutoAuth` - do not inject Authentication section automatically
* `pathInMiddlePanel` - show path link and HTTP verb in the middle panel instead of the right one
* `hideLoading` - do not show loading animation. Useful for small docs
* `nativeScrollbars` - use native scrollbar for sidemenu instead of perfect-scroll (scrolling performance optimization for big specs)
* `hideDownloadButton` - do not show "Download" spec button. **THIS DOESN'T MAKE YOUR SPEC PRIVATE**, it just hides the button.
* `disableSearch` - disable search indexing and search box
* `onlyRequiredInSamples` - shows only required fields in request samples.
* `theme` - ReDoc theme. Not documented yet. For details check source code: [theme.ts](https://github.com/Redocly/redoc/blob/master/src/theme.ts)
## Advanced usage of standalone version ## Advanced usage of standalone version
Instead of adding `spec-url` attribute to the `<redoc>` element you can initialize ReDoc via globally exposed `Redoc` object: Instead of adding `spec-url` attribute to the `<redoc>` element you can initialize ReDoc via globally exposed `Redoc` object:

View File

@ -32,7 +32,7 @@ const configDir = './benchmark/revisions/config.js';
console.log(`Writing config "${configDir}"`); console.log(`Writing config "${configDir}"`);
fs.writeFileSync(configDir, configFile); fs.writeFileSync(configDir, configFile);
console.log('Starging benchmark server'); console.log('Starting benchmark server');
const proc = spawn('npm', ['run', 'start:benchmark']); const proc = spawn('npm', ['run', 'start:benchmark']);
proc.stdout.on('data', data => { proc.stdout.on('data', data => {

View File

@ -7,7 +7,7 @@
# To run: # To run:
# To display the command line options: # To display the command line options:
# $ docker run --rm -it redoc-cli --help # $ docker run --rm -it redoc-cli --help
# .. will display the comand line help # .. will display the command line help
# #
# To turn `swagger.yml` file in the current directory, to html documentation 'redoc-static.html' # To turn `swagger.yml` file in the current directory, to html documentation 'redoc-static.html'
# $ docker run --rm -it -v $PWD:/data redoc-cli bundle swagger.yml # $ docker run --rm -it -v $PWD:/data redoc-cli bundle swagger.yml

View File

@ -14,7 +14,14 @@ import * as zlib from 'zlib';
import { createStore, loadAndBundleSpec, Redoc } from 'redoc'; import { createStore, loadAndBundleSpec, Redoc } from 'redoc';
import { watch } from 'chokidar'; import { watch } from 'chokidar';
import { createReadStream, existsSync, readFileSync, ReadStream, writeFileSync } from 'fs'; import {
createReadStream,
existsSync,
lstatSync,
readFileSync,
ReadStream,
writeFileSync,
} from 'fs';
import * as mkdirp from 'mkdirp'; import * as mkdirp from 'mkdirp';
import * as YargsParser from 'yargs'; import * as YargsParser from 'yargs';
@ -25,6 +32,7 @@ interface Options {
cdn?: boolean; cdn?: boolean;
output?: string; output?: string;
title?: string; title?: string;
disableGoogleFont?: boolean;
port?: number; port?: number;
templateFileName?: string; templateFileName?: string;
templateOptions?: any; templateOptions?: any;
@ -68,9 +76,11 @@ YargsParser.command(
watch: argv.watch as boolean, watch: argv.watch as boolean,
templateFileName: argv.template as string, templateFileName: argv.template as string,
templateOptions: argv.templateOptions || {}, templateOptions: argv.templateOptions || {},
redocOptions: argv.options || {}, redocOptions: getObjectOrJSON(argv.options),
}; };
console.log(config);
try { try {
await serve(argv.port as number, argv.spec as string, config); await serve(argv.port as number, argv.spec as string, config);
} catch (e) { } catch (e) {
@ -96,7 +106,12 @@ YargsParser.command(
yargs.options('title', { yargs.options('title', {
describe: 'Page Title', describe: 'Page Title',
type: 'string', type: 'string',
default: 'ReDoc documentation', });
yargs.options('disableGoogleFont', {
describe: 'Disable Google Font',
type: 'boolean',
default: false,
}); });
yargs.option('cdn', { yargs.option('cdn', {
@ -108,15 +123,16 @@ YargsParser.command(
yargs.demandOption('spec'); yargs.demandOption('spec');
return yargs; return yargs;
}, },
async argv => { async (argv: any) => {
const config: Options = { const config = {
ssr: true, ssr: true,
output: argv.o as string, output: argv.o as string,
cdn: argv.cdn as boolean, cdn: argv.cdn as boolean,
title: argv.title as string, title: argv.title as string,
disableGoogleFont: argv.disableGoogleFont as boolean,
templateFileName: argv.template as string, templateFileName: argv.template as string,
templateOptions: argv.templateOptions || {}, templateOptions: argv.templateOptions || {},
redocOptions: argv.options || {}, redocOptions: getObjectOrJSON(argv.options),
}; };
try { try {
@ -156,7 +172,9 @@ async function serve(port: number, pathToSpec: string, options: Options = {}) {
}, },
); );
} else if (request.url === '/') { } else if (request.url === '/') {
respondWithGzip(pageHTML, request, response); respondWithGzip(pageHTML, request, response, {
'Content-Type': 'text/html',
});
} else if (request.url === '/spec.json') { } else if (request.url === '/spec.json') {
const specStr = JSON.stringify(spec, null, 2); const specStr = JSON.stringify(spec, null, 2);
respondWithGzip(specStr, request, response, { respondWithGzip(specStr, request, response, {
@ -178,14 +196,14 @@ async function serve(port: number, pathToSpec: string, options: Options = {}) {
if (options.watch && existsSync(pathToSpec)) { if (options.watch && existsSync(pathToSpec)) {
const pathToSpecDirectory = resolve(dirname(pathToSpec)); const pathToSpecDirectory = resolve(dirname(pathToSpec));
const watchOptions = { const watchOptions = {
ignored: /(^|[\/\\])\../, ignored: [/(^|[\/\\])\../, /___jb_[a-z]+___$/],
ignoreInitial: true,
}; };
const watcher = watch(pathToSpecDirectory, watchOptions); const watcher = watch(pathToSpecDirectory, watchOptions);
const log = console.log.bind(console); const log = console.log.bind(console);
watcher
.on('change', async path => { const handlePath = async _path => {
log(`${path} changed, updating docs`);
try { try {
spec = await loadAndBundleSpec(pathToSpec); spec = await loadAndBundleSpec(pathToSpec);
pageHTML = await getPageHTML(spec, pathToSpec, options); pageHTML = await getPageHTML(spec, pathToSpec, options);
@ -193,6 +211,19 @@ async function serve(port: number, pathToSpec: string, options: Options = {}) {
} catch (e) { } catch (e) {
console.error('Error while updating: ', e.message); console.error('Error while updating: ', e.message);
} }
};
watcher
.on('change', async path => {
log(`${path} changed, updating docs`);
handlePath(path);
})
.on('add', async path => {
log(`File ${path} added, updating docs`);
handlePath(path);
})
.on('addDir', path => {
log(`↗ Directory ${path} added. Files in here will trigger reload.`);
}) })
.on('error', error => console.error(`Watcher error: ${error}`)) .on('error', error => console.error(`Watcher error: ${error}`))
.on('ready', () => log(`👀 Watching ${pathToSpecDirectory} for changes...`)); .on('ready', () => log(`👀 Watching ${pathToSpecDirectory} for changes...`));
@ -216,7 +247,15 @@ async function bundle(pathToSpec, options: Options = {}) {
async function getPageHTML( async function getPageHTML(
spec: any, spec: any,
pathToSpec: string, pathToSpec: string,
{ ssr, cdn, title, templateFileName, templateOptions, redocOptions = {} }: Options, {
ssr,
cdn,
title,
disableGoogleFont,
templateFileName,
templateOptions,
redocOptions = {},
}: Options,
) { ) {
let html; let html;
let css; let css;
@ -258,7 +297,8 @@ async function getPageHTML(
? '<script src="https://unpkg.com/redoc@next/bundles/redoc.standalone.js"></script>' ? '<script src="https://unpkg.com/redoc@next/bundles/redoc.standalone.js"></script>'
: `<script>${redocStandaloneSrc}</script>`) + css : `<script>${redocStandaloneSrc}</script>`) + css
: '<script src="redoc.standalone.js"></script>', : '<script src="redoc.standalone.js"></script>',
title, title: title || spec.info.title || 'ReDoc documentation',
disableGoogleFont,
templateOptions, templateOptions,
}); });
} }
@ -321,3 +361,25 @@ function handleError(error: Error) {
console.error(error.stack); console.error(error.stack);
process.exit(1); process.exit(1);
} }
function getObjectOrJSON(options) {
switch (typeof options) {
case 'object':
return options;
case 'string':
try {
if (existsSync(options) && lstatSync(options).isFile()) {
return JSON.parse(readFileSync(options, 'utf-8'));
} else {
return JSON.parse(options);
}
} catch (e) {
console.log(
`Encountered error:\n\n${options}\n\nis neither a file with a valid JSON object neither a stringified JSON object.`,
);
handleError(e);
}
default:
return {};
}
}

View File

@ -1,36 +1,35 @@
{ {
"name": "redoc-cli", "name": "redoc-cli",
"version": "0.8.4", "version": "0.9.5",
"description": "ReDoc's Command Line Interface", "description": "ReDoc's Command Line Interface",
"main": "index.js", "main": "index.js",
"bin": "index.js", "bin": "index.js",
"repository": "https://github.com/Redocly/redoc", "repository": "https://github.com/Redocly/redoc",
"author": "Roman Hotsiy <gotsijroman@gmail.com>", "author": "Roman Hotsiy <gotsijroman@gmail.com>",
"license": "MIT", "license": "MIT",
"engines": {
"node": ">= 8"
},
"dependencies": { "dependencies": {
"chokidar": "^2.0.4", "chokidar": "^3.0.2",
"handlebars": "^4.0.11", "handlebars": "^4.1.2",
"isarray": "^2.0.4", "isarray": "^2.0.5",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"mobx": "^4.2.0", "mobx": "^4.2.0",
"node-libs-browser": "^2.2.0", "node-libs-browser": "^2.2.1",
"react": "^16.8.4", "react": "^16.8.6",
"react-dom": "^16.8.4", "react-dom": "^16.8.6",
"redoc": "^2.0.0-rc.8-1", "redoc": "2.0.0-rc.21",
"styled-components": "^4.1.3", "styled-components": "^4.3.2",
"tslib": "^1.9.3", "tslib": "^1.10.0",
"yargs": "^12.0.5" "yargs": "^13.3.0"
},
"scripts": {
"ci-publish": "ci-publish"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },
"devDependencies": { "devDependencies": {
"@types/chokidar": "^1.7.5", "@types/chokidar": "^2.1.3",
"@types/handlebars": "^4.0.39", "@types/handlebars": "^4.0.39",
"@types/mkdirp": "^0.5.2", "@types/mkdirp": "^0.5.2"
"ci-publish": "^1.3.1"
} }
} }

View File

@ -13,7 +13,7 @@
} }
</style> </style>
{{{redocHead}}} {{{redocHead}}}
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet"> {{#unless disableGoogleFont}}<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">{{/unless}}
</head> </head>
<body> <body>

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# DockerHub cd into Dockerfile location before buil # DockerHub cd into Dockerfile location before build
# So we have to undo this. # So we have to undo this.
cd ../.. cd ../..
docker build -f config/docker/Dockerfile -t $IMAGE_NAME . docker build -f config/docker/Dockerfile -t $IMAGE_NAME .

View File

@ -53,7 +53,7 @@
}, },
{ {
"name": "Contacts", "name": "Contacts",
"description": "Contacts are Customer's address book.\nAll contact information used in Invoices, Subscriptions, Transacions, etc is enlisted here. Hovewer, changing a Contact won't change corresponding contact information in related resources\n" "description": "Contacts are Customer's address book.\nAll contact information used in Invoices, Subscriptions, Transacions, etc is enlisted here. However, changing a Contact won't change corresponding contact information in related resources\n"
}, },
{ {
"name": "Coupons", "name": "Coupons",
@ -93,7 +93,7 @@
}, },
{ {
"name": "Files", "name": "Files",
"description": "A File is an entity that can store a phyiscal file and some metadata. It also provides an easy access to\nits size, mime-type, user-defined tags and description thus allowing easy sorting and searching among stored\nfiles.\nThere are several methods of file uploading available: multipart/form-data encoded form, RAW POST (by sending\nfile contents as POST body), fetching from URL (by providing the file URL via 'url' param)\nAttachment is an entity that is used to link a File to one or multiple objects like Customer, Dispute, Payment,\nTransaction, Subscription, Plan, Product, Invoice, Note. That allows to quickly find and use files related to\nthose specific entities.\n" "description": "A File is an entity that can store a physical file and some metadata. It also provides an easy access to\nits size, mime-type, user-defined tags and description thus allowing easy sorting and searching among stored\nfiles.\nThere are several methods of file uploading available: multipart/form-data encoded form, RAW POST (by sending\nfile contents as POST body), fetching from URL (by providing the file URL via 'url' param)\nAttachment is an entity that is used to link a File to one or multiple objects like Customer, Dispute, Payment,\nTransaction, Subscription, Plan, Product, Invoice, Note. That allows to quickly find and use files related to\nthose specific entities.\n"
}, },
{ {
"name": "Gateway Accounts", "name": "Gateway Accounts",
@ -15623,7 +15623,7 @@
"description": "Reset user password\n", "description": "Reset user password\n",
"responses": { "responses": {
"201": { "201": {
"description": "Password was reseted successfully", "description": "Password was reset successfully",
"headers": { "headers": {
"Rate-Limit-Limit": { "Rate-Limit-Limit": {
"description": "The number of allowed requests in the current period", "description": "The number of allowed requests in the current period",
@ -23851,17 +23851,17 @@
"type": "string" "type": "string"
}, },
"totpRequired": { "totpRequired": {
"description": "The user setting of two-factor authentification", "description": "The user setting of two-factor authentication",
"readOnly": true, "readOnly": true,
"type": "boolean" "type": "boolean"
}, },
"totpSecret": { "totpSecret": {
"description": "The user TOTP key for authentification app (if TOTP enabled)", "description": "The user TOTP key for authentication app (if TOTP enabled)",
"readOnly": true, "readOnly": true,
"type": "string" "type": "string"
}, },
"totpUrl": { "totpUrl": {
"description": "The user link to QR-code for TOTP authentification app (if TOTP enabled)", "description": "The user link to QR-code for TOTP authentication app (if TOTP enabled)",
"readOnly": true, "readOnly": true,
"type": "string", "type": "string",
"format": "url" "format": "url"
@ -24250,7 +24250,7 @@
}, },
"bodyHtml": { "bodyHtml": {
"type": "string", "type": "string",
"description": "Leave empty to recieve \"text/plain\" email.\nThe template palceholders are allowed.\n" "description": "Leave empty to receive \"text/plain\" email.\nThe template palceholders are allowed.\n"
} }
}, },
"required": [ "required": [
@ -26495,15 +26495,15 @@
} }
}, },
"totpRequired": { "totpRequired": {
"description": "The user setting of two-factor authentification", "description": "The user setting of two-factor authentication",
"type": "boolean" "type": "boolean"
}, },
"totpSecret": { "totpSecret": {
"description": "The user TOTP key for authentification app (if TOTP enabled)", "description": "The user TOTP key for authentication app (if TOTP enabled)",
"type": "string" "type": "string"
}, },
"totpUrl": { "totpUrl": {
"description": "The user link to QR-code for TOTP authentification app (if TOTP enabled)", "description": "The user link to QR-code for TOTP authentication app (if TOTP enabled)",
"type": "string", "type": "string",
"format": "url" "format": "url"
}, },
@ -26984,7 +26984,7 @@
"collectionExpand": { "collectionExpand": {
"name": "expand", "name": "expand",
"in": "query", "in": "query",
"description": "Expand response to get full related object intead of ID. See the expand guide for more info.", "description": "Expand response to get full related object instead of ID. See the expand guide for more info.",
"schema": { "schema": {
"type": "string" "type": "string"
} }

View File

@ -11,7 +11,7 @@ const demos = [
value: 'https://api.apis.guru/v2/specs/googleapis.com/calendar/v3/swagger.yaml', value: 'https://api.apis.guru/v2/specs/googleapis.com/calendar/v3/swagger.yaml',
label: 'Google Calendar', label: 'Google Calendar',
}, },
{ value: 'https://api.apis.guru/v2/specs/slack.com/1.0.6/swagger.yaml', label: 'Slack' }, { value: 'https://api.apis.guru/v2/specs/slack.com/1.2.0/swagger.yaml', label: 'Slack' },
{ value: 'https://api.apis.guru/v2/specs/zoom.us/2.0.0/swagger.yaml', label: 'Zoom.us' }, { value: 'https://api.apis.guru/v2/specs/zoom.us/2.0.0/swagger.yaml', label: 'Zoom.us' },
{ {
value: 'https://api.apis.guru/v2/specs/graphhopper.com/1.0/swagger.yaml', value: 'https://api.apis.guru/v2/specs/graphhopper.com/1.0/swagger.yaml',

View File

@ -38,7 +38,7 @@ info:
OAuth2 - an open protocol to allow secure authorization in a simple OAuth2 - an open protocol to allow secure authorization in a simple
and standard method from web, mobile and desktop applications. and standard method from web, mobile and desktop applications.
<security-definitions /> <SecurityDefinitions />
version: 1.0.0 version: 1.0.0
title: Swagger Petstore title: Swagger Petstore
@ -63,6 +63,14 @@ tags:
description: Access to Petstore orders description: Access to Petstore orders
- name: user - name: user
description: Operations about user description: Operations about user
- name: pet_model
x-displayName: The Pet Model
description: |
<SchemaDefinition schemaRef="#/components/schemas/Pet" />
- name: store_model
x-displayName: The Order Model
description: |
<SchemaDefinition schemaRef="#/components/schemas/Order" exampleRef="#/components/examples/Order" showReadOnly={true} showWriteOnly={true} />
x-tagGroups: x-tagGroups:
- name: General - name: General
tags: tags:
@ -71,9 +79,21 @@ x-tagGroups:
- name: User Management - name: User Management
tags: tags:
- user - user
- name: Models
tags:
- pet_model
- store_model
paths: paths:
/pet: /pet:
parameters: parameters:
- name: Accept-Language
in: header
description: "The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US"
example: en-US
required: false
schema:
type: string
default: en-AU
- name: cookieParam - name: cookieParam
in: cookie in: cookie
description: Some cookie description: Some cookie
@ -286,7 +306,7 @@ paths:
tags: tags:
- pet - pet
summary: Finds Pets by status summary: Finds Pets by status
description: Multiple status values can be provided with comma seperated strings description: Multiple status values can be provided with comma separated strings
operationId: findPetsByStatus operationId: findPetsByStatus
parameters: parameters:
- name: status - name: status
@ -331,7 +351,7 @@ paths:
- pet - pet
summary: Finds Pets by tags summary: Finds Pets by tags
description: >- description: >-
Muliple tags can be provided with comma seperated strings. Use tag1, Multiple tags can be provided with comma separated strings. Use tag1,
tag2, tag3 for testing. tag2, tag3 for testing.
operationId: findPetsByTags operationId: findPetsByTags
deprecated: true deprecated: true
@ -611,7 +631,7 @@ paths:
type: integer type: integer
format: int32 format: int32
X-Expires-After: X-Expires-After:
description: date in UTC when toekn expires description: date in UTC when token expires
schema: schema:
type: string type: string
format: date-time format: date-time
@ -666,6 +686,7 @@ components:
type: string type: string
description: The measured skill for hunting description: The measured skill for hunting
default: lazy default: lazy
example: adventurous
enum: enum:
- clueless - clueless
- lazy - lazy
@ -717,6 +738,7 @@ components:
type: number type: number
description: Average amount of honey produced per day in ounces description: Average amount of honey produced per day in ounces
example: 3.14 example: 3.14
multipleOf: .01
required: required:
- honeyPerDay - honeyPerDay
Id: Id:
@ -754,6 +776,11 @@ components:
description: Indicates whenever order was completed or not description: Indicates whenever order was completed or not
type: boolean type: boolean
default: false default: false
readOnly: true
requestId:
description: Unique Request Id
type: string
writeOnly: true
xml: xml:
name: Order name: Order
Pet: Pet:
@ -866,14 +893,13 @@ components:
as well as digits as well as digits
format: password format: password
minLength: 8 minLength: 8
pattern: '(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])' pattern: '/(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])/'
example: drowssaP123 example: drowssaP123
phone: phone:
description: User phone number in international format description: User phone number in international format
type: string type: string
pattern: '^\+(?:[0-9]-?){6,14}[0-9]$' pattern: '/^\+(?:[0-9]-?){6,14}[0-9]$/'
example: +1-202-555-0192 example: +1-202-555-0192
nullable: true
userStatus: userStatus:
description: User status description: User status
type: integer type: integer
@ -926,3 +952,10 @@ components:
type: apiKey type: apiKey
name: api_key name: api_key
in: header in: header
examples:
Order:
value:
quantity: 1,
shipDate: 2018-10-19T16:46:45Z,
status: placed,
complete: false

View File

@ -1,5 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { render } from 'react-dom'; import { render } from 'react-dom';
// tslint:disable-next-line
import { AppContainer } from 'react-hot-loader'; import { AppContainer } from 'react-hot-loader';
// import DevTools from 'mobx-react-devtools'; // import DevTools from 'mobx-react-devtools';

View File

@ -631,7 +631,7 @@ paths:
X-Expires-After: X-Expires-After:
type: string type: string
format: date-time format: date-time
description: date in UTC when toekn expires description: date in UTC when token expires
'400': '400':
description: Invalid username/password supplied description: Invalid username/password supplied
/user/logout: /user/logout:

View File

@ -38,7 +38,6 @@ const babelLoader = mode => ({
['@babel/plugin-syntax-decorators', { legacy: true }], ['@babel/plugin-syntax-decorators', { legacy: true }],
'@babel/plugin-syntax-dynamic-import', '@babel/plugin-syntax-dynamic-import',
'@babel/plugin-syntax-jsx', '@babel/plugin-syntax-jsx',
mode !== 'production' ? 'react-hot-loader/babel' : undefined,
[ [
'babel-plugin-styled-components', 'babel-plugin-styled-components',
{ {
@ -50,6 +49,13 @@ const babelLoader = mode => ({
}, },
}); });
const babelHotLoader = {
loader: 'babel-loader',
options: {
plugins: ['react-hot-loader/babel'],
},
};
export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) => ({ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) => ({
entry: [ entry: [
root('../src/polyfills.ts'), root('../src/polyfills.ts'),
@ -105,7 +111,11 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
{ test: [/\.eot$/, /\.gif$/, /\.woff$/, /\.svg$/, /\.ttf$/], use: 'null-loader' }, { test: [/\.eot$/, /\.gif$/, /\.woff$/, /\.svg$/, /\.ttf$/], use: 'null-loader' },
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
use: [tsLoader(env), babelLoader(mode)], use: compact([
mode !== 'production' ? babelHotLoader : undefined,
tsLoader(env),
babelLoader(mode),
]),
exclude: [/node_modules/], exclude: [/node_modules/],
}, },
{ {

View File

@ -6,7 +6,7 @@ describe('Menu', () => {
it('should have valid items count', () => { it('should have valid items count', () => {
cy.get('.menu-content') cy.get('.menu-content')
.find('li') .find('li')
.should('have.length', 6 + (2 + 8 + 4) + (1 + 8)); .should('have.length', 6 + (2 + 8 + 1 + 4 + 2) + (1 + 8));
}); });
it('should sync active menu items while scroll', () => { it('should sync active menu items while scroll', () => {

View File

@ -42,7 +42,7 @@ describe('Servers', () => {
// TODO add cy-data attributes // TODO add cy-data attributes
cy.get('[data-section-id="operation/addPet"]').should( cy.get('[data-section-id="operation/addPet"]').should(
'contain', 'contain',
'http://localhost:' + win.location.port + '/e2e/pet', 'http://localhost:' + win.location.port + '/pet',
); );
}); });
}); });
@ -57,7 +57,7 @@ describe('Servers', () => {
// TODO add cy-data attributes // TODO add cy-data attributes
cy.get('[data-section-id="operation/addPet"]').should( cy.get('[data-section-id="operation/addPet"]').should(
'contain', 'contain',
'http://localhost:' + win.location.port + '/e2e/pet', 'http://localhost:' + win.location.port + '/pet',
); );
}); });
}); });

View File

@ -1,6 +1,6 @@
{ {
"name": "otx-redoc", "name": "redoc",
"version": "2.0.0-rc.2", "version": "2.0.0-rc.22",
"description": "ReDoc", "description": "ReDoc",
"repository": { "repository": {
"type": "git", "type": "git",
@ -28,7 +28,7 @@
"start": "webpack-dev-server --mode=development --env.playground --hot --config demo/webpack.config.ts", "start": "webpack-dev-server --mode=development --env.playground --hot --config demo/webpack.config.ts",
"start:prod": "webpack-dev-server --env.playground --mode=production --config demo/webpack.config.ts", "start:prod": "webpack-dev-server --env.playground --mode=production --config demo/webpack.config.ts",
"start:benchmark": "webpack-dev-server --mode=production --env.bench --config demo/webpack.config.ts", "start:benchmark": "webpack-dev-server --mode=production --env.bench --config demo/webpack.config.ts",
"test": "npm run lint && npm run unit && npm run bundlesize && npm run license-check", "test": "npm run lint && npm run unit && npm run license-check",
"unit": "jest --coverage", "unit": "jest --coverage",
"e2e": "cypress run", "e2e": "cypress run",
"e2e-ci": "cypress run --record", "e2e-ci": "cypress run --record",
@ -40,7 +40,7 @@
"bundle": "npm run bundle:clean && npm run bundle:lib && npm run bundle:standalone", "bundle": "npm run bundle:clean && npm run bundle:lib && npm run bundle:standalone",
"declarations": "tsc --emitDeclarationOnly -p tsconfig.lib.json && cp -R src/types typings/", "declarations": "tsc --emitDeclarationOnly -p tsconfig.lib.json && cp -R src/types typings/",
"stats": "webpack --env.standalone --json --profile --mode=production > stats.json", "stats": "webpack --env.standalone --json --profile --mode=production > stats.json",
"prettier": "prettier --write \"src/**/*.{ts,tsx}\"", "prettier": "prettier --write \"cli/index.ts\" \"src/**/*.{ts,tsx}\"",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 1", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 1",
"lint": "tslint --project tsconfig.json", "lint": "tslint --project tsconfig.json",
"benchmark": "node ./benchmark/benchmark.js", "benchmark": "node ./benchmark/benchmark.js",
@ -52,80 +52,81 @@
"docker:build": "docker build -f config/docker/Dockerfile -t redoc ." "docker:build": "docker build -f config/docker/Dockerfile -t redoc ."
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.3.4", "@babel/core": "7.7.5",
"@babel/plugin-syntax-decorators": "7.2.0", "@babel/plugin-syntax-decorators": "7.7.4",
"@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/plugin-syntax-dynamic-import": "^7.7.4",
"@babel/plugin-syntax-jsx": "7.2.0", "@babel/plugin-syntax-jsx": "7.7.4",
"@babel/plugin-syntax-typescript": "7.3.3", "@babel/plugin-syntax-typescript": "7.7.4",
"@cypress/webpack-preprocessor": "4.0.3", "@cypress/webpack-preprocessor": "4.1.1",
"@hot-loader/react-dom": "^16.8.4", "@hot-loader/react-dom": "^16.11.0",
"@types/chai": "4.1.7", "@types/chai": "4.2.7",
"@types/dompurify": "^0.0.32", "@types/dompurify": "^2.0.0",
"@types/enzyme": "^3.9.0", "@types/enzyme": "^3.10.4",
"@types/enzyme-to-json": "^1.5.3", "@types/enzyme-to-json": "^1.5.3",
"@types/jest": "^24.0.11", "@types/jest": "^24.0.23",
"@types/json-pointer": "^1.0.30", "@types/json-pointer": "^1.0.30",
"@types/lodash": "^4.14.122", "@types/lodash": "^4.14.149",
"@types/lunr": "^2.3.2", "@types/lunr": "^2.3.2",
"@types/mark.js": "^8.11.3", "@types/mark.js": "^8.11.5",
"@types/marked": "^0.6.3", "@types/marked": "^0.7.2",
"@types/prismjs": "^1.9.1", "@types/prismjs": "^1.16.0",
"@types/prop-types": "^15.7.0", "@types/prop-types": "^15.7.3",
"@types/react": "^16.8.7", "@types/react": "^16.9.16",
"@types/react-dom": "^16.8.2", "@types/react-dom": "^16.9.4",
"@types/react-hot-loader": "^4.1.0",
"@types/react-tabs": "^2.3.1", "@types/react-tabs": "^2.3.1",
"@types/styled-components": "^4.1.12", "@types/styled-components": "^4.4.1",
"@types/tapable": "1.0.4", "@types/tapable": "1.0.4",
"@types/webpack": "^4.4.25", "@types/webpack": "^4.41.0",
"@types/webpack-env": "^1.13.9", "@types/webpack-env": "^1.14.1",
"@types/yargs": "^12.0.9", "@types/yargs": "^13.0.3",
"babel-loader": "8.0.5", "babel-loader": "8.0.6",
"babel-plugin-styled-components": "^1.10.0", "babel-plugin-styled-components": "^1.10.6",
"beautify-benchmark": "^0.2.4", "beautify-benchmark": "^0.2.4",
"bundlesize": "^0.17.1", "bundlesize": "^0.18.0",
"conventional-changelog-cli": "^2.0.12", "conventional-changelog-cli": "^2.0.28",
"copy-webpack-plugin": "^5.0.0", "copy-webpack-plugin": "^5.1.1",
"core-js": "^2.6.5", "core-js": "^3.5.0",
"coveralls": "^3.0.3", "coveralls": "^3.0.9",
"css-loader": "^2.1.1", "css-loader": "^3.3.0",
"cypress": "~3.1.5", "cypress": "~3.7.0",
"deploy-to-gh-pages": "^1.3.6", "deploy-to-gh-pages": "^1.3.7",
"enzyme": "^3.9.0", "enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.10.0", "enzyme-adapter-react-16": "^1.15.1",
"enzyme-to-json": "^3.3.5", "enzyme-to-json": "^3.4.3",
"fork-ts-checker-webpack-plugin": "1.0.0", "fork-ts-checker-webpack-plugin": "3.1.1",
"html-webpack-plugin": "^3.1.0", "html-webpack-plugin": "^3.1.0",
"jest": "^24.3.1", "jest": "^24.9.0",
"license-checker": "^25.0.1", "license-checker": "^25.0.1",
"lodash": "^4.17.11", "lodash": "^4.17.15",
"mobx": "^4.3.1", "mobx": "^4.3.1",
"prettier": "^1.16.4", "prettier": "^1.19.1",
"prettier-eslint": "^8.8.2", "prettier-eslint": "^9.0.1",
"raf": "^3.4.1", "raf": "^3.4.1",
"react": "^16.8.4", "react": "^16.12.0",
"react-dom": "^16.8.4", "react-hot-loader": "^4.12.18",
"rimraf": "^2.6.3", "react-dom": "^16.12.0",
"rimraf": "^3.0.0",
"shelljs": "^0.8.3", "shelljs": "^0.8.3",
"source-map-loader": "^0.2.4", "source-map-loader": "^0.2.4",
"style-loader": "^0.23.1", "style-loader": "^1.0.1",
"styled-components": "^4.1.3", "styled-components": "^4.4.1",
"ts-jest": "24.0.0", "ts-jest": "24.2.0",
"ts-loader": "5.3.3", "ts-loader": "6.2.1",
"ts-node": "^8.0.3", "ts-node": "^8.5.4",
"tslint": "^5.13.1", "tslint": "^5.20.1",
"tslint-react": "^3.4.0", "tslint-react": "^4.1.0",
"typescript": "^3.3.3333", "typescript": "^3.7.3",
"unfetch": "^4.1.0", "unfetch": "^4.1.0",
"url-polyfill": "^1.1.5", "url-polyfill": "^1.1.7",
"webpack": "^4.29.6", "webpack": "^4.41.2",
"webpack-cli": "^3.2.3", "webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.2.1", "webpack-dev-server": "^3.9.0",
"webpack-node-externals": "^1.6.0", "webpack-node-externals": "^1.6.0",
"workerize-loader": "^1.0.4", "workerize-loader": "^1.1.0",
"yaml-js": "^0.2.3" "yaml-js": "^0.2.3"
}, },
"peerDependencies": { "peerDependencies": {
"core-js": "^3.1.4",
"mobx": "^4.2.0 || ^5.0.0", "mobx": "^4.2.0 || ^5.0.0",
"react": "^16.8.4", "react": "^16.8.4",
"react-dom": "^16.8.4", "react-dom": "^16.8.4",
@ -134,27 +135,27 @@
"dependencies": { "dependencies": {
"classnames": "^2.2.6", "classnames": "^2.2.6",
"decko": "^1.2.0", "decko": "^1.2.0",
"dompurify": "^1.0.10", "dompurify": "^2.0.7",
"eventemitter3": "^3.0.0", "eventemitter3": "^4.0.0",
"json-pointer": "^0.6.0", "json-pointer": "^0.6.0",
"json-schema-ref-parser": "^6.1.0", "json-schema-ref-parser": "^6.1.0",
"lunr": "2.3.6", "lunr": "2.3.8",
"mark.js": "^8.11.1", "mark.js": "^8.11.1",
"marked": "^0.6.1", "marked": "^0.7.0",
"memoize-one": "^5.0.0", "memoize-one": "~5.1.1",
"mobx-react": "^5.4.3", "mobx-react": "^6.1.4",
"openapi-sampler": "1.0.0-beta.14", "openapi-sampler": "1.0.0-beta.15",
"perfect-scrollbar": "^1.4.0", "perfect-scrollbar": "^1.4.0",
"polished": "^3.0.3", "polished": "^3.4.2",
"prismjs": "^1.15.0", "prismjs": "^1.17.1",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react-dropdown": "^1.6.4", "react-dropdown": "^1.6.4",
"react-hot-loader": "^4.8.0",
"react-tabs": "^3.0.0", "react-tabs": "^3.0.0",
"slugify": "^1.3.4", "slugify": "^1.3.6",
"stickyfill": "^1.1.1", "stickyfill": "^1.1.1",
"swagger2openapi": "^5.2.3", "swagger2openapi": "^5.3.1",
"tslib": "^1.9.3" "tslib": "^1.10.0",
"url-template": "^2.0.8"
}, },
"bundlesize": [ "bundlesize": [
{ {

View File

@ -5,11 +5,7 @@ import { ClipboardService } from '../services/ClipboardService';
export interface CopyButtonWrapperProps { export interface CopyButtonWrapperProps {
data: any; data: any;
children: ( children: (props: { renderCopyButton: () => React.ReactNode }) => React.ReactNode;
props: {
renderCopyButton: (() => React.ReactNode);
},
) => React.ReactNode;
} }
export class CopyButtonWrapper extends React.PureComponent< export class CopyButtonWrapper extends React.PureComponent<

View File

@ -55,7 +55,7 @@ export const StyledDropdown = styled(Dropdown)`
display: block; display: block;
height: 0; height: 0;
position: absolute; position: absolute;
right: 0.35em; right: 0.3em;
top: 50%; top: 50%;
margin-top: -0.125em; margin-top: -0.125em;
width: 0; width: 0;
@ -97,7 +97,7 @@ export const StyledDropdown = styled(Dropdown)`
export const SimpleDropdown = styled(StyledDropdown)` export const SimpleDropdown = styled(StyledDropdown)`
margin-left: 10px; margin-left: 10px;
text-transform: none; text-transform: none;
font-size: 0.929em; font-size: 0.969em;
.Dropdown-control { .Dropdown-control {
font-size: 1em; font-size: 1em;

View File

@ -63,7 +63,7 @@ export const PropertyNameCell = styled(PropertyCell)`
line-height: 20px; line-height: 20px;
white-space: nowrap; white-space: nowrap;
font-size: 0.929em; font-size: 0.929em;
font-family: ${props => props.theme.typography.headings.fontFamily}; font-family: ${props => props.theme.typography.code.fontFamily};
&.deprecated { &.deprecated {
${deprecatedCss}; ${deprecatedCss};

View File

@ -32,6 +32,7 @@ export const TypeName = styled(FieldLabel)`
export const TypeTitle = styled(FieldLabel)` export const TypeTitle = styled(FieldLabel)`
color: ${props => props.theme.schema.typeTitleColor}; color: ${props => props.theme.schema.typeTitleColor};
word-break: break-word;
`; `;
export const TypeFormat = TypeName; export const TypeFormat = TypeName;
@ -60,14 +61,6 @@ export const PatternLabel = styled(FieldLabel)`
&::after { &::after {
font-weight: bold; font-weight: bold;
} }
&::before {
content: ' /';
}
&::after {
content: '/ ';
}
`; `;
export const ExampleValue = styled(FieldLabel)` export const ExampleValue = styled(FieldLabel)`
@ -76,11 +69,9 @@ export const ExampleValue = styled(FieldLabel)`
background-color: ${transparentize(0.95, theme.colors.text.primary)}; background-color: ${transparentize(0.95, theme.colors.text.primary)};
color: ${transparentize(0.1, theme.colors.text.primary)}; color: ${transparentize(0.1, theme.colors.text.primary)};
margin: ${theme.spacing.unit}px;
padding: 0 ${theme.spacing.unit}px; padding: 0 ${theme.spacing.unit}px;
border: 1px solid ${transparentize(0.9, theme.colors.text.primary)}; border: 1px solid ${transparentize(0.9, theme.colors.text.primary)};
font-family: ${theme.typography.code.fontFamily}; font-family: ${theme.typography.code.fontFamily};
color: ${theme.typography.code.color};
}`}; }`};
& + & { & + & {
margin-left: 0; margin-left: 0;
@ -99,6 +90,7 @@ export const ConstraintItem = styled(FieldLabel)`
margin: 0 ${theme.spacing.unit}px; margin: 0 ${theme.spacing.unit}px;
padding: 0 ${theme.spacing.unit}px; padding: 0 ${theme.spacing.unit}px;
border: 1px solid ${transparentize(0.9, theme.colors.primary.main)}; border: 1px solid ${transparentize(0.9, theme.colors.primary.main)};
font-family: ${theme.typography.code.fontFamily};
}`}; }`};
& + & { & + & {
margin-left: 0; margin-left: 0;

View File

@ -1,14 +1,16 @@
import { SECTION_ATTR } from '../services/MenuStore'; import { SECTION_ATTR } from '../services/MenuStore';
import styled, { media } from '../styled-components'; import styled, { media } from '../styled-components';
export const MiddlePanel = styled.div` export const MiddlePanel = styled.div<{ compact?: boolean }>`
width: calc(100% - ${props => props.theme.rightPanel.width}); width: calc(100% - ${props => props.theme.rightPanel.width});
padding: 0 ${props => props.theme.spacing.sectionHorizontal}px; padding: 0 ${props => props.theme.spacing.sectionHorizontal}px;
${media.lessThan('medium', true)` ${({ compact, theme }) =>
media.lessThan('medium', true)`
width: 100%; width: 100%;
padding: ${props => padding: ${`${compact ? 0 : theme.spacing.sectionVertical}px ${
`${props.theme.spacing.sectionVertical}px ${props.theme.spacing.sectionHorizontal}px`}; theme.spacing.sectionHorizontal
}px`};
`}; `};
`; `;

View File

@ -11,13 +11,14 @@ export const OneOfLabel = styled.span`
font-size: 0.9em; font-size: 0.9em;
margin-right: 10px; margin-right: 10px;
color: ${props => props.theme.colors.primary.main}; color: ${props => props.theme.colors.primary.main};
font-family: Montserrat; font-family: ${props => props.theme.typography.headings.fontFamily};
} }
`; `;
export const OneOfButton = styled.li<{ active: boolean }>` export const OneOfButton = styled.li<{ active: boolean }>`
display: inline-block; display: inline-block;
margin-right: 10px; margin-right: 10px;
margin-bottom: 5px;
font-size: 0.8em; font-size: 0.8em;
cursor: pointer; cursor: pointer;
border: 1px solid ${props => props.theme.colors.primary.main}; border: 1px solid ${props => props.theme.colors.primary.main};

View File

@ -67,11 +67,7 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
)) || )) ||
null; null;
const version = const version = (info.version && <span>({info.version})</span>) || null;
(info.version && (
<span>({info.version})</span>
)) ||
null;
return ( return (
<Section> <Section>

View File

@ -60,7 +60,7 @@ export class ContentItem extends React.Component<ContentItemProps> {
} }
} }
const middlePanelWrap = component => <MiddlePanel>{component}</MiddlePanel>; const middlePanelWrap = component => <MiddlePanel compact={true}>{component}</MiddlePanel>;
@observer @observer
export class SectionItem extends React.Component<ContentItemProps> { export class SectionItem extends React.Component<ContentItemProps> {
@ -71,7 +71,7 @@ export class SectionItem extends React.Component<ContentItemProps> {
return ( return (
<> <>
<Row> <Row>
<MiddlePanel> <MiddlePanel compact={false}>
<Header> <Header>
<ShareLink to={this.props.item.id} /> <ShareLink to={this.props.item.id} />
{name} {name}

View File

@ -5,7 +5,7 @@ import { Markdown } from '../Markdown/Markdown';
import { OptionsContext } from '../OptionsProvider'; import { OptionsContext } from '../OptionsProvider';
import { SelectOnClick } from '../SelectOnClick/SelectOnClick'; import { SelectOnClick } from '../SelectOnClick/SelectOnClick';
import { getBasePath } from '../../utils'; import { expandDefaultServerVariables, getBasePath } from '../../utils';
import { import {
EndpointInfo, EndpointInfo,
HttpVerb, HttpVerb,
@ -60,21 +60,26 @@ export class Endpoint extends React.Component<EndpointProps, EndpointState> {
/> />
</EndpointInfo> </EndpointInfo>
<ServersOverlay expanded={expanded}> <ServersOverlay expanded={expanded}>
{operation.servers.map(server => ( {operation.servers.map(server => {
<ServerItem key={server.url}> const normalizedUrl = options.expandDefaultServerVariables
? expandDefaultServerVariables(server.url, server.variables)
: server.url;
return (
<ServerItem key={normalizedUrl}>
<Markdown source={server.description || ''} compact={true} /> <Markdown source={server.description || ''} compact={true} />
<SelectOnClick> <SelectOnClick>
<ServerUrl> <ServerUrl>
<span> <span>
{hideHostname || options.hideHostname {hideHostname || options.hideHostname
? getBasePath(server.url) ? getBasePath(normalizedUrl)
: server.url} : normalizedUrl}
</span> </span>
{operation.path} {operation.path}
</ServerUrl> </ServerUrl>
</SelectOnClick> </SelectOnClick>
</ServerItem> </ServerItem>
))} );
})}
</ServersOverlay> </ServersOverlay>
</OperationEndpointWrap> </OperationEndpointWrap>
)} )}

View File

@ -2,6 +2,7 @@ import * as React from 'react';
import { ExampleValue, FieldLabel } from '../../common-elements/fields'; import { ExampleValue, FieldLabel } from '../../common-elements/fields';
import { l } from '../../services/Labels'; import { l } from '../../services/Labels';
import { OptionsContext } from '../OptionsProvider';
export interface EnumValuesProps { export interface EnumValuesProps {
values: string[]; values: string[];
@ -9,8 +10,10 @@ export interface EnumValuesProps {
} }
export class EnumValues extends React.PureComponent<EnumValuesProps> { export class EnumValues extends React.PureComponent<EnumValuesProps> {
static contextType = OptionsContext;
render() { render() {
const { values, type } = this.props; const { values, type } = this.props;
const { enumSkipQuotes } = this.context;
if (!values.length) { if (!values.length) {
return null; return null;
} }
@ -20,12 +23,15 @@ export class EnumValues extends React.PureComponent<EnumValuesProps> {
<FieldLabel> <FieldLabel>
{type === 'array' ? l('enumArray') : ''}{' '} {type === 'array' ? l('enumArray') : ''}{' '}
{values.length === 1 ? l('enumSingleValue') : l('enum')}: {values.length === 1 ? l('enumSingleValue') : l('enum')}:
</FieldLabel> </FieldLabel>{' '}
{values.map((value, idx) => ( {values.map((value, idx) => {
const exampleValue = enumSkipQuotes ? value : JSON.stringify(value);
return (
<React.Fragment key={idx}> <React.Fragment key={idx}>
<ExampleValue>{JSON.stringify(value)}</ExampleValue>{' '} <ExampleValue>{exampleValue}</ExampleValue>{' '}
</React.Fragment> </React.Fragment>
))} );
})}
</div> </div>
); );
} }

View File

@ -65,8 +65,7 @@ export class Field extends React.Component<FieldProps> {
<FieldDetails {...this.props} /> <FieldDetails {...this.props} />
</PropertyDetailsCell> </PropertyDetailsCell>
</tr> </tr>
{field.expanded && {field.expanded && withSubSchema && (
withSubSchema && (
<tr key={field.name + 'inner'}> <tr key={field.name + 'inner'}>
<PropertyCellWithInner colSpan={2}> <PropertyCellWithInner colSpan={2}>
<InnerPropertiesWrap> <InnerPropertiesWrap>

View File

@ -4,6 +4,7 @@ import { ExampleValue, FieldLabel } from '../../common-elements/fields';
export interface FieldDetailProps { export interface FieldDetailProps {
value?: any; value?: any;
label: string; label: string;
raw?: boolean;
} }
export class FieldDetail extends React.PureComponent<FieldDetailProps> { export class FieldDetail extends React.PureComponent<FieldDetailProps> {
@ -11,12 +12,12 @@ export class FieldDetail extends React.PureComponent<FieldDetailProps> {
if (this.props.value === undefined) { if (this.props.value === undefined) {
return null; return null;
} }
const value = this.props.raw ? this.props.value : JSON.stringify(this.props.value);
return ( return (
<div> <div>
<FieldLabel> {this.props.label} </FieldLabel>{' '} <FieldLabel> {this.props.label} </FieldLabel> <ExampleValue>{value}</ExampleValue>
<ExampleValue>
{JSON.stringify(this.props.value)}
</ExampleValue>
</div> </div>
); );
} }

View File

@ -9,6 +9,7 @@ import {
TypePrefix, TypePrefix,
TypeTitle, TypeTitle,
} from '../../common-elements/fields'; } from '../../common-elements/fields';
import { serializeParameterValue } from '../../utils/openapi';
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation'; import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
import { Markdown } from '../Markdown/Markdown'; import { Markdown } from '../Markdown/Markdown';
import { EnumValues } from './EnumValues'; import { EnumValues } from './EnumValues';
@ -20,13 +21,31 @@ import { FieldDetail } from './FieldDetail';
import { Badge } from '../../common-elements/'; import { Badge } from '../../common-elements/';
import { l } from '../../services/Labels'; import { l } from '../../services/Labels';
import { OptionsContext } from '../OptionsProvider';
export class FieldDetails extends React.PureComponent<FieldProps> { export class FieldDetails extends React.PureComponent<FieldProps> {
static contextType = OptionsContext;
render() { render() {
const { showExamples, field, renderDiscriminatorSwitch } = this.props; const { showExamples, field, renderDiscriminatorSwitch } = this.props;
const { enumSkipQuotes, hideSchemaTitles } = this.context;
const { schema, description, example, deprecated } = field; const { schema, description, example, deprecated } = field;
const rawDefault = !!enumSkipQuotes || field.in === 'header'; // having quotes around header field default values is confusing and inappropriate
let exampleField: JSX.Element | null = null;
if (showExamples && example !== undefined) {
const label = l('example') + ':';
if (field.in && (field.style || field.serializationMime)) {
// decode for better readability in examples: see https://github.com/Redocly/redoc/issues/1138
const serializedValue = decodeURIComponent(serializeParameterValue(field, example));
exampleField = <FieldDetail label={label} value={serializedValue} raw={true} />;
} else {
exampleField = <FieldDetail label={label} value={example} />;
}
}
return ( return (
<div> <div>
<div> <div>
@ -40,10 +59,10 @@ export class FieldDetails extends React.PureComponent<FieldProps> {
&gt;{' '} &gt;{' '}
</TypeFormat> </TypeFormat>
)} )}
{schema.title && <TypeTitle> ({schema.title}) </TypeTitle>} {schema.title && !hideSchemaTitles && <TypeTitle> ({schema.title}) </TypeTitle>}
<ConstraintsView constraints={schema.constraints} /> <ConstraintsView constraints={schema.constraints} />
{schema.nullable && <NullableLabel> {l('nullable')} </NullableLabel>} {schema.nullable && <NullableLabel> {l('nullable')} </NullableLabel>}
{schema.pattern && <PatternLabel>{schema.pattern}</PatternLabel>} {schema.pattern && <PatternLabel> {schema.pattern} </PatternLabel>}
{schema.isCircular && <RecursiveLabel> {l('recursive')} </RecursiveLabel>} {schema.isCircular && <RecursiveLabel> {l('recursive')} </RecursiveLabel>}
</div> </div>
{deprecated && ( {deprecated && (
@ -51,9 +70,9 @@ export class FieldDetails extends React.PureComponent<FieldProps> {
<Badge type="warning"> {l('deprecated')} </Badge> <Badge type="warning"> {l('deprecated')} </Badge>
</div> </div>
)} )}
<FieldDetail label={l('default') + ':'} value={schema.default} /> <FieldDetail raw={rawDefault} label={l('default') + ':'} value={schema.default} />
{!renderDiscriminatorSwitch && <EnumValues type={schema.type} values={schema.enum} />}{' '} {!renderDiscriminatorSwitch && <EnumValues type={schema.type} values={schema.enum} />}{' '}
{showExamples && <FieldDetail label={l('example') + ':'} value={example} />} {exampleField}
{<Extensions extensions={{ ...field.extensions, ...schema.extensions }} />} {<Extensions extensions={{ ...field.extensions, ...schema.extensions }} />}
<div> <div>
<Markdown compact={true} source={description} /> <Markdown compact={true} source={description} />

View File

@ -5,6 +5,7 @@ import { SampleControls } from '../../common-elements';
import { CopyButtonWrapper } from '../../common-elements/CopyButtonWrapper'; import { CopyButtonWrapper } from '../../common-elements/CopyButtonWrapper';
import { PrismDiv } from '../../common-elements/PrismDiv'; import { PrismDiv } from '../../common-elements/PrismDiv';
import { jsonToHTML } from '../../utils/jsonToHtml'; import { jsonToHTML } from '../../utils/jsonToHtml';
import { OptionsContext } from '../OptionsProvider';
import { jsonStyles } from './style'; import { jsonStyles } from './style';
export interface JsonProps { export interface JsonProps {
@ -32,12 +33,18 @@ class Json extends React.PureComponent<JsonProps> {
<span onClick={this.expandAll}> Expand all </span> <span onClick={this.expandAll}> Expand all </span>
<span onClick={this.collapseAll}> Collapse all </span> <span onClick={this.collapseAll}> Collapse all </span>
</SampleControls> </SampleControls>
<OptionsContext.Consumer>
{options => (
<PrismDiv <PrismDiv
className={this.props.className} className={this.props.className}
// tslint:disable-next-line // tslint:disable-next-line
ref={node => (this.node = node!)} ref={node => (this.node = node!)}
dangerouslySetInnerHTML={{ __html: jsonToHTML(this.props.data) }} dangerouslySetInnerHTML={{
__html: jsonToHTML(this.props.data, options.jsonSampleExpandLevel),
}}
/> />
)}
</OptionsContext.Consumer>
</JsonViewerWrap> </JsonViewerWrap>
); );

View File

@ -25,7 +25,7 @@ export class AdvancedMarkdown extends React.Component<AdvancedMarkdownProps> {
renderWithOptionsAndStore(options: RedocNormalizedOptions, store?: AppStore) { renderWithOptionsAndStore(options: RedocNormalizedOptions, store?: AppStore) {
const { source, htmlWrap = i => i } = this.props; const { source, htmlWrap = i => i } = this.props;
if (!store) { if (!store) {
throw new Error('When using componentes in markdown, store prop must be provided'); throw new Error('When using components in markdown, store prop must be provided');
} }
const renderer = new MarkdownRenderer(options); const renderer = new MarkdownRenderer(options);

View File

@ -3,6 +3,7 @@ import * as React from 'react';
import { DropdownProps } from '../../common-elements/dropdown'; import { DropdownProps } from '../../common-elements/dropdown';
import { MediaContentModel, MediaTypeModel, SchemaModel } from '../../services/models'; import { MediaContentModel, MediaTypeModel, SchemaModel } from '../../services/models';
import { DropdownLabel, DropdownWrapper } from '../PayloadSamples/styled.elements';
export interface MediaTypeChildProps { export interface MediaTypeChildProps {
schema: SchemaModel; schema: SchemaModel;
@ -11,6 +12,8 @@ export interface MediaTypeChildProps {
export interface MediaTypesSwitchProps { export interface MediaTypesSwitchProps {
content?: MediaContentModel; content?: MediaContentModel;
withLabel?: boolean;
renderDropdown: (props: DropdownProps) => JSX.Element; renderDropdown: (props: DropdownProps) => JSX.Element;
children: (activeMime: MediaTypeModel) => JSX.Element; children: (activeMime: MediaTypeModel) => JSX.Element;
} }
@ -37,13 +40,25 @@ export class MediaTypesSwitch extends React.Component<MediaTypesSwitchProps> {
}; };
}); });
const Wrapper = ({ children }) =>
this.props.withLabel ? (
<DropdownWrapper>
<DropdownLabel>Content type</DropdownLabel>
{children}
</DropdownWrapper>
) : (
children
);
return ( return (
<> <>
<Wrapper>
{this.props.renderDropdown({ {this.props.renderDropdown({
value: options[activeMimeIdx], value: options[activeMimeIdx],
options, options,
onChange: this.switchMedia, onChange: this.switchMedia,
})} })}
</Wrapper>
{this.props.children(content.active)} {this.props.children(content.active)}
</> </>
); );

View File

@ -1,17 +1,33 @@
import * as React from 'react'; import * as React from 'react';
import { SmallTabs, Tab, TabList, TabPanel } from '../../common-elements'; import styled from '../../styled-components';
import { MediaTypeModel } from '../../services/models';
import { DropdownProps } from '../../common-elements';
import { MediaTypeModel } from '../../services/models';
import { Markdown } from '../Markdown/Markdown';
import { Example } from './Example'; import { Example } from './Example';
import { NoSampleLabel } from './styled.elements'; import { DropdownLabel, DropdownWrapper, NoSampleLabel } from './styled.elements';
export interface PayloadSamplesProps { export interface PayloadSamplesProps {
mediaType: MediaTypeModel; mediaType: MediaTypeModel;
renderDropdown: (props: DropdownProps) => JSX.Element;
} }
export class MediaTypeSamples extends React.Component<PayloadSamplesProps> { interface MediaTypeSamplesState {
activeIdx: number;
}
export class MediaTypeSamples extends React.Component<PayloadSamplesProps, MediaTypeSamplesState> {
state = {
activeIdx: 0,
};
switchMedia = ({ value }) => {
this.setState({
activeIdx: parseInt(value, 10),
});
};
render() { render() {
const { activeIdx } = this.state;
const examples = this.props.mediaType.examples || {}; const examples = this.props.mediaType.examples || {};
const mimeType = this.props.mediaType.name; const mimeType = this.props.mediaType.name;
@ -21,28 +37,46 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps> {
if (examplesNames.length === 0) { if (examplesNames.length === 0) {
return noSample; return noSample;
} }
if (examplesNames.length > 1) { if (examplesNames.length > 1) {
const options = examplesNames.map((name, idx) => {
return {
label: examples[name].summary || name,
value: idx.toString(),
};
});
const example = examples[examplesNames[activeIdx]];
const description = example.description;
return ( return (
<SmallTabs defaultIndex={0}> <SamplesWrapper>
<TabList> <DropdownWrapper>
{examplesNames.map(name => ( <DropdownLabel>Example</DropdownLabel>
<Tab key={name}> {examples[name].summary || name} </Tab> {this.props.renderDropdown({
))} value: options[activeIdx],
</TabList> options,
{examplesNames.map(name => ( onChange: this.switchMedia,
<TabPanel key={name}> })}
<Example example={examples[name]} mimeType={mimeType} /> </DropdownWrapper>
</TabPanel> <div>
))} {description && <Markdown source={description} />}
</SmallTabs> <Example example={example} mimeType={mimeType} />
</div>
</SamplesWrapper>
); );
} else { } else {
const name = examplesNames[0]; const example = examples[examplesNames[0]];
return ( return (
<div> <SamplesWrapper>
<Example example={examples[name]} mimeType={mimeType} /> {example.description && <Markdown source={example.description} />}
</div> <Example example={example} mimeType={mimeType} />
</SamplesWrapper>
); );
} }
} }
} }
const SamplesWrapper = styled.div`
margin-top: 15px;
`;

View File

@ -2,10 +2,9 @@ import { observer } from 'mobx-react';
import * as React from 'react'; import * as React from 'react';
import { MediaTypeSamples } from './MediaTypeSamples'; import { MediaTypeSamples } from './MediaTypeSamples';
import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch';
import { MediaContentModel } from '../../services/models'; import { MediaContentModel } from '../../services/models';
import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel'; import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel';
import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch';
import { InvertedSimpleDropdown, MimeLabel } from './styled.elements'; import { InvertedSimpleDropdown, MimeLabel } from './styled.elements';
export interface PayloadSamplesProps { export interface PayloadSamplesProps {
@ -21,8 +20,14 @@ export class PayloadSamples extends React.Component<PayloadSamplesProps> {
} }
return ( return (
<MediaTypesSwitch content={mimeContent} renderDropdown={this.renderDropdown}> <MediaTypesSwitch content={mimeContent} renderDropdown={this.renderDropdown} withLabel={true}>
{mediaType => <MediaTypeSamples key="samples" mediaType={mediaType} />} {mediaType => (
<MediaTypeSamples
key="samples"
mediaType={mediaType}
renderDropdown={this.renderDropdown}
/>
)}
</MediaTypesSwitch> </MediaTypesSwitch>
); );
} }

View File

@ -13,8 +13,7 @@ export function useExternalExample(example: ExampleModel, mimeType: string) {
prevRef.current = example; prevRef.current = example;
useEffect( useEffect(() => {
() => {
const load = async () => { const load = async () => {
setIsLoading(true); setIsLoading(true);
try { try {
@ -26,9 +25,7 @@ export function useExternalExample(example: ExampleModel, mimeType: string) {
}; };
load(); load();
}, }, [example, mimeType]);
[example, mimeType],
);
return value.current; return value.current;
} }

View File

@ -1,29 +1,48 @@
// @ts-ignore // @ts-ignore
import Dropdown from 'react-dropdown'; import Dropdown from 'react-dropdown';
import { transparentize } from 'polished';
import styled from '../../styled-components'; import styled from '../../styled-components';
import { StyledDropdown } from '../../common-elements'; import { StyledDropdown } from '../../common-elements';
export const MimeLabel = styled.div` export const MimeLabel = styled.div`
border-bottom: 1px solid rgba(255, 255, 255, 0.9); padding: 12px;
background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)};
margin: 0 0 10px 0; margin: 0 0 10px 0;
display: block; display: block;
color: rgba(255, 255, 255, 0.8); `;
export const DropdownLabel = styled.span`
font-family: ${({ theme }) => theme.typography.headings.fontFamily};
font-size: 12px;
position: absolute;
z-index: 1;
top: -11px;
left: 12px;
font-weight: ${({ theme }) => theme.typography.fontWeightBold};
color: ${({ theme }) => transparentize(0.6, theme.rightPanel.textColor)};
`;
export const DropdownWrapper = styled.div`
position: relative;
`; `;
export const InvertedSimpleDropdown = styled(StyledDropdown)` export const InvertedSimpleDropdown = styled(StyledDropdown)`
margin-left: 10px; margin-left: 10px;
text-transform: none; text-transform: none;
font-size: 0.929em; font-size: 0.929em;
border-bottom: 1px solid ${({ theme }) => theme.rightPanel.textColor};
margin: 0 0 10px 0; margin: 0 0 10px 0;
display: block; display: block;
background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)};
.Dropdown-control {
margin-top: 0;
}
.Dropdown-control, .Dropdown-control,
.Dropdown-control:hover { .Dropdown-control:hover {
font-size: 1em; font-size: 1em;
border: none; border: none;
padding: 0 1.2em 0 0; padding: 0.9em 1.6em 0.9em 0.9em;
background: transparent; background: transparent;
color: ${({ theme }) => theme.rightPanel.textColor}; color: ${({ theme }) => theme.rightPanel.textColor};
box-shadow: none; box-shadow: none;
@ -34,6 +53,7 @@ export const InvertedSimpleDropdown = styled(StyledDropdown)`
} }
.Dropdown-menu { .Dropdown-menu {
margin: 0; margin: 0;
margin-top: 2px;
} }
`; `;

View File

@ -1,6 +1,6 @@
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import * as React from 'react'; import * as React from 'react';
import { OperationModel, RedocNormalizedOptions } from '../../services'; import { isPayloadSample, OperationModel, RedocNormalizedOptions } from '../../services';
import { PayloadSamples } from '../PayloadSamples/PayloadSamples'; import { PayloadSamples } from '../PayloadSamples/PayloadSamples';
import { SourceCodeWithCopy } from '../SourceCode/SourceCode'; import { SourceCodeWithCopy } from '../SourceCode/SourceCode';
@ -19,15 +19,10 @@ export class RequestSamples extends React.Component<RequestSamplesProps> {
render() { render() {
const { operation } = this.props; const { operation } = this.props;
const requestBodyContent = operation.requestBody && operation.requestBody.content;
const hasBodySample = requestBodyContent && requestBodyContent.hasSample;
const samples = operation.codeSamples; const samples = operation.codeSamples;
const hasSamples = hasBodySample || samples.length > 0; const hasSamples = samples.length > 0;
const hideTabList = const hideTabList = samples.length === 1 ? this.context.hideSingleRequestSampleTab : false;
samples.length + (hasBodySample ? 1 : 0) === 1
? this.context.hideSingleRequestSampleTab
: false;
return ( return (
(hasSamples && ( (hasSamples && (
<div> <div>
@ -35,23 +30,21 @@ export class RequestSamples extends React.Component<RequestSamplesProps> {
<Tabs defaultIndex={0}> <Tabs defaultIndex={0}>
<TabList hidden={hideTabList}> <TabList hidden={hideTabList}>
{hasBodySample && <Tab key="payload"> Payload </Tab>}
{samples.map(sample => ( {samples.map(sample => (
<Tab key={sample.lang + '_' + (sample.label || '')}> <Tab key={sample.lang + '_' + (sample.label || '')}>
{sample.label !== undefined ? sample.label : sample.lang} {sample.label !== undefined ? sample.label : sample.lang}
</Tab> </Tab>
))} ))}
</TabList> </TabList>
{hasBodySample && (
<TabPanel key="payload">
<div>
<PayloadSamples content={requestBodyContent!} />
</div>
</TabPanel>
)}
{samples.map(sample => ( {samples.map(sample => (
<TabPanel key={sample.lang}> <TabPanel key={sample.lang + '_' + (sample.label || '')}>
{isPayloadSample(sample) ? (
<div>
<PayloadSamples content={sample.requestBodyContent} />
</div>
) : (
<SourceCodeWithCopy lang={sample.lang} source={sample.source} /> <SourceCodeWithCopy lang={sample.lang} source={sample.source} />
)}
</TabPanel> </TabPanel>
))} ))}
</Tabs> </Tabs>

View File

@ -28,8 +28,7 @@ export class ResponseView extends React.Component<{ response: ResponseModel }> {
code={code} code={code}
opened={expanded} opened={expanded}
/> />
{expanded && {expanded && !empty && (
!empty && (
<ResponseDetailsWrap> <ResponseDetailsWrap>
<ResponseDetails response={this.props.response} /> <ResponseDetails response={this.props.response} />
</ResponseDetailsWrap> </ResponseDetailsWrap>

View File

@ -22,7 +22,7 @@ export class ResponseTitle extends React.PureComponent<ResponseTitleProps> {
<ShelfIcon <ShelfIcon
size={'1.5em'} size={'1.5em'}
color={type} color={type}
direction={opened ? 'up' : 'down'} direction={opened ? 'down' : 'right'}
float={'left'} float={'left'}
/> />
)} )}

View File

@ -34,9 +34,9 @@ export class ObjectSchema extends React.Component<ObjectSchemaProps> {
const filteredFields = needFilter const filteredFields = needFilter
? fields.filter(item => { ? fields.filter(item => {
return ( return !(
(this.props.skipReadOnly && !item.schema.readOnly) || (this.props.skipReadOnly && item.schema.readOnly) ||
(this.props.skipWriteOnly && !item.schema.writeOnly) (this.props.skipWriteOnly && item.schema.writeOnly)
); );
}) })
: fields; : fields;

View File

@ -44,9 +44,7 @@ export class Schema extends React.Component<Partial<SchemaProps>> {
if (discriminatorProp !== undefined) { if (discriminatorProp !== undefined) {
if (!oneOf || !oneOf.length) { if (!oneOf || !oneOf.length) {
throw new Error( throw new Error(
`Looks like you are using discriminator wrong: you don't have any definition inherited from the ${ `Looks like you are using discriminator wrong: you don't have any definition inherited from the ${schema.title}`,
schema.title
}`,
); );
} }
return ( return (
@ -66,9 +64,9 @@ export class Schema extends React.Component<Partial<SchemaProps>> {
switch (type) { switch (type) {
case 'object': case 'object':
return <ObjectSchema {...this.props as any} />; return <ObjectSchema {...(this.props as any)} />;
case 'array': case 'array':
return <ArraySchema {...this.props as any} />; return <ArraySchema {...(this.props as any)} />;
} }
// TODO: maybe adjust FieldDetails to accept schema // TODO: maybe adjust FieldDetails to accept schema

View File

@ -0,0 +1,93 @@
import * as React from 'react';
import { DarkRightPanel, MiddlePanel, MimeLabel, Row, Section } from '../../common-elements';
import { MediaTypeModel, OpenAPIParser, RedocNormalizedOptions } from '../../services';
import styled from '../../styled-components';
import { OpenAPIMediaType } from '../../types';
import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel';
import { MediaTypeSamples } from '../PayloadSamples/MediaTypeSamples';
import { InvertedSimpleDropdown } from '../PayloadSamples/styled.elements';
import { Schema } from '../Schema';
export interface ObjectDescriptionProps {
schemaRef: string;
exampleRef?: string;
showReadOnly?: boolean;
showWriteOnly?: boolean;
parser: OpenAPIParser;
options: RedocNormalizedOptions;
}
export class SchemaDefinition extends React.PureComponent<ObjectDescriptionProps> {
private static getMediaType(schemaRef: string, exampleRef?: string): OpenAPIMediaType {
if (!schemaRef) {
return {};
}
const info: OpenAPIMediaType = {
schema: { $ref: schemaRef },
};
if (exampleRef) {
info.examples = { example: { $ref: exampleRef } };
}
return info;
}
private _mediaModel: MediaTypeModel;
private get mediaModel() {
const { parser, schemaRef, exampleRef, options } = this.props;
if (!this._mediaModel) {
this._mediaModel = new MediaTypeModel(
parser,
'json',
false,
SchemaDefinition.getMediaType(schemaRef, exampleRef),
options,
);
}
return this._mediaModel;
}
render() {
const { showReadOnly = true, showWriteOnly = false } = this.props;
return (
<Section>
<Row>
<MiddlePanel>
<Schema
skipWriteOnly={!showWriteOnly}
skipReadOnly={!showReadOnly}
schema={this.mediaModel.schema}
/>
</MiddlePanel>
<DarkRightPanel>
<MediaSamplesWrap>
<MediaTypeSamples renderDropdown={this.renderDropdown} mediaType={this.mediaModel} />
</MediaSamplesWrap>
</DarkRightPanel>
</Row>
</Section>
);
}
private renderDropdown = props => {
return <DropdownOrLabel Label={MimeLabel} Dropdown={InvertedSimpleDropdown} {...props} />;
};
}
const MediaSamplesWrap = styled.div`
background: ${({ theme }) => theme.codeSample.backgroundColor};
& > div,
& > pre {
padding: ${props => props.theme.spacing.unit * 4}px;
margin: 0;
}
& > div > pre {
padding: 0;
}
`;

View File

@ -49,7 +49,7 @@ export class OAuthFlow extends React.PureComponent<OAuthFlowProps> {
<strong> Scopes: </strong> <strong> Scopes: </strong>
</div> </div>
<ul> <ul>
{Object.keys(flow!.scopes).map(scope => ( {Object.keys(flow!.scopes || {}).map(scope => (
<li key={scope}> <li key={scope}>
<code>{scope}</code> - <Markdown inline={true} source={flow!.scopes[scope] || ''} /> <code>{scope}</code> - <Markdown inline={true} source={flow!.scopes[scope] || ''} />
</li> </li>
@ -80,7 +80,7 @@ export class SecurityDefs extends React.PureComponent<SecurityDefsProps> {
<table className="security-details"> <table className="security-details">
<tbody> <tbody>
<tr> <tr>
<th> Security scheme type: </th> <th> Security Scheme Type </th>
<td> {AUTH_TYPES[scheme.type] || scheme.type} </td> <td> {AUTH_TYPES[scheme.type] || scheme.type} </td>
</tr> </tr>
{scheme.apiKey ? ( {scheme.apiKey ? (

View File

@ -1,3 +1,4 @@
// import { observe } from 'mobx';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import * as React from 'react'; import * as React from 'react';
@ -15,7 +16,7 @@ export interface MenuItemProps {
@observer @observer
export class MenuItem extends React.Component<MenuItemProps> { export class MenuItem extends React.Component<MenuItemProps> {
ref: Element | null; ref = React.createRef<HTMLLabelElement>();
activate = (evt: React.MouseEvent<HTMLElement>) => { activate = (evt: React.MouseEvent<HTMLElement>) => {
this.props.onActivate!(this.props.item); this.props.onActivate!(this.props.item);
@ -31,42 +32,30 @@ export class MenuItem extends React.Component<MenuItemProps> {
} }
scrollIntoViewIfActive() { scrollIntoViewIfActive() {
if (this.props.item.active && this.ref) { if (this.props.item.active && this.ref.current) {
this.ref.scrollIntoViewIfNeeded(); this.ref.current.scrollIntoViewIfNeeded();
} }
} }
saveRef = ref => {
this.ref = ref;
};
render() { render() {
const { item, withoutChildren } = this.props; const { item, withoutChildren } = this.props;
return ( return (
<MenuItemLi <MenuItemLi onClick={this.activate} depth={item.depth} data-item-id={item.id}>
onClick={this.activate}
depth={item.depth}
ref={this.saveRef}
data-item-id={item.id}
>
{item.type === 'operation' ? ( {item.type === 'operation' ? (
<OperationMenuItemContent {...this.props} item={item as OperationModel} /> <OperationMenuItemContent {...this.props} item={item as OperationModel} />
) : ( ) : (
<MenuItemLabel depth={item.depth} active={item.active} type={item.type}> <MenuItemLabel depth={item.depth} active={item.active} type={item.type} ref={this.ref}>
<MenuItemTitle title={item.name}> <MenuItemTitle title={item.name}>
{item.name} {item.name}
{this.props.children} {this.props.children}
</MenuItemTitle> </MenuItemTitle>
{(item.depth > 0 && {(item.depth > 0 && item.items.length > 0 && (
item.items.length > 0 && (
<ShelfIcon float={'right'} direction={item.expanded ? 'down' : 'right'} /> <ShelfIcon float={'right'} direction={item.expanded ? 'down' : 'right'} />
)) || )) ||
null} null}
</MenuItemLabel> </MenuItemLabel>
)} )}
{!withoutChildren && {!withoutChildren && item.items && item.items.length > 0 && (
item.items &&
item.items.length > 0 && (
<MenuItems <MenuItems
expanded={item.expanded} expanded={item.expanded}
items={item.items} items={item.items}
@ -83,11 +72,24 @@ export interface OperationMenuItemContentProps {
} }
@observer @observer
class OperationMenuItemContent extends React.Component<OperationMenuItemContentProps> { export class OperationMenuItemContent extends React.Component<OperationMenuItemContentProps> {
ref = React.createRef<HTMLLabelElement>();
componentDidUpdate() {
if (this.props.item.active && this.ref.current) {
this.ref.current.scrollIntoViewIfNeeded();
}
}
render() { render() {
const { item } = this.props; const { item } = this.props;
return ( return (
<MenuItemLabel depth={item.depth} active={item.active} deprecated={item.deprecated}> <MenuItemLabel
depth={item.depth}
active={item.active}
deprecated={item.deprecated}
ref={this.ref}
>
<OperationBadge type={item.httpVerb}>{shortenHTTPVerb(item.httpVerb)}</OperationBadge> <OperationBadge type={item.httpVerb}>{shortenHTTPVerb(item.httpVerb)}</OperationBadge>
<MenuItemTitle width="calc(100% - 38px)"> <MenuItemTitle width="calc(100% - 38px)">
{item.name} {item.name}

View File

@ -5,11 +5,13 @@ import { StyledLink } from '../../../src/common-elements';
import { PerfectScrollbarWrap } from '../../common-elements/perfect-scrollbar'; import { PerfectScrollbarWrap } from '../../common-elements/perfect-scrollbar';
import { IMenuItem, MenuStore } from '../../services/MenuStore'; import { IMenuItem, MenuStore } from '../../services/MenuStore';
import { OptionsContext } from '../OptionsProvider';
import { MenuItems } from './MenuItems'; import { MenuItems } from './MenuItems';
import { RedocAttribution } from './styled.elements'; import { RedocAttribution } from './styled.elements';
@observer @observer
export class SideMenu extends React.Component<{ menu: MenuStore; className?: string }> { export class SideMenu extends React.Component<{ menu: MenuStore; className?: string }> {
static contextType = OptionsContext;
private _updateScroll?: () => void; private _updateScroll?: () => void;
render() { render() {
@ -33,6 +35,10 @@ export class SideMenu extends React.Component<{ menu: MenuStore; className?: str
} }
activate = (item: IMenuItem) => { activate = (item: IMenuItem) => {
if (item && item.active && this.context.menuToggle) {
return item.expanded ? item.collapse() : item.expand();
}
this.props.menu.activateAndScroll(item, true); this.props.menu.activateAndScroll(item, true);
setTimeout(() => { setTimeout(() => {
if (this._updateScroll) { if (this._updateScroll) {

View File

@ -104,7 +104,7 @@ export const menuItemDepth = {
font-size: 0.929em; font-size: 0.929em;
text-transform: ${({ theme }) => theme.menu.level1Items.textTransform}; text-transform: ${({ theme }) => theme.menu.level1Items.textTransform};
&:hover { &:hover {
color: ${props => props.theme.colors.primary.main}; color: ${props => props.theme.menu.activeTextColor};
} }
`, `,
2: css` 2: css`
@ -126,7 +126,7 @@ export const MenuItemLabel = styled.label.attrs((props: MenuItemLabelType) => ({
}), }),
}))<MenuItemLabelType>` }))<MenuItemLabelType>`
cursor: pointer; cursor: pointer;
color: ${props => (props.active ? props.theme.colors.primary.main : props.theme.menu.textColor)}; color: ${props => (props.active ? props.theme.menu.activeTextColor : props.theme.menu.textColor)};
margin: 0; margin: 0;
padding: 12.5px ${props => props.theme.spacing.unit * 4}px; padding: 12.5px ${props => props.theme.spacing.unit * 4}px;
${({ depth, type, theme }) => ${({ depth, type, theme }) =>

View File

@ -19,6 +19,10 @@ export interface StickySidebarProps {
menu: MenuStore; menu: MenuStore;
} }
export interface StickySidebarState {
offsetTop?: string;
}
const stickyfill = Stickyfill && Stickyfill(); const stickyfill = Stickyfill && Stickyfill();
const StyledStickySidebar = styled.div<{ open?: boolean }>` const StyledStickySidebar = styled.div<{ open?: boolean }>`
@ -40,7 +44,7 @@ const StyledStickySidebar = styled.div<{ open?: boolean }>`
position: fixed; position: fixed;
z-index: 20; z-index: 20;
width: 100%; width: 100%;
background: #ffffff; background: ${({ theme }) => theme.menu.backgroundColor};
display: ${props => (props.open ? 'flex' : 'none')}; display: ${props => (props.open ? 'flex' : 'none')};
`}; `};
@ -77,13 +81,26 @@ const FloatingButton = styled.div`
`; `;
@observer @observer
export class StickyResponsiveSidebar extends React.Component<StickySidebarProps> { export class StickyResponsiveSidebar extends React.Component<
StickySidebarProps,
StickySidebarState
> {
static contextType = OptionsContext;
context!: React.ContextType<typeof OptionsContext>;
state: StickySidebarState = { offsetTop: '0px' };
stickyElement: Element; stickyElement: Element;
componentDidMount() { componentDidMount() {
if (stickyfill) { if (stickyfill) {
stickyfill.add(this.stickyElement); stickyfill.add(this.stickyElement);
} }
// rerender when hydrating from SSR
// see https://github.com/facebook/react/issues/8017#issuecomment-256351955
this.setState({
offsetTop: this.getScrollYOffset(this.context),
});
} }
componentWillUnmount() { componentWillUnmount() {
@ -92,7 +109,7 @@ export class StickyResponsiveSidebar extends React.Component<StickySidebarProps>
} }
} }
getScrollYOffset(options) { getScrollYOffset(options: RedocNormalizedOptions) {
let top; let top;
if (this.props.scrollYOffset !== undefined) { if (this.props.scrollYOffset !== undefined) {
top = RedocNormalizedOptions.normalizeScrollYOffset(this.props.scrollYOffset)(); top = RedocNormalizedOptions.normalizeScrollYOffset(this.props.scrollYOffset)();
@ -105,22 +122,17 @@ export class StickyResponsiveSidebar extends React.Component<StickySidebarProps>
render() { render() {
const open = this.props.menu.sideBarOpened; const open = this.props.menu.sideBarOpened;
const style = options => { const top = this.state.offsetTop;
const top = this.getScrollYOffset(options);
return {
top,
height: `calc(100vh - ${top})`,
};
};
return ( return (
<OptionsContext.Consumer>
{options => (
<> <>
<StyledStickySidebar <StyledStickySidebar
open={open} open={open}
className={this.props.className} className={this.props.className}
style={style(options)} style={{
top,
height: `calc(100vh - ${top})`,
}}
// tslint:disable-next-line // tslint:disable-next-line
ref={el => { ref={el => {
this.stickyElement = el as any; this.stickyElement = el as any;
@ -132,16 +144,10 @@ export class StickyResponsiveSidebar extends React.Component<StickySidebarProps>
<AnimatedChevronButton open={open} /> <AnimatedChevronButton open={open} />
</FloatingButton> </FloatingButton>
</> </>
)}
</OptionsContext.Consumer>
); );
} }
private toggleNavMenu = () => { private toggleNavMenu = () => {
this.props.menu.toggleSidebar(); this.props.menu.toggleSidebar();
}; };
// private closeNavMenu = () => {
// this.setState({ open: false });
// };
} }

View File

@ -1,4 +1,4 @@
import memoize from 'memoize-one'; import * as memoize from 'memoize-one/dist/memoize-one.cjs'; // fixme: https://github.com/alexreardon/memoize-one/issues/37
import { Component, createContext } from 'react'; import { Component, createContext } from 'react';
import { AppStore } from '../services/'; import { AppStore } from '../services/';

View File

@ -24,14 +24,14 @@ describe('Components', () => {
}); });
test('should collapse/uncollapse', () => { test('should collapse/uncollapse', () => {
expect(component.html()).not.toContain('class="hoverable"'); // all are collapesed by default expect(component.html()).not.toContain('class="hoverable"'); // all are collapsed by default
const expandAll = component.find('div > span[children=" Expand all "]'); const expandAll = component.find('div > span[children=" Expand all "]');
expandAll.simulate('click'); expandAll.simulate('click');
expect(component.html()).toContain('class="hoverable"'); // all are collapesed expect(component.html()).toContain('class="hoverable"'); // all are collapsed
const collapseAll = component.find('div > span[children=" Collapse all "]'); const collapseAll = component.find('div > span[children=" Collapse all "]');
collapseAll.simulate('click'); collapseAll.simulate('click');
expect(component.html()).not.toContain('class="hoverable"'); // all are collapesed expect(component.html()).not.toContain('class="hoverable"'); // all are collapsed
}); });
test('should collapse/uncollapse', () => { test('should collapse/uncollapse', () => {

View File

@ -10,6 +10,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"description": "", "description": "",
"example": undefined, "example": undefined,
"expanded": false, "expanded": false,
"explode": false,
"in": undefined, "in": undefined,
"kind": "field", "kind": "field",
"name": "packSize", "name": "packSize",
@ -59,6 +60,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"description": "", "description": "",
"example": undefined, "example": undefined,
"expanded": false, "expanded": false,
"explode": false,
"in": undefined, "in": undefined,
"kind": "field", "kind": "field",
"name": "type", "name": "type",

View File

@ -28,3 +28,5 @@ export * from './OptionsProvider';
export * from './SideMenu/'; export * from './SideMenu/';
export * from './StickySidebar/StickyResponsiveSidebar'; export * from './StickySidebar/StickyResponsiveSidebar';
export * from './SearchBox/SearchBox'; export * from './SearchBox/SearchBox';
export * from './SchemaDefinition/SchemaDefinition';
export * from './SourceCode/SourceCode';

View File

@ -1,11 +1,12 @@
import 'core-js/es6/promise'; import 'core-js/es/promise';
import 'core-js/fn/array/find';
import 'core-js/fn/object/assign';
import 'core-js/fn/string/ends-with';
import 'core-js/fn/string/starts-with';
import 'core-js/es6/map'; import 'core-js/es/array/find';
import 'core-js/es6/symbol'; import 'core-js/es/object/assign';
import 'core-js/es/string/ends-with';
import 'core-js/es/string/starts-with';
import 'core-js/es/map';
import 'core-js/es/symbol';
import 'unfetch/polyfill/index'; import 'unfetch/polyfill/index';
import 'url-polyfill'; import 'url-polyfill';

View File

@ -10,8 +10,13 @@ import { RedocNormalizedOptions, RedocRawOptions } from './RedocNormalizedOption
import { ScrollService } from './ScrollService'; import { ScrollService } from './ScrollService';
import { SearchStore } from './SearchStore'; import { SearchStore } from './SearchStore';
import { SchemaDefinition } from '../components/SchemaDefinition/SchemaDefinition';
import { SecurityDefs } from '../components/SecuritySchemes/SecuritySchemes'; import { SecurityDefs } from '../components/SecuritySchemes/SecuritySchemes';
import { SECURITY_DEFINITIONS_COMPONENT_NAME } from '../utils/openapi'; import {
SCHEMA_DEFINITION_JSX_NAME,
SECURITY_DEFINITIONS_COMPONENT_NAME,
SECURITY_DEFINITIONS_JSX_NAME,
} from '../utils/openapi';
export interface StoreState { export interface StoreState {
menu: { menu: {
@ -96,6 +101,9 @@ export class AppStore {
dispose() { dispose() {
this.scroll.dispose(); this.scroll.dispose();
this.menu.dispose(); this.menu.dispose();
if (this.search) {
this.search.dispose();
}
if (this.disposer != null) { if (this.disposer != null) {
this.disposer(); this.disposer();
} }
@ -151,5 +159,18 @@ const DEFAULT_OPTIONS: RedocRawOptions = {
securitySchemes: store.spec.securitySchemes, securitySchemes: store.spec.securitySchemes,
}), }),
}, },
[SECURITY_DEFINITIONS_JSX_NAME]: {
component: SecurityDefs,
propsSelector: (store: AppStore) => ({
securitySchemes: store.spec.securitySchemes,
}),
},
[SCHEMA_DEFINITION_JSX_NAME]: {
component: SchemaDefinition,
propsSelector: (store: AppStore) => ({
parser: store.spec.parser,
options: store.options,
}),
},
}, },
}; };

View File

@ -28,7 +28,10 @@ export class ClipboardService {
if ((document as any).selection) { if ((document as any).selection) {
(document as any).selection.empty(); (document as any).selection.empty();
} else if (window.getSelection) { } else if (window.getSelection) {
window.getSelection().removeAllRanges(); const selection = window.getSelection();
if (selection) {
selection.removeAllRanges();
}
} }
} }

View File

@ -1,6 +1,6 @@
import * as marked from 'marked'; import * as marked from 'marked';
import { highlight, safeSlugify } from '../utils'; import { highlight, safeSlugify, unescapeHTMLChars } from '../utils';
import { AppStore } from './AppStore'; import { AppStore } from './AppStore';
import { RedocNormalizedOptions } from './RedocNormalizedOptions'; import { RedocNormalizedOptions } from './RedocNormalizedOptions';
@ -45,6 +45,14 @@ export class MarkdownRenderer {
return compRegexp.test(rawText); return compRegexp.test(rawText);
} }
static getTextBeforeHading(md: string, heading: string): string {
const headingLinePos = md.search(new RegExp(`^##?\\s+${heading}`, 'm'));
if (headingLinePos > -1) {
return md.substring(0, headingLinePos);
}
return md;
}
headings: MarkdownHeading[] = []; headings: MarkdownHeading[] = [];
currentTopHeading: MarkdownHeading; currentTopHeading: MarkdownHeading;
@ -65,6 +73,7 @@ export class MarkdownRenderer {
container: MarkdownHeading[] = this.headings, container: MarkdownHeading[] = this.headings,
parentId?: string, parentId?: string,
): MarkdownHeading { ): MarkdownHeading {
name = unescapeHTMLChars(name);
const item = { const item = {
id: parentId ? `${parentId}/${safeSlugify(name)}` : `section/${safeSlugify(name)}`, id: parentId ? `${parentId}/${safeSlugify(name)}` : `section/${safeSlugify(name)}`,
name, name,
@ -88,7 +97,7 @@ export class MarkdownRenderer {
} }
attachHeadingsDescriptions(rawText: string) { attachHeadingsDescriptions(rawText: string) {
const buildRegexp = heading => { const buildRegexp = (heading: MarkdownHeading) => {
return new RegExp(`##?\\s+${heading.name.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}`); return new RegExp(`##?\\s+${heading.name.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}`);
}; };
@ -118,7 +127,12 @@ export class MarkdownRenderer {
.trim(); .trim();
} }
headingRule = (text: string, level: number, raw: string, slugger: marked.Slugger) => { headingRule = (
text: string,
level: 1 | 2 | 3 | 4 | 5 | 6,
raw: string,
slugger: marked.Slugger,
) => {
if (level === 1) { if (level === 1) {
this.currentTopHeading = this.saveHeading(text, level); this.currentTopHeading = this.saveHeading(text, level);
} else if (level === 2) { } else if (level === 2) {

View File

@ -42,7 +42,7 @@ export class MenuBuilder {
const items: ContentItemModel[] = []; const items: ContentItemModel[] = [];
const tagsMap = MenuBuilder.getTagsWithOperations(spec); const tagsMap = MenuBuilder.getTagsWithOperations(spec);
items.push(...MenuBuilder.addMarkdownItems(spec.info.description || '', options)); items.push(...MenuBuilder.addMarkdownItems(spec.info.description || '', undefined, 1, options));
if (spec['x-tagGroups'] && spec['x-tagGroups'].length > 0) { if (spec['x-tagGroups'] && spec['x-tagGroups'].length > 0) {
items.push( items.push(
...MenuBuilder.getTagGroupsItems(parser, undefined, spec['x-tagGroups'], tagsMap, options), ...MenuBuilder.getTagGroupsItems(parser, undefined, spec['x-tagGroups'], tagsMap, options),
@ -59,14 +59,23 @@ export class MenuBuilder {
*/ */
static addMarkdownItems( static addMarkdownItems(
description: string, description: string,
parent: GroupModel | undefined,
initialDepth: number,
options: RedocNormalizedOptions, options: RedocNormalizedOptions,
): ContentItemModel[] { ): ContentItemModel[] {
const renderer = new MarkdownRenderer(options); const renderer = new MarkdownRenderer(options);
const headings = renderer.extractHeadings(description || ''); const headings = renderer.extractHeadings(description || '');
const mapHeadingsDeep = (parent, items, depth = 1) => if (headings.length && parent && parent.description) {
parent.description = MarkdownRenderer.getTextBeforeHading(
parent.description,
headings[0].name,
);
}
const mapHeadingsDeep = (_parent, items, depth = 1) =>
items.map(heading => { items.map(heading => {
const group = new GroupModel('section', heading, parent); const group = new GroupModel('section', heading, _parent);
group.depth = depth; group.depth = depth;
if (heading.items) { if (heading.items) {
group.items = mapHeadingsDeep(group, heading.items, depth + 1); group.items = mapHeadingsDeep(group, heading.items, depth + 1);
@ -82,11 +91,11 @@ export class MenuBuilder {
return group; return group;
}); });
return mapHeadingsDeep(undefined, headings); return mapHeadingsDeep(parent, headings, initialDepth);
} }
/** /**
* Returns array of OperationsGroup items for the tag groups (x-tagGroups vendor extenstion) * Returns array of OperationsGroup items for the tag groups (x-tagGroups vendor extension)
* @param tags value of `x-tagGroups` vendor extension * @param tags value of `x-tagGroups` vendor extension
*/ */
static getTagGroupsItems( static getTagGroupsItems(
@ -144,15 +153,22 @@ export class MenuBuilder {
} }
const item = new GroupModel('tag', tag, parent); const item = new GroupModel('tag', tag, parent);
item.depth = GROUP_DEPTH + 1; item.depth = GROUP_DEPTH + 1;
item.items = this.getOperationsItems(parser, item, tag, item.depth + 1, options);
// don't put empty tag into content, instead put its operations // don't put empty tag into content, instead put its operations
if (tag.name === '') { if (tag.name === '') {
const items = this.getOperationsItems(parser, undefined, tag, item.depth + 1, options); const items = [
...MenuBuilder.addMarkdownItems(tag.description || '', item, item.depth + 1, options),
...this.getOperationsItems(parser, undefined, tag, item.depth + 1, options),
];
res.push(...items); res.push(...items);
continue; continue;
} }
item.items = [
...MenuBuilder.addMarkdownItems(tag.description || '', item, item.depth + 1, options),
...this.getOperationsItems(parser, item, tag, item.depth + 1, options),
];
res.push(item); res.push(item);
} }
return res; return res;

View File

@ -116,7 +116,7 @@ export class MenuStore {
} }
if (isScrolledDown) { if (isScrolledDown) {
const el = this.getElementAt(itemIdx + 1); const el = this.getElementAtOrFirstChild(itemIdx + 1);
if (this.scroll.isElementBellow(el)) { if (this.scroll.isElementBellow(el)) {
break; break;
} }
@ -163,6 +163,18 @@ export class MenuStore {
return (item && querySelector(`[${SECTION_ATTR}="${item.id}"]`)) || null; return (item && querySelector(`[${SECTION_ATTR}="${item.id}"]`)) || null;
} }
/**
* get section/operation DOM Node related to the item or if it is group item, returns first item of the group
* @param idx item absolute index
*/
getElementAtOrFirstChild(idx: number): Element | null {
let item = this.flatItems[idx];
if (item && item.type === 'group') {
item = item.items[0];
}
return (item && querySelector(`[${SECTION_ATTR}="${item.id}"]`)) || null;
}
/** /**
* current active item * current active item
*/ */
@ -178,7 +190,7 @@ export class MenuStore {
* activate menu item * activate menu item
* @param item item to activate * @param item item to activate
* @param updateLocation [true] whether to update location * @param updateLocation [true] whether to update location
* @param rewriteHistory [false] whether to rewrite browser history (do not create new enrty) * @param rewriteHistory [false] whether to rewrite browser history (do not create new entry)
*/ */
@action @action
activate( activate(
@ -189,6 +201,11 @@ export class MenuStore {
if ((this.activeItem && this.activeItem.id) === (item && item.id)) { if ((this.activeItem && this.activeItem.id) === (item && item.id)) {
return; return;
} }
if (item && item.type === 'group') {
return;
}
this.deactivate(this.activeItem); this.deactivate(this.activeItem);
if (!item) { if (!item) {
this.history.replace('', rewriteHistory); this.history.replace('', rewriteHistory);

View File

@ -4,7 +4,11 @@ import { OpenAPIRef, OpenAPISchema, OpenAPISpec, Referenced } from '../types';
import { appendToMdHeading, IS_BROWSER } from '../utils/'; import { appendToMdHeading, IS_BROWSER } from '../utils/';
import { JsonPointer } from '../utils/JsonPointer'; import { JsonPointer } from '../utils/JsonPointer';
import { isNamedDefinition, SECURITY_DEFINITIONS_COMPONENT_NAME } from '../utils/openapi'; import {
isNamedDefinition,
SECURITY_DEFINITIONS_COMPONENT_NAME,
SECURITY_DEFINITIONS_JSX_NAME,
} from '../utils/openapi';
import { buildComponentComment, MarkdownRenderer } from './MarkdownRenderer'; import { buildComponentComment, MarkdownRenderer } from './MarkdownRenderer';
import { RedocNormalizedOptions } from './RedocNormalizedOptions'; import { RedocNormalizedOptions } from './RedocNormalizedOptions';
@ -40,6 +44,7 @@ class RefCounter {
export class OpenAPIParser { export class OpenAPIParser {
specUrl?: string; specUrl?: string;
spec: OpenAPISpec; spec: OpenAPISpec;
mergeRefs: Set<string>;
private _refCounter: RefCounter = new RefCounter(); private _refCounter: RefCounter = new RefCounter();
@ -53,6 +58,8 @@ export class OpenAPIParser {
this.spec = spec; this.spec = spec;
this.mergeRefs = new Set();
const href = IS_BROWSER ? window.location.href : ''; const href = IS_BROWSER ? window.location.href : '';
if (typeof specUrl === 'string') { if (typeof specUrl === 'string') {
this.specUrl = urlResolve(href, specUrl); this.specUrl = urlResolve(href, specUrl);
@ -74,7 +81,10 @@ export class OpenAPIParser {
) { ) {
// Automatically inject Authentication section with SecurityDefinitions component // Automatically inject Authentication section with SecurityDefinitions component
const description = spec.info.description || ''; const description = spec.info.description || '';
if (!MarkdownRenderer.containsComponent(description, SECURITY_DEFINITIONS_COMPONENT_NAME)) { if (
!MarkdownRenderer.containsComponent(description, SECURITY_DEFINITIONS_COMPONENT_NAME) &&
!MarkdownRenderer.containsComponent(description, SECURITY_DEFINITIONS_JSX_NAME)
) {
const comment = buildComponentComment(SECURITY_DEFINITIONS_COMPONENT_NAME); const comment = buildComponentComment(SECURITY_DEFINITIONS_COMPONENT_NAME);
spec.info.description = appendToMdHeading(description, 'Authentication', comment); spec.info.description = appendToMdHeading(description, 'Authentication', comment);
} }
@ -102,7 +112,7 @@ export class OpenAPIParser {
}; };
/** /**
* checks if the objectt is OpenAPI reference (containts $ref property) * checks if the object is OpenAPI reference (contains $ref property)
*/ */
isRef(obj: any): obj is OpenAPIRef { isRef(obj: any): obj is OpenAPIRef {
if (!obj) { if (!obj) {
@ -112,7 +122,7 @@ export class OpenAPIParser {
} }
/** /**
* resets visited enpoints. should be run after * resets visited endpoints. should be run after
*/ */
resetVisited() { resetVisited() {
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
@ -136,7 +146,7 @@ export class OpenAPIParser {
/** /**
* Resolve given reference object or return as is if it is not a reference * Resolve given reference object or return as is if it is not a reference
* @param obj object to dereference * @param obj object to dereference
* @param forceCircular whether to dereference even if it is cirular ref * @param forceCircular whether to dereference even if it is circular ref
*/ */
deref<T extends object>(obj: OpenAPIRef | T, forceCircular: boolean = false): T { deref<T extends object>(obj: OpenAPIRef | T, forceCircular: boolean = false): T {
if (this.isRef(obj)) { if (this.isRef(obj)) {
@ -167,16 +177,21 @@ export class OpenAPIParser {
} }
/** /**
* Merge allOf contsraints. * Merge allOf constraints.
* @param schema schema with allOF * @param schema schema with allOF
* @param $ref pointer of the schema * @param $ref pointer of the schema
* @param forceCircular whether to dereference children even if it is a cirular ref * @param forceCircular whether to dereference children even if it is a circular ref
*/ */
mergeAllOf( mergeAllOf(
schema: OpenAPISchema, schema: OpenAPISchema,
$ref?: string, $ref?: string,
forceCircular: boolean = false, forceCircular: boolean = false,
used$Refs = new Set<string>(),
): MergedOpenAPISchema { ): MergedOpenAPISchema {
if ($ref) {
used$Refs.add($ref);
}
schema = this.hoistOneOfs(schema); schema = this.hoistOneOfs(schema);
if (schema.allOf === undefined) { if (schema.allOf === undefined) {
@ -198,16 +213,25 @@ export class OpenAPIParser {
receiver.items = { ...receiver.items }; receiver.items = { ...receiver.items };
} }
const allOfSchemas = schema.allOf.map(subSchema => { const allOfSchemas = schema.allOf
.map(subSchema => {
if (subSchema && subSchema.$ref && used$Refs.has(subSchema.$ref)) {
return undefined;
}
const resolved = this.deref(subSchema, forceCircular); const resolved = this.deref(subSchema, forceCircular);
const subRef = subSchema.$ref || undefined; const subRef = subSchema.$ref || undefined;
const subMerged = this.mergeAllOf(resolved, subRef, forceCircular); const subMerged = this.mergeAllOf(resolved, subRef, forceCircular, used$Refs);
receiver.parentRefs!.push(...(subMerged.parentRefs || [])); receiver.parentRefs!.push(...(subMerged.parentRefs || []));
return { return {
$ref: subRef, $ref: subRef,
schema: subMerged, schema: subMerged,
}; };
}); })
.filter(child => child !== undefined) as Array<{
$ref: string | undefined;
schema: MergedOpenAPISchema;
}>;
for (const { $ref: subSchemaRef, schema: subSchema } of allOfSchemas) { for (const { $ref: subSchemaRef, schema: subSchema } of allOfSchemas) {
if ( if (
@ -251,13 +275,13 @@ export class OpenAPIParser {
} }
// merge rest of constraints // merge rest of constraints
// TODO: do more intelegent merge // TODO: do more intelligent merge
receiver = { ...subSchema, ...receiver }; receiver = { ...subSchema, ...receiver };
if (subSchemaRef) { if (subSchemaRef) {
receiver.parentRefs!.push(subSchemaRef); receiver.parentRefs!.push(subSchemaRef);
if (receiver.title === undefined && isNamedDefinition(subSchemaRef)) { if (receiver.title === undefined && isNamedDefinition(subSchemaRef)) {
// this is not so correct behaviour. comented out for now // this is not so correct behaviour. commented out for now
// ref: https://github.com/Redocly/redoc/issues/601 // ref: https://github.com/Redocly/redoc/issues/601
// receiver.title = JsonPointer.baseName(subSchemaRef); // receiver.title = JsonPointer.baseName(subSchemaRef);
} }
@ -272,8 +296,8 @@ export class OpenAPIParser {
* returns map of definition pointer to definition name * returns map of definition pointer to definition name
* @param $refs array of references to find derived from * @param $refs array of references to find derived from
*/ */
findDerived($refs: string[]): Dict<string> { findDerived($refs: string[]): Dict<string[] | string> {
const res: Dict<string> = {}; const res: Dict<string[]> = {};
const schemas = (this.spec.components && this.spec.components.schemas) || {}; const schemas = (this.spec.components && this.spec.components.schemas) || {};
for (const defName in schemas) { for (const defName in schemas) {
const def = this.deref(schemas[defName]); const def = this.deref(schemas[defName]);
@ -281,7 +305,7 @@ export class OpenAPIParser {
def.allOf !== undefined && def.allOf !== undefined &&
def.allOf.find(obj => obj.$ref !== undefined && $refs.indexOf(obj.$ref) > -1) def.allOf.find(obj => obj.$ref !== undefined && $refs.indexOf(obj.$ref) > -1)
) { ) {
res['#/components/schemas/' + defName] = def['x-discriminator-value'] || defName; res['#/components/schemas/' + defName] = [def['x-discriminator-value'] || defName];
} }
} }
return res; return res;

View File

@ -23,20 +23,28 @@ export interface RedocRawOptions {
showExtensions?: boolean | string | string[]; showExtensions?: boolean | string | string[];
showOtherInfoPanel?: boolean; showOtherInfoPanel?: boolean;
hideSingleRequestSampleTab?: boolean | string; hideSingleRequestSampleTab?: boolean | string;
menuToggle?: boolean | string;
jsonSampleExpandLevel?: number | string | 'all';
hideSchemaTitles?: boolean | string;
payloadSampleIdx?: number;
unstable_ignoreMimeParameters?: boolean; unstable_ignoreMimeParameters?: boolean;
allowedMdComponents?: Dict<MDXComponentMeta>; allowedMdComponents?: Dict<MDXComponentMeta>;
labels?: LabelsConfigRaw; labels?: LabelsConfigRaw;
enumSkipQuotes?: boolean | string;
expandDefaultServerVariables?: boolean;
} }
function argValueToBoolean(val?: string | boolean): boolean { function argValueToBoolean(val?: string | boolean, defaultValue?: boolean): boolean {
if (val === undefined) { if (val === undefined) {
return false; return defaultValue || false;
} }
if (typeof val === 'string') { if (typeof val === 'string') {
return true; return val === 'false' ? false : true;
} }
return val; return val;
} }
@ -111,6 +119,28 @@ export class RedocNormalizedOptions {
return value; return value;
} }
static normalizePayloadSampleIdx(value: RedocRawOptions['payloadSampleIdx']): number {
if (typeof value === 'number') {
return Math.max(0, value); // always greater or equal than 0
}
if (typeof value === 'string') {
return isFinite(value) ? parseInt(value, 10) : 0;
}
return 0;
}
private static normalizeJsonSampleExpandLevel(level?: number | string | 'all'): number {
if (level === 'all') {
return +Infinity;
}
if (!isNaN(Number(level))) {
return Math.ceil(Number(level));
}
return 2;
}
theme: ResolvedThemeInterface; theme: ResolvedThemeInterface;
scrollYOffset: () => number; scrollYOffset: () => number;
hideHostname: boolean; hideHostname: boolean;
@ -127,11 +157,18 @@ export class RedocNormalizedOptions {
showExtensions: boolean | string[]; showExtensions: boolean | string[];
hideSingleRequestSampleTab: boolean; hideSingleRequestSampleTab: boolean;
showOtherInfoPanel: boolean; showOtherInfoPanel: boolean;
menuToggle: boolean;
jsonSampleExpandLevel: number;
enumSkipQuotes: boolean;
hideSchemaTitles: boolean;
payloadSampleIdx: number;
/* tslint:disable-next-line */ /* tslint:disable-next-line */
unstable_ignoreMimeParameters: boolean; unstable_ignoreMimeParameters: boolean;
allowedMdComponents: Dict<MDXComponentMeta>; allowedMdComponents: Dict<MDXComponentMeta>;
expandDefaultServerVariables: boolean;
constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) { constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) {
raw = { ...defaults, ...raw }; raw = { ...defaults, ...raw };
const hook = raw.theme && raw.theme.extensionsHook; const hook = raw.theme && raw.theme.extensionsHook;
@ -159,9 +196,18 @@ export class RedocNormalizedOptions {
this.showExtensions = RedocNormalizedOptions.normalizeShowExtensions(raw.showExtensions); this.showExtensions = RedocNormalizedOptions.normalizeShowExtensions(raw.showExtensions);
this.showOtherInfoPanel = argValueToBoolean(raw.showOtherInfoPanel); this.showOtherInfoPanel = argValueToBoolean(raw.showOtherInfoPanel);
this.hideSingleRequestSampleTab = argValueToBoolean(raw.hideSingleRequestSampleTab); this.hideSingleRequestSampleTab = argValueToBoolean(raw.hideSingleRequestSampleTab);
this.menuToggle = argValueToBoolean(raw.menuToggle, true);
this.jsonSampleExpandLevel = RedocNormalizedOptions.normalizeJsonSampleExpandLevel(
raw.jsonSampleExpandLevel,
);
this.enumSkipQuotes = argValueToBoolean(raw.enumSkipQuotes);
this.hideSchemaTitles = argValueToBoolean(raw.hideSchemaTitles);
this.payloadSampleIdx = RedocNormalizedOptions.normalizePayloadSampleIdx(raw.payloadSampleIdx);
this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters); this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters);
this.allowedMdComponents = raw.allowedMdComponents || {}; this.allowedMdComponents = raw.allowedMdComponents || {};
this.expandDefaultServerVariables = argValueToBoolean(raw.expandDefaultServerVariables);
} }
} }

View File

@ -38,6 +38,10 @@ export class SearchStore<T> {
this.searchWorker.add(title, body, meta); this.searchWorker.add(title, body, meta);
} }
dispose() {
(this.searchWorker as any).terminate();
}
search(q: string) { search(q: string) {
return this.searchWorker.search<T>(q); return this.searchWorker.search<T>(q);
} }

View File

@ -2,7 +2,7 @@ import * as lunr from 'lunr';
try { try {
// tslint:disable-next-line // tslint:disable-next-line
require('core-js/es6/promise'); // bundle into worker require('core-js/es/promise'); // bundle into worker
} catch (_) { } catch (_) {
// nope // nope
} }

View File

@ -6,7 +6,7 @@ import { SecuritySchemesModel } from './models/SecuritySchemes';
import { OpenAPIParser } from './OpenAPIParser'; import { OpenAPIParser } from './OpenAPIParser';
import { RedocNormalizedOptions } from './RedocNormalizedOptions'; import { RedocNormalizedOptions } from './RedocNormalizedOptions';
/** /**
* Store that containts all the specification related information in the form of tree * Store that contains all the specification related information in the form of tree
*/ */
export class SpecStore { export class SpecStore {
parser: OpenAPIParser; parser: OpenAPIParser;

View File

@ -10,6 +10,13 @@
"in": "path", "in": "path",
"name": "test_name", "name": "test_name",
"schema": { "type": "string" } "schema": { "type": "string" }
},
"serializationParam": {
"in": "query",
"name": "serialization_test_name",
"schema": { "type": "array" },
"style": "form",
"explode": true
} }
}, },
"headers": { "headers": {

View File

@ -13,7 +13,7 @@ describe('History service', () => {
expect(fn).toHaveBeenCalled(); expect(fn).toHaveBeenCalled();
}); });
test('History subscribe should return unsubsribe function', () => { test('History subscribe should return unsubscribe function', () => {
const fn = jest.fn(); const fn = jest.fn();
const unsubscribe = history.subscribe(fn); const unsubscribe = history.subscribe(fn);
history.emit(); history.emit();

View File

@ -26,6 +26,23 @@ describe('Models', () => {
expect(field.schema.type).toEqual('string'); expect(field.schema.type).toEqual('string');
}); });
test('field details relevant for parameter serialization', () => {
const field = new FieldModel(
parser,
{
$ref: '#/components/parameters/serializationParam',
},
'#/components/parameters/serializationParam',
opts,
);
expect(field.name).toEqual('serialization_test_name');
expect(field.in).toEqual('query');
expect(field.schema.type).toEqual('array');
expect(field.style).toEqual('form');
expect(field.explode).toEqual(true);
});
test('field name should populated from name even if $ref (headers)', () => { test('field name should populated from name even if $ref (headers)', () => {
const field = new FieldModel( const field = new FieldModel(
parser, parser,

View File

@ -22,7 +22,7 @@ describe('Models', () => {
expect(resp.type).toEqual('error'); expect(resp.type).toEqual('error');
}); });
test('default should be sucessful by default', () => { test('default should be successful by default', () => {
const resp = new ResponseModel(parser, 'default', false, {}, opts); const resp = new ResponseModel(parser, 'default', false, {}, opts);
expect(resp.type).toEqual('success'); expect(resp.type).toEqual('success');
}); });

View File

@ -17,6 +17,7 @@ export class ApiInfoModel implements OpenAPIInfo {
constructor(private parser: OpenAPIParser) { constructor(private parser: OpenAPIParser) {
Object.assign(this, parser.spec.info); Object.assign(this, parser.spec.info);
this.description = parser.spec.info.description || ''; this.description = parser.spec.info.description || '';
const firstHeadingLinePos = this.description.search(/^##?\s+/m); const firstHeadingLinePos = this.description.search(/^##?\s+/m);
if (firstHeadingLinePos > -1) { if (firstHeadingLinePos > -1) {
this.description = this.description.substring(0, firstHeadingLinePos); this.description = this.description.substring(0, firstHeadingLinePos);

View File

@ -1,12 +1,30 @@
import { action, observable } from 'mobx'; import { action, observable } from 'mobx';
import { OpenAPIParameter, Referenced } from '../../types'; import {
OpenAPIParameter,
OpenAPIParameterLocation,
OpenAPIParameterStyle,
Referenced,
} from '../../types';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { extractExtensions } from '../../utils/openapi'; import { extractExtensions } from '../../utils/openapi';
import { OpenAPIParser } from '../OpenAPIParser'; import { OpenAPIParser } from '../OpenAPIParser';
import { SchemaModel } from './Schema'; import { SchemaModel } from './Schema';
function getDefaultStyleValue(parameterLocation: OpenAPIParameterLocation): OpenAPIParameterStyle {
switch (parameterLocation) {
case 'header':
return 'simple';
case 'query':
return 'form';
case 'path':
return 'simple';
default:
return 'form';
}
}
/** /**
* Field or Parameter model ready to be used by components * Field or Parameter model ready to be used by components
*/ */
@ -20,9 +38,13 @@ export class FieldModel {
description: string; description: string;
example?: string; example?: string;
deprecated: boolean; deprecated: boolean;
in?: string; in?: OpenAPIParameterLocation;
kind: string; kind: string;
extensions?: Dict<any>; extensions?: Dict<any>;
explode: boolean;
style?: OpenAPIParameterStyle;
serializationMime?: string;
constructor( constructor(
parser: OpenAPIParser, parser: OpenAPIParser,
@ -35,11 +57,29 @@ export class FieldModel {
this.name = infoOrRef.name || info.name; this.name = infoOrRef.name || info.name;
this.in = info.in; this.in = info.in;
this.required = !!info.required; this.required = !!info.required;
this.schema = new SchemaModel(parser, info.schema || {}, pointer, options);
let fieldSchema = info.schema;
let serializationMime = '';
if (!fieldSchema && info.in && info.content) {
serializationMime = Object.keys(info.content)[0];
fieldSchema = info.content[serializationMime] && info.content[serializationMime].schema;
}
this.schema = new SchemaModel(parser, fieldSchema || {}, pointer, options);
this.description = this.description =
info.description === undefined ? this.schema.description || '' : info.description; info.description === undefined ? this.schema.description || '' : info.description;
this.example = info.example || this.schema.example; this.example = info.example || this.schema.example;
if (serializationMime) {
this.serializationMime = serializationMime;
} else if (info.style) {
this.style = info.style;
} else if (this.in) {
this.style = getDefaultStyleValue(this.in);
}
this.explode = !!info.explode;
this.deprecated = info.deprecated === undefined ? !!this.schema.deprecated : info.deprecated; this.deprecated = info.deprecated === undefined ? !!this.schema.deprecated : info.deprecated;
parser.exitRef(infoOrRef); parser.exitRef(infoOrRef);

View File

@ -2,7 +2,7 @@ import { action, observable } from 'mobx';
import { OpenAPIExternalDocumentation, OpenAPITag } from '../../types'; import { OpenAPIExternalDocumentation, OpenAPITag } from '../../types';
import { safeSlugify } from '../../utils'; import { safeSlugify } from '../../utils';
import { MarkdownHeading } from '../MarkdownRenderer'; import { MarkdownHeading, MarkdownRenderer } from '../MarkdownRenderer';
import { ContentItemModel } from '../MenuBuilder'; import { ContentItemModel } from '../MenuBuilder';
import { IMenuItem, MenuItemGroupType } from '../MenuStore'; import { IMenuItem, MenuItemGroupType } from '../MenuStore';
@ -40,7 +40,15 @@ export class GroupModel implements IMenuItem {
this.type = type; this.type = type;
this.name = tagOrGroup['x-displayName'] || tagOrGroup.name; this.name = tagOrGroup['x-displayName'] || tagOrGroup.name;
this.level = (tagOrGroup as MarkdownHeading).level || 1; this.level = (tagOrGroup as MarkdownHeading).level || 1;
// remove sections from markdown, same as in ApiInfo
this.description = tagOrGroup.description || ''; this.description = tagOrGroup.description || '';
const items = (tagOrGroup as MarkdownHeading).items;
if (items && items.length) {
this.description = MarkdownRenderer.getTextBeforeHading(this.description, items[0].name);
}
this.parent = parent; this.parent = parent;
this.externalDocs = (tagOrGroup as OpenAPITag).externalDocs; this.externalDocs = (tagOrGroup as OpenAPITag).externalDocs;

View File

@ -21,7 +21,7 @@ export class MediaContentModel {
* @param isRequestType needed to know if skipe RO/RW fields in objects * @param isRequestType needed to know if skipe RO/RW fields in objects
*/ */
constructor( constructor(
public parser: OpenAPIParser, parser: OpenAPIParser,
info: Dict<OpenAPIMediaType>, info: Dict<OpenAPIMediaType>,
public isRequestType: boolean, public isRequestType: boolean,
options: RedocNormalizedOptions, options: RedocNormalizedOptions,

View File

@ -27,9 +27,23 @@ import { ContentItemModel, ExtendedOpenAPIOperation } from '../MenuBuilder';
import { OpenAPIParser } from '../OpenAPIParser'; import { OpenAPIParser } from '../OpenAPIParser';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { FieldModel } from './Field'; import { FieldModel } from './Field';
import { MediaContentModel } from './MediaContent';
import { RequestBodyModel } from './RequestBody'; import { RequestBodyModel } from './RequestBody';
import { ResponseModel } from './Response'; import { ResponseModel } from './Response';
interface XPayloadSample {
lang: 'payload';
label: string;
requestBodyContent: MediaContentModel;
source: string;
}
export function isPayloadSample(
sample: XPayloadSample | OpenAPIXCodeSample,
): sample is XPayloadSample {
return sample.lang === 'payload' && (sample as any).requestBodyContent;
}
/** /**
* Operation model ready to be used by components * Operation model ready to be used by components
*/ */
@ -62,7 +76,6 @@ export class OperationModel implements IMenuItem {
path: string; path: string;
servers: OpenAPIServer[]; servers: OpenAPIServer[];
security: SecurityRequirementModel[]; security: SecurityRequirementModel[];
codeSamples: OpenAPIXCodeSample[];
extensions: Dict<any>; extensions: Dict<any>;
constructor( constructor(
@ -89,7 +102,6 @@ export class OperationModel implements IMenuItem {
this.httpVerb = operationSpec.httpVerb; this.httpVerb = operationSpec.httpVerb;
this.deprecated = !!operationSpec.deprecated; this.deprecated = !!operationSpec.deprecated;
this.operationId = operationSpec.operationId; this.operationId = operationSpec.operationId;
this.codeSamples = operationSpec['x-code-samples'] || [];
this.path = operationSpec.pathName; this.path = operationSpec.pathName;
const pathInfo = parser.byRef<OpenAPIPath>( const pathInfo = parser.byRef<OpenAPIPath>(
@ -144,6 +156,30 @@ export class OperationModel implements IMenuItem {
); );
} }
@memoize
get codeSamples() {
let samples: Array<OpenAPIXCodeSample | XPayloadSample> =
this.operationSpec['x-code-samples'] || [];
const requestBodyContent = this.requestBody && this.requestBody.content;
if (requestBodyContent && requestBodyContent.hasSample) {
const insertInx = Math.min(samples.length, this.options.payloadSampleIdx);
samples = [
...samples.slice(0, insertInx),
{
lang: 'payload',
label: 'Payload',
source: '',
requestBodyContent,
},
...samples.slice(insertInx),
];
}
return samples;
}
@memoize @memoize
get parameters() { get parameters() {
const _parameters = mergeParams( const _parameters = mergeParams(
@ -154,11 +190,12 @@ export class OperationModel implements IMenuItem {
).map(paramOrRef => new FieldModel(this.parser, paramOrRef, this.pointer, this.options)); ).map(paramOrRef => new FieldModel(this.parser, paramOrRef, this.pointer, this.options));
if (this.options.sortPropsAlphabetically) { if (this.options.sortPropsAlphabetically) {
sortByField(_parameters, 'name'); return sortByField(_parameters, 'name');
} }
if (this.options.requiredPropsFirst) { if (this.options.requiredPropsFirst) {
sortByRequired(_parameters); return sortByRequired(_parameters);
} }
return _parameters; return _parameters;
} }

View File

@ -75,6 +75,7 @@ export class SchemaModel {
this.pointer = schemaOrRef.$ref || pointer || ''; this.pointer = schemaOrRef.$ref || pointer || '';
this.rawSchema = parser.deref(schemaOrRef); this.rawSchema = parser.deref(schemaOrRef);
this.schema = parser.mergeAllOf(this.rawSchema, this.pointer, isChild); this.schema = parser.mergeAllOf(this.rawSchema, this.pointer, isChild);
this.init(parser, isChild); this.init(parser, isChild);
parser.exitRef(schemaOrRef); parser.exitRef(schemaOrRef);
@ -125,6 +126,13 @@ export class SchemaModel {
if (!isChild && getDiscriminator(schema) !== undefined) { if (!isChild && getDiscriminator(schema) !== undefined) {
this.initDiscriminator(schema, parser); this.initDiscriminator(schema, parser);
return; return;
} else if (
isChild &&
Array.isArray(schema.oneOf) &&
schema.oneOf.find(s => s.$ref === this.pointer)
) {
// we hit allOf of the schema with the parent discriminator
delete schema.oneOf;
} }
if (schema.oneOf !== undefined) { if (schema.oneOf !== undefined) {
@ -216,7 +224,10 @@ export class SchemaModel {
) { ) {
const discriminator = getDiscriminator(schema)!; const discriminator = getDiscriminator(schema)!;
this.discriminatorProp = discriminator.propertyName; this.discriminatorProp = discriminator.propertyName;
const derived = parser.findDerived([...(schema.parentRefs || []), this.pointer]); const implicitInversedMapping = parser.findDerived([
...(schema.parentRefs || []),
this.pointer,
]);
if (schema.oneOf) { if (schema.oneOf) {
for (const variant of schema.oneOf) { for (const variant of schema.oneOf) {
@ -224,19 +235,41 @@ export class SchemaModel {
continue; continue;
} }
const name = JsonPointer.baseName(variant.$ref); const name = JsonPointer.baseName(variant.$ref);
derived[variant.$ref] = name; implicitInversedMapping[variant.$ref] = name;
} }
} }
const mapping = discriminator.mapping || {}; const mapping = discriminator.mapping || {};
const explicitInversedMapping = {};
for (const name in mapping) { for (const name in mapping) {
derived[mapping[name]] = name; const $ref = mapping[name];
if (Array.isArray(explicitInversedMapping[$ref])) {
explicitInversedMapping[$ref].push(name);
} else {
// overrides implicit mapping here
explicitInversedMapping[$ref] = [name];
}
} }
const refs = Object.keys(derived); const inversedMapping = { ...implicitInversedMapping, ...explicitInversedMapping };
this.oneOf = refs.map(ref => {
const innerSchema = new SchemaModel(parser, parser.byRef(ref)!, ref, this.options, true); const refs: Array<{ $ref; name }> = [];
innerSchema.title = derived[ref];
for (const $ref of Object.keys(inversedMapping)) {
const names = inversedMapping[$ref];
if (Array.isArray(names)) {
for (const name of names) {
refs.push({ $ref, name });
}
} else {
refs.push({ $ref, name: names });
}
}
this.oneOf = refs.map(({ $ref, name }) => {
const innerSchema = new SchemaModel(parser, parser.byRef($ref)!, $ref, this.options, true);
innerSchema.title = name;
return innerSchema; return innerSchema;
}); });
} }
@ -251,7 +284,7 @@ function buildFields(
const props = schema.properties || {}; const props = schema.properties || {};
const additionalProps = schema.additionalProperties; const additionalProps = schema.additionalProperties;
const defaults = schema.default || {}; const defaults = schema.default || {};
const fields = Object.keys(props || []).map(fieldName => { let fields = Object.keys(props || []).map(fieldName => {
let field = props[fieldName]; let field = props[fieldName];
if (!field) { if (!field) {
@ -280,11 +313,11 @@ function buildFields(
}); });
if (options.sortPropsAlphabetically) { if (options.sortPropsAlphabetically) {
sortByField(fields, 'name'); fields = sortByField(fields, 'name');
} }
if (options.requiredPropsFirst) { if (options.requiredPropsFirst) {
// if not sort alphabetically sort in the order from required keyword // if not sort alphabetically sort in the order from required keyword
sortByRequired(fields, !options.sortPropsAlphabetically ? schema.required : undefined); fields = sortByRequired(fields, !options.sortPropsAlphabetically ? schema.required : undefined);
} }
if (typeof additionalProps === 'object' || additionalProps === true) { if (typeof additionalProps === 'object' || additionalProps === true) {

View File

@ -60,7 +60,7 @@ describe('Utils', () => {
test('should behave like Object.assign on the top level', () => { test('should behave like Object.assign on the top level', () => {
const obj1 = { a: { a1: 'A1' }, c: 'C' }; const obj1 = { a: { a1: 'A1' }, c: 'C' };
const obj2 = { a: undefined, b: { b1: 'B1' } }; const obj2 = { a: undefined, b: { b1: 'B1' } };
expect(mergeObjects({}, obj1, obj2)).toEqual(Object.assign({}, obj1, obj2)); expect(mergeObjects({}, obj1, obj2)).toEqual({ ...obj1, ...obj2 });
}); });
test('should not merge array values, just override', () => { test('should not merge array values, just override', () => {
const obj1 = { a: ['A', 'B'] }; const obj1 = { a: ['A', 'B'] };

View File

@ -8,10 +8,13 @@ import {
mergeParams, mergeParams,
normalizeServers, normalizeServers,
pluralizeType, pluralizeType,
serializeParameterValue,
sortByRequired,
} from '../'; } from '../';
import { OpenAPIParser } from '../../services'; import { FieldModel, OpenAPIParser, RedocNormalizedOptions } from '../../services';
import { OpenAPIParameter } from '../../types'; import { OpenAPIParameter, OpenAPIParameterLocation, OpenAPIParameterStyle } from '../../types';
import { expandDefaultServerVariables } from '../openapi';
describe('Utils', () => { describe('Utils', () => {
describe('openapi getStatusCode', () => { describe('openapi getStatusCode', () => {
@ -247,7 +250,7 @@ describe('Utils', () => {
expect(res).toEqual([{ url: 'http://base.com/sandbox/test', description: '' }]); expect(res).toEqual([{ url: 'http://base.com/sandbox/test', description: '' }]);
}); });
it('should correcly resolve url with server relative path', () => { it('should correctly resolve url with server relative path', () => {
const res = normalizeServers('http://base.com/subpath/spec.yaml', [ const res = normalizeServers('http://base.com/subpath/spec.yaml', [
{ {
url: '/sandbox/test', url: '/sandbox/test',
@ -256,7 +259,7 @@ describe('Utils', () => {
expect(res).toEqual([{ url: 'http://base.com/sandbox/test', description: '' }]); expect(res).toEqual([{ url: 'http://base.com/sandbox/test', description: '' }]);
}); });
it('should correcly resolve url with relative path', () => { it('should correctly resolve url with relative path', () => {
const res = normalizeServers('http://base.com/subpath/spec.yaml', [ const res = normalizeServers('http://base.com/subpath/spec.yaml', [
{ {
url: 'sandbox/test', url: 'sandbox/test',
@ -296,11 +299,8 @@ describe('Utils', () => {
it('should expand variables', () => { it('should expand variables', () => {
const servers = normalizeServers('', [ const servers = normalizeServers('', [
{ {
url: '{protocol}{host}{basePath}', url: 'http://{host}{basePath}',
variables: { variables: {
protocol: {
default: 'http://',
},
host: { host: {
default: '127.0.0.1', default: '127.0.0.1',
}, },
@ -318,9 +318,15 @@ describe('Utils', () => {
}, },
]); ]);
expect(servers[0].url).toEqual('http://127.0.0.1/path/to/endpoint'); expect(expandDefaultServerVariables(servers[0].url, servers[0].variables)).toEqual(
expect(servers[1].url).toEqual('http://127.0.0.2:{port}'); 'http://127.0.0.1/path/to/endpoint',
expect(servers[2].url).toEqual('http://127.0.0.3'); );
expect(expandDefaultServerVariables(servers[1].url, servers[1].variables)).toEqual(
'http://127.0.0.2:{port}',
);
expect(expandDefaultServerVariables(servers[2].url, servers[2].variables)).toEqual(
'http://127.0.0.3',
);
}); });
}); });
@ -328,7 +334,8 @@ describe('Utils', () => {
const itemConstraintSchema = ( const itemConstraintSchema = (
min: number | undefined = undefined, min: number | undefined = undefined,
max: number | undefined = undefined, max: number | undefined = undefined,
) => ({ type: 'array', minItems: min, maxItems: max }); multipleOf: number | undefined = undefined,
) => ({ type: 'array', minItems: min, maxItems: max, multipleOf });
it('should not have a humanized constraint without schema constraints', () => { it('should not have a humanized constraint without schema constraints', () => {
expect(humanizeConstraints(itemConstraintSchema())).toHaveLength(0); expect(humanizeConstraints(itemConstraintSchema())).toHaveLength(0);
@ -350,9 +357,21 @@ describe('Utils', () => {
expect(humanizeConstraints(itemConstraintSchema(7, 7))).toContain('7 items'); expect(humanizeConstraints(itemConstraintSchema(7, 7))).toContain('7 items');
}); });
it('should have a humazined constraint when justMinItems is set, and it is equal to 1', () => { it('should have a humanized constraint when justMinItems is set, and it is equal to 1', () => {
expect(humanizeConstraints(itemConstraintSchema(1))).toContain('non-empty'); expect(humanizeConstraints(itemConstraintSchema(1))).toContain('non-empty');
}); });
it('should have a humanized constraint when multipleOf is set, and it is in format of /^0\\.0*1$/', () => {
expect(humanizeConstraints(itemConstraintSchema(undefined, undefined, 0.01))).toContain(
'decimal places <= 2',
);
});
it('should have a humanized constraint when multipleOf is set, and it is in format other than /^0\\.0*1$/', () => {
expect(humanizeConstraints(itemConstraintSchema(undefined, undefined, 0.5))).toContain(
'multiple of 0.5',
);
});
}); });
describe('OpenAPI pluralizeType', () => { describe('OpenAPI pluralizeType', () => {
@ -365,16 +384,623 @@ describe('Utils', () => {
expect(pluralizeType('array')).toEqual('arrays'); expect(pluralizeType('array')).toEqual('arrays');
}); });
it('should pluralize complex dislay types', () => { it('should pluralize complex display types', () => {
expect(pluralizeType('object (Pet)')).toEqual('objects (Pet)'); expect(pluralizeType('object (Pet)')).toEqual('objects (Pet)');
expect(pluralizeType('string <email>')).toEqual('strings <email>'); expect(pluralizeType('string <email>')).toEqual('strings <email>');
}); });
it('should pluralize oneOf-ed dislay types', () => { it('should pluralize oneOf-ed display types', () => {
expect(pluralizeType('object or string')).toEqual('objects or strings'); expect(pluralizeType('object or string')).toEqual('objects or strings');
expect(pluralizeType('object (Pet) or number <int64>')).toEqual( expect(pluralizeType('object (Pet) or number <int64>')).toEqual(
'objects (Pet) or numbers <int64>', 'objects (Pet) or numbers <int64>',
); );
}); });
it('should not pluralize display types that are already pluralized', () => {
expect(pluralizeType('strings')).toEqual('strings');
expect(pluralizeType('objects (Pet)')).toEqual('objects (Pet)');
expect(pluralizeType('strings <email>')).toEqual('strings <email>');
expect(pluralizeType('objects or strings')).toEqual('objects or strings');
expect(pluralizeType('objects (Pet) or numbers <int64>')).toEqual(
'objects (Pet) or numbers <int64>',
);
});
});
describe('openapi serializeParameter', () => {
interface TestCase {
style: OpenAPIParameterStyle;
explode: boolean;
expected: string;
}
interface TestValueTypeGroup {
value: any;
description: string;
cases: TestCase[];
}
interface TestLocationGroup {
location: OpenAPIParameterLocation;
name: string;
description: string;
cases: TestValueTypeGroup[];
}
const testCases: TestLocationGroup[] = [
{
location: 'path',
name: 'id',
description: 'path parameters',
cases: [
{
value: 5,
description: 'primitive values',
cases: [
{ style: 'simple', explode: false, expected: '5' },
{ style: 'simple', explode: true, expected: '5' },
{ style: 'label', explode: false, expected: '.5' },
{ style: 'label', explode: true, expected: '.5' },
{ style: 'matrix', explode: false, expected: ';id=5' },
{ style: 'matrix', explode: true, expected: ';id=5' },
],
},
{
value: [3, 4, 5],
description: 'array values',
cases: [
{ style: 'simple', explode: false, expected: '3,4,5' },
{ style: 'simple', explode: true, expected: '3,4,5' },
{ style: 'label', explode: false, expected: '.3,4,5' },
{ style: 'label', explode: true, expected: '.3.4.5' },
{ style: 'matrix', explode: false, expected: ';id=3,4,5' },
{ style: 'matrix', explode: true, expected: ';id=3;id=4;id=5' },
],
},
{
value: { role: 'admin', firstName: 'Alex' },
description: 'object values',
cases: [
{ style: 'simple', explode: false, expected: 'role,admin,firstName,Alex' },
{ style: 'simple', explode: true, expected: 'role=admin,firstName=Alex' },
{ style: 'label', explode: false, expected: '.role,admin,firstName,Alex' },
{ style: 'label', explode: true, expected: '.role=admin.firstName=Alex' },
{ style: 'matrix', explode: false, expected: ';id=role,admin,firstName,Alex' },
{ style: 'matrix', explode: true, expected: ';role=admin;firstName=Alex' },
],
},
],
},
{
location: 'query',
name: 'id',
description: 'query parameters',
cases: [
{
value: 5,
description: 'primitive values',
cases: [
{ style: 'form', explode: true, expected: 'id=5' },
{ style: 'form', explode: false, expected: 'id=5' },
],
},
{
value: [3, 4, 5],
description: 'array values',
cases: [
{ style: 'form', explode: true, expected: 'id=3&id=4&id=5' },
{ style: 'form', explode: false, expected: 'id=3,4,5' },
{ style: 'spaceDelimited', explode: true, expected: 'id=3&id=4&id=5' },
{ style: 'spaceDelimited', explode: false, expected: 'id=3%204%205' },
{ style: 'pipeDelimited', explode: true, expected: 'id=3&id=4&id=5' },
{ style: 'pipeDelimited', explode: false, expected: 'id=3|4|5' },
],
},
{
value: { role: 'admin', firstName: 'Alex' },
description: 'object values',
cases: [
{ style: 'form', explode: true, expected: 'role=admin&firstName=Alex' },
{ style: 'form', explode: false, expected: 'id=role,admin,firstName,Alex' },
{ style: 'deepObject', explode: true, expected: 'id[role]=admin&id[firstName]=Alex' },
],
},
],
},
{
location: 'cookie',
name: 'id',
description: 'cookie parameters',
cases: [
{
value: 5,
description: 'primitive values',
cases: [
{ style: 'form', explode: true, expected: 'id=5' },
{ style: 'form', explode: false, expected: 'id=5' },
],
},
{
value: [3, 4, 5],
description: 'array values',
cases: [
{ style: 'form', explode: true, expected: 'id=3&id=4&id=5' },
{ style: 'form', explode: false, expected: 'id=3,4,5' },
],
},
{
value: { role: 'admin', firstName: 'Alex' },
description: 'object values',
cases: [
{ style: 'form', explode: true, expected: 'role=admin&firstName=Alex' },
{ style: 'form', explode: false, expected: 'id=role,admin,firstName,Alex' },
],
},
],
},
{
location: 'header',
name: 'x-id',
description: 'header parameters',
cases: [
{
value: 5,
description: 'primitive values',
cases: [
{ style: 'simple', explode: false, expected: '5' },
{ style: 'simple', explode: true, expected: '5' },
],
},
{
value: [3, 4, 5],
description: 'array values',
cases: [
{ style: 'simple', explode: false, expected: '3,4,5' },
{ style: 'simple', explode: true, expected: '3,4,5' },
],
},
{
value: { role: 'admin', firstName: 'Alex' },
description: 'object values',
cases: [
{ style: 'simple', explode: false, expected: 'role,admin,firstName,Alex' },
{ style: 'simple', explode: true, expected: 'role=admin,firstName=Alex' },
],
},
],
},
];
testCases.forEach(locationTestGroup => {
describe(locationTestGroup.description, () => {
locationTestGroup.cases.forEach(valueTypeTestGroup => {
describe(valueTypeTestGroup.description, () => {
valueTypeTestGroup.cases.forEach(testCase => {
it(`should serialize correctly when style is ${testCase.style} and explode is ${testCase.explode}`, () => {
const parameter: OpenAPIParameter = {
name: locationTestGroup.name,
in: locationTestGroup.location,
style: testCase.style,
explode: testCase.explode,
};
const serialized = serializeParameterValue(parameter, valueTypeTestGroup.value);
expect(serialized).toEqual(testCase.expected);
});
});
});
});
});
});
describe('advanced serialization', () => {
it('should serialize correctly query parameter with content with application/json', () => {
const parameter: OpenAPIParameter = {
name: 'id',
in: 'query',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
};
const parser = new OpenAPIParser({ openapi: '3.0' } as any);
const opts = new RedocNormalizedOptions({});
const field = new FieldModel(parser, parameter, '', opts);
expect(serializeParameterValue(field, { name: 'test', age: 23 })).toEqual(
'id={"name":"test","age":23}',
);
});
it('should serialize correctly header parameter with content with application/json', () => {
const parameter: OpenAPIParameter = {
name: 'x-header',
in: 'header',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
};
const parser = new OpenAPIParser({ openapi: '3.0' } as any);
const opts = new RedocNormalizedOptions({});
const field = new FieldModel(parser, parameter, '', opts);
expect(serializeParameterValue(field, { name: 'test', age: 23 })).toEqual(
'{"name":"test","age":23}',
);
});
});
});
describe('OpenAPI sortByRequired', () => {
it('should equal to the old data when all items have no required props', () => {
let fields = [
{
name: 'loginName',
required: false,
},
{
name: 'displayName',
required: false,
},
{
name: 'email',
required: false,
},
{
name: 'space',
required: false,
},
{
name: 'type',
required: false,
},
{
name: 'depIds',
required: false,
},
{
name: 'depNames',
required: false,
},
{
name: 'password',
required: false,
},
{
name: 'pwdControl',
required: false,
},
{
name: 'csfLevel',
required: false,
},
{
name: 'priority',
required: false,
},
{
name: 'siteId',
required: false,
},
];
expect(sortByRequired(fields as FieldModel[])).toEqual(fields);
});
it('other item should be the same order when some of items are required', () => {
let fields = [
{
name: 'loginName',
required: true,
},
{
name: 'displayName',
required: false,
},
{
name: 'email',
required: true,
},
{
name: 'space',
required: false,
},
{
name: 'type',
required: false,
},
{
name: 'depIds',
required: false,
},
{
name: 'depNames',
required: false,
},
{
name: 'password',
required: false,
},
{
name: 'pwdControl',
required: false,
},
{
name: 'csfLevel',
required: false,
},
{
name: 'priority',
required: false,
},
{
name: 'siteId',
required: false,
},
];
let sortedFields = [
{
name: 'loginName',
required: true,
},
{
name: 'email',
required: true,
},
{
name: 'displayName',
required: false,
},
{
name: 'space',
required: false,
},
{
name: 'type',
required: false,
},
{
name: 'depIds',
required: false,
},
{
name: 'depNames',
required: false,
},
{
name: 'password',
required: false,
},
{
name: 'pwdControl',
required: false,
},
{
name: 'csfLevel',
required: false,
},
{
name: 'priority',
required: false,
},
{
name: 'siteId',
required: false,
},
];
expect(sortByRequired(fields as FieldModel[])).toEqual(sortedFields);
});
it('should the order of required items is as same as the order parameter ', () => {
let fields = [
{
name: 'loginName',
required: true,
},
{
name: 'displayName',
required: true,
},
{
name: 'email',
required: true,
},
{
name: 'space',
required: false,
},
{
name: 'type',
required: false,
},
{
name: 'depIds',
required: false,
},
{
name: 'depNames',
required: false,
},
{
name: 'password',
required: false,
},
{
name: 'pwdControl',
required: false,
},
{
name: 'csfLevel',
required: false,
},
{
name: 'priority',
required: false,
},
{
name: 'siteId',
required: false,
},
];
expect(
sortByRequired(fields as FieldModel[], ['siteId', 'displayName', 'loginName', 'email']),
).toEqual([
{
name: 'displayName',
required: true,
},
{
name: 'loginName',
required: true,
},
{
name: 'email',
required: true,
},
{
name: 'space',
required: false,
},
{
name: 'type',
required: false,
},
{
name: 'depIds',
required: false,
},
{
name: 'depNames',
required: false,
},
{
name: 'password',
required: false,
},
{
name: 'pwdControl',
required: false,
},
{
name: 'csfLevel',
required: false,
},
{
name: 'priority',
required: false,
},
{
name: 'siteId',
required: false,
},
]);
expect(sortByRequired(fields as FieldModel[], ['email', 'displayName'])).toEqual([
{
name: 'email',
required: true,
},
{
name: 'displayName',
required: true,
},
{
name: 'loginName',
required: true,
},
{
name: 'space',
required: false,
},
{
name: 'type',
required: false,
},
{
name: 'depIds',
required: false,
},
{
name: 'depNames',
required: false,
},
{
name: 'password',
required: false,
},
{
name: 'pwdControl',
required: false,
},
{
name: 'csfLevel',
required: false,
},
{
name: 'priority',
required: false,
},
{
name: 'siteId',
required: false,
},
]);
expect(sortByRequired(fields as FieldModel[], ['displayName'])).toEqual([
{
name: 'displayName',
required: true,
},
{
name: 'loginName',
required: true,
},
{
name: 'email',
required: true,
},
{
name: 'space',
required: false,
},
{
name: 'type',
required: false,
},
{
name: 'depIds',
required: false,
},
{
name: 'depNames',
required: false,
},
{
name: 'password',
required: false,
},
{
name: 'pwdControl',
required: false,
},
{
name: 'csfLevel',
required: false,
},
{
name: 'priority',
required: false,
},
{
name: 'siteId',
required: false,
},
]);
});
}); });
}); });

View File

@ -2,7 +2,7 @@ import slugify from 'slugify';
import { format, parse } from 'url'; import { format, parse } from 'url';
/** /**
* Maps over array passing `isLast` bool to iterator as the second arguemnt * Maps over array passing `isLast` bool to iterator as the second argument
*/ */
export function mapWithLast<T, P>(array: T[], iteratee: (item: T, isLast: boolean) => P) { export function mapWithLast<T, P>(array: T[], iteratee: (item: T, isLast: boolean) => P) {
const res: P[] = []; const res: P[] = [];
@ -83,7 +83,7 @@ export function appendToMdHeading(md: string, heading: string, content: string)
} }
// credits https://stackoverflow.com/a/46973278/1749888 // credits https://stackoverflow.com/a/46973278/1749888
export const mergeObjects = <T extends object = object>(target: T, ...sources: T[]): T => { export const mergeObjects = (target: any, ...sources: any[]): any => {
if (!sources.length) { if (!sources.length) {
return target; return target;
} }
@ -118,7 +118,7 @@ const isMergebleObject = (item): boolean => {
/** /**
* slugify() returns empty string when failed to slugify. * slugify() returns empty string when failed to slugify.
* so try to return minimun slugified-string with failed one which keeps original value * so try to return minimum slugified-string with failed one which keeps original value
* the regex codes are referenced with https://gist.github.com/mathewbyrne/1280286 * the regex codes are referenced with https://gist.github.com/mathewbyrne/1280286
*/ */
export function safeSlugify(value: string): string { export function safeSlugify(value: string): string {
@ -194,3 +194,7 @@ function parseURL(url: string) {
return new URL(url); return new URL(url);
} }
} }
export function unescapeHTMLChars(str: string): string {
return str.replace(/&#(\d+);/g, (_m, code) => String.fromCharCode(parseInt(code, 10)));
}

View File

@ -6,6 +6,7 @@ import 'prismjs/components/prism-coffeescript.js';
import 'prismjs/components/prism-cpp.js'; import 'prismjs/components/prism-cpp.js';
import 'prismjs/components/prism-csharp.js'; import 'prismjs/components/prism-csharp.js';
import 'prismjs/components/prism-go.js'; import 'prismjs/components/prism-go.js';
import 'prismjs/components/prism-http.js';
import 'prismjs/components/prism-java.js'; import 'prismjs/components/prism-java.js';
import 'prismjs/components/prism-lua.js'; import 'prismjs/components/prism-lua.js';
import 'prismjs/components/prism-markup-templating.js'; // dep of php import 'prismjs/components/prism-markup-templating.js'; // dep of php
@ -65,7 +66,7 @@ export function mapLang(lang: string): string {
* Highlight source code string using Prism.js * Highlight source code string using Prism.js
* @param source source code to highlight * @param source source code to highlight
* @param lang highlight language * @param lang highlight language
* @return highlighted souce code as **html string** * @return highlighted source code as **html string**
*/ */
export function highlight(source: string, lang: string = DEFAULT_LANG): string { export function highlight(source: string, lang: string = DEFAULT_LANG): string {
lang = lang.toLowerCase(); lang = lang.toLowerCase();
@ -73,5 +74,5 @@ export function highlight(source: string, lang: string = DEFAULT_LANG): string {
if (!grammar) { if (!grammar) {
grammar = Prism.languages[mapLang(lang)]; grammar = Prism.languages[mapLang(lang)];
} }
return Prism.highlight(source, grammar); return Prism.highlight(source, grammar, lang);
} }

View File

@ -1,11 +1,12 @@
let level = 1; let level = 1;
const COLLAPSE_LEVEL = 2;
export function jsonToHTML(json) { export function jsonToHTML(json, maxExpandLevel) {
level = 1; level = 1;
let output = ''; let output = '';
output += '<div class="redoc-json">'; output += '<div class="redoc-json">';
output += valueToHTML(json); output += '<code>';
output += valueToHTML(json, maxExpandLevel);
output += '</code>';
output += '</div>'; output += '</div>';
return output; return output;
} }
@ -21,8 +22,8 @@ function htmlEncode(t) {
: ''; : '';
} }
function escapeForStringLiteral(str: string) { function stringifyStringLiteral(str: string) {
return str.replace(/([\\"])/g, '\\$1'); return JSON.stringify(str).slice(1, -1);
} }
function decorateWithSpan(value, className) { function decorateWithSpan(value, className) {
@ -33,20 +34,20 @@ function punctuation(val) {
return '<span class="token punctuation">' + val + '</span>'; return '<span class="token punctuation">' + val + '</span>';
} }
function valueToHTML(value) { function valueToHTML(value, maxExpandLevel: number) {
const valueType = typeof value; const valueType = typeof value;
let output = ''; let output = '';
if (value === undefined || value === null) { if (value === undefined || value === null) {
output += decorateWithSpan('null', 'token keyword'); output += decorateWithSpan('null', 'token keyword');
} else if (value && value.constructor === Array) { } else if (value && value.constructor === Array) {
level++; level++;
output += arrayToHTML(value); output += arrayToHTML(value, maxExpandLevel);
level--; level--;
} else if (value && value.constructor === Date) { } else if (value && value.constructor === Date) {
output += decorateWithSpan('"' + value.toISOString() + '"', 'token string'); output += decorateWithSpan('"' + value.toISOString() + '"', 'token string');
} else if (valueType === 'object') { } else if (valueType === 'object') {
level++; level++;
output += objectToHTML(value); output += objectToHTML(value, maxExpandLevel);
level--; level--;
} else if (valueType === 'number') { } else if (valueType === 'number') {
output += decorateWithSpan(value, 'token number'); output += decorateWithSpan(value, 'token number');
@ -57,11 +58,11 @@ function valueToHTML(value) {
'<a href="' + '<a href="' +
value + value +
'">' + '">' +
htmlEncode(escapeForStringLiteral(value)) + htmlEncode(stringifyStringLiteral(value)) +
'</a>' + '</a>' +
decorateWithSpan('"', 'token string'); decorateWithSpan('"', 'token string');
} else { } else {
output += decorateWithSpan('"' + escapeForStringLiteral(value) + '"', 'token string'); output += decorateWithSpan('"' + stringifyStringLiteral(value) + '"', 'token string');
} }
} else if (valueType === 'boolean') { } else if (valueType === 'boolean') {
output += decorateWithSpan(value, 'token boolean'); output += decorateWithSpan(value, 'token boolean');
@ -70,8 +71,8 @@ function valueToHTML(value) {
return output; return output;
} }
function arrayToHTML(json) { function arrayToHTML(json, maxExpandLevel: number) {
const collapsed = level > COLLAPSE_LEVEL ? 'collapsed' : ''; const collapsed = level > maxExpandLevel ? 'collapsed' : '';
let output = `<div class="collapser"></div>${punctuation( let output = `<div class="collapser"></div>${punctuation(
'[', '[',
)}<span class="ellipsis"></span><ul class="array collapsible">`; )}<span class="ellipsis"></span><ul class="array collapsible">`;
@ -80,7 +81,7 @@ function arrayToHTML(json) {
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
hasContents = true; hasContents = true;
output += '<li><div class="hoverable ' + collapsed + '">'; output += '<li><div class="hoverable ' + collapsed + '">';
output += valueToHTML(json[i]); output += valueToHTML(json[i], maxExpandLevel);
if (i < length - 1) { if (i < length - 1) {
output += ','; output += ',';
} }
@ -93,8 +94,8 @@ function arrayToHTML(json) {
return output; return output;
} }
function objectToHTML(json) { function objectToHTML(json, maxExpandLevel: number) {
const collapsed = level > COLLAPSE_LEVEL ? 'collapsed' : ''; const collapsed = level > maxExpandLevel ? 'collapsed' : '';
const keys = Object.keys(json); const keys = Object.keys(json);
const length = keys.length; const length = keys.length;
let output = `<div class="collapser"></div>${punctuation( let output = `<div class="collapser"></div>${punctuation(
@ -106,7 +107,7 @@ function objectToHTML(json) {
hasContents = true; hasContents = true;
output += '<li><div class="hoverable ' + collapsed + '">'; output += '<li><div class="hoverable ' + collapsed + '">';
output += '<span class="property token string">"' + htmlEncode(key) + '"</span>: '; output += '<span class="property token string">"' + htmlEncode(key) + '"</span>: ';
output += valueToHTML(json[key]); output += valueToHTML(json[key], maxExpandLevel);
if (i < length - 1) { if (i < length - 1) {
output += punctuation(','); output += punctuation(',');
} }

View File

@ -5,9 +5,9 @@ import { OpenAPISpec } from '../types';
export async function loadAndBundleSpec(specUrlOrObject: object | string): Promise<OpenAPISpec> { export async function loadAndBundleSpec(specUrlOrObject: object | string): Promise<OpenAPISpec> {
const parser = new JsonSchemaRefParser(); const parser = new JsonSchemaRefParser();
const spec = await parser.bundle(specUrlOrObject, { const spec = (await parser.bundle(specUrlOrObject, {
resolve: { http: { withCredentials: false } }, resolve: { http: { withCredentials: false } },
} as object); } as object)) as any;
if (spec.swagger !== undefined) { if (spec.swagger !== undefined) {
return convertSwagger2OpenAPI(spec); return convertSwagger2OpenAPI(spec);

View File

@ -1,17 +1,20 @@
import { dirname } from 'path'; import { dirname } from 'path';
const URLtemplate = require('url-template');
import { FieldModel } from '../services/models';
import { OpenAPIParser } from '../services/OpenAPIParser'; import { OpenAPIParser } from '../services/OpenAPIParser';
import { import {
OpenAPIEncoding, OpenAPIEncoding,
OpenAPIMediaType, OpenAPIMediaType,
OpenAPIOperation, OpenAPIOperation,
OpenAPIParameter, OpenAPIParameter,
OpenAPIParameterStyle,
OpenAPISchema, OpenAPISchema,
OpenAPIServer, OpenAPIServer,
Referenced, Referenced,
} from '../types'; } from '../types';
import { IS_BROWSER } from './dom'; import { IS_BROWSER } from './dom';
import { isNumeric, removeQueryString, resolveUrl, stripTrailingSlash } from './helpers'; import { isNumeric, removeQueryString, resolveUrl } from './helpers';
function isWildcardStatusCode(statusCode: string | number): statusCode is string { function isWildcardStatusCode(statusCode: string | number): statusCode is string {
return typeof statusCode === 'string' && /\dxx/i.test(statusCode); return typeof statusCode === 'string' && /\dxx/i.test(statusCode);
@ -135,43 +138,13 @@ export function isFormUrlEncoded(contentType: string): boolean {
return contentType === 'application/x-www-form-urlencoded'; return contentType === 'application/x-www-form-urlencoded';
} }
function formEncodeField(fieldVal: any, fieldName: string, explode: boolean): string { function delimitedEncodeField(fieldVal: any, fieldName: string, delimiter: string): string {
if (!fieldVal || !fieldVal.length) {
return fieldName + '=';
}
if (Array.isArray(fieldVal)) { if (Array.isArray(fieldVal)) {
if (explode) { return fieldVal.map(v => v.toString()).join(delimiter);
return fieldVal.map(val => `${fieldName}=${val}`).join('&');
} else {
return fieldName + '=' + fieldVal.map(val => val.toString()).join(',');
}
} else if (typeof fieldVal === 'object') {
if (explode) {
return Object.keys(fieldVal)
.map(k => `${k}=${fieldVal[k]}`)
.join('&');
} else {
return (
fieldName +
'=' +
Object.keys(fieldVal)
.map(k => `${k},${fieldVal[k]}`)
.join(',')
);
}
} else {
return fieldName + '=' + fieldVal.toString();
}
}
function delimitedEncodeField(fieldVal: any, fieldName: string, delimeter: string): string {
if (Array.isArray(fieldVal)) {
return fieldVal.map(v => v.toString()).join(delimeter);
} else if (typeof fieldVal === 'object') { } else if (typeof fieldVal === 'object') {
return Object.keys(fieldVal) return Object.keys(fieldVal)
.map(k => `${k}${delimeter}${fieldVal[k]}`) .map(k => `${k}${delimiter}${fieldVal[k]}`)
.join(delimeter); .join(delimiter);
} else { } else {
return fieldName + '=' + fieldVal.toString(); return fieldName + '=' + fieldVal.toString();
} }
@ -191,9 +164,21 @@ function deepObjectEncodeField(fieldVal: any, fieldName: string): string {
} }
} }
function serializeFormValue(name: string, explode: boolean, value: any) {
// Use RFC6570 safe name ([a-zA-Z0-9_]) and replace with our name later
// e.g. URI.template doesn't parse names with hyphen (-) which are valid query param names
const safeName = '__redoc_param_name__';
const suffix = explode ? '*' : '';
const template = URLtemplate.parse(`{?${safeName}${suffix}}`);
return template
.expand({ [safeName]: value })
.substring(1)
.replace(/__redoc_param_name__/g, name);
}
/* /*
* Should be used only for url-form-encoded body payloads * Should be used only for url-form-encoded body payloads
* To be used for parmaters should be extended with other style values * To be used for parameters should be extended with other style values
*/ */
export function urlFormEncodePayload( export function urlFormEncodePayload(
payload: object, payload: object,
@ -208,8 +193,7 @@ export function urlFormEncodePayload(
const { style = 'form', explode = true } = encoding[fieldName] || {}; const { style = 'form', explode = true } = encoding[fieldName] || {};
switch (style) { switch (style) {
case 'form': case 'form':
return formEncodeField(fieldVal, fieldName, explode); return serializeFormValue(fieldName, explode, fieldVal);
break;
case 'spaceDelimited': case 'spaceDelimited':
return delimitedEncodeField(fieldVal, fieldName, '%20'); return delimitedEncodeField(fieldVal, fieldName, '%20');
case 'pipeDelimited': case 'pipeDelimited':
@ -226,6 +210,154 @@ export function urlFormEncodePayload(
} }
} }
function serializePathParameter(
name: string,
style: OpenAPIParameterStyle,
explode: boolean,
value: any,
): string {
const suffix = explode ? '*' : '';
let prefix = '';
if (style === 'label') {
prefix = '.';
} else if (style === 'matrix') {
prefix = ';';
}
// Use RFC6570 safe name ([a-zA-Z0-9_]) and replace with our name later
// e.g. URI.template doesn't parse names with hyphen (-) which are valid query param names
const safeName = '__redoc_param_name__';
const template = URLtemplate.parse(`{${prefix}${safeName}${suffix}}`);
return template.expand({ [safeName]: value }).replace(/__redoc_param_name__/g, name);
}
function serializeQueryParameter(
name: string,
style: OpenAPIParameterStyle,
explode: boolean,
value: any,
): string {
switch (style) {
case 'form':
return serializeFormValue(name, explode, value);
case 'spaceDelimited':
if (!Array.isArray(value)) {
console.warn('The style spaceDelimited is only applicable to arrays');
return '';
}
if (explode) {
return serializeFormValue(name, explode, value);
}
return `${name}=${value.join('%20')}`;
case 'pipeDelimited':
if (!Array.isArray(value)) {
console.warn('The style pipeDelimited is only applicable to arrays');
return '';
}
if (explode) {
return serializeFormValue(name, explode, value);
}
return `${name}=${value.join('|')}`;
case 'deepObject':
if (!explode || Array.isArray(value) || typeof value !== 'object') {
console.warn('The style deepObject is only applicable for objects with explode=true');
return '';
}
return deepObjectEncodeField(value, name);
default:
console.warn('Unexpected style for query: ' + style);
return '';
}
}
function serializeHeaderParameter(
style: OpenAPIParameterStyle,
explode: boolean,
value: any,
): string {
switch (style) {
case 'simple':
const suffix = explode ? '*' : '';
// name is not important here, so use RFC6570 safe name ([a-zA-Z0-9_])
const name = '__redoc_param_name__';
const template = URLtemplate.parse(`{${name}${suffix}}`);
return decodeURIComponent(template.expand({ [name]: value }));
default:
console.warn('Unexpected style for header: ' + style);
return '';
}
}
function serializeCookieParameter(
name: string,
style: OpenAPIParameterStyle,
explode: boolean,
value: any,
): string {
switch (style) {
case 'form':
return serializeFormValue(name, explode, value);
default:
console.warn('Unexpected style for cookie: ' + style);
return '';
}
}
export function serializeParameterValueWithMime(value: any, mime: string): string {
if (isJsonLike(mime)) {
return JSON.stringify(value);
} else {
console.warn(`Parameter serialization as ${mime} is not supported`);
return '';
}
}
export function serializeParameterValue(
parameter: OpenAPIParameter & { serializationMime?: string },
value: any,
): string {
const { name, style, explode = false, serializationMime } = parameter;
if (serializationMime) {
switch (parameter.in) {
case 'path':
case 'header':
return serializeParameterValueWithMime(value, serializationMime);
case 'cookie':
case 'query':
return `${name}=${serializeParameterValueWithMime(value, serializationMime)}`;
default:
console.warn('Unexpected parameter location: ' + parameter.in);
return '';
}
}
if (!style) {
console.warn(`Missing style attribute or content for parameter ${name}`);
return '';
}
switch (parameter.in) {
case 'path':
return serializePathParameter(name, style, explode, value);
case 'query':
return serializeQueryParameter(name, style, explode, value);
case 'header':
return serializeHeaderParameter(style, explode, value);
case 'cookie':
return serializeCookieParameter(name, style, explode, value);
default:
console.warn('Unexpected parameter location: ' + parameter.in);
return '';
}
}
export function langFromMime(contentType: string): string { export function langFromMime(contentType: string): string {
if (contentType.search(/xml/i) !== -1) { if (contentType.search(/xml/i) !== -1) {
return 'xml'; return 'xml';
@ -237,6 +369,17 @@ export function isNamedDefinition(pointer?: string): boolean {
return /^#\/components\/schemas\/[^\/]+$/.test(pointer || ''); return /^#\/components\/schemas\/[^\/]+$/.test(pointer || '');
} }
function humanizeMultipleOfConstraint(multipleOf: number | undefined): string | undefined {
if (multipleOf === undefined) {
return;
}
const strigifiedMultipleOf = multipleOf.toString(10);
if (!/^0\.0*1$/.test(strigifiedMultipleOf)) {
return `multiple of ${strigifiedMultipleOf}`;
}
return `decimal places <= ${strigifiedMultipleOf.split('.')[1].length}`;
}
function humanizeRangeConstraint( function humanizeRangeConstraint(
description: string, description: string,
min: number | undefined, min: number | undefined,
@ -275,6 +418,11 @@ export function humanizeConstraints(schema: OpenAPISchema): string[] {
res.push(arrayRange); res.push(arrayRange);
} }
const multipleOfConstraint = humanizeMultipleOfConstraint(schema.multipleOf);
if (multipleOfConstraint !== undefined) {
res.push(multipleOfConstraint);
}
let numberRange; let numberRange;
if (schema.minimum !== undefined && schema.maximum !== undefined) { if (schema.minimum !== undefined && schema.maximum !== undefined) {
numberRange = schema.exclusiveMinimum ? '( ' : '[ '; numberRange = schema.exclusiveMinimum ? '( ' : '[ ';
@ -297,25 +445,29 @@ export function humanizeConstraints(schema: OpenAPISchema): string[] {
return res; return res;
} }
export function sortByRequired( export function sortByRequired(fields: FieldModel[], order: string[] = []) {
fields: Array<{ required: boolean; name: string }>, const unrequiredFields: FieldModel[] = [];
order: string[] = [], const orderedFields: FieldModel[] = [];
) { const unorderedFields: FieldModel[] = [];
fields.sort((a, b) => {
if (!a.required && b.required) { fields.forEach(field => {
return 1; if (field.required) {
} else if (a.required && !b.required) { order.includes(field.name) ? orderedFields.push(field) : unorderedFields.push(field);
return -1;
} else if (a.required && b.required) {
return order.indexOf(a.name) - order.indexOf(b.name);
} else { } else {
return 0; unrequiredFields.push(field);
} }
}); });
orderedFields.sort((a, b) => order.indexOf(a.name) - order.indexOf(b.name));
return [...orderedFields, ...unorderedFields, ...unrequiredFields];
} }
export function sortByField<T extends string>(fields: Array<{ [P in T]: string }>, param: T) { export function sortByField(
fields.sort((a, b) => { fields: FieldModel[],
param: keyof Pick<FieldModel, 'name' | 'description' | 'kind'>,
) {
return [...fields].sort((a, b) => {
return a[param].localeCompare(b[param]); return a[param].localeCompare(b[param]);
}); });
} }
@ -331,7 +483,7 @@ export function mergeParams(
operationParamNames[param.name + '_' + param.in] = true; operationParamNames[param.name + '_' + param.in] = true;
}); });
// filter out path params overriden by operation ones with the same name // filter out path params overridden by operation ones with the same name
pathParams = pathParams.filter(param => { pathParams = pathParams.filter(param => {
param = parser.shalowDeref(param); param = parser.shalowDeref(param);
return !operationParamNames[param.name + '_' + param.in]; return !operationParamNames[param.name + '_' + param.in];
@ -356,7 +508,7 @@ export function mergeSimilarMediaTypes(types: Dict<OpenAPIMediaType>): Dict<Open
return mergedTypes; return mergedTypes;
} }
function expandVariables(url: string, variables: object = {}) { export function expandDefaultServerVariables(url: string, variables: object = {}) {
return url.replace( return url.replace(
/(?:{)(\w+)(?:})/g, /(?:{)(\w+)(?:})/g,
(match, name) => (variables[name] && variables[name].default) || match, (match, name) => (variables[name] && variables[name].default) || match,
@ -378,28 +530,31 @@ export function normalizeServers(
const baseUrl = specUrl === undefined ? removeQueryString(getHref()) : dirname(specUrl); const baseUrl = specUrl === undefined ? removeQueryString(getHref()) : dirname(specUrl);
if (servers.length === 0) { if (servers.length === 0) {
return [ // Behaviour defined in OpenAPI spec: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#openapi-object
servers = [
{ {
url: stripTrailingSlash(baseUrl), url: '/',
}, },
]; ];
} }
function normalizeUrl(url: string, variables: object | undefined): string { function normalizeUrl(url: string): string {
url = expandVariables(url, variables);
return resolveUrl(baseUrl, url); return resolveUrl(baseUrl, url);
} }
return servers.map(server => { return servers.map(server => {
return { return {
...server, ...server,
url: normalizeUrl(server.url, server.variables), url: normalizeUrl(server.url),
description: server.description || '', description: server.description || '',
}; };
}); });
} }
export const SECURITY_DEFINITIONS_COMPONENT_NAME = 'security-definitions'; export const SECURITY_DEFINITIONS_COMPONENT_NAME = 'security-definitions';
export const SECURITY_DEFINITIONS_JSX_NAME = 'SecurityDefinitions';
export const SCHEMA_DEFINITION_JSX_NAME = 'SchemaDefinition';
export let SECURITY_SCHEMES_SECTION_PREFIX = 'section/Authentication/'; export let SECURITY_SCHEMES_SECTION_PREFIX = 'section/Authentication/';
export function setSecuritySchemePrefix(prefix: string) { export function setSecuritySchemePrefix(prefix: string) {
SECURITY_SCHEMES_SECTION_PREFIX = prefix; SECURITY_SCHEMES_SECTION_PREFIX = prefix;
@ -446,6 +601,6 @@ export function extractExtensions(obj: object, showExtensions: string[] | true):
export function pluralizeType(displayType: string): string { export function pluralizeType(displayType: string): string {
return displayType return displayType
.split(' or ') .split(' or ')
.map(type => type.replace(/^(string|object|number|integer|array|boolean)( ?.*)/, '$1s$2')) .map(type => type.replace(/^(string|object|number|integer|array|boolean)s?( ?.*)/, '$1s$2'))
.join(' or '); .join(' or ');
} }

View File

@ -34,6 +34,7 @@
"e2e/**" "e2e/**"
], ],
"include": [ "include": [
"cli/index.ts",
"./custom.d.ts", "./custom.d.ts",
"./demo/playground/hmr-playground.tsx", "./demo/playground/hmr-playground.tsx",
"./src/**/*.ts?", "./src/**/*.ts?",

View File

@ -17,7 +17,7 @@
"quotemark": [true, "single", "avoid-template", "jsx-double"], "quotemark": [true, "single", "avoid-template", "jsx-double"],
"variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore", "allow-pascal-case"], "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore", "allow-pascal-case"],
"arrow-parens": [true, "ban-single-arg-parens"], "arrow-parens": [true, "ban-single-arg-parens"],
"no-submodule-imports": [true, "prismjs", "perfect-scrollbar", "react-dom", "core-js"], "no-submodule-imports": [true, "prismjs", "perfect-scrollbar", "react-dom", "core-js", "memoize-one"],
"object-literal-key-quotes": [true, "as-needed"], "object-literal-key-quotes": [true, "as-needed"],
"no-unused-expression": [true, "allow-tagged-template"], "no-unused-expression": [true, "allow-tagged-template"],
"semicolon": [true, "always", "ignore-bound-class-methods"], "semicolon": [true, "always", "ignore-bound-class-methods"],

View File

@ -5,7 +5,7 @@ import * as webpack from 'webpack';
import * as path from 'path'; import * as path from 'path';
const nodeExternals = require('webpack-node-externals')({ const nodeExternals = require('webpack-node-externals')({
// bundle in moudules that need transpiling + non-js (e.g. css) // bundle in modules that need transpiling + non-js (e.g. css)
whitelist: [ whitelist: [
'swagger2openapi', 'swagger2openapi',
/reftools/, /reftools/,

6577
yarn.lock

File diff suppressed because it is too large Load Diff