Merge tag 'v2.0.0-rc.68' into sections-at-the-end

This commit is contained in:
Roberto Fernández 2022-05-12 07:58:46 +02:00
commit eb08635e56
74 changed files with 1330 additions and 714 deletions

2
.github/CODEOWNERS vendored Normal file
View File

@ -0,0 +1,2 @@
* @Redocly/keyboard-warriors
/docs/ @Redocly/technical-writers

View File

@ -98,3 +98,37 @@ jobs:
run: npm publish run: npm publish
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
dockerhub:
needs: [publish]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Docker meta
id: docker_meta
uses: crazy-max/ghaction-docker-meta@v1
with:
images: redocly/redoc
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./config/docker/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}

View File

@ -1,3 +1,58 @@
# [2.0.0-rc.68](https://github.com/Redocly/redoc/compare/v2.0.0-rc.67...v2.0.0-rc.68) (2022-05-10)
### Bug Fixes
* examples in json schema object([5b9aa27](https://github.com/Redocly/redoc/commit/5b9aa27af03a1c4616f7e0195afeba47d1deeaa0))
* handle error when definition load fails ([#1979](https://github.com/Redocly/redoc/issues/1979)) ([508ebd5](https://github.com/Redocly/redoc/commit/508ebd58a3d66f2337e9641852322458a1bd9e6b))
* large text in examples value ([#1974](https://github.com/Redocly/redoc/issues/1974)) ([60bc603](https://github.com/Redocly/redoc/commit/60bc603e9bb85a0c9c7ac38f7014876d397f0191))
* not show scopes if keys empty or not exist ([#1975](https://github.com/Redocly/redoc/issues/1975)) ([4e793f0](https://github.com/Redocly/redoc/commit/4e793f07a81fa8bcd4ad384d1f87b3e6c290edb7))
* remove dropdown-aria and use native select ([#1954](https://github.com/Redocly/redoc/issues/1954)) ([186f5a9](https://github.com/Redocly/redoc/commit/186f5a98bd466b1820121aadb865291bef8c6755))
* make Redoc lib compatible with Webpack 5 ([#1982](https://github.com/Redocly/redoc/issues/1982)) ([867861](https://github.com/Redocly/redoc/commit/8678615a0e19c9484b4cd495d70293b542d196a5))
### Features
* implement configurable minimum characer length to init search ([#1402](https://github.com/Redocly/redoc/issues/1402)) ([0fa08fa](https://github.com/Redocly/redoc/commit/0fa08faab1c176a4bfc5a553e8e8f8b07aca659f))
* support OAS 3.1 unevaluatedProperties ([#1978](https://github.com/Redocly/redoc/issues/1978)) ([0755ac6](https://github.com/Redocly/redoc/commit/0755ac6f04514eb0c08f90afceeda7858206b435))
* publish dockerhub ([#1971](https://github.com/Redocly/redoc/issues/1971)) ([7e01a0](https://github.com/Redocly/redoc/commit/7e01a0cfe2ad8d06075bfc66ef3860edbef033f8))
# [2.0.0-rc.67](https://github.com/Redocly/redoc/compare/v2.0.0-rc.66...v2.0.0-rc.67) (2022-04-28)
### Bug Fixes
* Expand/Collapse all buttons disappears for flat structures ([#1424](https://github.com/Redocly/redoc/issues/1424)) ([2ca8e08](https://github.com/Redocly/redoc/commit/2ca8e081baea6996eb01b5df27b8cd88331d5c96))
* improve markdown render with CRLF ([#1953](https://github.com/Redocly/redoc/issues/1953)) ([aba2d1a](https://github.com/Redocly/redoc/commit/aba2d1ad2d8dda9f52055c36ebde1323457dfd3e))
* issue with navigation when operationId contains backslash or quotes ([#1513](https://github.com/Redocly/redoc/issues/1513)) ([8f7e56c](https://github.com/Redocly/redoc/commit/8f7e56c747d88be5c5eb5c4bbaee0ff69e9cb2ec))
* prefix operation ids with parent id ([#1245](https://github.com/Redocly/redoc/issues/1245)) ([fd8917e](https://github.com/Redocly/redoc/commit/fd8917e5c109840c1bfa4c2c0902b6dcec200286))
### Features
* add optional BASE_PATH to Docker config ([#1378](https://github.com/Redocly/redoc/issues/1378)) ([90f71c0](https://github.com/Redocly/redoc/commit/90f71c0d77719871910cfba883a32ad131bef059))
* theme add sidebar activeBackgroundColor and activeTextColor ([#1600](https://github.com/Redocly/redoc/issues/1600)) ([6716b08](https://github.com/Redocly/redoc/commit/6716b08e8871d95880e9f5a6c5491038002754e8))
# [2.0.0-rc.66](https://github.com/Redocly/redoc/compare/v2.0.0-rc.65...v2.0.0-rc.66) (2022-03-30)
### Bug Fixes
* add handle local files for serve command ([#1810](https://github.com/Redocly/redoc/issues/1810)) ([117071e](https://github.com/Redocly/redoc/commit/117071ee83a32d9b3350d8afe2bdb6365a44e2ec))
* move comma out of code block in SecurityRequirement.tsx ([#1924](https://github.com/Redocly/redoc/issues/1924)) ([ab3e8a8](https://github.com/Redocly/redoc/commit/ab3e8a8f80f453066c5495e73ac932a8fef0830a))
* rename bandle command and add deprecate notice ([#1935](https://github.com/Redocly/redoc/issues/1935)) ([eb096b6](https://github.com/Redocly/redoc/commit/eb096b69be52568fc581027161c7d0c4b26c56c1))
### Features
* add support for displaying operationId in the sidebar ([#1927](https://github.com/Redocly/redoc/issues/1927)) ([09786f2](https://github.com/Redocly/redoc/commit/09786f2a5ade6303ea00512483b172347721ca70))
* add nonce support ([#1566](https://github.com/Redocly/redoc/issues/1566)) ([c75ac9c](https://github.com/Redocly/redoc/commit/c75ac9cf70012e2d539b379aab2f0974d088db07))
* h2 set color form theme.colors.text.primary ([#1491](https://github.com/Redocly/redoc/pull/1491)) ([25be93](https://github.com/Redocly/redoc/commit/25be934bb184d7b2b6b47d004b3c83ce4d16a2c6))
# [2.0.0-rc.65](https://github.com/Redocly/redoc/compare/v2.0.0-rc.64...v2.0.0-rc.65) (2022-03-15) # [2.0.0-rc.65](https://github.com/Redocly/redoc/compare/v2.0.0-rc.64...v2.0.0-rc.65) (2022-03-15)
@ -6,7 +61,8 @@
* auth link scroll for Firerox ([#1922](https://github.com/Redocly/redoc/issues/1922)) ([fe67e9c](https://github.com/Redocly/redoc/commit/fe67e9c332fee716582a00d60fdf34767bff22d4)) * auth link scroll for Firerox ([#1922](https://github.com/Redocly/redoc/issues/1922)) ([fe67e9c](https://github.com/Redocly/redoc/commit/fe67e9c332fee716582a00d60fdf34767bff22d4))
* improve customization fab ([#1891](https://github.com/Redocly/redoc/issues/1891)) ([635f379](https://github.com/Redocly/redoc/commit/635f379eb086268c91eef715148eca8f080cfb86)) * improve customization fab ([#1891](https://github.com/Redocly/redoc/issues/1891)) ([635f379](https://github.com/Redocly/redoc/commit/635f379eb086268c91eef715148eca8f080cfb86))
* sanitize array of items ([#1920](https://github.com/Redocly/redoc/issues/1920)) ([059bd80](https://github.com/Redocly/redoc/commit/059bd8000e5fd65753d5ca9e0c47940394e0c79b)) * sanitize array of items ([#1920](https://github.com/Redocly/redoc/issues/1920)) ([059bd80](https://github.com/Redocly/redoc/commit/059bd8000e5fd65753d5ca9e0c47940394e0c79b))
* use x-displayName in securityDefinitions [#1444](https://github.com/Redocly/redoc/pull/1444)) ([ac6fb4](https://github.com/Redocly/redoc/commit/ * use x-displayName in securityDefinitions ([#1444](https://github.com/Redocly/redoc/pull/1444)) ([ac6fb4](https://github.com/Redocly/redoc/commit/ac6fb458a4eee8d0da4b63f9bafc7669adc8af03))
* deprecated badge on one of any of buttons ([#1930](https://github.com/Redocly/redoc/pull/1930)) ([f60b47](https://github.com/Redocly/redoc/commit/f60b4758330dd756d670309827da60d3465b672a))

View File

@ -131,11 +131,11 @@ Additionally, all the 1.x releases are hosted on our GitHub Pages-based CDN **(d
## Lint OpenAPI definitions ## Lint OpenAPI definitions
Redocly's OpenAPI CLI is an open source command-line tool that you can use to lint Redocly's CLI is an [open source command-line tool](https://github.com/Redocly/redocly-cli) that you can use to lint
your OpenAPI definition. Linting helps you to catch errors and inconsistencies in your your OpenAPI definition. Linting helps you to catch errors and inconsistencies in your
OpenAPI definition before publishing. OpenAPI definition before publishing.
Refer to [Lint configuration](https://redoc.ly/docs/cli/guides/lint/) in the OpenAPI documentation for more information. Refer to [Redocly configuration](https://redocly.com/docs/cli/configuration/) in the OpenAPI documentation for more information.
## Deployment ## Deployment
@ -174,14 +174,11 @@ replace the `spec-url` attribute with the url or local file address to your defi
For step-by-step instructions for how to get started using Redoc For step-by-step instructions for how to get started using Redoc
to render your OpenAPI definition, refer to the to render your OpenAPI definition, refer to the
[**Redoc quickstart guide**](https://redoc.ly/docs/redoc/quickstart/intro/). [**Redoc quickstart guide**](https://redocly.com/docs/redoc/quickstart/) and [**How to use the HTML element**](https://redocly.com/docs/redoc/deployment/html/).
See [**IE11 Support Notes**](docs/usage-with-ie11.md) for information on
IE support for Redoc.
## Redoc CLI ## Redoc CLI
For more information on Redoc's commmand-line interface, refer to For more information on Redoc's commmand-line interface, refer to
[**Using the Redoc CLI**](https://redoc.ly/docs/redoc/quickstart/cli/). [**Using the Redoc CLI**](https://redocly.com/docs/redoc/deployment/cli/).
## Configuration ## Configuration
@ -191,7 +188,7 @@ You can inject the Security Definitions widget into any place in your definition
For more information, refer to [Security definitions injection](docs/security-definitions-injection.md). For more information, refer to [Security definitions injection](docs/security-definitions-injection.md).
### OpenAPI specification extensions ### OpenAPI specification extensions
Redoc uses the following [specification extensions](https://swagger.io/specification/#specificationExtensions): Redoc uses the following [specification extensions](https://redocly.com/docs/api-reference-docs/spec-extensions/):
* [`x-logo`](docs/redoc-vendor-extensions.md#x-logo) - is used to specify API logo * [`x-logo`](docs/redoc-vendor-extensions.md#x-logo) - is used to specify API logo
* [`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-codeSamples`](docs/redoc-vendor-extensions.md#x-codeSamples) - specify operation code samples * [`x-codeSamples`](docs/redoc-vendor-extensions.md#x-codeSamples) - specify operation code samples
@ -210,6 +207,7 @@ Redoc uses the following [specification extensions](https://swagger.io/specifica
You can use all of the following options with the standalone version of the <redoc> tag by kebab-casing them. For example, `scrollYOffset` becomes `scroll-y-offset`, and `expandResponses` becomes `expand-responses`. You can use all of the following options with the standalone version of the <redoc> tag by kebab-casing them. For example, `scrollYOffset` becomes `scroll-y-offset`, and `expandResponses` becomes `expand-responses`.
* `disableSearch` - disable search indexing and search box. * `disableSearch` - disable search indexing and search box.
* `minCharacterLengthToInitSearch` - set minimal characters length to init search, default `3`, minimal `1`.
* `expandDefaultServerVariables` - enable expanding default server variables, default `false`. * `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. * `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.
* `generatedPayloadSamplesMaxDepth` - set the maximum render depth for JSON payload samples (responses and request body). The default value is `10`. * `generatedPayloadSamplesMaxDepth` - set the maximum render depth for JSON payload samples (responses and request body). The default value is `10`.
@ -246,9 +244,11 @@ You can use all of the following options with the standalone version of the <red
* `payloadSampleIdx` - if set, payload sample will be inserted at this index or last. Indexes start from 0. * `payloadSampleIdx` - if set, payload sample will be inserted at this index or last. Indexes start from 0.
* `theme` - ReDoc theme. For details check [theme docs](#redoc-theme-object). * `theme` - ReDoc theme. For details check [theme docs](#redoc-theme-object).
* `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!** * `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!**
* `nonce` - if set, the provided value will be injected in every injected HTML element in the `nonce` attribute. Useful when using CSP, see https://webpack.js.org/guides/csp/.
* `sideNavStyle` - can be specified in various ways: * `sideNavStyle` - can be specified in various ways:
* **summary-only**: displays a summary in the sidebar navigation item. (**default**) * **summary-only**: displays a summary in the sidebar navigation item. (**default**)
* **path-only**: displays a path in the sidebar navigation item. * **path-only**: displays a path in the sidebar navigation item.
* **id-only**: displays the operation id with a fallback to the path in the sidebar navigation item.
### `<redoc>` theme object ### `<redoc>` theme object
* `spacing` * `spacing`
@ -286,21 +286,25 @@ You can use all of the following options with the standalone version of the <red
* `color`: # COMPUTED: colors.primary.main * `color`: # COMPUTED: colors.primary.main
* `visited`: # COMPUTED: typography.links.color * `visited`: # COMPUTED: typography.links.color
* `hover`: # COMPUTED: lighten(0.2 typography.links.color) * `hover`: # COMPUTED: lighten(0.2 typography.links.color)
* `menu` * `sidebar`
* `width`: '260px' * `width`: '260px'
* `backgroundColor`: '#fafafa' * `backgroundColor`: '#fafafa'
* `textColor`: '#333333' * `textColor`: '#333333'
* `activeTextColor`: # COMPUTED: theme.menu.textColor (if set by user) or theme.colors.primary.main * `activeTextColor`: # COMPUTED: theme.sidebar.textColor (if set by user) or theme.colors.primary.main
* `groupItems` # Group headings * `groupItems` # Group headings
* `activeBackgroundColor`: # COMPUTED: theme.sidebar.backgroundColor
* `activeTextColor`: # COMPUTED: theme.sidebar.activeTextColor
* `textTransform`: 'uppercase' * `textTransform`: 'uppercase'
* `level1Items` # Level 1 items like tags or section 1st level items * `level1Items` # Level 1 items like tags or section 1st level items
* `activeBackgroundColor`: # COMPUTED: theme.sidebar.backgroundColor
* `activeTextColor`: # COMPUTED: theme.sidebar.activeTextColor
* `textTransform`: 'none' * `textTransform`: 'none'
* `arrow` # menu arrow * `arrow` # sidebar arrow
* `size`: '1.5em' * `size`: '1.5em'
* `color`: # COMPUTED: theme.menu.textColor * `color`: # COMPUTED: theme.sidebar.textColor
* `logo` * `logo`
* `maxHeight`: # COMPUTED: menu.width * `maxHeight`: # COMPUTED: sidebar.width
* `maxWidth`: # COMPUTED: menu.width * `maxWidth`: # COMPUTED: sidebar.width
* `gutter`: '2px' # logo image padding * `gutter`: '2px' # logo image padding
* `rightPanel` * `rightPanel`
* `backgroundColor`: '#263238' * `backgroundColor`: '#263238'

View File

@ -12,20 +12,23 @@ or using [npx](https://medium.com/@maybekatz/introducing-npx-an-npm-package-runn
The two following commands are available: The two following commands are available:
- `redoc-cli serve [spec]` - starts the server with `spec` rendered with ReDoc. - `redoc-cli serve [spec]` - starts the server with `spec` rendered with ReDoc.
Supports a server-side rendering mode (`--ssr`), Supports a server-side rendering mode (`--ssr`)
and can watch the spec (`--watch`) to automatically reload the page whenever it changes. and can watch the spec (`--watch`) to automatically reload the page whenever it changes.\
- `redoc-cli bundle [spec]` - bundles `spec` and ReDoc into a **zero-dependency** HTML file. Deprecated. Use `npx @redocly/openapi-cli preview-docs [spec]`
- `redoc-cli bundle [spec]` - bundles `spec` and Redoc into a **zero-dependency** HTML file.\
Deprecated. Use Use "build" command instead.
- `redoc-cli build [spec]` - build `spec` and Redoc into a **zero-dependency** HTML file.
Some examples: Some examples:
- Bundle with the main color changed to `orange`:<br/> - Bundle with the main color changed to `orange`:<br/>
`$ redoc-cli bundle [spec] --options.theme.colors.primary.main=orange` `$ redoc-cli build [spec] --options.theme.colors.primary.main=orange`
- Serve with the `nativeScrollbars` option set to true:<br/> - Serve with the `nativeScrollbars` option set to true:<br/>
`$ redoc-cli serve [spec] --options.nativeScrollbars` `$ redoc-cli serve [spec] --options.nativeScrollbars`
- Bundle using a custom [Handlebars](https://handlebarsjs.com/) template - Bundle using a custom [Handlebars](https://handlebarsjs.com/) template
(check the [default template](https://github.com/Redocly/redoc/blob/master/cli/template.hbs) for an example):<br/> (check the [default template](https://github.com/Redocly/redoc/blob/master/cli/template.hbs) for an example):<br/>
`$ redoc-cli bundle [spec] -t custom.hbs` `$ redoc-cli build [spec] -t custom.hbs`
- Bundle using a custom template and add custom `templateOptions`:<br/> - Bundle using a custom template and add custom `templateOptions`:<br/>
`$ redoc-cli bundle [spec] -t custom.hbs --templateOptions.metaDescription "Page meta description"` `$ redoc-cli build [spec] -t custom.hbs --templateOptions.metaDescription "Page meta description"`
For more details, run `redoc-cli --help`. For more details, run `redoc-cli --help`.

View File

@ -6,7 +6,7 @@ import { ServerStyleSheet } from 'styled-components';
import { compile } from 'handlebars'; import { compile } from 'handlebars';
import { createServer, IncomingMessage, ServerResponse } from 'http'; import { createServer, IncomingMessage, ServerResponse } from 'http';
import { dirname, join, resolve } from 'path'; import { dirname, join, resolve, extname as getExtName } from 'path';
import * as zlib from 'zlib'; import * as zlib from 'zlib';
@ -39,9 +39,78 @@ interface Options {
redocOptions?: any; redocOptions?: any;
} }
export const mimeTypes = {
'.html': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.wav': 'audio/wav',
'.mp4': 'video/mp4',
'.woff': 'application/font-woff',
'.ttf': 'application/font-ttf',
'.eot': 'application/vnd.ms-fontobject',
'.otf': 'application/font-otf',
'.wasm': 'application/wasm',
};
const BUNDLES_DIR = dirname(require.resolve('redoc')); const BUNDLES_DIR = dirname(require.resolve('redoc'));
/* tslint:disable-next-line */ const builderForBuildCommand = yargs => {
yargs.positional('spec', {
describe: 'path or URL to your spec',
});
yargs.option('o', {
describe: 'Output file',
alias: 'output',
type: 'string',
default: 'redoc-static.html',
});
yargs.options('title', {
describe: 'Page Title',
type: 'string',
});
yargs.options('disableGoogleFont', {
describe: 'Disable Google Font',
type: 'boolean',
default: false,
});
yargs.option('cdn', {
describe: 'Do not include ReDoc source code into html page, use link to CDN instead',
type: 'boolean',
default: false,
});
yargs.demandOption('spec');
return yargs;
};
const handlerForBuildCommand = async (argv: any) => {
const config = {
ssr: true,
output: argv.o as string,
cdn: argv.cdn as boolean,
title: argv.title as string,
disableGoogleFont: argv.disableGoogleFont as boolean,
templateFileName: argv.template as string,
templateOptions: argv.templateOptions || {},
redocOptions: getObjectOrJSON(argv.options),
};
try {
await bundle(argv.spec, config);
} catch (e) {
handleError(e);
}
};
YargsParser.command( YargsParser.command(
'serve <spec>', 'serve <spec>',
'start the server', 'start the server',
@ -104,60 +173,32 @@ YargsParser.command(
handleError(e); handleError(e);
} }
}, },
[
res => {
console.log(
`\n⚠ This command is deprecated. Use "npx @redocly/openapi-cli preview-docs petstore.yaml"\n`,
);
return res;
},
],
) )
.command(
'build <spec>',
'build definition into zero-dependency HTML-file',
builderForBuildCommand,
handlerForBuildCommand,
)
.command( .command(
'bundle <spec>', 'bundle <spec>',
'bundle spec into zero-dependency HTML-file', 'bundle spec into zero-dependency HTML-file [deprecated]',
yargs => { builderForBuildCommand,
yargs.positional('spec', { handlerForBuildCommand,
describe: 'path or URL to your spec', [
}); res => {
console.log(`\n⚠ This command is deprecated. Use "build" command instead.\n`);
yargs.option('o', { return res;
describe: 'Output file', },
alias: 'output', ],
type: 'string',
default: 'redoc-static.html',
});
yargs.options('title', {
describe: 'Page Title',
type: 'string',
});
yargs.options('disableGoogleFont', {
describe: 'Disable Google Font',
type: 'boolean',
default: false,
});
yargs.option('cdn', {
describe: 'Do not include ReDoc source code into html page, use link to CDN instead',
type: 'boolean',
default: false,
});
yargs.demandOption('spec');
return yargs;
},
async (argv: any) => {
const config = {
ssr: true,
output: argv.o as string,
cdn: argv.cdn as boolean,
title: argv.title as string,
disableGoogleFont: argv.disableGoogleFont as boolean,
templateFileName: argv.template as string,
templateOptions: argv.templateOptions || {},
redocOptions: getObjectOrJSON(argv.options),
};
try {
await bundle(argv.spec, config);
} catch (e) {
handleError(e);
}
},
) )
.demandCommand() .demandCommand()
.options('t', { .options('t', {
@ -197,9 +238,19 @@ async function serve(host: string, port: number, pathToSpec: string, options: Op
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}); });
} else { } else {
response.writeHead(404); try {
response.write('Not found'); const filePath = join(dirname(pathToSpec), request.url || '');
response.end(); const extname = String(getExtName(filePath)).toLowerCase() as keyof typeof mimeTypes;
const contentType = mimeTypes[extname] || 'application/octet-stream';
respondWithGzip(createReadStream(filePath), request, response, {
'Content-Type': contentType,
});
} catch (e) {
response.writeHead(404);
response.write('Not found');
response.end();
}
} }
console.timeEnd('GET ' + request.url); console.timeEnd('GET ' + request.url);

146
cli/npm-shrinkwrap.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "redoc-cli", "name": "redoc-cli",
"version": "0.13.8", "version": "0.13.11",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "redoc-cli", "name": "redoc-cli",
"version": "0.13.8", "version": "0.13.11",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"chokidar": "^3.5.1", "chokidar": "^3.5.1",
@ -17,9 +17,9 @@
"node-libs-browser": "^2.2.1", "node-libs-browser": "^2.2.1",
"react": "^17.0.1", "react": "^17.0.1",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"redoc": "2.0.0-rc.64", "redoc": "2.0.0-rc.67",
"styled-components": "^5.3.0", "styled-components": "^5.3.0",
"yargs": "^17.0.1" "yargs": "^17.3.1"
}, },
"bin": { "bin": {
"redoc-cli": "index.js" "redoc-cli": "index.js"
@ -216,9 +216,9 @@
} }
}, },
"node_modules/@redocly/openapi-core": { "node_modules/@redocly/openapi-core": {
"version": "1.0.0-beta.80", "version": "1.0.0-beta.91",
"resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.0.0-beta.80.tgz", "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.0.0-beta.91.tgz",
"integrity": "sha512-IAQECLt/fDxjlfNdLGnJszt40BaiA6b78+zB6+7Rk8ums0HHLfwWFJPMTzh1bzJ5f+sZ4zDBi4gaIJ1n4XGCHg==", "integrity": "sha512-8RhZGn5jSoy3oZE0sAdXxhPPHrqKgy2JVJzLqjgX9LDjNf7cXOTYOXkXIkjv1tfZHFBV/H7c08rRLEdxnzn0dg==",
"dependencies": { "dependencies": {
"@redocly/ajv": "^8.6.4", "@redocly/ajv": "^8.6.4",
"@types/node": "^14.11.8", "@types/node": "^14.11.8",
@ -236,9 +236,9 @@
} }
}, },
"node_modules/@redocly/openapi-core/node_modules/@types/node": { "node_modules/@redocly/openapi-core/node_modules/@types/node": {
"version": "14.18.9", "version": "14.18.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.9.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.12.tgz",
"integrity": "sha512-j11XSuRuAlft6vLDEX4RvhqC0KxNxx6QIyMXNb0vHHSNPXTPeiy3algESWmOOIzEtiEL0qiowPU3ewW9hHVa7Q==" "integrity": "sha512-q4jlIR71hUpWTnGhXWcakgkZeHa3CCjcQcnuzU8M891BAWA2jHiziiWEPEkdS5pFsz7H9HJiy8BrK7tBRNrY7A=="
}, },
"node_modules/@redocly/react-dropdown-aria": { "node_modules/@redocly/react-dropdown-aria": {
"version": "2.0.12", "version": "2.0.12",
@ -1273,9 +1273,7 @@
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
"os": [ "os": ["darwin"],
"darwin"
],
"engines": { "engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
@ -1698,9 +1696,9 @@
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.0.4", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
}, },
@ -1709,9 +1707,9 @@
} }
}, },
"node_modules/minimist": { "node_modules/minimist": {
"version": "1.2.5", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
}, },
"node_modules/mkdirp": { "node_modules/mkdirp": {
"version": "1.0.4", "version": "1.0.4",
@ -2219,11 +2217,11 @@
} }
}, },
"node_modules/redoc": { "node_modules/redoc": {
"version": "2.0.0-rc.64", "version": "2.0.0-rc.67",
"resolved": "https://registry.npmjs.org/redoc/-/redoc-2.0.0-rc.64.tgz", "resolved": "https://registry.npmjs.org/redoc/-/redoc-2.0.0-rc.67.tgz",
"integrity": "sha512-zrM/vcONpbmyDUOpk7Ai7R/yZrT7W1+8ANJUB3b5kzaLQUx4VbrDoT4D6HHHquOnKx+5We4nOPPAi8fi/cqa8g==", "integrity": "sha512-u6rEKB0LylSisN+mFa3flj7zf+prXDB+G02foqC9BOlcXkUYXHFDZM4L3BTBL/DstyGTgjhe2dA9csAjIVti/g==",
"dependencies": { "dependencies": {
"@redocly/openapi-core": "^1.0.0-beta.54", "@redocly/openapi-core": "^1.0.0-beta.88",
"@redocly/react-dropdown-aria": "^2.0.11", "@redocly/react-dropdown-aria": "^2.0.11",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"decko": "^1.2.0", "decko": "^1.2.0",
@ -2238,7 +2236,7 @@
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"perfect-scrollbar": "^1.5.1", "perfect-scrollbar": "^1.5.1",
"polished": "^4.1.3", "polished": "^4.1.3",
"prismjs": "^1.24.1", "prismjs": "^1.27.0",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react-tabs": "^3.2.2", "react-tabs": "^3.2.2",
"slugify": "~1.4.7", "slugify": "~1.4.7",
@ -2493,24 +2491,24 @@
} }
}, },
"node_modules/string-width": { "node_modules/string-width": {
"version": "4.2.2", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dependencies": { "dependencies": {
"emoji-regex": "^8.0.0", "emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0", "is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.0" "strip-ansi": "^6.0.1"
}, },
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/strip-ansi": { "node_modules/strip-ansi": {
"version": "6.0.0", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": { "dependencies": {
"ansi-regex": "^5.0.0" "ansi-regex": "^5.0.1"
}, },
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -2953,28 +2951,28 @@
"integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==" "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A=="
}, },
"node_modules/yargs": { "node_modules/yargs": {
"version": "17.0.1", "version": "17.3.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.0.1.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz",
"integrity": "sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ==", "integrity": "sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA==",
"dependencies": { "dependencies": {
"cliui": "^7.0.2", "cliui": "^7.0.2",
"escalade": "^3.1.1", "escalade": "^3.1.1",
"get-caller-file": "^2.0.5", "get-caller-file": "^2.0.5",
"require-directory": "^2.1.1", "require-directory": "^2.1.1",
"string-width": "^4.2.0", "string-width": "^4.2.3",
"y18n": "^5.0.5", "y18n": "^5.0.5",
"yargs-parser": "^20.2.2" "yargs-parser": "^21.0.0"
}, },
"engines": { "engines": {
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/yargs-parser": { "node_modules/yargs-parser": {
"version": "20.2.7", "version": "21.0.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz",
"integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==", "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==",
"engines": { "engines": {
"node": ">=10" "node": ">=12"
} }
} }
}, },
@ -3148,9 +3146,9 @@
} }
}, },
"@redocly/openapi-core": { "@redocly/openapi-core": {
"version": "1.0.0-beta.80", "version": "1.0.0-beta.91",
"resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.0.0-beta.80.tgz", "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.0.0-beta.91.tgz",
"integrity": "sha512-IAQECLt/fDxjlfNdLGnJszt40BaiA6b78+zB6+7Rk8ums0HHLfwWFJPMTzh1bzJ5f+sZ4zDBi4gaIJ1n4XGCHg==", "integrity": "sha512-8RhZGn5jSoy3oZE0sAdXxhPPHrqKgy2JVJzLqjgX9LDjNf7cXOTYOXkXIkjv1tfZHFBV/H7c08rRLEdxnzn0dg==",
"requires": { "requires": {
"@redocly/ajv": "^8.6.4", "@redocly/ajv": "^8.6.4",
"@types/node": "^14.11.8", "@types/node": "^14.11.8",
@ -3165,9 +3163,9 @@
}, },
"dependencies": { "dependencies": {
"@types/node": { "@types/node": {
"version": "14.18.9", "version": "14.18.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.9.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.12.tgz",
"integrity": "sha512-j11XSuRuAlft6vLDEX4RvhqC0KxNxx6QIyMXNb0vHHSNPXTPeiy3algESWmOOIzEtiEL0qiowPU3ewW9hHVa7Q==" "integrity": "sha512-q4jlIR71hUpWTnGhXWcakgkZeHa3CCjcQcnuzU8M891BAWA2jHiziiWEPEkdS5pFsz7H9HJiy8BrK7tBRNrY7A=="
} }
} }
}, },
@ -4415,17 +4413,17 @@
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
}, },
"minimatch": { "minimatch": {
"version": "3.0.4", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
}, },
"minimist": { "minimist": {
"version": "1.2.5", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
}, },
"mkdirp": { "mkdirp": {
"version": "1.0.4", "version": "1.0.4",
@ -4824,11 +4822,11 @@
} }
}, },
"redoc": { "redoc": {
"version": "2.0.0-rc.64", "version": "2.0.0-rc.67",
"resolved": "https://registry.npmjs.org/redoc/-/redoc-2.0.0-rc.64.tgz", "resolved": "https://registry.npmjs.org/redoc/-/redoc-2.0.0-rc.67.tgz",
"integrity": "sha512-zrM/vcONpbmyDUOpk7Ai7R/yZrT7W1+8ANJUB3b5kzaLQUx4VbrDoT4D6HHHquOnKx+5We4nOPPAi8fi/cqa8g==", "integrity": "sha512-u6rEKB0LylSisN+mFa3flj7zf+prXDB+G02foqC9BOlcXkUYXHFDZM4L3BTBL/DstyGTgjhe2dA9csAjIVti/g==",
"requires": { "requires": {
"@redocly/openapi-core": "^1.0.0-beta.54", "@redocly/openapi-core": "^1.0.0-beta.88",
"@redocly/react-dropdown-aria": "^2.0.11", "@redocly/react-dropdown-aria": "^2.0.11",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"decko": "^1.2.0", "decko": "^1.2.0",
@ -4843,7 +4841,7 @@
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"perfect-scrollbar": "^1.5.1", "perfect-scrollbar": "^1.5.1",
"polished": "^4.1.3", "polished": "^4.1.3",
"prismjs": "^1.24.1", "prismjs": "^1.27.0",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react-tabs": "^3.2.2", "react-tabs": "^3.2.2",
"slugify": "~1.4.7", "slugify": "~1.4.7",
@ -5050,21 +5048,21 @@
} }
}, },
"string-width": { "string-width": {
"version": "4.2.2", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"requires": { "requires": {
"emoji-regex": "^8.0.0", "emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0", "is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.0" "strip-ansi": "^6.0.1"
} }
}, },
"strip-ansi": { "strip-ansi": {
"version": "6.0.0", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"requires": { "requires": {
"ansi-regex": "^5.0.0" "ansi-regex": "^5.0.1"
} }
}, },
"style-loader": { "style-loader": {
@ -5378,23 +5376,23 @@
"integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==" "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A=="
}, },
"yargs": { "yargs": {
"version": "17.0.1", "version": "17.3.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.0.1.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz",
"integrity": "sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ==", "integrity": "sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA==",
"requires": { "requires": {
"cliui": "^7.0.2", "cliui": "^7.0.2",
"escalade": "^3.1.1", "escalade": "^3.1.1",
"get-caller-file": "^2.0.5", "get-caller-file": "^2.0.5",
"require-directory": "^2.1.1", "require-directory": "^2.1.1",
"string-width": "^4.2.0", "string-width": "^4.2.3",
"y18n": "^5.0.5", "y18n": "^5.0.5",
"yargs-parser": "^20.2.2" "yargs-parser": "^21.0.0"
} }
}, },
"yargs-parser": { "yargs-parser": {
"version": "20.2.7", "version": "21.0.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz",
"integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==" "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg=="
} }
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "redoc-cli", "name": "redoc-cli",
"version": "0.13.8", "version": "0.13.11",
"description": "ReDoc's Command Line Interface", "description": "ReDoc's Command Line Interface",
"main": "index.js", "main": "index.js",
"bin": "index.js", "bin": "index.js",
@ -19,9 +19,9 @@
"node-libs-browser": "^2.2.1", "node-libs-browser": "^2.2.1",
"react": "^17.0.1", "react": "^17.0.1",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"redoc": "2.0.0-rc.64", "redoc": "2.0.0-rc.67",
"styled-components": "^5.3.0", "styled-components": "^5.3.0",
"yargs": "^17.0.1" "yargs": "^17.3.1"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

View File

@ -26,6 +26,7 @@ FROM nginx:alpine
ENV PAGE_TITLE="ReDoc" ENV PAGE_TITLE="ReDoc"
ENV PAGE_FAVICON="favicon.png" ENV PAGE_FAVICON="favicon.png"
ENV BASE_PATH=
ENV SPEC_URL="http://petstore.swagger.io/v2/swagger.json" ENV SPEC_URL="http://petstore.swagger.io/v2/swagger.json"
ENV PORT=80 ENV PORT=80
ENV REDOC_OPTIONS= ENV REDOC_OPTIONS=

View File

@ -45,9 +45,10 @@ Another issue with OpenShift is that the default exposed port `80` cannot be use
- `PAGE_TITLE` (default `"ReDoc"`) - page title - `PAGE_TITLE` (default `"ReDoc"`) - page title
- `PAGE_FAVICON` (default `"favicon.png"`) - URL to page favicon - `PAGE_FAVICON` (default `"favicon.png"`) - URL to page favicon
- `BASE_PATH` (optional) - prepend favicon & standalone bundle with this path
- `SPEC_URL` (default `"http://petstore.swagger.io/v2/swagger.json"`) - URL to spec - `SPEC_URL` (default `"http://petstore.swagger.io/v2/swagger.json"`) - URL to spec
- `PORT` (default `80`) - nginx port - `PORT` (default `80`) - nginx port
- `REDOC_OPTIONS` - [`<redoc>` tag attributes](https://github.com/Redocly/redoc#redoc-tag-attributes) - `REDOC_OPTIONS` (optional) - [`<redoc>` tag attributes](https://github.com/Redocly/redoc#redoc-tag-attributes)
## Build ## Build

View File

@ -4,6 +4,7 @@ set -e
sed -i -e "s|%PAGE_TITLE%|$PAGE_TITLE|g" /usr/share/nginx/html/index.html sed -i -e "s|%PAGE_TITLE%|$PAGE_TITLE|g" /usr/share/nginx/html/index.html
sed -i -e "s|%PAGE_FAVICON%|$PAGE_FAVICON|g" /usr/share/nginx/html/index.html sed -i -e "s|%PAGE_FAVICON%|$PAGE_FAVICON|g" /usr/share/nginx/html/index.html
sed -i -e "s|%BASE_PATH%|$BASE_PATH|g" /usr/share/nginx/html/index.html
sed -i -e "s|%SPEC_URL%|$SPEC_URL|g" /usr/share/nginx/html/index.html sed -i -e "s|%SPEC_URL%|$SPEC_URL|g" /usr/share/nginx/html/index.html
sed -i -e "s|%REDOC_OPTIONS%|${REDOC_OPTIONS}|g" /usr/share/nginx/html/index.html sed -i -e "s|%REDOC_OPTIONS%|${REDOC_OPTIONS}|g" /usr/share/nginx/html/index.html
sed -i -e "s|\(listen\s*\) [0-9]*|\1 ${PORT}|g" /etc/nginx/nginx.conf sed -i -e "s|\(listen\s*\) [0-9]*|\1 ${PORT}|g" /etc/nginx/nginx.conf

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>%PAGE_TITLE%</title> <title>%PAGE_TITLE%</title>
<link rel="icon" href="%PAGE_FAVICON%" /> <link rel="icon" href="%BASE_PATH%%PAGE_FAVICON%" />
<style> <style>
body { body {
margin: 0; margin: 0;
@ -23,6 +23,6 @@
<body> <body>
<redoc spec-url="%SPEC_URL%" %REDOC_OPTIONS%></redoc> <redoc spec-url="%SPEC_URL%" %REDOC_OPTIONS%></redoc>
<script src="redoc.standalone.js"></script> <script src="%BASE_PATH%redoc.standalone.js"></script>
</body> </body>
</html> </html>

View File

@ -1,7 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { render } from 'react-dom'; import { render } from 'react-dom';
import styled from 'styled-components'; import styled from 'styled-components';
import { resolve as urlResolve } from 'url';
import { RedocStandalone } from '../src'; import { RedocStandalone } from '../src';
import ComboBox from './ComboBox'; import ComboBox from './ComboBox';
import FileInput from './components/FileInput'; import FileInput from './components/FileInput';
@ -87,7 +86,7 @@ class DemoApp extends React.Component<
let proxiedUrl = specUrl; let proxiedUrl = specUrl;
if (specUrl !== DEFAULT_SPEC) { if (specUrl !== DEFAULT_SPEC) {
proxiedUrl = cors proxiedUrl = cors
? '\\\\cors.redoc.ly/' + urlResolve(window.location.href, specUrl) ? '\\\\cors.redoc.ly/' + new URL(specUrl, window.location.href).href
: specUrl; : specUrl;
} }
return ( return (

View File

@ -93,7 +93,7 @@ paths:
parameters: parameters:
- name: Accept-Language - name: Accept-Language
in: header in: header
description: "The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US" description: 'The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US'
example: en-US example: en-US
required: false required: false
schema: schema:
@ -182,6 +182,16 @@ paths:
} }
requestBody: requestBody:
$ref: '#/components/requestBodies/Pet' $ref: '#/components/requestBodies/Pet'
delete:
tags:
- pet
summary: OperationId with quotes
operationId: deletePetBy"Id
get:
tags:
- pet
summary: OperationId with backslash
operationId: delete\PetById
'/pet/{petId}': '/pet/{petId}':
get: get:
tags: tags:
@ -259,7 +269,7 @@ paths:
required: false required: false
schema: schema:
type: string type: string
example: "Bearer <TOKEN>" example: 'Bearer <TOKEN>'
- name: petId - name: petId
in: path in: path
description: Pet id to delete description: Pet id to delete
@ -295,6 +305,9 @@ paths:
content: content:
application/json: application/json:
schema: schema:
unevaluatedProperties:
type: integer
format: int32
$ref: '#/components/schemas/ApiResponse' $ref: '#/components/schemas/ApiResponse'
security: security:
- petstore_auth: - petstore_auth:
@ -432,7 +445,7 @@ paths:
application/json: application/json:
example: example:
status: 400 status: 400
message: "Invalid Order" message: 'Invalid Order'
requestBody: requestBody:
content: content:
application/json: application/json:
@ -894,11 +907,11 @@ paths:
type: string type: string
examples: examples:
response: response:
value: <Message> OK </Message> value: <Message> OK </Message>
text/plain: text/plain:
examples: examples:
response: response:
value: OK value: OK
'400': '400':
description: Invalid username/password supplied description: Invalid username/password supplied
/user/logout: /user/logout:
@ -925,7 +938,7 @@ components:
content: content:
multipart/form-data: multipart/form-data:
schema: schema:
$ref: "#/components/schemas/Cat" $ref: '#/components/schemas/Cat'
responses: responses:
'200': '200':
description: update Cat details description: update Cat details
@ -940,7 +953,7 @@ components:
content: content:
multipart/form-data: multipart/form-data:
schema: schema:
$ref: "#/components/schemas/Cat" $ref: '#/components/schemas/Cat'
responses: responses:
'200': '200':
description: create Cat details description: create Cat details
@ -1073,8 +1086,8 @@ components:
properties: properties:
id: id:
externalDocs: externalDocs:
description: "Find more info here" description: 'Find more info here'
url: "https://example.com" url: 'https://example.com'
description: Pet ID description: Pet ID
$ref: '#/components/schemas/Id' $ref: '#/components/schemas/Id'
category: category:
@ -1251,9 +1264,9 @@ webhooks:
content: content:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/Pet" $ref: '#/components/schemas/Pet'
responses: responses:
"200": '200':
description: Return a 200 status to indicate that the data was received successfully description: Return a 200 status to indicate that the data was received successfully
myWebhook: myWebhook:
$ref: '#/components/pathItems/webhooks' $ref: '#/components/pathItems/webhooks'

View File

@ -46,12 +46,12 @@ section in the documentation.
If you want to view your Redoc output locally, you can simulate an HTTP server. If you want to view your Redoc output locally, you can simulate an HTTP server.
#### Redocly OpenAPI CLI #### Redocly CLI
Redocly OpenAPI CLI is an open source command-line tool that includes a command Redocly CLI is an open source command-line tool that includes a command
for simulating an HTTP server to provide a preview of your OpenAPI definition locally. for simulating an HTTP server to provide a preview of your OpenAPI definition locally.
If you have [OpenAPI CLI](https://redocly.com/docs/cli/#installation-and-usage) installed, `cd` into your If you have [Redocly CLI](https://redocly.com/docs/cli/#installation-and-usage) installed, `cd` into your
project directory and run the following command: project directory and run the following command:
```bash ```bash
@ -72,7 +72,7 @@ openapi preview-docs -p 8888 openapi.yaml
Replace `openapi.yaml` in the example command with the file path to your OpenAPI definition. Replace `openapi.yaml` in the example command with the file path to your OpenAPI definition.
For more information about the `preview-docs` command, refer to For more information about the `preview-docs` command, refer to
[OpenAPI CLI commands](https://redocly.com/docs/cli/commands/preview-docs/#preview-docs) in the OpenAPI CLI documentation. [Redocly CLI commands](https://redocly.com/docs/cli/commands/preview-docs/#preview-docs) in the Redocly CLI documentation.
#### Python #### Python

View File

@ -44,12 +44,12 @@ other security reasons.
If you want to view your Redoc output locally, you can simulate an HTTP server. If you want to view your Redoc output locally, you can simulate an HTTP server.
#### Using Redocly OpenAPI CLI #### Using Redocly CLI
Redocly OpenAPI CLI is an open source command-line tool that includes a command Redocly CLI is an open source command-line tool that includes a command
for simulating an HTTP server to provide a preview of your OpenAPI definition locally. for simulating an HTTP server to provide a preview of your OpenAPI definition locally.
If you have [OpenAPI CLI](https://redocly.com/docs/cli/#installation-and-usage) installed, `cd` into your If you have [Redocly CLI](https://redocly.com/docs/cli/#installation-and-usage) installed, `cd` into your
project directory and run the following command: project directory and run the following command:
```bash ```bash

View File

@ -1,24 +0,0 @@
# Usage With IE11
## Standalone package
IE11 is supported by default if you use ReDoc as a standalone package.
## Usage as a React component
If you use ReDoc as a React component you should include the following polyfills in your project:
```js
import 'core-js/es6/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/es6/symbol';
import 'unfetch/polyfill/index'; // or any other fetch polyfill
import 'url-polyfill';
```

View File

@ -49,7 +49,7 @@ describe('Menu', () => {
cy.location('hash').should('equal', '#tag/pet'); cy.location('hash').should('equal', '#tag/pet');
cy.contains('[role=menuitem]', 'Find pet by ID').click({ force: true }); cy.contains('[role=menuitem]', 'Find pet by ID').click({ force: true });
cy.location('hash').should('equal', '#operation/getPetById'); cy.location('hash').should('equal', '#tag/pet/operation/getPetById');
}); });
it('should deactivate tag when other is activated', () => { it('should deactivate tag when other is activated', () => {
@ -76,4 +76,20 @@ describe('Menu', () => {
.then($h5 => $h5[0].firstChild!.nodeValue!.trim()) .then($h5 => $h5[0].firstChild!.nodeValue!.trim())
.should('eq', 'Response Schema:'); .should('eq', 'Response Schema:');
}); });
it('should be able to open the operation details when the operation IDs have quotes', () => {
cy.visit('e2e/standalone-3-1.html');
cy.get('label span[title="pet"]').click({ multiple: true, force: true });
cy.get('li').contains('OperationId with quotes').click({ multiple: true, force: true });
cy.get('h2').contains('OperationId with quotes').should('be.visible');
cy.url().should('include', 'deletePetBy%22Id');
});
it.only('should encode URL when the operation IDs have backslashes', () => {
cy.visit('e2e/standalone-3-1.html');
cy.get('label span[title="pet"]').click({ multiple: true, force: true });
cy.get('li').contains('OperationId with backslash').click({ multiple: true, force: true });
cy.get('h2').contains('OperationId with backslash').should('be.visible');
cy.url().should('include', 'delete%5CPetById');
});
}); });

View File

@ -21,12 +21,12 @@ describe('Servers', () => {
initReDoc(win, spec, {}); initReDoc(win, spec, {});
// TODO add cy-data attributes // TODO add cy-data attributes
cy.get('[data-section-id="operation/addPet"]').should( cy.get('[data-section-id="tag/pet/operation/addPet"]').should(
'contain', 'contain',
'http://petstore.swagger.io/v2/pet', 'http://petstore.swagger.io/v2/pet',
); );
cy.get('[data-section-id="operation/addPet"]').should( cy.get('[data-section-id="tag/pet/operation/addPet"]').should(
'contain', 'contain',
'http://petstore.swagger.io/sandbox/pet', 'http://petstore.swagger.io/sandbox/pet',
); );
@ -40,7 +40,7 @@ describe('Servers', () => {
initReDoc(win, spec, {}); initReDoc(win, spec, {});
// TODO add cy-data attributes // TODO add cy-data attributes
cy.get('[data-section-id="operation/addPet"]').should( cy.get('[data-section-id="tag/pet/operation/addPet"]').should(
'contain', 'contain',
'http://localhost:' + win.location.port + '/pet', 'http://localhost:' + win.location.port + '/pet',
); );
@ -55,7 +55,7 @@ describe('Servers', () => {
initReDoc(win, spec, {}); initReDoc(win, spec, {});
// TODO add cy-data attributes // TODO add cy-data attributes
cy.get('[data-section-id="operation/addPet"]').should( cy.get('[data-section-id="tag/pet/operation/addPet"]').should(
'contain', 'contain',
'http://localhost:' + win.location.port + '/pet', 'http://localhost:' + win.location.port + '/pet',
); );

View File

@ -0,0 +1,19 @@
describe('Supporting both operation/* and parent/*/operation* urls', () => {
beforeEach(() => {
cy.visit('e2e/standalone.html');
});
it('should supporting operation/* url', () => {
cy.url().then(loc => {
cy.visit(loc + '#operation/updatePet');
cy.get('li[data-item-id="tag/pet/operation/updatePet"]').should('be.visible');
});
});
it('should supporting parent/*/operation url', () => {
cy.url().then(loc => {
cy.visit(loc + '#tag/pet/operation/addPet');
cy.get('li[data-item-id="tag/pet/operation/addPet"]').should('be.visible');
});
});
});

357
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,13 @@
{ {
"name": "redoc", "name": "redoc",
"version": "2.0.0-rc.65", "version": "2.0.0-rc.68",
"description": "ReDoc", "description": "ReDoc",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/Redocly/redoc" "url": "git://github.com/Redocly/redoc"
}, },
"browserslist": [ "browserslist": [
"defaults", "defaults"
"ie 11"
], ],
"engines": { "engines": {
"node": ">=6.9", "node": ">=6.9",
@ -33,7 +32,7 @@
"start": "webpack serve --mode=development --env playground --hot --config demo/webpack.config.ts", "start": "webpack serve --mode=development --env playground --hot --config demo/webpack.config.ts",
"start:prod": "webpack serve --env playground --mode=production --config demo/webpack.config.ts", "start:prod": "webpack serve --env playground --mode=production --config demo/webpack.config.ts",
"start:benchmark": "webpack serve --mode=production --env.bench --config demo/webpack.config.ts", "start:benchmark": "webpack serve --mode=production --env.bench --config demo/webpack.config.ts",
"test": "npm run lint && npm run unit && npm run license-check", "test": "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",
@ -49,13 +48,13 @@
"stats": "webpack --env production --env standalone --json --profile --mode=production > stats.json", "stats": "webpack --env production --env standalone --json --profile --mode=production > stats.json",
"prettier": "prettier --write \"cli/index.ts\" \"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": "eslint 'src/**/*.{js,ts,tsx}' --cache", "lint": "eslint --fix 'src/**/*.{js,ts,tsx}' --cache",
"benchmark": "node ./benchmark/benchmark.js", "benchmark": "node ./benchmark/benchmark.js",
"start:demo": "webpack serve --hot --config demo/webpack.config.ts --mode=development", "start:demo": "webpack serve --hot --config demo/webpack.config.ts --mode=development",
"compile:cli": "tsc custom.d.ts cli/index.ts --target es6 --module commonjs --types yargs", "compile:cli": "tsc custom.d.ts cli/index.ts --target es6 --module commonjs --types yargs",
"build:demo": "webpack --mode=production --config demo/webpack.config.ts", "build:demo": "webpack --mode=production --config demo/webpack.config.ts",
"deploy:demo": "aws s3 sync demo/dist s3://production-redoc-demo --acl=public-read", "deploy:demo": "aws s3 sync demo/dist s3://production-redoc-demo --acl=public-read",
"license-check": "license-checker --production --onlyAllow 'MIT;ISC;Apache-2.0;BSD;BSD-2-Clause;BSD-3-Clause;CC-BY-4.0' --summary", "license-check": "license-checker --production --onlyAllow 'MIT;ISC;Apache-2.0;BSD;BSD-2-Clause;BSD-3-Clause;CC-BY-4.0;Python-2.0' --summary",
"docker:build": "docker build -f config/docker/Dockerfile -t redoc .", "docker:build": "docker build -f config/docker/Dockerfile -t redoc .",
"prepare": "husky install", "prepare": "husky install",
"pre-commit": "pretty-quick --staged && npm run lint" "pre-commit": "pretty-quick --staged && npm run lint"
@ -72,7 +71,7 @@
"@types/json-pointer": "^1.0.30", "@types/json-pointer": "^1.0.30",
"@types/lunr": "^2.3.3", "@types/lunr": "^2.3.3",
"@types/mark.js": "^8.11.5", "@types/mark.js": "^8.11.5",
"@types/marked": "^4.0.1", "@types/marked": "^4.0.3",
"@types/node": "^15.6.1", "@types/node": "^15.6.1",
"@types/prismjs": "^1.16.5", "@types/prismjs": "^1.16.5",
"@types/prop-types": "^15.7.3", "@types/prop-types": "^15.7.3",
@ -139,8 +138,7 @@
"styled-components": "^4.1.1 || ^5.1.1" "styled-components": "^4.1.1 || ^5.1.1"
}, },
"dependencies": { "dependencies": {
"@redocly/openapi-core": "^1.0.0-beta.54", "@redocly/openapi-core": "^1.0.0-beta.95",
"@redocly/react-dropdown-aria": "^2.0.11",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"decko": "^1.2.0", "decko": "^1.2.0",
"dompurify": "^2.2.8", "dompurify": "^2.2.8",
@ -148,7 +146,7 @@
"json-pointer": "^0.6.2", "json-pointer": "^0.6.2",
"lunr": "^2.3.9", "lunr": "^2.3.9",
"mark.js": "^8.11.1", "mark.js": "^8.11.1",
"marked": "^4.0.10", "marked": "^4.0.15",
"mobx-react": "^7.2.0", "mobx-react": "^7.2.0",
"openapi-sampler": "^1.2.1", "openapi-sampler": "^1.2.1",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",

View File

@ -0,0 +1,68 @@
import * as React from 'react';
import styled from '../../styled-components';
import { ArrowIconProps, DropdownProps, DropdownOption } from './types';
const ArrowSvg = ({ className, style }: ArrowIconProps): JSX.Element => (
<svg
className={className}
style={style}
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="6 9 12 15 18 9" />
</svg>
);
const ArrowIcon = styled(ArrowSvg)`
position: absolute;
pointer-events: none;
z-index: 1;
top: 50%;
-webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%);
transform: translateY(-50%);
right: 8px;
margin: auto;
text-align: center;
polyline {
color: ${props => props.variant === 'dark' && 'white'};
}
`;
const DropdownComponent = (props: DropdownProps): JSX.Element => {
const { options, onChange, placeholder, value = '', variant, className } = props;
const handleOnChange = event => {
const { selectedIndex } = event.target;
const index = placeholder ? selectedIndex - 1 : selectedIndex;
onChange(options[index]);
};
return (
<div className={className}>
<ArrowIcon variant={variant} />
<select onChange={handleOnChange} value={value} className="dropdown-select">
{placeholder && (
<option disabled hidden value={placeholder}>
{placeholder}
</option>
)}
{options.map(({ idx, value, title }: DropdownOption, index) => (
<option key={idx || value + index} value={value}>
{title || value}
</option>
))}
</select>
<label>{value}</label>
</div>
);
};
export const Dropdown = React.memo<DropdownProps>(DropdownComponent);

View File

@ -0,0 +1,2 @@
export * from './styled';
export * from './types';

View File

@ -0,0 +1,94 @@
import styled from 'styled-components';
import { Dropdown as DropdownComponent } from './Dropdown';
export const Dropdown = styled(DropdownComponent)<{
fullWidth?: boolean;
}>`
label {
box-sizing: border-box;
min-width: 100px;
outline: none;
display: inline-block;
font-family: ${props => props.theme.typography.headings.fontFamily};
color: ${({ theme }) => theme.colors.text.primary};
vertical-align: bottom;
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
text-transform: none;
padding: 0 22px 0 4px;
font-size: 0.929em;
line-height: 1.5em;
font-family: inherit;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.dropdown-select {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
border: none;
appearance: none;
cursor: pointer;
color: ${({ theme }) => theme.colors.text.primary};
line-height: inherit;
font-family: inherit;
}
box-sizing: border-box;
min-width: 100px;
outline: none;
display: inline-block;
border-radius: 2px;
border: 1px solid rgba(38, 50, 56, 0.5);
vertical-align: bottom;
padding: 2px 0px 2px 6px;
position: relative;
width: auto;
background: white;
color: #263238;
font-family: ${props => props.theme.typography.headings.fontFamily};
font-size: 0.929em;
line-height: 1.5em;
cursor: pointer;
transition: border 0.25s ease, color 0.25s ease, box-shadow 0.25s ease;
&:hover,
&:focus-within {
border: 1px solid ${props => props.theme.colors.primary.main};
color: ${props => props.theme.colors.primary.main};
box-shadow: 0px 0px 0px 1px ${props => props.theme.colors.primary.main};
}
`;
export const SimpleDropdown = styled(Dropdown)`
margin-left: 10px;
text-transform: none;
font-size: 0.969em;
font-size: 1em;
border: none;
padding: 0 1.2em 0 0;
background: transparent;
&:hover,
&:focus-within {
border: none;
box-shadow: none;
label {
color: ${props => props.theme.colors.primary.main};
text-shadow: 0px 0px 0px ${props => props.theme.colors.primary.main};
}
}
`;
export const MimeLabel = styled.span`
margin-left: 10px;
text-transform: none;
font-size: 0.929em;
color: black;
`;

View File

@ -0,0 +1,25 @@
export interface DropdownOption {
idx?: number;
value: string;
title?: string;
serverUrl?: string;
label?: string;
}
export interface DropdownProps {
options: DropdownOption[];
onChange: (option: DropdownOption) => void;
ariaLabel?: string;
className?: string;
placeholder?: string;
value?: string;
dense?: boolean;
fullWidth?: boolean;
variant?: 'dark' | 'light';
}
export interface ArrowIconProps {
className?: string;
variant?: 'light' | 'dark';
style?: React.CSSProperties;
}

View File

@ -1,143 +0,0 @@
import Dropdown from '@redocly/react-dropdown-aria';
import styled from '../styled-components';
export interface DropdownOption {
idx: number;
value: string;
}
export interface DropdownProps {
options: DropdownOption[];
value: string;
onChange: (option: DropdownOption) => void;
ariaLabel: string;
}
export const StyledDropdown = styled(Dropdown)`
&& {
box-sizing: border-box;
min-width: 100px;
outline: none;
display: inline-block;
border-radius: 2px;
border: 1px solid rgba(38, 50, 56, 0.5);
vertical-align: bottom;
padding: 2px 0px 2px 6px;
position: relative;
width: auto;
background: white;
color: #263238;
font-family: ${props => props.theme.typography.headings.fontFamily};
font-size: 0.929em;
line-height: 1.5em;
cursor: pointer;
transition: border 0.25s ease, color 0.25s ease, box-shadow 0.25s ease;
&:hover,
&:focus-within {
border: 1px solid ${props => props.theme.colors.primary.main};
color: ${props => props.theme.colors.primary.main};
box-shadow: 0px 0px 0px 1px ${props => props.theme.colors.primary.main};
}
.dropdown-selector {
display: inline-flex;
padding: 0;
height: auto;
padding-right: 20px;
position: relative;
margin-bottom: 5px;
}
.dropdown-selector-value {
font-family: ${props => props.theme.typography.headings.fontFamily};
position: relative;
font-size: 0.929em;
width: 100%;
line-height: 1;
vertical-align: middle;
color: #263238;
left: 0;
transition: color 0.25s ease, text-shadow 0.25s ease;
}
.dropdown-arrow {
position: absolute;
right: 3px;
top: 50%;
transform: translateY(-50%);
border-color: ${props => props.theme.colors.primary.main} transparent transparent;
border-style: solid;
border-width: 0.35em 0.35em 0;
width: 0;
svg {
display: none;
}
}
.dropdown-selector-content {
position: absolute;
margin-top: 2px;
left: -2px;
right: 0;
z-index: 10;
min-width: 100px;
background: white;
border: 1px solid rgba(38, 50, 56, 0.2);
box-shadow: 0px 2px 4px 0px rgba(34, 36, 38, 0.12), 0px 2px 10px 0px rgba(34, 36, 38, 0.08);
max-height: 220px;
overflow: auto;
}
.dropdown-option {
font-size: 0.9em;
color: #263238;
cursor: pointer;
padding: 0.4em;
background-color: #ffffff;
&[aria-selected='true'] {
background-color: rgba(0, 0, 0, 0.05);
}
&:hover {
background-color: rgba(38, 50, 56, 0.12);
}
}
input {
cursor: pointer;
height: 1px;
background-color: transparent;
}
}
`;
export const SimpleDropdown = styled(StyledDropdown)`
&& {
margin-left: 10px;
text-transform: none;
font-size: 0.969em;
font-size: 1em;
border: none;
padding: 0 1.2em 0 0;
background: transparent;
&:hover,
&:focus-within {
border: none;
box-shadow: none;
.dropdown-selector-value {
color: ${props => props.theme.colors.primary.main};
text-shadow: 0px 0px 0px ${props => props.theme.colors.primary.main};
}
}
}
`;
export const MimeLabel = styled.span`
margin-left: 10px;
text-transform: none;
font-size: 0.929em;
color: black;
`;

View File

@ -71,6 +71,7 @@ export const PatternLabel = styled(FieldLabel)`
export const ExampleValue = styled(FieldLabel)` export const ExampleValue = styled(FieldLabel)`
border-radius: 2px; border-radius: 2px;
word-break: break-word;
${({ theme }) => ` ${({ theme }) => `
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)};

View File

@ -15,21 +15,22 @@ export const headerCommonMixin = level => css`
export const H1 = styled.h1` export const H1 = styled.h1`
${headerCommonMixin(1)}; ${headerCommonMixin(1)};
color: ${({ theme }) => theme.colors.primary.main}; color: ${({ theme }) => theme.colors.text.primary};
${extensionsHook('H1')}; ${extensionsHook('H1')};
`; `;
export const H2 = styled.h2` export const H2 = styled.h2`
${headerCommonMixin(2)}; ${headerCommonMixin(2)};
color: black; color: ${({ theme }) => theme.colors.text.primary};
margin: 0 0 20px;
${extensionsHook('H2')}; ${extensionsHook('H2')};
`; `;
export const H3 = styled.h2` export const H3 = styled.h2`
${headerCommonMixin(3)}; ${headerCommonMixin(3)};
color: black; color: ${({ theme }) => theme.colors.text.primary};
${extensionsHook('H3')}; ${extensionsHook('H3')};
`; `;

View File

@ -4,8 +4,8 @@ export * from './linkify';
export * from './shelfs'; export * from './shelfs';
export * from './fields-layout'; export * from './fields-layout';
export * from './schema'; export * from './schema';
export * from './dropdown';
export * from './mixins'; export * from './mixins';
export * from './tabs'; export * from './tabs';
export * from './samples'; export * from './samples';
export * from './perfect-scrollbar'; export * from './perfect-scrollbar';
export * from './Dropdown';

View File

@ -67,7 +67,7 @@ function navigate(history: HistoryService, event: React.MouseEvent<HTMLAnchorEle
!isModifiedEvent(event) // ignore clicks with modifier keys !isModifiedEvent(event) // ignore clicks with modifier keys
) { ) {
event.preventDefault(); event.preventDefault();
history.replace(to); history.replace(encodeURI(to));
} }
} }

View File

@ -21,7 +21,14 @@ export class CallbackSamples extends React.Component<CallbackSamplesProps> {
context: RedocNormalizedOptions; context: RedocNormalizedOptions;
private renderDropdown = props => { private renderDropdown = props => {
return <DropdownOrLabel Label={MimeLabel} Dropdown={InvertedSimpleDropdown} {...props} />; return (
<DropdownOrLabel
Label={MimeLabel}
Dropdown={InvertedSimpleDropdown}
{...props}
variant="dark"
/>
);
}; };
render() { render() {

View File

@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { DropdownProps, MimeLabel, SimpleDropdown } from '../../common-elements/dropdown'; import { DropdownProps, MimeLabel, SimpleDropdown } from '../../common-elements/Dropdown';
export interface DropdownOrLabelProps extends DropdownProps { export interface DropdownOrLabelProps extends DropdownProps {
Label?: React.ComponentClass; Label?: React.ComponentClass;
@ -12,5 +12,5 @@ export function DropdownOrLabel(props: DropdownOrLabelProps): JSX.Element {
if (props.options.length === 1) { if (props.options.length === 1) {
return <Label>{props.options[0].value}</Label>; return <Label>{props.options[0].value}</Label>;
} }
return <Dropdown {...props} searchable={false} />; return <Dropdown {...props} />;
} }

View File

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { FieldLabel, ExampleValue } from '../../common-elements/fields'; import { FieldLabel, ExampleValue } from '../../common-elements/fields';
import { getSerializedValue } from '../../utils'; import { getSerializedValue, isArray } from '../../utils';
import { l } from '../../services/Labels'; import { l } from '../../services/Labels';
import { FieldModel } from '../../services'; import { FieldModel } from '../../services';
@ -15,22 +15,31 @@ export function Examples({ field }: { field: FieldModel }) {
return ( return (
<> <>
<FieldLabel> {l('examples')}: </FieldLabel> <FieldLabel> {l('examples')}: </FieldLabel>
<ExamplesList> {isArray(field.examples) ? (
{Object.values(field.examples).map((example, idx) => { field.examples.map((example, idx) => {
const value = getSerializedValue(field, example);
const stringifyValue = field.in ? String(value) : JSON.stringify(value);
return ( return (
<li key={idx}> <React.Fragment key={idx}>
<ExampleValue>{stringifyValue}</ExampleValue>{' '}
</React.Fragment>
);
})
) : (
<ExamplesList>
{Object.values(field.examples).map((example, idx) => (
<li key={idx + example.value}>
<ExampleValue>{getSerializedValue(field, example.value)}</ExampleValue> -{' '} <ExampleValue>{getSerializedValue(field, example.value)}</ExampleValue> -{' '}
{example.summary || example.description} {example.summary || example.description}
</li> </li>
); ))}
})} </ExamplesList>
</ExamplesList> )}
</> </>
); );
} }
const ExamplesList = styled.ul` const ExamplesList = styled.ul`
margin-top: 1em; margin-top: 1em;
padding-left: 0; list-style-position: outside;
list-style-position: inside;
`; `;

View File

@ -1,7 +1,7 @@
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import * as React from 'react'; import * as React from 'react';
import { DropdownProps, DropdownOption } from '../../common-elements/dropdown'; import { DropdownProps, DropdownOption } from '../../common-elements/Dropdown';
import { DropdownLabel, DropdownWrapper } from '../PayloadSamples/styled.elements'; import { DropdownLabel, DropdownWrapper } from '../PayloadSamples/styled.elements';
export interface GenericChildrenSwitcherProps<T> { export interface GenericChildrenSwitcherProps<T> {
@ -32,8 +32,8 @@ export class GenericChildrenSwitcher<T> extends React.Component<
}; };
} }
switchItem = ({ idx }) => { switchItem = ({ idx }: DropdownOption) => {
if (this.props.items) { if (this.props.items && idx !== undefined) {
this.setState({ this.setState({
activeItemIdx: idx, activeItemIdx: idx,
}); });

View File

@ -26,27 +26,37 @@ class Json extends React.PureComponent<JsonProps> {
return <CopyButtonWrapper data={this.props.data}>{this.renderInner}</CopyButtonWrapper>; return <CopyButtonWrapper data={this.props.data}>{this.renderInner}</CopyButtonWrapper>;
} }
renderInner = ({ renderCopyButton }) => ( renderInner = ({ renderCopyButton }) => {
<JsonViewerWrap> const showFoldingButtons = this.props.data && Object.values(this.props.data).some(
<SampleControls> (value) => typeof value === 'object' && value !== null,
{renderCopyButton()} );
<button onClick={this.expandAll}> Expand all </button>
<button onClick={this.collapseAll}> Collapse all </button> return (
</SampleControls> <JsonViewerWrap>
<OptionsContext.Consumer> <SampleControls>
{options => ( {renderCopyButton()}
<PrismDiv {showFoldingButtons &&
className={this.props.className} <>
// tslint:disable-next-line <button onClick={this.expandAll}> Expand all </button>
ref={node => (this.node = node!)} <button onClick={this.collapseAll}> Collapse all </button>
dangerouslySetInnerHTML={{ </>
__html: jsonToHTML(this.props.data, options.jsonSampleExpandLevel), }
}} </SampleControls>
/> <OptionsContext.Consumer>
)} {options => (
</OptionsContext.Consumer> <PrismDiv
</JsonViewerWrap> className={this.props.className}
); // tslint:disable-next-line
ref={node => (this.node = node!)}
dangerouslySetInnerHTML={{
__html: jsonToHTML(this.props.data, options.jsonSampleExpandLevel),
}}
/>
)}
</OptionsContext.Consumer>
</JsonViewerWrap>
);
};
expandAll = () => { expandAll = () => {
const elements = this.node.getElementsByClassName('collapsible'); const elements = this.node.getElementsByClassName('collapsible');

View File

@ -1,7 +1,7 @@
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import * as React from 'react'; import * as React from 'react';
import { DropdownProps } from '../../common-elements/dropdown'; import { DropdownOption, 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'; import { DropdownLabel, DropdownWrapper } from '../PayloadSamples/styled.elements';
@ -20,8 +20,8 @@ export interface MediaTypesSwitchProps {
@observer @observer
export class MediaTypesSwitch extends React.Component<MediaTypesSwitchProps> { export class MediaTypesSwitch extends React.Component<MediaTypesSwitchProps> {
switchMedia = ({ idx }) => { switchMedia = ({ idx }: DropdownOption) => {
if (this.props.content) { if (this.props.content && idx !== undefined) {
this.props.content.activate(idx); this.props.content.activate(idx);
} }
}; };

View File

@ -17,6 +17,7 @@ import { RequestSamples } from '../RequestSamples/RequestSamples';
import { ResponsesList } from '../Responses/ResponsesList'; import { ResponsesList } from '../Responses/ResponsesList';
import { ResponseSamples } from '../ResponseSamples/ResponseSamples'; import { ResponseSamples } from '../ResponseSamples/ResponseSamples';
import { SecurityRequirements } from '../SecurityRequirement/SecurityRequirement'; import { SecurityRequirements } from '../SecurityRequirement/SecurityRequirement';
import { SECTION_ATTR } from '../../services';
const Description = styled.div` const Description = styled.div`
margin-bottom: ${({ theme }) => theme.spacing.unit * 6}px; margin-bottom: ${({ theme }) => theme.spacing.unit * 6}px;
@ -37,7 +38,7 @@ export class Operation extends React.Component<OperationProps> {
return ( return (
<OptionsContext.Consumer> <OptionsContext.Consumer>
{options => ( {options => (
<Row> <Row {...{ [SECTION_ATTR]: operation.operationHash }} id={operation.operationHash}>
<MiddlePanel> <MiddlePanel>
<H2> <H2>
<ShareLink to={operation.id} /> <ShareLink to={operation.id} />

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import styled from '../../styled-components'; import styled from '../../styled-components';
import { DropdownProps } from '../../common-elements'; import { DropdownOption, DropdownProps } from '../../common-elements';
import { MediaTypeModel } from '../../services/models'; import { MediaTypeModel } from '../../services/models';
import { Markdown } from '../Markdown/Markdown'; import { Markdown } from '../Markdown/Markdown';
import { Example } from './Example'; import { Example } from './Example';
@ -21,10 +21,12 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps, Media
state = { state = {
activeIdx: 0, activeIdx: 0,
}; };
switchMedia = ({ idx }) => { switchMedia = ({ idx }: DropdownOption) => {
this.setState({ if (idx !== undefined) {
activeIdx: idx, this.setState({
}); activeIdx: idx,
});
}
}; };
render() { render() {
const { activeIdx } = this.state; const { activeIdx } = this.state;

View File

@ -33,6 +33,13 @@ export class PayloadSamples extends React.Component<PayloadSamplesProps> {
} }
private renderDropdown = props => { private renderDropdown = props => {
return <DropdownOrLabel Label={MimeLabel} Dropdown={InvertedSimpleDropdown} {...props} />; return (
<DropdownOrLabel
Label={MimeLabel}
Dropdown={InvertedSimpleDropdown}
{...props}
variant="dark"
/>
);
}; };
} }

View File

@ -1,6 +1,6 @@
import { transparentize } from 'polished'; import { transparentize } from 'polished';
import styled from '../../styled-components'; import styled from '../../styled-components';
import { StyledDropdown } from '../../common-elements'; import { Dropdown } from '../../common-elements/Dropdown';
export const MimeLabel = styled.div` export const MimeLabel = styled.div`
padding: 0.9em; padding: 0.9em;
@ -27,46 +27,27 @@ export const DropdownWrapper = styled.div`
position: relative; position: relative;
`; `;
export const InvertedSimpleDropdown = styled(StyledDropdown)` export const InvertedSimpleDropdown = styled(Dropdown)`
&& { label {
margin-left: 10px; color: ${({ theme }) => theme.rightPanel.textColor};
text-transform: none; text-overflow: ellipsis;
font-size: 0.929em; white-space: nowrap;
margin: 0 0 10px 0; overflow: hidden;
display: block;
background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)};
font-size: 1em; font-size: 1em;
text-transform: none;
border: none;
}
margin: 0 0 10px 0;
display: block;
background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)};
border: none;
padding: 0.9em 1.6em 0.9em 0.9em;
box-shadow: none;
&:hover,
&:focus-within {
border: none; border: none;
padding: 0.9em 1.6em 0.9em 0.9em;
box-shadow: none; box-shadow: none;
&:hover, background-color: ${({ theme }) => transparentize(0.3, theme.rightPanel.backgroundColor)};
&:focus-within {
border: none;
box-shadow: none;
}
&:focus-within {
background-color: ${({ theme }) => transparentize(0.3, theme.rightPanel.backgroundColor)};
}
.dropdown-arrow {
border-top-color: ${({ theme }) => theme.rightPanel.textColor};
}
.dropdown-selector-value {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
color: ${({ theme }) => theme.rightPanel.textColor};
}
.dropdown-selector-content {
margin: 0;
margin-top: 2px;
.dropdown-option {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
} }
`; `;

View File

@ -17,12 +17,21 @@ export interface RedocStandaloneProps {
onLoaded?: (e?: Error) => any; onLoaded?: (e?: Error) => any;
} }
declare let __webpack_nonce__: string;
export const RedocStandalone = function (props: RedocStandaloneProps) { export const RedocStandalone = function (props: RedocStandaloneProps) {
const { spec, specUrl, options = {}, onLoaded } = props; const { spec, specUrl, options = {}, onLoaded } = props;
const hideLoading = argValueToBoolean(options.hideLoading, false); const hideLoading = argValueToBoolean(options.hideLoading, false);
const normalizedOpts = new RedocNormalizedOptions(options); const normalizedOpts = new RedocNormalizedOptions(options);
if (normalizedOpts.nonce !== undefined) {
try {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__webpack_nonce__ = normalizedOpts.nonce;
} catch {} // If we have exception, Webpack was not used to run this.
}
return ( return (
<ErrorBoundary> <ErrorBoundary>
<StoreBuilder spec={spec} specUrl={specUrl} options={options} onLoaded={onLoaded}> <StoreBuilder spec={spec} specUrl={specUrl} options={options} onLoaded={onLoaded}>

View File

@ -1,7 +1,7 @@
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import * as React from 'react'; import * as React from 'react';
import { DropdownOption, StyledDropdown } from '../../common-elements/dropdown'; import { DropdownOption, Dropdown } from '../../common-elements/Dropdown';
import { SchemaModel } from '../../services/models'; import { SchemaModel } from '../../services/models';
@observer @observer
@ -43,7 +43,7 @@ export class DiscriminatorDropdown extends React.Component<{
this.sortOptions(options, enumValues); this.sortOptions(options, enumValues);
return ( return (
<StyledDropdown <Dropdown
value={activeValue} value={activeValue}
options={options} options={options}
onChange={this.changeActiveChild} onChange={this.changeActiveChild}
@ -53,6 +53,8 @@ export class DiscriminatorDropdown extends React.Component<{
} }
changeActiveChild = (option: DropdownOption) => { changeActiveChild = (option: DropdownOption) => {
this.props.parent.activateOneOf(option.idx); if (option.idx !== undefined) {
this.props.parent.activateOneOf(option.idx);
}
}; };
} }

View File

@ -11,6 +11,7 @@ import { ObjectSchema } from './ObjectSchema';
import { OneOfSchema } from './OneOfSchema'; import { OneOfSchema } from './OneOfSchema';
import { l } from '../../services/Labels'; import { l } from '../../services/Labels';
import { isArray } from '../../utils/helpers';
export interface SchemaOptions { export interface SchemaOptions {
showTitle?: boolean; showTitle?: boolean;
@ -68,7 +69,7 @@ export class Schema extends React.Component<Partial<SchemaProps>> {
return <OneOfSchema schema={schema} {...rest} />; return <OneOfSchema schema={schema} {...rest} />;
} }
const types = Array.isArray(type) ? type : [type]; const types = isArray(type) ? type : [type];
if (types.includes('object')) { if (types.includes('object')) {
if (schema.fields?.length) { if (schema.fields?.length) {
return <ObjectSchema {...(this.props as any)} level={level} />; return <ObjectSchema {...(this.props as any)} level={level} />;

View File

@ -75,7 +75,14 @@ export class SchemaDefinition extends React.PureComponent<ObjectDescriptionProps
} }
private renderDropdown = props => { private renderDropdown = props => {
return <DropdownOrLabel Label={MimeLabel} Dropdown={InvertedSimpleDropdown} {...props} />; return (
<DropdownOrLabel
Label={MimeLabel}
Dropdown={InvertedSimpleDropdown}
{...props}
variant="dark"
/>
);
}; };
} }

View File

@ -7,6 +7,7 @@ import { MenuItem } from '../SideMenu/MenuItem';
import { MarkerService } from '../../services/MarkerService'; import { MarkerService } from '../../services/MarkerService';
import { SearchResult } from '../../services/SearchWorker.worker'; import { SearchResult } from '../../services/SearchWorker.worker';
import { OptionsContext } from '../OptionsProvider';
import { bind, debounce } from 'decko'; import { bind, debounce } from 'decko';
import { PerfectScrollbarWrap } from '../../common-elements/perfect-scrollbar'; import { PerfectScrollbarWrap } from '../../common-elements/perfect-scrollbar';
import { import {
@ -37,6 +38,8 @@ export interface SearchBoxState {
export class SearchBox extends React.PureComponent<SearchBoxProps, SearchBoxState> { export class SearchBox extends React.PureComponent<SearchBoxProps, SearchBoxState> {
activeItemRef: MenuItem | null = null; activeItemRef: MenuItem | null = null;
static contextType = OptionsContext;
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
@ -114,8 +117,9 @@ export class SearchBox extends React.PureComponent<SearchBoxProps, SearchBoxStat
} }
search = (event: React.ChangeEvent<HTMLInputElement>) => { search = (event: React.ChangeEvent<HTMLInputElement>) => {
const { minCharacterLengthToInitSearch } = this.context;
const q = event.target.value; const q = event.target.value;
if (q.length < 3) { if (q.length < minCharacterLengthToInitSearch) {
this.clearResults(q); this.clearResults(q);
return; return;
} }

View File

@ -6,6 +6,23 @@ import { Link, UnderlinedHeader } from '../../common-elements/';
import { SecurityRequirementModel } from '../../services/models/SecurityRequirement'; import { SecurityRequirementModel } from '../../services/models/SecurityRequirement';
import { linksCss } from '../Markdown/styled.elements'; import { linksCss } from '../Markdown/styled.elements';
const ScopeNameList = styled.ul`
display: inline;
list-style: none;
padding: 0;
li {
display: inherit;
&:after {
content: ',';
}
&:last-child:after {
content: none;
}
}
`;
const ScopeName = styled.code` const ScopeName = styled.code`
font-size: ${props => props.theme.typography.code.fontSize}; font-size: ${props => props.theme.typography.code.fontSize};
font-family: ${props => props.theme.typography.code.fontFamily}; font-family: ${props => props.theme.typography.code.fontFamily};
@ -14,13 +31,6 @@ const ScopeName = styled.code`
padding: 0.2em; padding: 0.2em;
display: inline-block; display: inline-block;
line-height: 1; line-height: 1;
&:after {
content: ',';
}
&:last-child:after {
content: none;
}
`; `;
const SecurityRequirementAndWrap = styled.span` const SecurityRequirementAndWrap = styled.span`
@ -72,9 +82,13 @@ export class SecurityRequirement extends React.PureComponent<SecurityRequirement
<SecurityRequirementAndWrap key={scheme.id}> <SecurityRequirementAndWrap key={scheme.id}>
<Link to={scheme.sectionId}>{scheme.displayName}</Link> <Link to={scheme.sectionId}>{scheme.displayName}</Link>
{scheme.scopes.length > 0 && ' ('} {scheme.scopes.length > 0 && ' ('}
{scheme.scopes.map(scope => ( <ScopeNameList>
<ScopeName key={scope}>{scope}</ScopeName> {scheme.scopes.map(scope => (
))} <li key={scope}>
<ScopeName>{scope}</ScopeName>
</li>
))}
</ScopeNameList>
{scheme.scopes.length > 0 && ') '} {scheme.scopes.length > 0 && ') '}
</SecurityRequirementAndWrap> </SecurityRequirementAndWrap>
); );

View File

@ -23,6 +23,7 @@ export interface OAuthFlowProps {
export class OAuthFlow extends React.PureComponent<OAuthFlowProps> { export class OAuthFlow extends React.PureComponent<OAuthFlowProps> {
render() { render() {
const { type, flow } = this.props; const { type, flow } = this.props;
const scopesNames = Object.keys(flow?.scopes || {});
return ( return (
<tr> <tr>
<th> {type} OAuth Flow </th> <th> {type} OAuth Flow </th>
@ -45,16 +46,21 @@ export class OAuthFlow extends React.PureComponent<OAuthFlowProps> {
{flow!.refreshUrl} {flow!.refreshUrl}
</div> </div>
)} )}
<div> {!!scopesNames.length && (
<strong> Scopes: </strong> <>
</div> <div>
<ul> <strong> Scopes: </strong>
{Object.keys(flow!.scopes || {}).map(scope => ( </div>
<li key={scope}> <ul>
<code>{scope}</code> - <Markdown inline={true} source={flow!.scopes[scope] || ''} /> {scopesNames.map(scope => (
</li> <li key={scope}>
))} <code>{scope}</code> -{' '}
</ul> <Markdown inline={true} source={flow!.scopes[scope] || ''} />
</li>
))}
</ul>
</>
)}
</td> </td>
</tr> </tr>
); );

View File

@ -25,8 +25,8 @@ export class SideMenu extends React.Component<{ menu: MenuStore; className?: str
> >
<MenuItems items={store.items} onActivate={this.activate} root={true} /> <MenuItems items={store.items} onActivate={this.activate} root={true} />
<RedocAttribution> <RedocAttribution>
<a target="_blank" rel="noopener noreferrer" href="https://github.com/Redocly/redoc"> <a target="_blank" rel="noopener noreferrer" href="https://redocly.com/redoc/">
Documentation Powered by ReDoc Documentation Powered by Redocly
</a> </a>
</RedocAttribution> </RedocAttribution>
</PerfectScrollbarWrap> </PerfectScrollbarWrap>

View File

@ -66,11 +66,15 @@ export const OperationBadge = styled.span.attrs((props: { type: string }) => ({
} }
`; `;
function menuItemActiveBg(depth, { theme }: { theme: ResolvedThemeInterface }): string { function menuItemActive(
depth,
{ theme }: { theme: ResolvedThemeInterface },
option: string,
): string {
if (depth > 1) { if (depth > 1) {
return darken(0.1, theme.sidebar.backgroundColor); return theme.sidebar.level1Items[option];
} else if (depth === 1) { } else if (depth === 1) {
return darken(0.05, theme.sidebar.backgroundColor); return theme.sidebar.groupItems[option];
} else { } else {
return ''; return '';
} }
@ -102,17 +106,10 @@ export const menuItemDepth = {
font-size: 0.8em; font-size: 0.8em;
padding-bottom: 0; padding-bottom: 0;
cursor: default; cursor: default;
color: ${props => props.theme.sidebar.textColor};
`, `,
1: css` 1: css`
font-size: 0.929em; font-size: 0.929em;
text-transform: ${({ theme }) => theme.sidebar.level1Items.textTransform}; text-transform: ${({ theme }) => theme.sidebar.level1Items.textTransform};
&:hover {
color: ${props => props.theme.sidebar.activeTextColor};
}
`,
2: css`
color: ${props => props.theme.sidebar.textColor};
`, `,
}; };
@ -131,7 +128,9 @@ export const MenuItemLabel = styled.label.attrs((props: MenuItemLabelType) => ({
}))<MenuItemLabelType>` }))<MenuItemLabelType>`
cursor: pointer; cursor: pointer;
color: ${props => color: ${props =>
props.active ? props.theme.sidebar.activeTextColor : props.theme.sidebar.textColor}; props.active
? menuItemActive(props.depth, props, 'activeTextColor')
: props.theme.sidebar.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 }) =>
@ -140,12 +139,16 @@ export const MenuItemLabel = styled.label.attrs((props: MenuItemLabelType) => ({
justify-content: space-between; justify-content: space-between;
font-family: ${props => props.theme.typography.headings.fontFamily}; font-family: ${props => props.theme.typography.headings.fontFamily};
${props => menuItemDepth[props.depth]}; ${props => menuItemDepth[props.depth]};
background-color: ${props => (props.active ? menuItemActiveBg(props.depth, props) : '')}; background-color: ${props =>
props.active
? menuItemActive(props.depth, props, 'activeBackgroundColor')
: props.theme.sidebar.backgroundColor};
${props => (props.deprecated && deprecatedCss) || ''}; ${props => (props.deprecated && deprecatedCss) || ''};
&:hover { &:hover {
background-color: ${props => menuItemActiveBg(props.depth, props)}; color: ${props => menuItemActive(props.depth, props, 'activeTextColor')};
background-color: ${props => menuItemActive(props.depth, props, 'activeBackgroundColor')};
} }
${ShelfIcon} { ${ShelfIcon} {

View File

@ -33,6 +33,10 @@ export function StoreBuilder(props: StoreBuilderProps) {
const { spec, specUrl, options, onLoaded, children } = props; const { spec, specUrl, options, onLoaded, children } = props;
const [resolvedSpec, setResolvedSpec] = React.useState<any>(null); const [resolvedSpec, setResolvedSpec] = React.useState<any>(null);
const [error, setError] = React.useState<Error | null>(null);
if (error) {
throw error;
}
React.useEffect(() => { React.useEffect(() => {
async function load() { async function load() {
@ -40,8 +44,13 @@ export function StoreBuilder(props: StoreBuilderProps) {
return undefined; return undefined;
} }
setResolvedSpec(null); setResolvedSpec(null);
const resolved = await loadAndBundleSpec(spec || specUrl!); try {
setResolvedSpec(resolved); const resolved = await loadAndBundleSpec(spec || specUrl!);
setResolvedSpec(resolved);
} catch (e) {
setError(e);
throw e;
}
} }
load(); load();
}, [spec, specUrl]); }, [spec, specUrl]);

View File

@ -42,5 +42,13 @@ describe('Components', () => {
expect(ClipboardService.copySelected as jest.Mock).toHaveBeenCalled(); expect(ClipboardService.copySelected as jest.Mock).toHaveBeenCalled();
}); });
test('Expand/Collapse buttons disappears for flat structures', () => {
const flatData = { a: 1, b: '2', c: null };
const flatDataComponent = mount(withTheme(<JsonViewer data={flatData} />));
expect(flatDataComponent.html()).not.toContain('Expand all');
expect(flatDataComponent.html()).not.toContain('Collapse all');
});
}); });
}); });

View File

@ -19,6 +19,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"displayType": "object", "displayType": "object",
"enum": Array [], "enum": Array [],
"example": undefined, "example": undefined,
"examples": undefined,
"externalDocs": undefined, "externalDocs": undefined,
"format": undefined, "format": undefined,
"isCircular": undefined, "isCircular": undefined,
@ -39,6 +40,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"displayType": "object", "displayType": "object",
"enum": Array [], "enum": Array [],
"example": undefined, "example": undefined,
"examples": undefined,
"externalDocs": undefined, "externalDocs": undefined,
"fields": Array [ "fields": Array [
FieldModel { FieldModel {
@ -65,6 +67,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"displayType": "number", "displayType": "number",
"enum": Array [], "enum": Array [],
"example": undefined, "example": undefined,
"examples": undefined,
"externalDocs": undefined, "externalDocs": undefined,
"format": undefined, "format": undefined,
"isCircular": undefined, "isCircular": undefined,
@ -89,8 +92,10 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"jsonSampleExpandLevel": 2, "jsonSampleExpandLevel": 2,
"maxDisplayedEnumValues": undefined, "maxDisplayedEnumValues": undefined,
"menuToggle": true, "menuToggle": true,
"minCharacterLengthToInitSearch": 3,
"nativeScrollbars": false, "nativeScrollbars": false,
"noAutoAuth": false, "noAutoAuth": false,
"nonce": undefined,
"onlyRequiredInSamples": false, "onlyRequiredInSamples": false,
"pathInMiddlePanel": false, "pathInMiddlePanel": false,
"payloadSampleIdx": 0, "payloadSampleIdx": 0,
@ -224,9 +229,13 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
}, },
"backgroundColor": "#fafafa", "backgroundColor": "#fafafa",
"groupItems": Object { "groupItems": Object {
"activeBackgroundColor": "#e1e1e1",
"activeTextColor": "#32329f",
"textTransform": "uppercase", "textTransform": "uppercase",
}, },
"level1Items": Object { "level1Items": Object {
"activeBackgroundColor": "#ededed",
"activeTextColor": "#32329f",
"textTransform": "none", "textTransform": "none",
}, },
"textColor": "#333333", "textColor": "#333333",
@ -311,6 +320,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"displayType": "string", "displayType": "string",
"enum": Array [], "enum": Array [],
"example": undefined, "example": undefined,
"examples": undefined,
"externalDocs": undefined, "externalDocs": undefined,
"format": undefined, "format": undefined,
"isCircular": undefined, "isCircular": undefined,
@ -335,8 +345,10 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"jsonSampleExpandLevel": 2, "jsonSampleExpandLevel": 2,
"maxDisplayedEnumValues": undefined, "maxDisplayedEnumValues": undefined,
"menuToggle": true, "menuToggle": true,
"minCharacterLengthToInitSearch": 3,
"nativeScrollbars": false, "nativeScrollbars": false,
"noAutoAuth": false, "noAutoAuth": false,
"nonce": undefined,
"onlyRequiredInSamples": false, "onlyRequiredInSamples": false,
"pathInMiddlePanel": false, "pathInMiddlePanel": false,
"payloadSampleIdx": 0, "payloadSampleIdx": 0,
@ -470,9 +482,13 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
}, },
"backgroundColor": "#fafafa", "backgroundColor": "#fafafa",
"groupItems": Object { "groupItems": Object {
"activeBackgroundColor": "#e1e1e1",
"activeTextColor": "#32329f",
"textTransform": "uppercase", "textTransform": "uppercase",
}, },
"level1Items": Object { "level1Items": Object {
"activeBackgroundColor": "#ededed",
"activeTextColor": "#32329f",
"textTransform": "none", "textTransform": "none",
}, },
"textColor": "#333333", "textColor": "#333333",
@ -557,8 +573,10 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"jsonSampleExpandLevel": 2, "jsonSampleExpandLevel": 2,
"maxDisplayedEnumValues": undefined, "maxDisplayedEnumValues": undefined,
"menuToggle": true, "menuToggle": true,
"minCharacterLengthToInitSearch": 3,
"nativeScrollbars": false, "nativeScrollbars": false,
"noAutoAuth": false, "noAutoAuth": false,
"nonce": undefined,
"onlyRequiredInSamples": false, "onlyRequiredInSamples": false,
"pathInMiddlePanel": false, "pathInMiddlePanel": false,
"payloadSampleIdx": 0, "payloadSampleIdx": 0,
@ -692,9 +710,13 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
}, },
"backgroundColor": "#fafafa", "backgroundColor": "#fafafa",
"groupItems": Object { "groupItems": Object {
"activeBackgroundColor": "#e1e1e1",
"activeTextColor": "#32329f",
"textTransform": "uppercase", "textTransform": "uppercase",
}, },
"level1Items": Object { "level1Items": Object {
"activeBackgroundColor": "#ededed",
"activeTextColor": "#32329f",
"textTransform": "none", "textTransform": "none",
}, },
"textColor": "#333333", "textColor": "#333333",
@ -794,6 +816,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"displayType": "object", "displayType": "object",
"enum": Array [], "enum": Array [],
"example": undefined, "example": undefined,
"examples": undefined,
"externalDocs": undefined, "externalDocs": undefined,
"fields": Array [ "fields": Array [
FieldModel { FieldModel {
@ -820,6 +843,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"displayType": "string", "displayType": "string",
"enum": Array [], "enum": Array [],
"example": undefined, "example": undefined,
"examples": undefined,
"externalDocs": undefined, "externalDocs": undefined,
"format": undefined, "format": undefined,
"isCircular": undefined, "isCircular": undefined,
@ -844,8 +868,10 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"jsonSampleExpandLevel": 2, "jsonSampleExpandLevel": 2,
"maxDisplayedEnumValues": undefined, "maxDisplayedEnumValues": undefined,
"menuToggle": true, "menuToggle": true,
"minCharacterLengthToInitSearch": 3,
"nativeScrollbars": false, "nativeScrollbars": false,
"noAutoAuth": false, "noAutoAuth": false,
"nonce": undefined,
"onlyRequiredInSamples": false, "onlyRequiredInSamples": false,
"pathInMiddlePanel": false, "pathInMiddlePanel": false,
"payloadSampleIdx": 0, "payloadSampleIdx": 0,
@ -979,9 +1005,13 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
}, },
"backgroundColor": "#fafafa", "backgroundColor": "#fafafa",
"groupItems": Object { "groupItems": Object {
"activeBackgroundColor": "#e1e1e1",
"activeTextColor": "#32329f",
"textTransform": "uppercase", "textTransform": "uppercase",
}, },
"level1Items": Object { "level1Items": Object {
"activeBackgroundColor": "#ededed",
"activeTextColor": "#32329f",
"textTransform": "none", "textTransform": "none",
}, },
"textColor": "#333333", "textColor": "#333333",
@ -1066,6 +1096,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"displayType": "number", "displayType": "number",
"enum": Array [], "enum": Array [],
"example": undefined, "example": undefined,
"examples": undefined,
"externalDocs": undefined, "externalDocs": undefined,
"format": undefined, "format": undefined,
"isCircular": undefined, "isCircular": undefined,
@ -1090,8 +1121,10 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"jsonSampleExpandLevel": 2, "jsonSampleExpandLevel": 2,
"maxDisplayedEnumValues": undefined, "maxDisplayedEnumValues": undefined,
"menuToggle": true, "menuToggle": true,
"minCharacterLengthToInitSearch": 3,
"nativeScrollbars": false, "nativeScrollbars": false,
"noAutoAuth": false, "noAutoAuth": false,
"nonce": undefined,
"onlyRequiredInSamples": false, "onlyRequiredInSamples": false,
"pathInMiddlePanel": false, "pathInMiddlePanel": false,
"payloadSampleIdx": 0, "payloadSampleIdx": 0,
@ -1225,9 +1258,13 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
}, },
"backgroundColor": "#fafafa", "backgroundColor": "#fafafa",
"groupItems": Object { "groupItems": Object {
"activeBackgroundColor": "#e1e1e1",
"activeTextColor": "#32329f",
"textTransform": "uppercase", "textTransform": "uppercase",
}, },
"level1Items": Object { "level1Items": Object {
"activeBackgroundColor": "#ededed",
"activeTextColor": "#32329f",
"textTransform": "none", "textTransform": "none",
}, },
"textColor": "#333333", "textColor": "#333333",
@ -1312,8 +1349,10 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"jsonSampleExpandLevel": 2, "jsonSampleExpandLevel": 2,
"maxDisplayedEnumValues": undefined, "maxDisplayedEnumValues": undefined,
"menuToggle": true, "menuToggle": true,
"minCharacterLengthToInitSearch": 3,
"nativeScrollbars": false, "nativeScrollbars": false,
"noAutoAuth": false, "noAutoAuth": false,
"nonce": undefined,
"onlyRequiredInSamples": false, "onlyRequiredInSamples": false,
"pathInMiddlePanel": false, "pathInMiddlePanel": false,
"payloadSampleIdx": 0, "payloadSampleIdx": 0,
@ -1447,9 +1486,13 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
}, },
"backgroundColor": "#fafafa", "backgroundColor": "#fafafa",
"groupItems": Object { "groupItems": Object {
"activeBackgroundColor": "#e1e1e1",
"activeTextColor": "#32329f",
"textTransform": "uppercase", "textTransform": "uppercase",
}, },
"level1Items": Object { "level1Items": Object {
"activeBackgroundColor": "#ededed",
"activeTextColor": "#32329f",
"textTransform": "none", "textTransform": "none",
}, },
"textColor": "#333333", "textColor": "#333333",
@ -1557,8 +1600,10 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"jsonSampleExpandLevel": 2, "jsonSampleExpandLevel": 2,
"maxDisplayedEnumValues": undefined, "maxDisplayedEnumValues": undefined,
"menuToggle": true, "menuToggle": true,
"minCharacterLengthToInitSearch": 3,
"nativeScrollbars": false, "nativeScrollbars": false,
"noAutoAuth": false, "noAutoAuth": false,
"nonce": undefined,
"onlyRequiredInSamples": false, "onlyRequiredInSamples": false,
"pathInMiddlePanel": false, "pathInMiddlePanel": false,
"payloadSampleIdx": 0, "payloadSampleIdx": 0,
@ -1692,9 +1737,13 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
}, },
"backgroundColor": "#fafafa", "backgroundColor": "#fafafa",
"groupItems": Object { "groupItems": Object {
"activeBackgroundColor": "#e1e1e1",
"activeTextColor": "#32329f",
"textTransform": "uppercase", "textTransform": "uppercase",
}, },
"level1Items": Object { "level1Items": Object {
"activeBackgroundColor": "#ededed",
"activeTextColor": "#32329f",
"textTransform": "none", "textTransform": "none",
}, },
"textColor": "#333333", "textColor": "#333333",
@ -1791,6 +1840,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"displayType": "object", "displayType": "object",
"enum": Array [], "enum": Array [],
"example": undefined, "example": undefined,
"examples": undefined,
"externalDocs": undefined, "externalDocs": undefined,
"fields": Array [ "fields": Array [
FieldModel { FieldModel {
@ -1817,6 +1867,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"displayType": "number", "displayType": "number",
"enum": Array [], "enum": Array [],
"example": undefined, "example": undefined,
"examples": undefined,
"externalDocs": undefined, "externalDocs": undefined,
"format": undefined, "format": undefined,
"isCircular": undefined, "isCircular": undefined,
@ -1841,8 +1892,10 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"jsonSampleExpandLevel": 2, "jsonSampleExpandLevel": 2,
"maxDisplayedEnumValues": undefined, "maxDisplayedEnumValues": undefined,
"menuToggle": true, "menuToggle": true,
"minCharacterLengthToInitSearch": 3,
"nativeScrollbars": false, "nativeScrollbars": false,
"noAutoAuth": false, "noAutoAuth": false,
"nonce": undefined,
"onlyRequiredInSamples": false, "onlyRequiredInSamples": false,
"pathInMiddlePanel": false, "pathInMiddlePanel": false,
"payloadSampleIdx": 0, "payloadSampleIdx": 0,
@ -1976,9 +2029,13 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
}, },
"backgroundColor": "#fafafa", "backgroundColor": "#fafafa",
"groupItems": Object { "groupItems": Object {
"activeBackgroundColor": "#e1e1e1",
"activeTextColor": "#32329f",
"textTransform": "uppercase", "textTransform": "uppercase",
}, },
"level1Items": Object { "level1Items": Object {
"activeBackgroundColor": "#ededed",
"activeTextColor": "#32329f",
"textTransform": "none", "textTransform": "none",
}, },
"textColor": "#333333", "textColor": "#333333",
@ -2063,6 +2120,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"displayType": "string", "displayType": "string",
"enum": Array [], "enum": Array [],
"example": undefined, "example": undefined,
"examples": undefined,
"externalDocs": undefined, "externalDocs": undefined,
"format": undefined, "format": undefined,
"isCircular": undefined, "isCircular": undefined,
@ -2087,8 +2145,10 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"jsonSampleExpandLevel": 2, "jsonSampleExpandLevel": 2,
"maxDisplayedEnumValues": undefined, "maxDisplayedEnumValues": undefined,
"menuToggle": true, "menuToggle": true,
"minCharacterLengthToInitSearch": 3,
"nativeScrollbars": false, "nativeScrollbars": false,
"noAutoAuth": false, "noAutoAuth": false,
"nonce": undefined,
"onlyRequiredInSamples": false, "onlyRequiredInSamples": false,
"pathInMiddlePanel": false, "pathInMiddlePanel": false,
"payloadSampleIdx": 0, "payloadSampleIdx": 0,
@ -2222,9 +2282,13 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
}, },
"backgroundColor": "#fafafa", "backgroundColor": "#fafafa",
"groupItems": Object { "groupItems": Object {
"activeBackgroundColor": "#e1e1e1",
"activeTextColor": "#32329f",
"textTransform": "uppercase", "textTransform": "uppercase",
}, },
"level1Items": Object { "level1Items": Object {
"activeBackgroundColor": "#ededed",
"activeTextColor": "#32329f",
"textTransform": "none", "textTransform": "none",
}, },
"textColor": "#333333", "textColor": "#333333",
@ -2309,8 +2373,10 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"jsonSampleExpandLevel": 2, "jsonSampleExpandLevel": 2,
"maxDisplayedEnumValues": undefined, "maxDisplayedEnumValues": undefined,
"menuToggle": true, "menuToggle": true,
"minCharacterLengthToInitSearch": 3,
"nativeScrollbars": false, "nativeScrollbars": false,
"noAutoAuth": false, "noAutoAuth": false,
"nonce": undefined,
"onlyRequiredInSamples": false, "onlyRequiredInSamples": false,
"pathInMiddlePanel": false, "pathInMiddlePanel": false,
"payloadSampleIdx": 0, "payloadSampleIdx": 0,
@ -2444,9 +2510,13 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
}, },
"backgroundColor": "#fafafa", "backgroundColor": "#fafafa",
"groupItems": Object { "groupItems": Object {
"activeBackgroundColor": "#e1e1e1",
"activeTextColor": "#32329f",
"textTransform": "uppercase", "textTransform": "uppercase",
}, },
"level1Items": Object { "level1Items": Object {
"activeBackgroundColor": "#ededed",
"activeTextColor": "#32329f",
"textTransform": "none", "textTransform": "none",
}, },
"textColor": "#333333", "textColor": "#333333",
@ -2567,6 +2637,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"displayType": "number", "displayType": "number",
"enum": Array [], "enum": Array [],
"example": undefined, "example": undefined,
"examples": undefined,
"externalDocs": undefined, "externalDocs": undefined,
"format": undefined, "format": undefined,
"isCircular": undefined, "isCircular": undefined,
@ -2623,6 +2694,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"displayType": "string", "displayType": "string",
"enum": Array [], "enum": Array [],
"example": undefined, "example": undefined,
"examples": undefined,
"externalDocs": undefined, "externalDocs": undefined,
"format": undefined, "format": undefined,
"isCircular": undefined, "isCircular": undefined,

View File

@ -33,10 +33,10 @@ exports[`Components SchemaView OneOf deprecated should match snapshot 1`] = `
<div> <div>
<div> <div>
<span <span
class="sc-fbIWvP sc-FRrlG CMpTe bBFKjV" class="sc-laZMeE sc-iNiQyp jWaWWE jrLlAa"
/> />
<span <span
class="sc-fbIWvP sc-fXazdy CMpTe gJKPGC" class="sc-laZMeE sc-jffHpj jWaWWE cThoNa"
> >
string string
</span> </span>
@ -44,7 +44,7 @@ exports[`Components SchemaView OneOf deprecated should match snapshot 1`] = `
<div> <div>
<div <div
class="sc-iBzEeX sc-cOifOu dFWqin hjSJYo" class="sc-iJCRrE sc-ciSkZP jCdxGr emlfPd"
/> />
</div> </div>
</div> </div>

View File

@ -4,7 +4,7 @@ export {
Row, Row,
RightPanel, RightPanel,
Section, Section,
StyledDropdown, Dropdown,
SimpleDropdown, SimpleDropdown,
} from './common-elements/'; } from './common-elements/';
export type { DropdownOption } from './common-elements'; export type { DropdownOption } from './common-elements';

View File

@ -101,7 +101,9 @@ export class MarkdownRenderer {
attachHeadingsDescriptions(rawText: string) { attachHeadingsDescriptions(rawText: string) {
const buildRegexp = (heading: MarkdownHeading) => { const buildRegexp = (heading: MarkdownHeading) => {
return new RegExp(`##?\\s+${heading.name.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}\s*\n`); return new RegExp(
`##?\\s+${heading.name.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}\s*(\n|\r\n)`,
);
}; };
const flatHeadings = this.flattenHeadings(this.headings); const flatHeadings = this.flattenHeadings(this.headings);

View File

@ -5,7 +5,7 @@ import { SpecStore } from './models';
import { history as historyInst, HistoryService } from './HistoryService'; import { history as historyInst, HistoryService } from './HistoryService';
import { ScrollService } from './ScrollService'; import { ScrollService } from './ScrollService';
import { flattenByProp, SECURITY_SCHEMES_SECTION_PREFIX } from '../utils'; import { escapeHTMLAttrChars, flattenByProp, SECURITY_SCHEMES_SECTION_PREFIX } from '../utils';
import { GROUP_DEPTH } from './MenuBuilder'; import { GROUP_DEPTH } from './MenuBuilder';
export type MenuItemGroupType = 'group' | 'tag' | 'section'; export type MenuItemGroupType = 'group' | 'tag' | 'section';
@ -48,7 +48,7 @@ export class MenuStore {
if (!id) { if (!id) {
return; return;
} }
scroll.scrollIntoViewBySelector(`[${SECTION_ATTR}="${id}"]`); scroll.scrollIntoViewBySelector(`[${SECTION_ATTR}="${escapeHTMLAttrChars(id)}"]`);
} }
/** /**
@ -154,7 +154,7 @@ export class MenuStore {
item = this.flatItems.find(i => SECURITY_SCHEMES_SECTION_PREFIX.startsWith(i.id)); item = this.flatItems.find(i => SECURITY_SCHEMES_SECTION_PREFIX.startsWith(i.id));
this.activateAndScroll(item, false); this.activateAndScroll(item, false);
} }
this.scroll.scrollIntoViewBySelector(`[${SECTION_ATTR}="${id}"]`); this.scroll.scrollIntoViewBySelector(`[${SECTION_ATTR}="${escapeHTMLAttrChars(id)}"]`);
} }
}; };
@ -164,7 +164,7 @@ export class MenuStore {
*/ */
getElementAt(idx: number): Element | null { getElementAt(idx: number): Element | null {
const item = this.flatItems[idx]; const item = this.flatItems[idx];
return (item && querySelector(`[${SECTION_ATTR}="${item.id}"]`)) || null; return (item && querySelector(`[${SECTION_ATTR}="${escapeHTMLAttrChars(item.id)}"]`)) || null;
} }
/** /**
@ -176,7 +176,7 @@ export class MenuStore {
if (item && item.type === 'group') { if (item && item.type === 'group') {
item = item.items[0]; item = item.items[0];
} }
return (item && querySelector(`[${SECTION_ATTR}="${item.id}"]`)) || null; return (item && querySelector(`[${SECTION_ATTR}="${escapeHTMLAttrChars(item.id)}"]`)) || null;
} }
/** /**
@ -225,7 +225,7 @@ export class MenuStore {
this.activeItemIdx = item.absoluteIdx!; this.activeItemIdx = item.absoluteIdx!;
if (updateLocation) { if (updateLocation) {
this.history.replace(item.id, rewriteHistory); this.history.replace(encodeURI(item.id), rewriteHistory);
} }
item.activate(); item.activate();

View File

@ -1,8 +1,6 @@
import { resolve as urlResolve } from 'url';
import { OpenAPIRef, OpenAPISchema, OpenAPISpec, Referenced } from '../types'; import { OpenAPIRef, OpenAPISchema, OpenAPISpec, Referenced } from '../types';
import { appendToMdHeading, IS_BROWSER } from '../utils/'; import { appendToMdHeading, isArray, IS_BROWSER } from '../utils/';
import { JsonPointer } from '../utils/JsonPointer'; import { JsonPointer } from '../utils/JsonPointer';
import { import {
getDefinitionName, getDefinitionName,
@ -62,7 +60,7 @@ export class OpenAPIParser {
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 = new URL(specUrl, href).href;
} }
} }
@ -365,7 +363,7 @@ export class OpenAPIParser {
const allOf = schema.allOf; const allOf = schema.allOf;
for (let i = 0; i < allOf.length; i++) { for (let i = 0; i < allOf.length; i++) {
const sub = allOf[i]; const sub = allOf[i];
if (Array.isArray(sub.oneOf)) { if (isArray(sub.oneOf)) {
const beforeAllOf = allOf.slice(0, i); const beforeAllOf = allOf.slice(0, i);
const afterAllOf = allOf.slice(i + 1); const afterAllOf = allOf.slice(i + 1);
return { return {

View File

@ -1,6 +1,6 @@
import defaultTheme, { ResolvedThemeInterface, resolveTheme, ThemeInterface } from '../theme'; import defaultTheme, { ResolvedThemeInterface, resolveTheme, ThemeInterface } from '../theme';
import { querySelector } from '../utils/dom'; import { querySelector } from '../utils/dom';
import { isNumeric, mergeObjects } from '../utils/helpers'; import { isArray, isNumeric, mergeObjects } from '../utils/helpers';
import { LabelsConfigRaw, setRedocLabels } from './Labels'; import { LabelsConfigRaw, setRedocLabels } from './Labels';
import { MDXComponentMeta } from './MarkdownRenderer'; import { MDXComponentMeta } from './MarkdownRenderer';
@ -8,6 +8,7 @@ import { MDXComponentMeta } from './MarkdownRenderer';
export enum SideNavStyleEnum { export enum SideNavStyleEnum {
SummaryOnly = 'summary-only', SummaryOnly = 'summary-only',
PathOnly = 'path-only', PathOnly = 'path-only',
IdOnly = 'id-only',
} }
export interface RedocRawOptions { export interface RedocRawOptions {
@ -54,7 +55,9 @@ export interface RedocRawOptions {
ignoreNamedSchemas?: string[] | string; ignoreNamedSchemas?: string[] | string;
hideSchemaPattern?: boolean; hideSchemaPattern?: boolean;
generatedPayloadSamplesMaxDepth?: number; generatedPayloadSamplesMaxDepth?: number;
nonce?: string;
hideFab?: boolean; hideFab?: boolean;
minCharacterLengthToInitSearch?: number;
} }
export function argValueToBoolean(val?: string | boolean, defaultValue?: boolean): boolean { export function argValueToBoolean(val?: string | boolean, defaultValue?: boolean): boolean {
@ -171,6 +174,8 @@ export class RedocNormalizedOptions {
return value; return value;
case SideNavStyleEnum.PathOnly: case SideNavStyleEnum.PathOnly:
return SideNavStyleEnum.PathOnly; return SideNavStyleEnum.PathOnly;
case SideNavStyleEnum.IdOnly:
return SideNavStyleEnum.IdOnly;
default: default:
return defaultValue; return defaultValue;
} }
@ -257,6 +262,9 @@ export class RedocNormalizedOptions {
hideSchemaPattern: boolean; hideSchemaPattern: boolean;
generatedPayloadSamplesMaxDepth: number; generatedPayloadSamplesMaxDepth: number;
hideFab: boolean; hideFab: boolean;
minCharacterLengthToInitSearch: number;
nonce?: string;
constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) { constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) {
raw = { ...defaults, ...raw }; raw = { ...defaults, ...raw };
@ -319,7 +327,7 @@ export class RedocNormalizedOptions {
this.expandDefaultServerVariables = argValueToBoolean(raw.expandDefaultServerVariables); this.expandDefaultServerVariables = argValueToBoolean(raw.expandDefaultServerVariables);
this.maxDisplayedEnumValues = argValueToNumber(raw.maxDisplayedEnumValues); this.maxDisplayedEnumValues = argValueToNumber(raw.maxDisplayedEnumValues);
const ignoreNamedSchemas = Array.isArray(raw.ignoreNamedSchemas) const ignoreNamedSchemas = isArray(raw.ignoreNamedSchemas)
? raw.ignoreNamedSchemas ? raw.ignoreNamedSchemas
: raw.ignoreNamedSchemas?.split(',').map(s => s.trim()); : raw.ignoreNamedSchemas?.split(',').map(s => s.trim());
this.ignoreNamedSchemas = new Set(ignoreNamedSchemas); this.ignoreNamedSchemas = new Set(ignoreNamedSchemas);
@ -328,6 +336,8 @@ export class RedocNormalizedOptions {
RedocNormalizedOptions.normalizeGeneratedPayloadSamplesMaxDepth( RedocNormalizedOptions.normalizeGeneratedPayloadSamplesMaxDepth(
raw.generatedPayloadSamplesMaxDepth, raw.generatedPayloadSamplesMaxDepth,
); );
this.nonce = raw.nonce;
this.hideFab = argValueToBoolean(raw.hideFab); this.hideFab = argValueToBoolean(raw.hideFab);
this.minCharacterLengthToInitSearch = argValueToNumber(raw.minCharacterLengthToInitSearch) || 3;
} }
} }

View File

@ -0,0 +1,60 @@
{
"openapi": "3.1.0",
"info": {
"title": "Schema definition with unevaluatedProperties",
"version": "1.0.0"
},
"servers": [
{
"url": "example.com"
}
],
"components": {
"schemas": {
"Test": {
"type": "object",
"unevaluatedProperties": true,
"properties": {
"$ref": "#/components/schemas/Cat"
}
},
"Test2": {
"type": "object",
"unevaluatedProperties": true,
"anyOf": [
{
"$ref": "#/components/schemas/Cat"
},
{
"$ref": "#/components/schemas/Dog"
}
]
},
"Test3": {
"type": "object",
"unevaluatedProperties": {
"type": "boolean"
},
"properties": {
"$ref": "#/components/schemas/Cat"
}
},
"Cat": {
"type": "object",
"properties": {
"color": {
"type": "string"
}
}
},
"Dog": {
"type": "object",
"properties": {
"size": {
"type": "string"
}
}
}
}
}
}

View File

@ -48,5 +48,33 @@ describe('Models', () => {
expect(schema.fields).toHaveLength(1); expect(schema.fields).toHaveLength(1);
expect(schema.pointer).toBe('#/components/schemas/Child'); expect(schema.pointer).toBe('#/components/schemas/Child');
}); });
test('schemaDefinition should resolve unevaluatedProperties in properties', () => {
const spec = require('../fixtures/3.1/unevaluatedProperties.json');
parser = new OpenAPIParser(spec, undefined, opts);
const schema = new SchemaModel(parser, spec.components.schemas.Test, '', opts);
expect(schema.fields).toHaveLength(2);
expect(schema.fields![1].kind).toEqual('additionalProperties');
expect(schema.fields![1].schema.type).toEqual('any');
});
test('schemaDefinition should resolve unevaluatedProperties in anyOf', () => {
const spec = require('../fixtures/3.1/unevaluatedProperties.json');
parser = new OpenAPIParser(spec, undefined, opts);
const schema = new SchemaModel(parser, spec.components.schemas.Test2, '', opts);
expect(schema.oneOf![0].fields).toHaveLength(2);
expect(schema.oneOf![0].fields![1].kind).toEqual('additionalProperties');
expect(schema.oneOf![1].fields).toHaveLength(2);
expect(schema.oneOf![1].fields![1].kind).toEqual('additionalProperties');
});
test('schemaDefinition should resolve unevaluatedProperties type boolean', () => {
const spec = require('../fixtures/3.1/unevaluatedProperties.json');
parser = new OpenAPIParser(spec, undefined, opts);
const schema = new SchemaModel(parser, spec.components.schemas.Test3, '', opts);
expect(schema.fields).toHaveLength(2);
expect(schema.fields![1].kind).toEqual('additionalProperties');
expect(schema.fields![1].schema.type).toEqual('boolean');
});
}); });
}); });

View File

@ -1,5 +1,3 @@
import { resolve as urlResolve } from 'url';
import { OpenAPIEncoding, OpenAPIExample, Referenced } from '../../types'; import { OpenAPIEncoding, OpenAPIExample, Referenced } from '../../types';
import { isFormUrlEncoded, isJsonLike, urlFormEncodePayload } from '../../utils/openapi'; import { isFormUrlEncoded, isJsonLike, urlFormEncodePayload } from '../../utils/openapi';
import { OpenAPIParser } from '../OpenAPIParser'; import { OpenAPIParser } from '../OpenAPIParser';
@ -23,7 +21,7 @@ export class ExampleModel {
this.summary = example.summary; this.summary = example.summary;
this.description = example.description; this.description = example.description;
if (example.externalValue) { if (example.externalValue) {
this.externalValueUrl = urlResolve(parser.specUrl || '', example.externalValue); this.externalValueUrl = new URL(example.externalValue, parser.specUrl || '').href;
} }
parser.exitRef(infoOrRef); parser.exitRef(infoOrRef);

View File

@ -12,7 +12,7 @@ import { extractExtensions } from '../../utils/openapi';
import { OpenAPIParser } from '../OpenAPIParser'; import { OpenAPIParser } from '../OpenAPIParser';
import { SchemaModel } from './Schema'; import { SchemaModel } from './Schema';
import { ExampleModel } from './Example'; import { ExampleModel } from './Example';
import { mapValues } from '../../utils/helpers'; import { isArray, mapValues } from '../../utils/helpers';
const DEFAULT_SERIALIZATION: Record< const DEFAULT_SERIALIZATION: Record<
OpenAPIParameterLocation, OpenAPIParameterLocation,
@ -48,7 +48,7 @@ export class FieldModel {
required: boolean; required: boolean;
description: string; description: string;
example?: string; example?: string;
examples?: Record<string, ExampleModel>; examples?: Record<string, ExampleModel> | any[];
deprecated: boolean; deprecated: boolean;
in?: OpenAPIParameterLocation; in?: OpenAPIParameterLocation;
kind: string; kind: string;
@ -85,11 +85,14 @@ export class FieldModel {
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 (info.examples !== undefined) { if (info.examples !== undefined || this.schema.examples !== undefined) {
this.examples = mapValues( const exampleValue = info.examples || this.schema.examples;
info.examples, this.examples = isArray(exampleValue)
(example, name) => new ExampleModel(parser, example, name, info.encoding), ? exampleValue
); : mapValues(
exampleValue!,
(example, name) => new ExampleModel(parser, example, name, info.encoding),
);
} }
if (serializationMime) { if (serializationMime) {

View File

@ -70,6 +70,7 @@ export class OperationModel implements IMenuItem {
pointer: string; pointer: string;
operationId?: string; operationId?: string;
operationHash?: string;
httpVerb: string; httpVerb: string;
deprecated: boolean; deprecated: boolean;
path: string; path: string;
@ -107,7 +108,12 @@ export class OperationModel implements IMenuItem {
this.name = getOperationSummary(operationSpec); this.name = getOperationSummary(operationSpec);
this.sidebarLabel = options.sideNavStyle === SideNavStyleEnum.PathOnly ? this.path : this.name; this.sidebarLabel =
options.sideNavStyle === SideNavStyleEnum.IdOnly
? this.operationId || this.path
: options.sideNavStyle === SideNavStyleEnum.PathOnly
? this.path
: this.name;
if (this.isCallback) { if (this.isCallback) {
// NOTE: Callbacks by default should not inherit the specification's global `security` definition. // NOTE: Callbacks by default should not inherit the specification's global `security` definition.
@ -119,9 +125,10 @@ export class OperationModel implements IMenuItem {
// TODO: update getting pathInfo for overriding servers on path level // TODO: update getting pathInfo for overriding servers on path level
this.servers = normalizeServers('', operationSpec.servers || operationSpec.pathServers || []); this.servers = normalizeServers('', operationSpec.servers || operationSpec.pathServers || []);
} else { } else {
this.operationHash = operationSpec.operationId && 'operation/' + operationSpec.operationId;
this.id = this.id =
operationSpec.operationId !== undefined operationSpec.operationId !== undefined
? 'operation/' + operationSpec.operationId ? (parent ? parent.id + '/' : '') + this.operationHash
: parent !== undefined : parent !== undefined
? parent.id + this.pointer ? parent.id + this.pointer
: this.pointer; : this.pointer;

View File

@ -11,6 +11,7 @@ import {
detectType, detectType,
extractExtensions, extractExtensions,
humanizeConstraints, humanizeConstraints,
isArray,
isNamedDefinition, isNamedDefinition,
isPrimitiveType, isPrimitiveType,
JsonPointer, JsonPointer,
@ -41,6 +42,7 @@ export class SchemaModel {
deprecated: boolean; deprecated: boolean;
pattern?: string; pattern?: string;
example?: any; example?: any;
examples?: any[];
enum: any[]; enum: any[];
default?: any; default?: any;
readOnly: boolean; readOnly: boolean;
@ -103,7 +105,7 @@ export class SchemaModel {
} }
hasType(type: string) { hasType(type: string) {
return this.type === type || (Array.isArray(this.type) && this.type.includes(type)); return this.type === type || (isArray(this.type) && this.type.includes(type));
} }
init(parser: OpenAPIParser, isChild: boolean) { init(parser: OpenAPIParser, isChild: boolean) {
@ -117,6 +119,7 @@ export class SchemaModel {
this.format = schema.format; this.format = schema.format;
this.enum = schema.enum || []; this.enum = schema.enum || [];
this.example = schema.example; this.example = schema.example;
this.examples = schema.examples;
this.deprecated = !!schema.deprecated; this.deprecated = !!schema.deprecated;
this.pattern = schema.pattern; this.pattern = schema.pattern;
this.externalDocs = schema.externalDocs; this.externalDocs = schema.externalDocs;
@ -134,17 +137,14 @@ export class SchemaModel {
this.maxItems = schema.maxItems; this.maxItems = schema.maxItems;
if (!!schema.nullable || schema['x-nullable']) { if (!!schema.nullable || schema['x-nullable']) {
if ( if (isArray(this.type) && !this.type.some(value => value === null || value === 'null')) {
Array.isArray(this.type) &&
!this.type.some(value => value === null || value === 'null')
) {
this.type = [...this.type, 'null']; this.type = [...this.type, 'null'];
} else if (!Array.isArray(this.type) && (this.type !== null || this.type !== 'null')) { } else if (!isArray(this.type) && (this.type !== null || this.type !== 'null')) {
this.type = [this.type, 'null']; this.type = [this.type, 'null'];
} }
} }
this.displayType = Array.isArray(this.type) this.displayType = isArray(this.type)
? this.type.map(item => (item === null ? 'null' : item)).join(' or ') ? this.type.map(item => (item === null ? 'null' : item)).join(' or ')
: this.type; : this.type;
@ -157,7 +157,7 @@ export class SchemaModel {
return; return;
} else if ( } else if (
isChild && isChild &&
Array.isArray(schema.oneOf) && isArray(schema.oneOf) &&
schema.oneOf.find(s => s.$ref === this.pointer) schema.oneOf.find(s => s.$ref === this.pointer)
) { ) {
// we hit allOf of the schema with the parent discriminator // we hit allOf of the schema with the parent discriminator
@ -196,7 +196,7 @@ export class SchemaModel {
if (this.items.isPrimitive) { if (this.items.isPrimitive) {
this.enum = this.items.enum; this.enum = this.items.enum;
} }
if (Array.isArray(this.type)) { if (isArray(this.type)) {
const filteredType = this.type.filter(item => item !== 'array'); const filteredType = this.type.filter(item => item !== 'array');
if (filteredType.length) this.displayType += ` or ${filteredType.join(' or ')}`; if (filteredType.length) this.displayType += ` or ${filteredType.join(' or ')}`;
} }
@ -295,7 +295,7 @@ export class SchemaModel {
for (const name in mapping) { for (const name in mapping) {
const $ref = mapping[name]; const $ref = mapping[name];
if (Array.isArray(explicitInversedMapping[$ref])) { if (isArray(explicitInversedMapping[$ref])) {
explicitInversedMapping[$ref].push(name); explicitInversedMapping[$ref].push(name);
} else { } else {
// overrides implicit mapping here // overrides implicit mapping here
@ -311,7 +311,7 @@ export class SchemaModel {
for (const $ref of Object.keys(inversedMapping)) { for (const $ref of Object.keys(inversedMapping)) {
const names = inversedMapping[$ref]; const names = inversedMapping[$ref];
if (Array.isArray(names)) { if (isArray(names)) {
for (const name of names) { for (const name of names) {
refs.push({ $ref, name }); refs.push({ $ref, name });
} }
@ -364,7 +364,7 @@ function buildFields(
options: RedocNormalizedOptions, options: RedocNormalizedOptions,
): FieldModel[] { ): FieldModel[] {
const props = schema.properties || {}; const props = schema.properties || {};
const additionalProps = schema.additionalProperties; const additionalProps = schema.additionalProperties || schema.unevaluatedProperties;
const defaults = schema.default; const defaults = schema.default;
let fields = Object.keys(props || []).map(fieldName => { let fields = Object.keys(props || []).map(fieldName => {
let field = props[fieldName]; let field = props[fieldName];

View File

@ -139,9 +139,13 @@ const defaultTheme: ThemeInterface = {
? theme.sidebar.textColor ? theme.sidebar.textColor
: theme.colors.primary.main, : theme.colors.primary.main,
groupItems: { groupItems: {
activeBackgroundColor: theme => darken(0.1, theme.sidebar.backgroundColor),
activeTextColor: theme => theme.sidebar.activeTextColor,
textTransform: 'uppercase', textTransform: 'uppercase',
}, },
level1Items: { level1Items: {
activeBackgroundColor: theme => darken(0.05, theme.sidebar.backgroundColor),
activeTextColor: theme => theme.sidebar.activeTextColor,
textTransform: 'none', textTransform: 'none',
}, },
arrow: { arrow: {
@ -319,9 +323,13 @@ export interface ResolvedThemeInterface {
textColor: string; textColor: string;
activeTextColor: string; activeTextColor: string;
groupItems: { groupItems: {
activeBackgroundColor: string;
activeTextColor: string;
textTransform: string; textTransform: string;
}; };
level1Items: { level1Items: {
activeBackgroundColor: string;
activeTextColor: string;
textTransform: string; textTransform: string;
}; };
arrow: { arrow: {

View File

@ -114,6 +114,7 @@ export interface OpenAPISchema {
type?: string | string[]; type?: string | string[];
properties?: { [name: string]: OpenAPISchema }; properties?: { [name: string]: OpenAPISchema };
additionalProperties?: boolean | OpenAPISchema; additionalProperties?: boolean | OpenAPISchema;
unevaluatedProperties?: boolean | OpenAPISchema;
description?: string; description?: string;
default?: any; default?: any;
items?: OpenAPISchema; items?: OpenAPISchema;
@ -146,6 +147,7 @@ export interface OpenAPISchema {
minProperties?: number; minProperties?: number;
enum?: any[]; enum?: any[];
example?: any; example?: any;
examples?: any[];
const?: string; const?: string;
contentEncoding?: string; contentEncoding?: string;
contentMediaType?: string; contentMediaType?: string;

View File

@ -2330,6 +2330,20 @@ and standard method from web, mobile and desktop applications.
"openapi": "3.1.0", "openapi": "3.1.0",
"paths": Object { "paths": Object {
"/pet": Object { "/pet": Object {
"delete": Object {
"operationId": "deletePetBy\\"Id",
"summary": "OperationId with quotes",
"tags": Array [
"pet",
],
},
"get": Object {
"operationId": "delete\\\\PetById",
"summary": "OperationId with backslash",
"tags": Array [
"pet",
],
},
"parameters": Array [ "parameters": Array [
Object { Object {
"description": "The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US", "description": "The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US",
@ -2762,6 +2776,10 @@ try {
"application/json": Object { "application/json": Object {
"schema": Object { "schema": Object {
"$ref": "#/components/schemas/ApiResponse", "$ref": "#/components/schemas/ApiResponse",
"unevaluatedProperties": Object {
"format": "int32",
"type": "integer",
},
}, },
}, },
}, },

View File

@ -146,7 +146,14 @@ describe('Utils', () => {
string: ['pattern', 'minLength', 'maxLength'], string: ['pattern', 'minLength', 'maxLength'],
array: ['items', 'maxItems', 'minItems', 'uniqueItems'], array: ['items', 'maxItems', 'minItems', 'uniqueItems'],
object: ['maxProperties', 'minProperties', 'required', 'additionalProperties', 'properties'], object: [
'maxProperties',
'minProperties',
'required',
'additionalProperties',
'unevaluatedProperties',
'properties',
],
}; };
Object.keys(tests).forEach(name => { Object.keys(tests).forEach(name => {
@ -212,6 +219,17 @@ describe('Utils', () => {
expect(isPrimitiveType(schema)).toEqual(false); expect(isPrimitiveType(schema)).toEqual(false);
}); });
it('should return false for array contains array type and schema has items (unevaluatedProperties)', () => {
const schema = {
type: ['array'],
items: {
type: 'object',
unevaluatedProperties: true,
},
};
expect(isPrimitiveType(schema)).toEqual(false);
});
it('should return false for array contains object and array types and schema has items', () => { it('should return false for array contains object and array types and schema has items', () => {
const schema = { const schema = {
type: ['array', 'object'], type: ['array', 'object'],
@ -223,6 +241,17 @@ describe('Utils', () => {
expect(isPrimitiveType(schema)).toEqual(false); expect(isPrimitiveType(schema)).toEqual(false);
}); });
it('should return false for array contains object and array types and schema has items (unevaluatedProperties)', () => {
const schema = {
type: ['array', 'object'],
items: {
type: 'object',
unevaluatedProperties: true,
},
};
expect(isPrimitiveType(schema)).toEqual(false);
});
it('should return false for array contains object and array types and schema has properties', () => { it('should return false for array contains object and array types and schema has properties', () => {
const schema = { const schema = {
type: ['array', 'object'], type: ['array', 'object'],
@ -281,6 +310,17 @@ describe('Utils', () => {
expect(isPrimitiveType(schema)).toEqual(false); expect(isPrimitiveType(schema)).toEqual(false);
}); });
it('should return false for object with unevaluatedProperties', () => {
const schema = {
type: 'array',
items: {
type: 'object',
unevaluatedProperties: true,
},
};
expect(isPrimitiveType(schema)).toEqual(false);
});
it('should work with externally provided type', () => { it('should work with externally provided type', () => {
const schema = { const schema = {
properties: { properties: {

View File

@ -1,5 +1,4 @@
import slugify from 'slugify'; import slugify from 'slugify';
import { format, parse } from 'url';
/** /**
* Maps over array passing `isLast` bool to iterator as the second argument * Maps over array passing `isLast` bool to iterator as the second argument
@ -113,7 +112,7 @@ const isObject = (item: any): boolean => {
}; };
const isMergebleObject = (item): boolean => { const isMergebleObject = (item): boolean => {
return isObject(item) && !Array.isArray(item); return isObject(item) && !isArray(item);
}; };
/** /**
@ -146,18 +145,23 @@ export function isAbsoluteUrl(url: string) {
export function resolveUrl(url: string, to: string) { export function resolveUrl(url: string, to: string) {
let res; let res;
if (to.startsWith('//')) { if (to.startsWith('//')) {
const { protocol: specProtocol } = parse(url); try {
res = `${specProtocol || 'https:'}${to}`; res = `${new URL(url).protocol || 'https:'}${to}`;
} catch {
res = `https:${to}`;
}
} else if (isAbsoluteUrl(to)) { } else if (isAbsoluteUrl(to)) {
res = to; res = to;
} else if (!to.startsWith('/')) { } else if (!to.startsWith('/')) {
res = stripTrailingSlash(url) + '/' + to; res = stripTrailingSlash(url) + '/' + to;
} else { } else {
const urlObj = parse(url); try {
res = format({ const urlObj = new URL(url);
...urlObj, urlObj.pathname = to;
pathname: to, res = urlObj.href;
}); } catch {
res = to;
}
} }
return stripTrailingSlash(res); return stripTrailingSlash(res);
} }
@ -195,8 +199,17 @@ function parseURL(url: string) {
} }
} }
export function escapeHTMLAttrChars(str: string): string {
return str.replace(/["\\]/g, '\\$&');
}
export function unescapeHTMLChars(str: string): string { export function unescapeHTMLChars(str: string): string {
return str return str
.replace(/&#(\d+);/g, (_m, code) => String.fromCharCode(parseInt(code, 10))) .replace(/&#(\d+);/g, (_m, code) => String.fromCharCode(parseInt(code, 10)))
.replace(/&amp;/g, '&'); .replace(/&amp;/g, '&')
.replace(/&quot;/g, '"');
}
export function isArray(value: unknown): value is Array<any> {
return Array.isArray(value);
} }

View File

@ -1,4 +1,6 @@
import type { Source, Document } from '@redocly/openapi-core'; import type { Source, Document } from '@redocly/openapi-core';
// eslint-disable-next-line import/no-internal-modules
import type { ResolvedConfig } from '@redocly/openapi-core/lib/config';
// eslint-disable-next-line import/no-internal-modules // eslint-disable-next-line import/no-internal-modules
import { bundle } from '@redocly/openapi-core/lib/bundle'; import { bundle } from '@redocly/openapi-core/lib/bundle';
@ -11,7 +13,7 @@ import { OpenAPISpec } from '../types';
import { IS_BROWSER } from './dom'; import { IS_BROWSER } from './dom';
export async function loadAndBundleSpec(specUrlOrObject: object | string): Promise<OpenAPISpec> { export async function loadAndBundleSpec(specUrlOrObject: object | string): Promise<OpenAPISpec> {
const config = new Config({}); const config = new Config({} as ResolvedConfig);
const bundleOpts = { const bundleOpts = {
config, config,
base: IS_BROWSER ? window.location.href : process.cwd(), base: IS_BROWSER ? window.location.href : process.cwd(),

View File

@ -16,7 +16,7 @@ import {
Referenced, Referenced,
} from '../types'; } from '../types';
import { IS_BROWSER } from './dom'; import { IS_BROWSER } from './dom';
import { isNumeric, removeQueryString, resolveUrl } from './helpers'; import { isNumeric, removeQueryString, resolveUrl, isArray } 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);
@ -97,11 +97,12 @@ const schemaKeywordTypes = {
minProperties: 'object', minProperties: 'object',
required: 'object', required: 'object',
additionalProperties: 'object', additionalProperties: 'object',
unevaluatedProperties: 'object',
properties: 'object', properties: 'object',
}; };
export function detectType(schema: OpenAPISchema): string { export function detectType(schema: OpenAPISchema): string {
if (schema.type !== undefined && !Array.isArray(schema.type)) { if (schema.type !== undefined && !isArray(schema.type)) {
return schema.type; return schema.type;
} }
const keywords = Object.keys(schemaKeywordTypes); const keywords = Object.keys(schemaKeywordTypes);
@ -124,16 +125,19 @@ export function isPrimitiveType(
} }
let isPrimitive = true; let isPrimitive = true;
const isArray = Array.isArray(type); const isArrayType = isArray(type);
if (type === 'object' || (isArray && type?.includes('object'))) { if (type === 'object' || (isArrayType && type?.includes('object'))) {
isPrimitive = isPrimitive =
schema.properties !== undefined schema.properties !== undefined
? Object.keys(schema.properties).length === 0 ? Object.keys(schema.properties).length === 0
: schema.additionalProperties === undefined; : schema.additionalProperties === undefined && schema.unevaluatedProperties === undefined;
} }
if (schema.items !== undefined && (type === 'array' || (isArray && type?.includes('array')))) { if (
schema.items !== undefined &&
(type === 'array' || (isArrayType && type?.includes('array')))
) {
isPrimitive = isPrimitiveType(schema.items, schema.items.type); isPrimitive = isPrimitiveType(schema.items, schema.items.type);
} }
@ -149,7 +153,7 @@ export function isFormUrlEncoded(contentType: string): boolean {
} }
function delimitedEncodeField(fieldVal: any, fieldName: string, delimiter: string): string { function delimitedEncodeField(fieldVal: any, fieldName: string, delimiter: string): string {
if (Array.isArray(fieldVal)) { if (isArray(fieldVal)) {
return fieldVal.map(v => v.toString()).join(delimiter); return fieldVal.map(v => v.toString()).join(delimiter);
} else if (typeof fieldVal === 'object') { } else if (typeof fieldVal === 'object') {
return Object.keys(fieldVal) return Object.keys(fieldVal)
@ -161,7 +165,7 @@ function delimitedEncodeField(fieldVal: any, fieldName: string, delimiter: strin
} }
function deepObjectEncodeField(fieldVal: any, fieldName: string): string { function deepObjectEncodeField(fieldVal: any, fieldName: string): string {
if (Array.isArray(fieldVal)) { if (isArray(fieldVal)) {
console.warn('deepObject style cannot be used with array value:' + fieldVal.toString()); console.warn('deepObject style cannot be used with array value:' + fieldVal.toString());
return ''; return '';
} else if (typeof fieldVal === 'object') { } else if (typeof fieldVal === 'object') {
@ -194,7 +198,7 @@ export function urlFormEncodePayload(
payload: object, payload: object,
encoding: { [field: string]: OpenAPIEncoding } = {}, encoding: { [field: string]: OpenAPIEncoding } = {},
) { ) {
if (Array.isArray(payload)) { if (isArray(payload)) {
throw new Error('Payload must have fields: ' + payload.toString()); throw new Error('Payload must have fields: ' + payload.toString());
} else { } else {
return Object.keys(payload) return Object.keys(payload)
@ -253,7 +257,7 @@ function serializeQueryParameter(
case 'form': case 'form':
return serializeFormValue(name, explode, value); return serializeFormValue(name, explode, value);
case 'spaceDelimited': case 'spaceDelimited':
if (!Array.isArray(value)) { if (!isArray(value)) {
console.warn('The style spaceDelimited is only applicable to arrays'); console.warn('The style spaceDelimited is only applicable to arrays');
return ''; return '';
} }
@ -263,7 +267,7 @@ function serializeQueryParameter(
return `${name}=${value.join('%20')}`; return `${name}=${value.join('%20')}`;
case 'pipeDelimited': case 'pipeDelimited':
if (!Array.isArray(value)) { if (!isArray(value)) {
console.warn('The style pipeDelimited is only applicable to arrays'); console.warn('The style pipeDelimited is only applicable to arrays');
return ''; return '';
} }
@ -273,7 +277,7 @@ function serializeQueryParameter(
return `${name}=${value.join('|')}`; return `${name}=${value.join('|')}`;
case 'deepObject': case 'deepObject':
if (!explode || Array.isArray(value) || typeof value !== 'object') { if (!explode || isArray(value) || typeof value !== 'object') {
console.warn('The style deepObject is only applicable for objects with explode=true'); console.warn('The style deepObject is only applicable for objects with explode=true');
return ''; return '';
} }
@ -329,7 +333,7 @@ export function serializeParameterValueWithMime(value: any, mime: string): strin
} }
export function serializeParameterValue( export function serializeParameterValue(
parameter: OpenAPIParameter & { serializationMime?: string }, parameter: (OpenAPIParameter & { serializationMime?: string }) | FieldModel,
value: any, value: any,
): string { ): string {
const { name, style, explode = false, serializationMime } = parameter; const { name, style, explode = false, serializationMime } = parameter;

View File

@ -8,6 +8,7 @@ const nodeExternals = require('webpack-node-externals')({
// bundle in modules that need transpiling + non-js (e.g. css) // bundle in modules that need transpiling + non-js (e.g. css)
allowlist: [ allowlist: [
'swagger2openapi', 'swagger2openapi',
'marked',
/reftools/, /reftools/,
'oas-resolver', 'oas-resolver',
'oas-kit-common', 'oas-kit-common',
@ -65,11 +66,12 @@ export default (env: { standalone?: boolean; browser?: boolean } = {}) => ({
'node-fetch': 'null', 'node-fetch': 'null',
'node-fetch-h2': 'null', 'node-fetch-h2': 'null',
yaml: 'null', yaml: 'null',
url: 'null',
'safe-json-stringify': 'null', 'safe-json-stringify': 'null',
} }
: (context, request, callback) => { : (context, request, callback) => {
// ignore node-fetch dep of swagger2openapi as it is not used // ignore node-fetch dep of swagger2openapi as it is not used
if (/esprima|node-fetch|node-fetch-h2|\/yaml|safe-json-stringify$/i.test(request)) { if (/esprima|node-fetch|node-fetch-h2|\/yaml|safe-json-stringify|url$/i.test(request)) {
return callback(null, 'var undefined'); return callback(null, 'var undefined');
} }
return nodeExternals(context, request, callback); return nodeExternals(context, request, callback);