Merge commit 'cfb6f0fde0d5b3bc57f8eb6c7cb90e6f39017116'

# Conflicts:
#	src/common-elements/panels.ts
#	src/components/ApiLogo/ApiLogo.tsx
This commit is contained in:
Eduardo Perez 2018-12-11 14:02:40 -05:00
commit ce84bafad5
57 changed files with 5100 additions and 2937 deletions

12
.dockerignore Normal file
View File

@ -0,0 +1,12 @@
*
!src/
!config
!demo/favicon.png
!custom.d.ts
!typings/styled-patch.d.ts
!tsconfig.json
!webpack.config.ts
!package.json
!yarn.lock

View File

@ -27,7 +27,7 @@ You will need [Node.js](http://nodejs.org) at `v8.0.0+` and [Yarn](https://yarnp
After cloning the repo, run: After cloning the repo, run:
```bash ```bash
$ yarn install # or yarn $ yarn install # or npm
``` ```
### Commonly used NPM scripts ### Commonly used NPM scripts

View File

@ -1,3 +1,42 @@
# [2.0.0-rc.0](https://github.com/Rebilly/ReDoc/compare/v2.0.0-alpha.41...v2.0.0-rc.0) (2018-11-27)
### Bug Fixes
* false-positive recursive detection with oneOf ([59eaa8d](https://github.com/Rebilly/ReDoc/commit/59eaa8d)), closes [#723](https://github.com/Rebilly/ReDoc/issues/723) [#585](https://github.com/Rebilly/ReDoc/issues/585)
* fix hideHostname also hiding basePath ([b5f3224](https://github.com/Rebilly/ReDoc/commit/b5f3224)), closes [#677](https://github.com/Rebilly/ReDoc/issues/677)
* fix spacing with nested markdown lists ([f2f6909](https://github.com/Rebilly/ReDoc/commit/f2f6909)), closes [#718](https://github.com/Rebilly/ReDoc/issues/718)
* improve scrolling performance in Chrome with non-wrapped json examples ([a69c402](https://github.com/Rebilly/ReDoc/commit/a69c402))
* nested oneOf button spacing ([3673720](https://github.com/Rebilly/ReDoc/commit/3673720)), closes [#719](https://github.com/Rebilly/ReDoc/issues/719)
* onLoaded callback not run on spec error ([e77df0c](https://github.com/Rebilly/ReDoc/commit/e77df0c)), closes [#690](https://github.com/Rebilly/ReDoc/issues/690)
* theme improvments by [@stasiukanya](https://github.com/stasiukanya) ([e2d0cd5](https://github.com/Rebilly/ReDoc/commit/e2d0cd5))
* **cli:** old peer dependency issue with styled-components ([#699](https://github.com/Rebilly/ReDoc/issues/699)) ([9e2853c](https://github.com/Rebilly/ReDoc/commit/9e2853c))
### Features
* Add feature to specify href for logo explicitly ([#645](https://github.com/Rebilly/ReDoc/issues/645)) ([87fd7d7](https://github.com/Rebilly/ReDoc/commit/87fd7d7))
* add support for markdown in Server Object ([155d214](https://github.com/Rebilly/ReDoc/commit/155d214))
* Add support for minLength and maxLength constraint humanization ([#700](https://github.com/Rebilly/ReDoc/issues/700)) ([f40568b](https://github.com/Rebilly/ReDoc/commit/f40568b)), closes [#42](https://github.com/Rebilly/ReDoc/issues/42) [/github.com/Rebilly/ReDoc/issues/42#issuecomment-371883853](https://github.com//github.com/Rebilly/ReDoc/issues/42/issues/issuecomment-371883853)
<a name="2.0.0-alpha.41"></a>
# [2.0.0-alpha.41](https://github.com/Rebilly/ReDoc/compare/v2.0.0-alpha.40...v2.0.0-alpha.41) (2018-10-18)
### Bug Fixes
* add null check in dispose method ([#675](https://github.com/Rebilly/ReDoc/issues/675)) ([6b7c5b7](https://github.com/Rebilly/ReDoc/commit/6b7c5b7))
* extensionHook not being used ([a4a4013](https://github.com/Rebilly/ReDoc/commit/a4a4013)), closes [#665](https://github.com/Rebilly/ReDoc/issues/665)
* fix issue with broken markdown caused by marked bug ([70cf293](https://github.com/Rebilly/ReDoc/commit/70cf293))
### Peer dependencies updates
* ReDoc now requires `styled-components@^4.0.1` to be installed if used as React component
<a name="2.0.0-alpha.40"></a> <a name="2.0.0-alpha.40"></a>
# [2.0.0-alpha.40](https://github.com/Rebilly/ReDoc/compare/v2.0.0-alpha.39...v2.0.0-alpha.40) (2018-10-05) # [2.0.0-alpha.40](https://github.com/Rebilly/ReDoc/compare/v2.0.0-alpha.39...v2.0.0-alpha.40) (2018-10-05)

View File

@ -5,7 +5,7 @@
[![Build Status](https://travis-ci.org/Rebilly/ReDoc.svg?branch=master)](https://travis-ci.org/Rebilly/ReDoc) [![Coverage Status](https://coveralls.io/repos/Rebilly/ReDoc/badge.svg?branch=master&service=github)](https://coveralls.io/github/Rebilly/ReDoc?branch=master) [![dependencies Status](https://david-dm.org/Rebilly/ReDoc/status.svg)](https://david-dm.org/Rebilly/ReDoc) [![devDependencies Status](https://david-dm.org/Rebilly/ReDoc/dev-status.svg)](https://david-dm.org/Rebilly/ReDoc#info=devDependencies) [![npm](http://img.shields.io/npm/v/redoc.svg)](https://www.npmjs.com/package/redoc) [![License](https://img.shields.io/npm/l/redoc.svg)](https://github.com/Rebilly/ReDoc/blob/master/LICENSE) [![Build Status](https://travis-ci.org/Rebilly/ReDoc.svg?branch=master)](https://travis-ci.org/Rebilly/ReDoc) [![Coverage Status](https://coveralls.io/repos/Rebilly/ReDoc/badge.svg?branch=master&service=github)](https://coveralls.io/github/Rebilly/ReDoc?branch=master) [![dependencies Status](https://david-dm.org/Rebilly/ReDoc/status.svg)](https://david-dm.org/Rebilly/ReDoc) [![devDependencies Status](https://david-dm.org/Rebilly/ReDoc/dev-status.svg)](https://david-dm.org/Rebilly/ReDoc#info=devDependencies) [![npm](http://img.shields.io/npm/v/redoc.svg)](https://www.npmjs.com/package/redoc) [![License](https://img.shields.io/npm/l/redoc.svg)](https://github.com/Rebilly/ReDoc/blob/master/LICENSE)
[![bundle size](http://img.badgesize.io/https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js?compression=gzip&max=300000)](https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js) [![npm](https://img.shields.io/npm/dm/redoc.svg)](https://www.npmjs.com/package/redoc) [![](https://data.jsdelivr.com/v1/package/npm/redoc/badge)](https://www.jsdelivr.com/package/npm/redoc) [![bundle size](http://img.badgesize.io/https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js?compression=gzip&max=300000)](https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js) [![npm](https://img.shields.io/npm/dm/redoc.svg)](https://www.npmjs.com/package/redoc) [![](https://data.jsdelivr.com/v1/package/npm/redoc/badge)](https://www.jsdelivr.com/package/npm/redoc) [![Docker Build Status](https://img.shields.io/docker/build/redocly/redoc.svg)](https://hub.docker.com/r/redocly/redoc/)
</div> </div>
@ -184,6 +184,17 @@ You can also specify `onLoaded` callback which will be called each time Redoc ha
/> />
``` ```
## The Docker way
ReDoc is available as pre-built Docker image in official [Docker Hub repository](https://hub.docker.com/r/redocly/redoc/). You may simply pull & run it:
docker pull redocly/redoc
docker run -p 8080:80 redocly/redoc
Also you may rewrite some predefined environment variables defined in [Dockerfile](./config/docker/Dockerfile). By default ReDoc starts with demo Petstore spec located at `http://petstore.swagger.io/v2/swagger.json`, but you may change this URL using environment variable `SPEC_URL`:
docker run -p 8080:80 -e SPEC_URL=https://api.example.com/openapi.json redocly/redoc
## ReDoc CLI ## ReDoc CLI
[See here](https://github.com/Rebilly/ReDoc/blob/master/cli/README.md) [See here](https://github.com/Rebilly/ReDoc/blob/master/cli/README.md)
@ -194,7 +205,7 @@ You can also specify `onLoaded` callback which will be called each time Redoc ha
You can inject Security Definitions widget into any place of your specification `description`. Check out details [here](docs/security-definitions-injection.md). You can inject Security Definitions widget into any place of your specification `description`. Check out details [here](docs/security-definitions-injection.md).
### Swagger vendor extensions ### Swagger vendor extensions
ReDoc makes use of the following [vendor extensions](http://swagger.io/specification/#vendorExtensions): ReDoc makes use of the following [vendor extensions](https://swagger.io/specification/#specificationExtensions):
* [`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-code-samples`](docs/redoc-vendor-extensions.md#x-code-samples) - specify operation code samples * [`x-code-samples`](docs/redoc-vendor-extensions.md#x-code-samples) - specify operation code samples

23
cli/Dockerfile Normal file
View File

@ -0,0 +1,23 @@
# Package the 'redoc-cli' as a docker image.
#
# To build:
# $ cd <Redoc project directory>
# $ docker build -t redoc-cli -f cli/Dockerfile .
#
# To run:
# To display the command line options:
# $ docker run --rm -it redoc-cli --help
# .. will display the comand line help
#
# To turn `swagger.yml` file in the current directory, to html documentation 'redoc-static.html'
# $ docker run --rm -it -v $PWD:/data redoc-cli bundle swagger.yml
FROM node:alpine
RUN npm install -g redoc-cli
WORKDIR /data
EXPOSE 8080
ENTRYPOINT ["redoc-cli"]
CMD []

View File

@ -5,15 +5,16 @@ import { renderToString } from 'react-dom/server';
import { ServerStyleSheet } from 'styled-components'; import { ServerStyleSheet } from 'styled-components';
import { compile } from 'handlebars'; import { compile } from 'handlebars';
import { createServer, ServerRequest, ServerResponse } from 'http'; import { createServer, IncomingMessage, ServerResponse } from 'http';
import { dirname, join } from 'path'; import { dirname, join, resolve } from 'path';
import * as zlib from 'zlib'; import * as zlib from 'zlib';
// @ts-ignore // @ts-ignore
import { createStore, loadAndBundleSpec, Redoc } from 'redoc'; import { createStore, loadAndBundleSpec, Redoc } from 'redoc';
import { createReadStream, existsSync, readFileSync, ReadStream, watch, writeFileSync } from 'fs'; import {watch} from 'chokidar';
import { createReadStream, existsSync, readFileSync, ReadStream, writeFileSync } from 'fs';
import * as mkdirp from 'mkdirp'; import * as mkdirp from 'mkdirp';
import * as YargsParser from 'yargs'; import * as YargsParser from 'yargs';
@ -167,28 +168,25 @@ async function serve(port: number, pathToSpec: string, options: Options = {}) {
server.listen(port, () => console.log(`Server started: http://127.0.0.1:${port}`)); server.listen(port, () => console.log(`Server started: http://127.0.0.1:${port}`));
if (options.watch && existsSync(pathToSpec)) { if (options.watch && existsSync(pathToSpec)) {
const pathToSpecDirectory = dirname(pathToSpec); const pathToSpecDirectory = resolve(dirname(pathToSpec));
const watchOptions = { const watchOptions = {
recursive: true, ignored: /(^|[\/\\])\../,
}; };
watch( const watcher = watch(pathToSpecDirectory, watchOptions);
pathToSpecDirectory, const log = console.log.bind(console);
watchOptions, watcher
debounce(async (event, filename) => { .on('change', async path => {
if (event === 'change' || event === 'rename') { log(`${path} changed, updating docs`);
console.log(`${join(pathToSpecDirectory, filename)} changed, updating docs`);
try { try {
spec = await loadAndBundleSpec(pathToSpec); spec = await loadAndBundleSpec(pathToSpec);
pageHTML = await getPageHTML(spec, pathToSpec, options); pageHTML = await getPageHTML(spec, pathToSpec, options);
console.log('Updated successfully'); log('Updated successfully');
} catch (e) { } catch (e) {
console.error('Error while updating: ', e.message); console.error('Error while updating: ', e.message);
} }})
} .on('error', error => console.error(`Watcher error: ${error}`))
}, 2200), .on('ready', () => log(`👀 Watching ${pathToSpecDirectory} for changes...`));
);
console.log(`👀 Watching ${pathToSpecDirectory} for changes...`);
} }
} }
@ -258,7 +256,7 @@ async function getPageHTML(
// credits: https://stackoverflow.com/a/9238214/1749888 // credits: https://stackoverflow.com/a/9238214/1749888
function respondWithGzip( function respondWithGzip(
contents: string | ReadStream, contents: string | ReadStream,
request: ServerRequest, request: IncomingMessage,
response: ServerResponse, response: ServerResponse,
headers = {}, headers = {},
) { ) {
@ -291,17 +289,6 @@ function respondWithGzip(
} }
} }
function debounce(callback: (...args) => void, time: number) {
let interval;
return (...args) => {
clearTimeout(interval);
interval = setTimeout(() => {
interval = null;
callback(...args);
}, time);
};
}
function isURL(str: string): boolean { function isURL(str: string): boolean {
return /^(https?:)\/\//m.test(str); return /^(https?:)\/\//m.test(str);
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "redoc-cli", "name": "redoc-cli",
"version": "0.6.4", "version": "0.7.0",
"description": "ReDoc's Command Line Interface", "description": "ReDoc's Command Line Interface",
"main": "index.js", "main": "index.js",
"bin": "index.js", "bin": "index.js",
@ -8,16 +8,17 @@
"author": "Roman Hotsiy <gotsijroman@gmail.com>", "author": "Roman Hotsiy <gotsijroman@gmail.com>",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"chokidar": "^2.0.4",
"handlebars": "^4.0.11", "handlebars": "^4.0.11",
"isarray": "^2.0.4", "isarray": "^2.0.4",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"mobx": "^4.2.0", "mobx": "^4.2.0",
"react": "^16.4.2", "react": "^16.6.3",
"react-dom": "^16.4.2", "react-dom": "^16.6.3",
"redoc": "^2.0.0-alpha.37", "redoc": "^2.0.0-alpha.41",
"styled-components": "^3.4.0", "styled-components": "^4.1.1",
"tslib": "^1.9.3", "tslib": "^1.9.3",
"yargs": "^12.0.1" "yargs": "^12.0.5"
}, },
"scripts": { "scripts": {
"ci-publish": "ci-publish" "ci-publish": "ci-publish"
@ -26,6 +27,7 @@
"access": "public" "access": "public"
}, },
"devDependencies": { "devDependencies": {
"@types/chokidar": "^1.7.5",
"@types/handlebars": "^4.0.39", "@types/handlebars": "^4.0.39",
"@types/mkdirp": "^0.5.2", "@types/mkdirp": "^0.5.2",
"ci-publish": "^1.3.1" "ci-publish": "^1.3.1"

File diff suppressed because it is too large Load Diff

View File

@ -9,10 +9,16 @@ FROM node:alpine
RUN apk update && apk add --no-cache git RUN apk update && apk add --no-cache git
# generate bundle # Install dependencies
WORKDIR /build WORKDIR /build
COPY . /build COPY package.json yarn.lock /build/
RUN yarn install --frozen-lockfile --ignore-optional --ignore-scripts RUN yarn install --frozen-lockfile --ignore-optional --ignore-scripts
# copy only required for the build files
COPY src /build/src
COPY webpack.config.ts tsconfig.json custom.d.ts /build/
COPY typings/styled-patch.d.ts /build/typings/styled-patch.d.ts
RUN npm run bundle:standalone RUN npm run bundle:standalone
FROM nginx:alpine FROM nginx:alpine

View File

@ -1,8 +1,4 @@
# Redoc docker image # Official ReDoc Docker Image
## Build
docker build -t redoc .
## Usage ## Usage
@ -24,3 +20,7 @@ Serve local file:
- `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/Rebilly/ReDoc#redoc-tag-attributes) - `REDOC_OPTIONS` - [`<redoc>` tag attributes](https://github.com/Rebilly/ReDoc#redoc-tag-attributes)
## Build
docker build -t redoc .

6
config/docker/hooks/build Executable file
View File

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

View File

@ -20,7 +20,7 @@
<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="redoc.standalone.js"></script>
</body> </body>
</html> </html>

View File

@ -4,16 +4,10 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import styled, { StyledFunction } from 'styled-components'; import styled from '../src/styled-components';
function withProps<T, U extends HTMLElement = HTMLElement>( const DropDownItem = styled.li<{ active?: boolean }>`
styledFunction: StyledFunction<React.HTMLProps<U>>, ${(props: any) => (props.active ? 'background-color: #eee' : '')};
): StyledFunction<T & React.HTMLProps<U>> {
return styledFunction;
}
const DropDownItem = withProps<{ active: boolean }>(styled.li)`
${props => ((props as any).active ? 'background-color: #eee' : '')};
padding: 13px 16px; padding: 13px 16px;
&:hover { &:hover {
background-color: #eee; background-color: #eee;

View File

@ -296,6 +296,8 @@ paths:
style: form style: form
schema: schema:
type: array type: array
minItems: 1
maxItems: 3
items: items:
type: string type: string
enum: enum:
@ -784,6 +786,7 @@ components:
photoUrls: photoUrls:
description: The list of URL to a cute photos featuring pet description: The list of URL to a cute photos featuring pet
type: array type: array
maxItems: 20
xml: xml:
name: photoUrl name: photoUrl
wrapped: true wrapped: true
@ -796,6 +799,7 @@ components:
tags: tags:
description: Tags attached to the pet description: Tags attached to the pet
type: array type: array
minItems: 1
xml: xml:
name: tag name: tag
wrapped: true wrapped: true

View File

@ -36,6 +36,7 @@ const babelLoader = mode => ({
plugins: compact([ plugins: compact([
['@babel/plugin-syntax-typescript', { isTSX: true }], ['@babel/plugin-syntax-typescript', { isTSX: true }],
['@babel/plugin-syntax-decorators', { legacy: true }], ['@babel/plugin-syntax-decorators', { legacy: true }],
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-syntax-jsx', '@babel/plugin-syntax-jsx',
mode !== 'production' ? 'react-hot-loader/babel' : undefined, mode !== 'production' ? 'react-hot-loader/babel' : undefined,
[ [
@ -96,7 +97,7 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
use: [tsLoader(env), babelLoader(mode)], use: [tsLoader(env), babelLoader(mode)],
exclude: ['node_modules'], exclude: [/node_modules/],
}, },
{ {
test: /\.css$/, test: /\.css$/,
@ -132,7 +133,11 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
new webpack.NamedModulesPlugin(), new webpack.NamedModulesPlugin(),
new webpack.optimize.ModuleConcatenationPlugin(), new webpack.optimize.ModuleConcatenationPlugin(),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
template: env.playground ? 'demo/playground/index.html' : 'demo/index.html', template: env.playground
? 'demo/playground/index.html'
: env.bench
? 'benchmark/index.html'
: 'demo/index.html',
}), }),
new ForkTsCheckerWebpackPlugin(), new ForkTsCheckerWebpackPlugin(),
ignore(/js-yaml\/dumper\.js$/), ignore(/js-yaml\/dumper\.js$/),

View File

@ -1,10 +1,10 @@
# ReDoc vendor extensions # ReDoc vendor extensions
ReDoc makes use of the following [vendor extensions](http://swagger.io/specification/#vendorExtensions) ReDoc makes use of the following [vendor extensions](https://swagger.io/specification/#specificationExtensions)
### Swagger Object vendor extensions ### Swagger Object vendor extensions
Extend OpenAPI root [Swagger Object](http://swagger.io/specification/#swaggerObject) Extend OpenAPI root [Swagger Object](https://swagger.io/specification/#oasObject)
#### x-servers #### x-servers
Backported from OpenAPI 3.0 [`servers`](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.md#server-object). Currently doesn't support templates. Backported from OpenAPI 3.0 [`servers`](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md#server-object). Currently doesn't support templates.
#### x-tagGroups #### x-tagGroups
@ -96,6 +96,7 @@ The information about API logo
| url | string | The URL pointing to the spec logo. MUST be in the format of a URL. It SHOULD be an absolute URL so your API definition is usable from any location | url | string | The URL pointing to the spec logo. MUST be in the format of a URL. It SHOULD be an absolute URL so your API definition is usable from any location
| backgroundColor | string | background color to be used. MUST be RGB color in [hexadecimal format] (https://en.wikipedia.org/wiki/Web_colors#Hex_triplet) | backgroundColor | string | background color to be used. MUST be RGB color in [hexadecimal format] (https://en.wikipedia.org/wiki/Web_colors#Hex_triplet)
| altText | string | Text to use for alt tag on the logo. Defaults to 'logo' if nothing is provided. | altText | string | Text to use for alt tag on the logo. Defaults to 'logo' if nothing is provided.
| href | string | The URL pointing to the contact page. Default to 'info.contact.url' field of the OAS.
###### x-logo example ###### x-logo example
@ -205,7 +206,7 @@ Extends OpenAPI [Parameter Object](http://swagger.io/specification/#parameterObj
`x-examples` are rendered in the JSON tab on the right panel of ReDoc. `x-examples` are rendered in the JSON tab on the right panel of ReDoc.
### Response Object vendor extensions ### Response Object vendor extensions
Extends OpenAPI [Response Object](https://swagger.io/specification/#responseObject) Extends OpenAPI [Response Object](https://swagger.io/specification/#requestBodyObject)
#### x-summary #### x-summary
| Field Name | Type | Description | | Field Name | Type | Description |

View File

@ -1,6 +1,6 @@
{ {
"name": "redoc", "name": "redoc",
"version": "2.0.0-alpha.40", "version": "2.0.0-rc.0",
"description": "ReDoc", "description": "ReDoc",
"repository": { "repository": {
"type": "git", "type": "git",
@ -48,84 +48,87 @@
"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": "npm run build:demo && deploy-to-gh-pages --update demo/dist", "deploy:demo": "npm run build:demo && deploy-to-gh-pages --update demo/dist",
"license-check": "license-checker --production --onlyAllow 'MIT;ISC;Apache-2.0;BSD;BSD-2-Clause;BSD-3-Clause' --summary" "license-check": "license-checker --production --onlyAllow 'MIT;ISC;Apache-2.0;BSD;BSD-2-Clause;BSD-3-Clause' --summary",
"docker:build": "docker build -f config/docker/Dockerfile -t redoc ."
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.0.0-rc.2", "@babel/core": "7.1.6",
"@babel/plugin-syntax-decorators": "7.0.0-rc.2", "@babel/plugin-syntax-decorators": "7.1.0",
"@babel/plugin-syntax-jsx": "7.0.0-rc.2", "@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-syntax-typescript": "7.0.0-rc.2", "@babel/plugin-syntax-jsx": "7.0.0",
"@cypress/webpack-preprocessor": "2.0.1", "@babel/plugin-syntax-typescript": "7.1.5",
"@types/chai": "4.1.4", "@cypress/webpack-preprocessor": "4.0.2",
"@types/dompurify": "^0.0.31", "@types/chai": "4.1.7",
"@types/enzyme": "^3.1.13", "@types/dompurify": "^0.0.32",
"@types/enzyme": "^3.1.15",
"@types/enzyme-to-json": "^1.5.2", "@types/enzyme-to-json": "^1.5.2",
"@types/jest": "^23.3.1", "@types/jest": "^23.3.9",
"@types/json-pointer": "^1.0.30", "@types/json-pointer": "^1.0.30",
"@types/lodash": "^4.14.116", "@types/lodash": "^4.14.118",
"@types/lunr": "^2.1.6", "@types/lunr": "^2.1.6",
"@types/mark.js": "^8.11.1", "@types/mark.js": "^8.11.1",
"@types/marked": "^0.4.1", "@types/marked": "^0.4.2",
"@types/prismjs": "^1.6.4", "@types/prismjs": "^1.6.4",
"@types/prop-types": "^15.5.5", "@types/prop-types": "^15.5.6",
"@types/react": "^16.4.11", "@types/react": "^16.7.7",
"@types/react-dom": "^16.0.7", "@types/react-dom": "^16.0.10",
"@types/react-hot-loader": "^4.1.0", "@types/react-hot-loader": "^4.1.0",
"@types/react-tabs": "^1.0.5", "@types/react-tabs": "^2.3.0",
"@types/styled-components": "^4.1.1",
"@types/tapable": "1.0.4", "@types/tapable": "1.0.4",
"@types/webpack": "^4.4.11", "@types/webpack": "^4.4.19",
"@types/webpack-env": "^1.13.0", "@types/webpack-env": "^1.13.0",
"@types/yargs": "^11.1.1", "@types/yargs": "^12.0.1",
"babel-loader": "8.0.0-beta.2", "babel-loader": "8.0.4",
"babel-plugin-styled-components": "^1.5.1", "babel-plugin-styled-components": "^1.9.0",
"beautify-benchmark": "^0.2.4", "beautify-benchmark": "^0.2.4",
"bundlesize": "^0.17.0", "bundlesize": "^0.17.0",
"conventional-changelog-cli": "^2.0.5", "conventional-changelog-cli": "^2.0.11",
"copy-webpack-plugin": "^4.5.2", "copy-webpack-plugin": "^4.6.0",
"core-js": "^2.5.7", "core-js": "^2.5.7",
"coveralls": "^3.0.2", "coveralls": "^3.0.2",
"css-loader": "^1.0.0", "css-loader": "^1.0.1",
"cypress": "~3.1.0", "cypress": "~3.1.2",
"deploy-to-gh-pages": "^1.3.6", "deploy-to-gh-pages": "^1.3.6",
"enzyme": "^3.4.4", "enzyme": "^3.7.0",
"enzyme-adapter-react-16": "^1.2.0", "enzyme-adapter-react-16": "^1.7.0",
"enzyme-to-json": "^3.3.4", "enzyme-to-json": "^3.3.4",
"fork-ts-checker-webpack-plugin": "0.4.3", "fork-ts-checker-webpack-plugin": "0.5.0",
"html-webpack-plugin": "^3.1.0", "html-webpack-plugin": "^3.1.0",
"jest": "^23.5.0", "jest": "^23.6.0",
"license-checker": "^20.2.0", "license-checker": "^24.0.1",
"lodash": "^4.17.10", "lodash": "^4.17.11",
"mobx": "^4.3.1", "mobx": "^4.3.1",
"prettier": "^1.14.2", "prettier": "^1.15.2",
"prettier-eslint": "^8.8.2", "prettier-eslint": "^8.8.2",
"puppeteer": "^1.7.0", "puppeteer": "^1.10.0",
"raf": "^3.4.0", "raf": "^3.4.1",
"react": "^16.4.2", "react": "^16.6.3",
"react-dom": "^16.4.2", "react-dom": "^16.6.3",
"rimraf": "^2.6.2", "rimraf": "^2.6.2",
"shelljs": "^0.8.1", "shelljs": "^0.8.3",
"source-map-loader": "^0.2.4", "source-map-loader": "^0.2.4",
"style-loader": "^0.22.1", "style-loader": "^0.23.1",
"styled-components": "^3.4.5", "styled-components": "^4.1.1",
"swagger2openapi": "^3.2.8", "swagger2openapi": "^3.2.14",
"ts-jest": "23.0.1", "ts-jest": "23.10.5",
"ts-loader": "4.5.0", "ts-loader": "5.3.1",
"ts-node": "^7.0.1", "ts-node": "^7.0.1",
"tslint": "^5.11.0", "tslint": "^5.11.0",
"tslint-react": "^3.4.0", "tslint-react": "^3.4.0",
"typescript": "^3.0.1", "typescript": "^3.1.6",
"webpack": "^4.17.1", "webpack": "^4.26.1",
"webpack-cli": "^3.1.0", "webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.5", "webpack-dev-server": "^3.1.10",
"webpack-node-externals": "^1.6.0", "webpack-node-externals": "^1.6.0",
"workerize-loader": "^1.0.3", "workerize-loader": "^1.0.4",
"yaml-js": "^0.2.3" "yaml-js": "^0.2.3"
}, },
"peerDependencies": { "peerDependencies": {
"mobx": "^4.2.0", "mobx": "^4.2.0",
"react": "^16.2.0", "react": "^16.2.0",
"react-dom": "^16.2.0", "react-dom": "^16.2.0",
"styled-components": "^3.4.0" "styled-components": "^4.0.1"
}, },
"dependencies": { "dependencies": {
"classnames": "^2.2.6", "classnames": "^2.2.6",
@ -133,10 +136,10 @@
"dompurify": "^1.0.7", "dompurify": "^1.0.7",
"eventemitter3": "^3.0.0", "eventemitter3": "^3.0.0",
"json-pointer": "^0.6.0", "json-pointer": "^0.6.0",
"json-schema-ref-parser": "^5.1.2", "json-schema-ref-parser": "^6.0.1",
"lunr": "^2.3.2", "lunr": "^2.3.2",
"mark.js": "^8.11.1", "mark.js": "^8.11.1",
"marked": "^0.5.1", "marked": "^0.5.2",
"memoize-one": "^4.0.0", "memoize-one": "^4.0.0",
"mobx-react": "^5.2.5", "mobx-react": "^5.2.5",
"openapi-sampler": "1.0.0-beta.14", "openapi-sampler": "1.0.0-beta.14",
@ -151,10 +154,6 @@
"stickyfill": "^1.1.1", "stickyfill": "^1.1.1",
"tslib": "^1.9.3" "tslib": "^1.9.3"
}, },
"resolutions": {
"@types/chai": "4.0.8",
"@types/tapable": "1.0.0"
},
"bundlesize": [ "bundlesize": [
{ {
"path": "./bundles/redoc.standalone.js", "path": "./bundles/redoc.standalone.js",
@ -162,30 +161,8 @@
} }
], ],
"jest": { "jest": {
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"setupTestFrameworkScriptFile": "<rootDir>/src/setupTests.ts", "setupTestFrameworkScriptFile": "<rootDir>/src/setupTests.ts",
"testPathIgnorePatterns": [ "preset": "ts-jest",
"/node_modules/",
"/benchmark/"
],
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json"
],
"moduleNameMapper": {
"\\.(css|less)$": "<rootDir>/src/empty.js"
},
"globals": {
"ts-jest": {
"skipBabel": true
}
},
"collectCoverageFrom": [ "collectCoverageFrom": [
"src/**/*.{ts,tsx}" "src/**/*.{ts,tsx}"
], ],
@ -195,8 +172,16 @@
"text-summary" "text-summary"
], ],
"coveragePathIgnorePatterns": [ "coveragePathIgnorePatterns": [
"\\.d\\.ts$" "\\.d\\.ts$",
] "/benchmark/",
"/node_modules/"
],
"modulePathIgnorePatterns": [
"/benchmark/"
],
"moduleNameMapper": {
"\\.(css|less)$": "<rootDir>/src/empty.js"
}
}, },
"prettier": { "prettier": {
"singleQuote": true, "singleQuote": true,

View File

@ -19,6 +19,7 @@ const Tip = styled.div`
border-radius: 4px; border-radius: 4px;
padding: 0.3em 0.6em; padding: 0.3em 0.6em;
text-align: center; text-align: center;
box-shadow: 0px 0px 5px 0px rgba(204, 204, 204, 1);
`; `;
const Content = styled.div` const Content = styled.div`

View File

@ -1,7 +1,6 @@
import Dropdown from 'react-dropdown'; import Dropdown from 'react-dropdown';
import { StyledComponentClass } from 'styled-components'; import styled from '../styled-components';
import styled, { withProps } from '../styled-components';
export interface DropdownOption { export interface DropdownOption {
label: string; label: string;
@ -14,7 +13,7 @@ export interface DropdownProps {
onChange: (val: DropdownOption) => void; onChange: (val: DropdownOption) => void;
} }
export const StyledDropdown = withProps<DropdownProps>(styled(Dropdown))` export const StyledDropdown = styled(Dropdown)`
min-width: 100px; min-width: 100px;
display: inline-block; display: inline-block;
position: relative; position: relative;
@ -24,7 +23,7 @@ export const StyledDropdown = withProps<DropdownProps>(styled(Dropdown))`
.Dropdown-control { .Dropdown-control {
font-family: ${props => props.theme.typography.headings.fontFamily}; font-family: ${props => props.theme.typography.headings.fontFamily};
position: relative; position: relative;
font-size: .929em; font-size: 0.929em;
width: 100%; width: 100%;
line-height: 1.5em; line-height: 1.5em;
vertical-align: middle; vertical-align: middle;
@ -39,6 +38,8 @@ export const StyledDropdown = withProps<DropdownProps>(styled(Dropdown))`
margin-top: 5px; margin-top: 5px;
background: white; background: white;
box-sizing: border-box;
&:hover { &:hover {
border-color: ${props => props.theme.colors.primary.main}; border-color: ${props => props.theme.colors.primary.main};
color: ${props => props.theme.colors.primary.main}; color: ${props => props.theme.colors.primary.main};
@ -84,14 +85,14 @@ export const StyledDropdown = withProps<DropdownProps>(styled(Dropdown))`
padding: 0.4em; padding: 0.4em;
&.is-selected { &.is-selected {
background-color: rgba(0, 0, 0, 0.05) background-color: rgba(0, 0, 0, 0.05);
} }
&:hover { &:hover {
background-color: rgba(38, 50, 56, 0.12) background-color: rgba(38, 50, 56, 0.12);
} }
} }
` as StyledComponentClass<any, DropdownProps>; `;
export const SimpleDropdown = styled(StyledDropdown)` export const SimpleDropdown = styled(StyledDropdown)`
margin-left: 10px; margin-left: 10px;
@ -105,7 +106,7 @@ export const SimpleDropdown = styled(StyledDropdown)`
background: transparent; background: transparent;
&:hover { &:hover {
color: ${props => props.theme.colors.main}; color: ${props => props.theme.colors.primary.main};
box-shadow: none; box-shadow: none;
} }
} }

View File

@ -1,6 +1,6 @@
// import { transparentize } from 'polished'; // import { transparentize } from 'polished';
import styled, { extensionsHook, withProps } from '../styled-components'; import styled, { extensionsHook } from '../styled-components';
import { deprecatedCss } from './mixins'; import { deprecatedCss } from './mixins';
export const PropertiesTableCaption = styled.caption` export const PropertiesTableCaption = styled.caption`
@ -10,7 +10,7 @@ export const PropertiesTableCaption = styled.caption`
color: ${props => props.theme.colors.text.secondary}; color: ${props => props.theme.colors.text.secondary};
`; `;
export const PropertyCell = styled.td` export const PropertyCell = styled.td<{ kind?: string }>`
border-left: 1px solid ${props => props.theme.schema.linesColor}; border-left: 1px solid ${props => props.theme.schema.linesColor};
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
@ -58,7 +58,7 @@ export const PropertyCellWithInner = styled(PropertyCell)`
padding: 0; padding: 0;
`; `;
export const PropertyNameCell = withProps<{ kind?: string }>(styled(PropertyCell))` export const PropertyNameCell = styled(PropertyCell)`
vertical-align: top; vertical-align: top;
line-height: 20px; line-height: 20px;
white-space: nowrap; white-space: nowrap;

View File

@ -8,7 +8,7 @@ const headerFontSize = {
export const headerCommonMixin = level => css` export const headerCommonMixin = level => css`
font-family: ${props => props.theme.typography.headings.fontFamily}; font-family: ${props => props.theme.typography.headings.fontFamily};
font-weight: 400; font-weight: ${({ theme }) => theme.typography.headings.fontWeight};
font-size: ${headerFontSize[level]}; font-size: ${headerFontSize[level]};
`; `;

View File

@ -1,5 +1,5 @@
import { SECTION_ATTR } from '../services/MenuStore'; import { SECTION_ATTR } from '../services/MenuStore';
import styled, { media, withProps } from '../styled-components'; import styled, { media } from '../styled-components';
export const MiddlePanel = styled.div` export const MiddlePanel = styled.div`
width: calc(100% - ${props => props.theme.rightPanel.width}); width: calc(100% - ${props => props.theme.rightPanel.width});
@ -12,18 +12,16 @@ export const MiddlePanel = styled.div`
`}; `};
`; `;
export const Section = withProps<{ underlined?: boolean }>( export const Section = styled.div.attrs(props => ({
styled.div.attrs({ [SECTION_ATTR]: props.id,
[SECTION_ATTR]: props => props.id, className: props.className ? `section ${props.className}` : 'section',
className: 'section', })) <{ underlined?: boolean }>`
} as any),
)`
padding: ${props => props.theme.spacing.sectionVertical}px 0; padding: ${props => props.theme.spacing.sectionVertical}px 0;
${media.lessThan('medium', true)` ${media.lessThan('medium', true)`
padding: 0; padding: 0;
`} `}
${props => ${(props: any) =>
(props.underlined && (props.underlined &&
` `
position: relative; position: relative;
@ -42,7 +40,7 @@ export const Section = withProps<{ underlined?: boolean }>(
export const RightPanel = styled.div` export const RightPanel = styled.div`
width: ${props => props.theme.rightPanel.width}; width: ${props => props.theme.rightPanel.width};
color: #fafbfc; color: ${({ theme }) => theme.rightPanel.textColor};
background-color: ${props => props.theme.rightPanel.backgroundColor}; background-color: ${props => props.theme.rightPanel.backgroundColor};
padding: 0 ${props => props.theme.spacing.sectionHorizontal}px; padding: 0 ${props => props.theme.spacing.sectionHorizontal}px;

View File

@ -4,7 +4,7 @@ import PerfectScrollbarType, * as PerfectScrollbarNamespace from 'perfect-scroll
import psStyles from 'perfect-scrollbar/css/perfect-scrollbar.css'; import psStyles from 'perfect-scrollbar/css/perfect-scrollbar.css';
import { OptionsContext } from '../components/OptionsProvider'; import { OptionsContext } from '../components/OptionsProvider';
import styled, { injectGlobal } from '../styled-components'; import styled, { createGlobalStyle } from '../styled-components';
/* /*
* perfect scrollbar umd bundle uses exports assignment while module uses default export * perfect scrollbar umd bundle uses exports assignment while module uses default export
@ -14,7 +14,7 @@ import styled, { injectGlobal } from '../styled-components';
const PerfectScrollbarConstructor = const PerfectScrollbarConstructor =
PerfectScrollbarNamespace.default || ((PerfectScrollbarNamespace as any) as PerfectScrollbarType); PerfectScrollbarNamespace.default || ((PerfectScrollbarNamespace as any) as PerfectScrollbarType);
injectGlobal`${psStyles && psStyles.toString()}`; const PSStyling = createGlobalStyle`${psStyles && psStyles.toString()}`;
const StyledScrollWrapper = styled.div` const StyledScrollWrapper = styled.div`
position: relative; position: relative;
@ -58,9 +58,12 @@ export class PerfectScrollbar extends React.Component<PerfectScrollbarProps> {
} }
return ( return (
<StyledScrollWrapper className={`scrollbar-container ${className}`} innerRef={this.handleRef}> <>
<PSStyling />
<StyledScrollWrapper className={`scrollbar-container ${className}`} ref={this.handleRef}>
{children} {children}
</StyledScrollWrapper> </StyledScrollWrapper>
</>
); );
} }
} }

View File

@ -1,7 +1,7 @@
import styled, { withProps } from '../styled-components'; import styled from '../styled-components';
export const OneOfList = styled.ul` export const OneOfList = styled.ul`
margin: 0; margin: 0 0 3px 0;
padding: 0; padding: 0;
list-style: none; list-style: none;
display: inline-block; display: inline-block;
@ -15,7 +15,7 @@ export const OneOfLabel = styled.span`
} }
`; `;
export const OneOfButton = withProps<{ active: boolean }>(styled.li)` export const OneOfButton = styled.li<{ active: boolean }>`
display: inline-block; display: inline-block;
margin-right: 10px; margin-right: 10px;
font-size: 0.8em; font-size: 0.8em;

View File

@ -1,6 +1,6 @@
import * as classnames from 'classnames'; import * as classnames from 'classnames';
import * as React from 'react'; import * as React from 'react';
import styled, { withProps } from '../styled-components'; import styled from '../styled-components';
const directionMap = { const directionMap = {
left: '90deg', left: '90deg',
@ -49,7 +49,7 @@ export const ShelfIcon = styled(IntShelfIcon)`
} }
`; `;
export const Badge = withProps<{ type: string }>(styled.span)` export const Badge = styled.span<{ type: string }>`
display: inline-block; display: inline-block;
padding: 0 5px; padding: 0 5px;
margin: 0; margin: 0;

View File

@ -1,4 +1,6 @@
import { darken } from 'polished';
import { Tabs as ReactTabs } from 'react-tabs'; import { Tabs as ReactTabs } from 'react-tabs';
import styled from '../styled-components'; import styled from '../styled-components';
export { Tab, TabList, TabPanel } from 'react-tabs'; export { Tab, TabList, TabPanel } from 'react-tabs';
@ -14,14 +16,14 @@ export const Tabs = styled(ReactTabs)`
padding: 5px 10px; padding: 5px 10px;
display: inline-block; display: inline-block;
background-color: rgba(0, 0, 0, 0.2); background-color: ${({ theme }) => darken(0.05, theme.rightPanel.backgroundColor)};
border-bottom: 1px solid rgba(0, 0, 0, 0.5); border-bottom: 1px solid rgba(0, 0, 0, 0.5);
cursor: pointer; cursor: pointer;
text-align: center; text-align: center;
outline: none; outline: none;
color: #ccc; color: ${({ theme }) => darken(theme.colors.tonalOffset, theme.rightPanel.textColor)};
margin: 5px; margin: 5px;
border: 1px solid #181f22; border: 1px solid ${({ theme }) => darken(0.1, theme.rightPanel.backgroundColor)};
border-radius: 5px; border-radius: 5px;
min-width: 60px; min-width: 60px;
font-size: 0.9em; font-size: 0.9em;
@ -29,7 +31,7 @@ export const Tabs = styled(ReactTabs)`
&.react-tabs__tab--selected { &.react-tabs__tab--selected {
color: ${props => props.theme.colors.text.primary}; color: ${props => props.theme.colors.text.primary};
background: #e2e2e2; background: ${({ theme }) => theme.rightPanel.textColor};
} }
&:only-child { &:only-child {
@ -55,7 +57,7 @@ export const Tabs = styled(ReactTabs)`
} }
} }
> .react-tabs__tab-panel { > .react-tabs__tab-panel {
background: #171e21; background: ${({ theme }) => theme.codeSample.backgroundColor};
& > div, & > div,
& > pre { & > pre {
padding: 20px; padding: 20px;
@ -74,7 +76,7 @@ export const SmallTabs = styled(Tabs)`
font-size: 13px; font-size: 13px;
font-weight: normal; font-weight: normal;
border-bottom: 1px dashed; border-bottom: 1px dashed;
color: #787b7d; color: ${({ theme }) => darken(theme.colors.tonalOffset, theme.rightPanel.textColor)};
border-radius: 0; border-radius: 0;
background: none; background: none;
@ -83,7 +85,7 @@ export const SmallTabs = styled(Tabs)`
} }
&.react-tabs__tab--selected { &.react-tabs__tab--selected {
color: #babcbf; color: ${({ theme }) => theme.rightPanel.textColor};
background: none; background: none;
} }
} }

View File

@ -12,6 +12,8 @@ export class ApiLogo extends React.Component<{ info: OpenAPIInfo }> {
return null; return null;
} }
const logoHref = logoInfo.href || (info.contact && info.contact.url);
// Use the english word logo if no alt text is provided // Use the english word logo if no alt text is provided
const altText = logoInfo.altText ? logoInfo.altText : 'logo'; const altText = logoInfo.altText ? logoInfo.altText : 'logo';
@ -25,7 +27,7 @@ export class ApiLogo extends React.Component<{ info: OpenAPIInfo }> {
return ( return (
<LogoWrap className="api-logo"> <LogoWrap className="api-logo">
{info.contact && info.contact.url ? LinkWrap(info.contact.url)(logo) : logo}{' '} {logoHref ? LinkWrap(logoHref)(logo) : logo}
</LogoWrap> </LogoWrap>
); );
} }

View File

@ -1,9 +1,11 @@
import * as React from 'react'; import * as React from 'react';
import { ShelfIcon } from '../../common-elements'; import { ShelfIcon } from '../../common-elements';
import { OperationModel } from '../../services'; import { OperationModel } from '../../services';
import { Markdown } from '../Markdown/Markdown';
import { OptionsContext } from '../OptionsProvider'; import { OptionsContext } from '../OptionsProvider';
import { SelectOnClick } from '../SelectOnClick/SelectOnClick'; import { SelectOnClick } from '../SelectOnClick/SelectOnClick';
import { getBasePath } from '../../utils';
import { import {
EndpointInfo, EndpointInfo,
HttpVerb, HttpVerb,
@ -60,10 +62,14 @@ export class Endpoint extends React.Component<EndpointProps, EndpointState> {
<ServersOverlay expanded={expanded}> <ServersOverlay expanded={expanded}>
{operation.servers.map(server => ( {operation.servers.map(server => (
<ServerItem key={server.url}> <ServerItem key={server.url}>
<div>{server.description}</div> <Markdown source={server.description || ''} compact={true} />
<SelectOnClick> <SelectOnClick>
<ServerUrl> <ServerUrl>
{!(hideHostname || options.hideHostname) && <span>{server.url}</span>} <span>
{hideHostname || options.hideHostname
? getBasePath(server.url)
: server.url}
</span>
{operation.path} {operation.path}
</ServerUrl> </ServerUrl>
</SelectOnClick> </SelectOnClick>

View File

@ -1,4 +1,4 @@
import styled, { withProps } from '../../styled-components'; import styled from '../../styled-components';
export const OperationEndpointWrap = styled.div` export const OperationEndpointWrap = styled.div`
cursor: pointer; cursor: pointer;
@ -14,10 +14,11 @@ export const ServerRelativeURL = styled.span`
text-overflow: ellipsis; text-overflow: ellipsis;
`; `;
export const EndpointInfo = withProps<{ expanded?: boolean; inverted?: boolean }>(styled.div)` export const EndpointInfo = styled.div<{ expanded?: boolean; inverted?: boolean }>`
padding: 10px 30px 10px ${props => (props.inverted ? '10px' : '20px')}; padding: 10px 30px 10px ${props => (props.inverted ? '10px' : '20px')};
border-radius: ${props => (props.inverted ? '0' : '4px 4px 0 0')}; border-radius: ${props => (props.inverted ? '0' : '4px 4px 0 0')};
background-color: ${props => (props.inverted ? 'transparent' : '#222d32')}; background-color: ${props =>
props.inverted ? 'transparent' : props.theme.codeSample.backgroundColor};
display: flex; display: flex;
white-space: nowrap; white-space: nowrap;
align-items: center; align-items: center;
@ -33,12 +34,12 @@ export const EndpointInfo = withProps<{ expanded?: boolean; inverted?: boolean }
} }
`; `;
export const HttpVerb = withProps<{ type: string }>(styled.span).attrs({ export const HttpVerb = styled.span.attrs((props: { type: string }) => ({
className: props => `http-verb ${props.type}`, className: `http-verb ${props.type}`,
})` }))<{ type: string }>`
font-size: 0.929em; font-size: 0.929em;
line-height: 20px; line-height: 20px;
background-color: ${props => props.theme.colors.http[props.type] || '#999999'}; background-color: ${(props: any) => props.theme.colors.http[props.type] || '#999999'};
color: #ffffff; color: #ffffff;
padding: 3px 10px; padding: 3px 10px;
text-transform: uppercase; text-transform: uppercase;
@ -46,7 +47,7 @@ export const HttpVerb = withProps<{ type: string }>(styled.span).attrs({
margin: 0; margin: 0;
`; `;
export const ServersOverlay = withProps<{ expanded: boolean }>(styled.div)` export const ServersOverlay = styled.div<{ expanded: boolean }>`
position: absolute; position: absolute;
width: 100%; width: 100%;
z-index: 100; z-index: 100;

View File

@ -1,10 +1,10 @@
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import * as React from 'react'; import * as React from 'react';
import styled, { withProps } from '../../styled-components'; import styled from '../../styled-components';
import { OpenAPIExternalDocumentation } from '../../types'; import { OpenAPIExternalDocumentation } from '../../types';
import { linksCss } from '../Markdown/styled.elements'; import { linksCss } from '../Markdown/styled.elements';
const LinkWrap = withProps<{ compact?: boolean }>(styled.div)` const LinkWrap = styled.div<{ compact?: boolean }>`
${linksCss}; ${linksCss};
${({ compact }) => (!compact ? 'margin: 1em 0' : '')} ${({ compact }) => (!compact ? 'margin: 1em 0' : '')}
`; `;

View File

@ -35,7 +35,7 @@ class Json extends React.PureComponent<JsonProps> {
<PrismDiv <PrismDiv
className={this.props.className} className={this.props.className}
// tslint:disable-next-line // tslint:disable-next-line
innerRef={node => (this.node = node!)} ref={node => (this.node = node!)}
dangerouslySetInnerHTML={{ __html: jsonToHTML(this.props.data) }} dangerouslySetInnerHTML={{ __html: jsonToHTML(this.props.data) }}
/> />
</JsonViewerWrap> </JsonViewerWrap>

View File

@ -9,6 +9,7 @@ export const jsonStyles = css`
font-size: ${props => props.theme.typography.code.fontSize}; font-size: ${props => props.theme.typography.code.fontSize};
white-space: ${({ theme }) => (theme.typography.code.wrap ? 'pre-wrap' : 'pre')}; white-space: ${({ theme }) => (theme.typography.code.wrap ? 'pre-wrap' : 'pre')};
contain: content;
overflow-x: auto; overflow-x: auto;
.callback-function { .callback-function {

View File

@ -1,9 +1,9 @@
import * as React from 'react'; import * as React from 'react';
import styled, { withProps } from '../../styled-components'; import styled from '../../styled-components';
import { Spinner } from './Spinner.svg'; import { Spinner } from './Spinner.svg';
const LoadingMessage = withProps<{ color: string }>(styled.div)` const LoadingMessage = styled.div<{ color: string }>`
font-family: helvetica, sans; font-family: helvetica, sans;
width: 100%; width: 100%;
text-align: center; text-align: center;

View File

@ -1,6 +1,8 @@
import { headerCommonMixin, linkifyMixin } from '../../common-elements'; import { headerCommonMixin, linkifyMixin } from '../../common-elements';
import { PrismDiv } from '../../common-elements/PrismDiv'; import { PrismDiv } from '../../common-elements/PrismDiv';
import styled, { css, extensionsHook, withProps } from '../../styled-components'; import styled, { css, extensionsHook, ResolvedThemeInterface } from '../../styled-components';
import { StyledComponent } from 'styled-components';
export const linksCss = css` export const linksCss = css`
a { a {
@ -17,9 +19,11 @@ export const linksCss = css`
} }
`; `;
export const StyledMarkdownBlock = withProps<{ compact?: boolean; inline?: boolean }>( export const StyledMarkdownBlock = styled(PrismDiv as StyledComponent<
styled(PrismDiv), 'div',
)` ResolvedThemeInterface,
{ compact?: boolean; inline?: boolean }
>)`
font-family: ${props => props.theme.typography.fontFamily}; font-family: ${props => props.theme.typography.fontFamily};
font-weight: ${props => props.theme.typography.fontWeightRegular}; font-weight: ${props => props.theme.typography.fontWeightRegular};
@ -68,6 +72,7 @@ export const StyledMarkdownBlock = withProps<{ compact?: boolean; inline?: boole
border: 1px solid rgba(38, 50, 56, 0.1); border: 1px solid rgba(38, 50, 56, 0.1);
padding: 0.1em 0.25em 0.2em; padding: 0.1em 0.25em 0.2em;
font-size: ${props => props.theme.typography.code.fontSize}; font-size: ${props => props.theme.typography.code.fontSize};
font-weight: ${({ theme }) => theme.typography.code.fontWeight};
word-break: break-word; word-break: break-word;
} }
@ -113,9 +118,11 @@ export const StyledMarkdownBlock = withProps<{ compact?: boolean; inline?: boole
padding-left: 2em; padding-left: 2em;
margin: 0; margin: 0;
margin-bottom: 1em; margin-bottom: 1em;
// > li {
// margin: 0.5em 0; ul, ol {
// } margin-bottom: 0;
margin-top: 0;
}
} }
table { table {
@ -135,7 +142,7 @@ export const StyledMarkdownBlock = withProps<{ compact?: boolean; inline?: boole
border-top: 1px solid #ccc; border-top: 1px solid #ccc;
&:nth-child(2n) { &:nth-child(2n) {
background-color: #f8f8f8; background-color: ${({ theme }) => theme.schema.nestedBackground};
} }
} }

View File

@ -32,7 +32,7 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps> {
} }
if (examplesNames.length > 1) { if (examplesNames.length > 1) {
return ( return (
<SmallTabs> <SmallTabs defaultIndex={0}>
<TabList> <TabList>
{examplesNames.map(name => ( {examplesNames.map(name => (
<Tab key={name}> {examples[name].summary || name} </Tab> <Tab key={name}> {examples[name].summary || name} </Tab>

View File

@ -1,3 +1,5 @@
// @ts-ignore
import Dropdown from 'react-dropdown';
import styled from '../../styled-components'; import styled from '../../styled-components';
import { StyledDropdown } from '../../common-elements'; import { StyledDropdown } from '../../common-elements';
@ -13,7 +15,7 @@ export const InvertedSimpleDropdown = styled(StyledDropdown)`
margin-left: 10px; margin-left: 10px;
text-transform: none; text-transform: none;
font-size: 0.929em; font-size: 0.929em;
border-bottom: 1px solid rgba(255, 255, 255, 0.9); border-bottom: 1px solid ${({ theme }) => theme.rightPanel.textColor};
margin: 0 0 10px 0; margin: 0 0 10px 0;
display: block; display: block;
@ -23,11 +25,11 @@ export const InvertedSimpleDropdown = styled(StyledDropdown)`
border: none; border: none;
padding: 0 1.2em 0 0; padding: 0 1.2em 0 0;
background: transparent; background: transparent;
color: rgba(255, 255, 255, 0.9); color: ${({ theme }) => theme.rightPanel.textColor};
box-shadow: none; box-shadow: none;
.Dropdown-arrow { .Dropdown-arrow {
border-top-color: rgba(255, 255, 255, 0.9); border-top-color: ${({ theme }) => theme.rightPanel.textColor};
} }
} }
.Dropdown-menu { .Dropdown-menu {

View File

@ -43,7 +43,14 @@ export const BackgroundStub = styled.div`
top: 0; top: 0;
bottom: 0; bottom: 0;
right: 0; right: 0;
width: calc((100% - ${({ theme }) => theme.menu.width}) * 0.4); width: ${({ theme }) => {
if (theme.rightPanel.width.endsWith('%')) {
const percents = parseInt(theme.rightPanel.width, 10);
return `calc((100% - ${theme.menu.width}) * ${percents / 100})`;
} else {
return theme.rightPanel.width;
}
}};
${media.lessThan('medium', true)` ${media.lessThan('medium', true)`
display: none; display: none;
`}; `};

View File

@ -1,5 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { darken } from 'polished';
import styled from '../../styled-components'; import styled from '../../styled-components';
import { MenuItemLabel } from '../SideMenu/styled.elements'; import { MenuItemLabel } from '../SideMenu/styled.elements';
@ -7,19 +8,20 @@ export const SearchWrap = styled.div`
padding: 5px 0; padding: 5px 0;
`; `;
export const SearchInput = styled.input.attrs({ export const SearchInput = styled.input.attrs(() => ({
className: 'search-input', className: 'search-input',
})` }))`
width: calc(100% - ${props => props.theme.spacing.unit * 8}px); width: calc(100% - ${props => props.theme.spacing.unit * 8}px);
box-sizing: border-box; box-sizing: border-box;
margin: 0 ${props => props.theme.spacing.unit * 4}px; margin: 0 ${props => props.theme.spacing.unit * 4}px;
padding: 5px ${props => props.theme.spacing.unit * 2}px 5px padding: 5px ${props => props.theme.spacing.unit * 2}px 5px
${props => props.theme.spacing.unit * 4}px; ${props => props.theme.spacing.unit * 4}px;
border: 0; border: 0;
border-bottom: 1px solid #e1e1e1; border-bottom: 1px solid ${({ theme }) => darken(0.1, theme.menu.backgroundColor)};
font-family: ${({ theme }) => theme.typography.fontFamily};
font-weight: bold; font-weight: bold;
font-size: 13px; font-size: 13px;
color: ${props => props.theme.colors.text}; color: ${props => props.theme.menu.textColor};
background-color: transparent; background-color: transparent;
outline: none; outline: none;
`; `;
@ -44,7 +46,7 @@ export const SearchIcon = styled((props: { className?: string }) => (
width: 0.9em; width: 0.9em;
path { path {
fill: ${props => props.theme.colors.text}; fill: ${props => props.theme.menu.textColor};
} }
`; `;

View File

@ -46,7 +46,7 @@ export class MenuItem extends React.Component<MenuItemProps> {
<MenuItemLi <MenuItemLi
onClick={this.activate} onClick={this.activate}
depth={item.depth} depth={item.depth}
innerRef={this.saveRef} ref={this.saveRef}
data-item-id={item.id} data-item-id={item.id}
> >
{item.type === 'operation' ? ( {item.type === 'operation' ? (
@ -80,20 +80,14 @@ export class MenuItem extends React.Component<MenuItemProps> {
export interface OperationMenuItemContentProps { export interface OperationMenuItemContentProps {
item: OperationModel; item: OperationModel;
className?: string;
} }
@observer @observer
class OperationMenuItemContent extends React.Component<OperationMenuItemContentProps> { class OperationMenuItemContent extends React.Component<OperationMenuItemContentProps> {
render() { render() {
const { item, className } = this.props; const { item } = this.props;
return ( return (
<MenuItemLabel <MenuItemLabel depth={item.depth} active={item.active} deprecated={item.deprecated}>
className={className}
depth={item.depth}
active={item.active}
deprecated={item.deprecated}
>
<OperationBadge type={item.httpVerb}>{shortenHTTPVerb(item.httpVerb)}</OperationBadge> <OperationBadge type={item.httpVerb}>{shortenHTTPVerb(item.httpVerb)}</OperationBadge>
<MenuItemTitle width="calc(100% - 38px)"> <MenuItemTitle width="calc(100% - 38px)">
{item.name} {item.name}

View File

@ -1,11 +1,12 @@
import * as classnames from 'classnames'; import * as classnames from 'classnames';
import { darken } from 'polished';
import { deprecatedCss, ShelfIcon } from '../../common-elements'; import { deprecatedCss, ShelfIcon } from '../../common-elements';
import styled, { css, withProps } from '../../styled-components'; import styled, { css } from '../../styled-components';
export const OperationBadge = withProps<{ type: string }>(styled.span).attrs({ export const OperationBadge = styled.span.attrs((props: { type: string }) => ({
className: props => `operation-type ${props.type}`, className: `operation-type ${props.type}`,
})` }))<{ type: string }>`
width: 32px; width: 32px;
display: inline-block; display: inline-block;
height: ${props => props.theme.typography.code.fontSize}; height: ${props => props.theme.typography.code.fontSize};
@ -61,17 +62,17 @@ export const OperationBadge = withProps<{ type: string }>(styled.span).attrs({
} }
`; `;
function menuItemActiveBg(depth): string { function menuItemActiveBg(depth, { theme }): string {
if (depth > 1) { if (depth > 1) {
return '#e1e1e1'; return darken(0.1, theme.menu.backgroundColor);
} else if (depth === 1) { } else if (depth === 1) {
return '#f0f0f0'; return darken(0.05, theme.menu.backgroundColor);
} else { } else {
return ''; return '';
} }
} }
export const MenuItemUl = withProps<{ expanded: boolean }>(styled.ul)` export const MenuItemUl = styled.ul<{ expanded: boolean }>`
margin: 0; margin: 0;
padding: 0; padding: 0;
@ -82,7 +83,7 @@ export const MenuItemUl = withProps<{ expanded: boolean }>(styled.ul)`
${props => (props.expanded ? '' : 'display: none;')}; ${props => (props.expanded ? '' : 'display: none;')};
`; `;
export const MenuItemLi = withProps<{ depth: number }>(styled.li)` export const MenuItemLi = styled.li<{ depth: number }>`
list-style: none inside none; list-style: none inside none;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -97,7 +98,7 @@ 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.colors.text.primary}; color: ${props => props.theme.menu.textColor};
`, `,
1: css` 1: css`
font-size: 0.929em; font-size: 0.929em;
@ -107,27 +108,25 @@ export const menuItemDepth = {
} }
`, `,
2: css` 2: css`
color: ${props => props.theme.colors.text.primary}; color: ${props => props.theme.menu.textColor};
`, `,
}; };
export const MenuItemLabel = withProps<{ export interface MenuItemLabelType {
depth: number; depth: number;
active: boolean; active: boolean;
deprecated?: boolean; deprecated?: boolean;
type?: string; type?: string;
}>( }
styled.label.attrs({
export const MenuItemLabel = styled.label.attrs((props: MenuItemLabelType) => ({
role: 'menuitem', role: 'menuitem',
className: props => className: classnames('-depth' + props.depth, {
classnames('-depth' + props.depth, {
active: props.active, active: props.active,
}), }),
}), }))<MenuItemLabelType>`
)`
cursor: pointer; cursor: pointer;
color: ${props => color: ${props => (props.active ? props.theme.colors.primary.main : props.theme.menu.textColor)};
props.active ? props.theme.colors.primary.main : props.theme.colors.text.primary};
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 }) =>
@ -136,12 +135,12 @@ export const MenuItemLabel = withProps<{
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) : '')}; background-color: ${props => (props.active ? menuItemActiveBg(props.depth, props) : '')};
${props => (props.deprecated && deprecatedCss) || ''}; ${props => (props.deprecated && deprecatedCss) || ''};
&:hover { &:hover {
background-color: ${props => menuItemActiveBg(props.depth)}; background-color: ${props => menuItemActiveBg(props.depth, props)};
} }
${ShelfIcon} { ${ShelfIcon} {
@ -153,7 +152,7 @@ export const MenuItemLabel = withProps<{
} }
`; `;
export const MenuItemTitle = withProps<{ width?: string }>(styled.span)` export const MenuItemTitle = styled.span<{ width?: string }>`
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
width: ${props => (props.width ? props.width : 'auto')}; width: ${props => (props.width ? props.width : 'auto')};
@ -173,8 +172,8 @@ export const RedocAttribution = styled.div`
a, a,
a:visited, a:visited,
a:hover { a:hover {
color: ${theme.colors.text.primary} !important; color: ${theme.menu.textColor} !important;
border-top: 1px solid #e1e1e1; border-top: 1px solid ${darken(0.1, theme.menu.backgroundColor)};
padding: ${theme.spacing.unit}px 0; padding: ${theme.spacing.unit}px 0;
display: block; display: block;
} }

View File

@ -3,7 +3,7 @@ import * as React from 'react';
import { MenuStore } from '../../services/MenuStore'; import { MenuStore } from '../../services/MenuStore';
import { RedocNormalizedOptions, RedocRawOptions } from '../../services/RedocNormalizedOptions'; import { RedocNormalizedOptions, RedocRawOptions } from '../../services/RedocNormalizedOptions';
import styled, { media, withProps } from '../../styled-components'; import styled, { media } from '../../styled-components';
import { IS_BROWSER } from '../../utils/index'; import { IS_BROWSER } from '../../utils/index';
import { OptionsContext } from '../OptionsProvider'; import { OptionsContext } from '../OptionsProvider';
import { AnimatedChevronButton } from './ChevronSvg'; import { AnimatedChevronButton } from './ChevronSvg';
@ -21,7 +21,7 @@ export interface StickySidebarProps {
const stickyfill = Stickyfill && Stickyfill(); const stickyfill = Stickyfill && Stickyfill();
const StyledStickySidebar = withProps<{ open?: boolean }>(styled.div)` const StyledStickySidebar = styled.div<{ open?: boolean }>`
width: ${props => props.theme.menu.width}; width: ${props => props.theme.menu.width};
background-color: ${props => props.theme.menu.backgroundColor}; background-color: ${props => props.theme.menu.backgroundColor};
overflow: hidden; overflow: hidden;
@ -122,7 +122,7 @@ export class StickyResponsiveSidebar extends React.Component<StickySidebarProps>
className={this.props.className} className={this.props.className}
style={style(options)} style={style(options)}
// tslint:disable-next-line // tslint:disable-next-line
innerRef={el => { ref={el => {
this.stickyElement = el as any; this.stickyElement = el as any;
}} }}
> >

View File

@ -52,7 +52,14 @@ export class StoreBuilder extends Component<StoreBuilderProps, StoreBuilderState
if (!spec) { if (!spec) {
return undefined; return undefined;
} }
try {
return new AppStore(spec, specUrl, options); return new AppStore(spec, specUrl, options);
} catch (e) {
if (this.props.onLoaded) {
this.props.onLoaded(e);
}
throw e;
}
} }
componentDidMount() { componentDidMount() {

View File

@ -4,7 +4,9 @@ import defaultTheme from '../theme';
export default class TestThemeProvider extends React.Component { export default class TestThemeProvider extends React.Component {
render() { render() {
return <ThemeProvider theme={defaultTheme}>{this.props.children}</ThemeProvider>; return (
<ThemeProvider theme={defaultTheme}>{React.Children.only(this.props.children)}</ThemeProvider>
);
} }
} }

View File

@ -1,4 +1,4 @@
import { observe } from 'mobx'; import { Lambda, observe } from 'mobx';
import { OpenAPISpec } from '../types'; import { OpenAPISpec } from '../types';
import { loadAndBundleSpec } from '../utils/loadAndBundleSpec'; import { loadAndBundleSpec } from '../utils/loadAndBundleSpec';
@ -58,7 +58,7 @@ export class AppStore {
marker = new MarkerService(); marker = new MarkerService();
private scroll: ScrollService; private scroll: ScrollService;
private disposer; private disposer: Lambda | null = null;
constructor( constructor(
spec: OpenAPISpec, spec: OpenAPISpec,
@ -96,8 +96,10 @@ export class AppStore {
dispose() { dispose() {
this.scroll.dispose(); this.scroll.dispose();
this.menu.dispose(); this.menu.dispose();
if (this.disposer != null) {
this.disposer(); this.disposer();
} }
}
/** /**
* serializes store * serializes store

View File

@ -291,6 +291,12 @@ export class OpenAPIParser {
return res; return res;
} }
exitParents(shema: MergedOpenAPISchema) {
for (const parent$ref of shema.parentRefs || []) {
this.exitRef({ $ref: parent$ref });
}
}
private hoistOneOfs(schema: OpenAPISchema) { private hoistOneOfs(schema: OpenAPISchema) {
if (schema.allOf === undefined) { if (schema.allOf === undefined) {
return schema; return schema;
@ -304,9 +310,14 @@ export class OpenAPIParser {
const afterAllOf = allOf.slice(i + 1); const afterAllOf = allOf.slice(i + 1);
return { return {
oneOf: sub.oneOf.map(part => { oneOf: sub.oneOf.map(part => {
return this.mergeAllOf({ const merged = this.mergeAllOf({
allOf: [...beforeAllOf, part, ...afterAllOf], allOf: [...beforeAllOf, part, ...afterAllOf],
}); });
// each oneOf should be independent so exiting all the parent refs
// otherwise it will cause false-positive recursive detection
this.exitParents(merged);
return merged;
}), }),
}; };
} }

View File

@ -126,14 +126,13 @@ export class RedocNormalizedOptions {
allowedMdComponents: Dict<MDXComponentMeta>; allowedMdComponents: Dict<MDXComponentMeta>;
constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) { constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) {
let hook;
raw = { ...defaults, ...raw }; raw = { ...defaults, ...raw };
if (raw.theme && raw.theme.extensionsHook) { const hook = raw.theme && raw.theme.extensionsHook;
hook = raw.theme.extensionsHook; this.theme = resolveTheme(
raw.theme.extensionsHook = undefined; mergeObjects({} as any, defaultTheme, { ...raw.theme, extensionsHook: undefined }),
} );
this.theme = resolveTheme(mergeObjects({} as any, defaultTheme, raw.theme || {}));
this.theme.extensionsHook = hook; this.theme.extensionsHook = hook as any;
this.scrollYOffset = RedocNormalizedOptions.normalizeScrollYOffset(raw.scrollYOffset); this.scrollYOffset = RedocNormalizedOptions.normalizeScrollYOffset(raw.scrollYOffset);
this.hideHostname = RedocNormalizedOptions.normalizeHideHostname(raw.hideHostname); this.hideHostname = RedocNormalizedOptions.normalizeHideHostname(raw.hideHostname);

View File

@ -21,15 +21,15 @@ describe('Models', () => {
parser = new OpenAPIParser(spec, undefined, opts); parser = new OpenAPIParser(spec, undefined, opts);
const schema = new SchemaModel(parser, spec.components.schemas.Test, '', opts); const schema = new SchemaModel(parser, spec.components.schemas.Test, '', opts);
expect(schema.fields).toHaveLength(3); expect(schema.fields).toHaveLength(3);
const oneOfField = schema.fields[0]; const oneOfField = schema.fields![0];
expect(oneOfField.schema.displayType).toBe('Foo (object) or Bar (object)'); expect(oneOfField.schema.displayType).toBe('Foo (object) or Bar (object)');
expect(oneOfField.schema.oneOf[0].title).toBe('Foo'); expect(oneOfField.schema.oneOf![0].title).toBe('Foo');
expect(oneOfField.schema.oneOf[1].title).toBe('Bar'); expect(oneOfField.schema.oneOf![1].title).toBe('Bar');
const anyOfField = schema.fields[1]; const anyOfField = schema.fields![1];
expect(anyOfField.schema.displayType).toBe('Foo (object) or Bar (object)'); expect(anyOfField.schema.displayType).toBe('Foo (object) or Bar (object)');
expect(anyOfField.schema.oneOf[0].title).toBe('Foo'); expect(anyOfField.schema.oneOf![0].title).toBe('Foo');
expect(anyOfField.schema.oneOf[1].title).toBe('Bar'); expect(anyOfField.schema.oneOf![1].title).toBe('Bar');
}); });
test('oneOf/allOf schema complex displayType', () => { test('oneOf/allOf schema complex displayType', () => {

View File

@ -11,6 +11,9 @@ export class ApiInfoModel implements OpenAPIInfo {
contact?: OpenAPIContact; contact?: OpenAPIContact;
license?: OpenAPILicense; license?: OpenAPILicense;
downloadLink?: string;
downloadFileName?: string;
constructor(private parser: OpenAPIParser) { constructor(private parser: OpenAPIParser) {
Object.assign(this, parser.spec.info); Object.assign(this, parser.spec.info);
this.description = parser.spec.info.description || ''; this.description = parser.spec.info.description || '';
@ -18,9 +21,12 @@ export class ApiInfoModel implements OpenAPIInfo {
if (firstHeadingLinePos > -1) { if (firstHeadingLinePos > -1) {
this.description = this.description.substring(0, firstHeadingLinePos); this.description = this.description.substring(0, firstHeadingLinePos);
} }
this.downloadLink = this.getDownloadLink();
this.downloadFileName = this.getDownloadFileName();
} }
get downloadLink(): string | undefined { private getDownloadLink(): string | undefined {
if (this.parser.specUrl) { if (this.parser.specUrl) {
return this.parser.specUrl; return this.parser.specUrl;
} }
@ -33,7 +39,7 @@ export class ApiInfoModel implements OpenAPIInfo {
} }
} }
get downloadFileName(): string | undefined { private getDownloadFileName(): string | undefined {
if (!this.parser.specUrl) { if (!this.parser.specUrl) {
return 'swagger.json'; return 'swagger.json';
} }

View File

@ -75,11 +75,7 @@ export class SchemaModel {
this.init(parser, isChild); this.init(parser, isChild);
parser.exitRef(schemaOrRef); parser.exitRef(schemaOrRef);
parser.exitParents(this.schema);
for (const parent$ref of this.schema.parentRefs || []) {
// exit all the refs visited during allOf traverse
parser.exitRef({ $ref: parent$ref });
}
if (options.showExtensions) { if (options.showExtensions) {
this.extensions = extractExtensions(this.schema, options.showExtensions); this.extensions = extractExtensions(this.schema, options.showExtensions);
@ -164,20 +160,28 @@ export class SchemaModel {
} }
private initOneOf(oneOf: OpenAPISchema[], parser: OpenAPIParser) { private initOneOf(oneOf: OpenAPISchema[], parser: OpenAPIParser) {
this.oneOf = oneOf!.map( this.oneOf = oneOf!.map((variant, idx) => {
(variant, idx) => const merged = parser.mergeAllOf(variant, this.pointer + '/oneOf/' + idx);
new SchemaModel(
const schema = new SchemaModel(
parser, parser,
// merge base schema into each of oneOf's subschemas // merge base schema into each of oneOf's subschemas
{ {
// variant may already have allOf so merge it to not get overwritten // variant may already have allOf so merge it to not get overwritten
...parser.mergeAllOf(variant, this.pointer + '/oneOf/' + idx), ...merged,
allOf: [{ ...this.schema, oneOf: undefined, anyOf: undefined }], allOf: [{ ...this.schema, oneOf: undefined, anyOf: undefined }],
} as OpenAPISchema, } as OpenAPISchema,
this.pointer + '/oneOf/' + idx, this.pointer + '/oneOf/' + idx,
this.options, this.options,
),
); );
// each oneOf should be independent so exiting all the parent refs
// otherwise it will cause false-positive recursive detection
parser.exitParents(merged);
return schema;
});
this.displayType = this.oneOf this.displayType = this.oneOf
.map(schema => { .map(schema => {
let name = let name =

View File

@ -1,24 +1,13 @@
import * as React from 'react';
import * as styledComponents from 'styled-components'; import * as styledComponents from 'styled-components';
import { ResolvedThemeInterface } from './theme'; import { ResolvedThemeInterface } from './theme';
export { ResolvedThemeInterface }; export { ResolvedThemeInterface };
export type InterpolationFunction<P> = styledComponents.InterpolationFunction<P>;
export type StyledFunction<T> = styledComponents.ThemedStyledFunction<T, ResolvedThemeInterface>;
function withProps<T, U extends HTMLElement = HTMLElement>(
styledFunction: StyledFunction<React.HTMLProps<U>>,
): StyledFunction<T & React.HTMLProps<U>> {
return styledFunction;
}
const { const {
default: styled, default: styled,
css, css,
injectGlobal, createGlobalStyle,
keyframes, keyframes,
ThemeProvider, ThemeProvider,
} = (styledComponents as any) as styledComponents.ThemedStyledComponentsModule< } = (styledComponents as any) as styledComponents.ThemedStyledComponentsModule<
@ -54,7 +43,7 @@ export const media = {
}, },
}; };
export { css, injectGlobal, keyframes, ThemeProvider, withProps }; export { css, createGlobalStyle, keyframes, ThemeProvider };
export default styled; export default styled;
export function extensionsHook(styledName: string) { export function extensionsHook(styledName: string) {

View File

@ -95,7 +95,7 @@ const defaultTheme: ThemeInterface = {
}, },
typography: { typography: {
fontSize: '14px', fontSize: '14px',
lineHeight: '1.5', lineHeight: '1.5em',
fontWeightRegular: '400', fontWeightRegular: '400',
fontWeightBold: '600', fontWeightBold: '600',
fontWeightLight: '300', fontWeightLight: '300',
@ -104,6 +104,7 @@ const defaultTheme: ThemeInterface = {
optimizeSpeed: true, optimizeSpeed: true,
headings: { headings: {
fontFamily: 'Montserrat, sans-serif', fontFamily: 'Montserrat, sans-serif',
fontWeight: '400',
}, },
code: { code: {
fontSize: '13px', fontSize: '13px',
@ -111,18 +112,19 @@ const defaultTheme: ThemeInterface = {
lineHeight: ({ typography }) => typography.lineHeight, lineHeight: ({ typography }) => typography.lineHeight,
fontWeight: ({ typography }) => typography.fontWeightRegular, fontWeight: ({ typography }) => typography.fontWeightRegular,
color: '#e53935', color: '#e53935',
backgroundColor: 'rgba(38, 50, 56, 0.04)', backgroundColor: 'rgba(38, 50, 56, 0.05)',
wrap: false, wrap: false,
}, },
links: { links: {
color: ({ colors }) => colors.primary.main, color: ({ colors }) => colors.primary.main,
visited: ({ colors }) => colors.primary.main, visited: ({ typography }) => typography.links.color,
hover: ({ colors }) => lighten(0.2, colors.primary.main), hover: ({ typography }) => lighten(0.2, typography.links.color),
}, },
}, },
menu: { menu: {
width: '260px', width: '260px',
backgroundColor: '#fafafa', backgroundColor: '#fafafa',
textColor: '#333333',
groupItems: { groupItems: {
textTransform: 'uppercase', textTransform: 'uppercase',
}, },
@ -131,7 +133,7 @@ const defaultTheme: ThemeInterface = {
}, },
arrow: { arrow: {
size: '1.5em', size: '1.5em',
color: theme => theme.colors.text.primary, color: theme => theme.menu.textColor,
}, },
}, },
logo: { logo: {
@ -143,6 +145,9 @@ const defaultTheme: ThemeInterface = {
width: '40%', width: '40%',
textColor: '#ffffff', textColor: '#ffffff',
}, },
codeSample: {
backgroundColor: ({ rightPanel }) => darken(0.1, rightPanel.backgroundColor),
},
}; };
export default defaultTheme; export default defaultTheme;
@ -275,6 +280,7 @@ export interface ResolvedThemeInterface {
}; };
headings: { headings: {
fontFamily: string; fontFamily: string;
fontWeight: string;
}; };
links: { links: {
@ -286,6 +292,7 @@ export interface ResolvedThemeInterface {
menu: { menu: {
width: string; width: string;
backgroundColor: string; backgroundColor: string;
textColor: string;
groupItems: { groupItems: {
textTransform: string; textTransform: string;
}; };
@ -306,6 +313,9 @@ export interface ResolvedThemeInterface {
textColor: string; textColor: string;
width: string; width: string;
}; };
codeSample: {
backgroundColor: string;
};
extensionsHook?: (name: string, props: any) => string; extensionsHook?: (name: string, props: any) => string;
} }

View File

@ -2,6 +2,7 @@ import {
detectType, detectType,
getOperationSummary, getOperationSummary,
getStatusCodeType, getStatusCodeType,
humanizeConstraints,
isOperationName, isOperationName,
isPrimitiveType, isPrimitiveType,
mergeParams, mergeParams,
@ -321,4 +322,35 @@ describe('Utils', () => {
expect(servers[2].url).toEqual('http://127.0.0.3'); expect(servers[2].url).toEqual('http://127.0.0.3');
}); });
}); });
describe('openapi humanizeConstraints', () => {
const itemConstraintSchema = (
min: number | undefined = undefined,
max: number | undefined = undefined,
) => ({ type: 'array', minItems: min, maxItems: max });
it('should not have a humanized constraint without schema constraints', () => {
expect(humanizeConstraints(itemConstraintSchema())).toHaveLength(0);
});
it('should have a humanized constraint when minItems is set', () => {
expect(humanizeConstraints(itemConstraintSchema(2))).toContain('>= 2 items');
});
it('should have a humanized constraint when maxItems is set', () => {
expect(humanizeConstraints(itemConstraintSchema(undefined, 8))).toContain('<= 8 items');
});
it('should have a humanized constraint when minItems and maxItems are both set', () => {
expect(humanizeConstraints(itemConstraintSchema(2, 8))).toContain('[ 2 .. 8 ] items');
});
it('should have a humanized constraint when minItems and maxItems are the same', () => {
expect(humanizeConstraints(itemConstraintSchema(7, 7))).toContain('7 items');
});
it('should have a humazined constraint when justMinItems is set, and it is equal to 1', () => {
expect(humanizeConstraints(itemConstraintSchema(1))).toContain('non-empty');
});
});
}); });

View File

@ -161,3 +161,7 @@ export function resolveUrl(url: string, to: string) {
} }
return stripTrailingSlash(res); return stripTrailingSlash(res);
} }
export function getBasePath(serverUrl: string): string {
return new URL(serverUrl).pathname;
}

View File

@ -141,29 +141,44 @@ export function isNamedDefinition(pointer?: string): boolean {
return /^#\/components\/schemas\/[^\/]+$/.test(pointer || ''); return /^#\/components\/schemas\/[^\/]+$/.test(pointer || '');
} }
function humanizeRangeConstraint(
description: string,
min: number | undefined,
max: number | undefined,
): string | undefined {
let stringRange;
if (min !== undefined && max !== undefined) {
if (min === max) {
stringRange = `${min} ${description}`;
} else {
stringRange = `[ ${min} .. ${max} ] ${description}`;
}
} else if (max !== undefined) {
stringRange = `<= ${max} ${description}`;
} else if (min !== undefined) {
if (min === 1) {
stringRange = 'non-empty';
} else {
stringRange = `>= ${min} ${description}`;
}
}
return stringRange;
}
export function humanizeConstraints(schema: OpenAPISchema): string[] { export function humanizeConstraints(schema: OpenAPISchema): string[] {
const res: string[] = []; const res: string[] = [];
let stringRange; const stringRange = humanizeRangeConstraint('characters', schema.minLength, schema.maxLength);
if (schema.minLength !== undefined && schema.maxLength !== undefined) {
if (schema.minLength === schema.maxLength) {
stringRange = `${schema.minLength} characters`;
} else {
stringRange = `[ ${schema.minLength} .. ${schema.maxLength} ] characters`;
}
} else if (schema.maxLength !== undefined) {
stringRange = `<= ${schema.maxLength} characters`;
} else if (schema.minLength !== undefined) {
if (schema.minLength === 1) {
stringRange = 'non-empty';
} else {
stringRange = `>= ${schema.minLength} characters`;
}
}
if (stringRange !== undefined) { if (stringRange !== undefined) {
res.push(stringRange); res.push(stringRange);
} }
const arrayRange = humanizeRangeConstraint('items', schema.minItems, schema.maxItems);
if (arrayRange !== undefined) {
res.push(arrayRange);
}
let numberRange; let numberRange;
if (schema.minimum !== undefined && schema.maximum !== undefined) { if (schema.minimum !== undefined && schema.maximum !== undefined) {
numberRange = schema.exclusiveMinimum ? '( ' : '[ '; numberRange = schema.exclusiveMinimum ? '( ' : '[ ';

View File

@ -1,43 +1,21 @@
import * as styledComponents from 'styled-components'; import * as styledComponents from 'styled-components';
// Styled components typings for using babel-plugin BEFORE typescript // FIXME
declare module 'styled-components' { declare module 'styled-components' {
interface ThemedStyledFunction<P, T, O = P> { export interface ThemedStyledFunction<
// adding "| string[]" for transpileTemplateLiterals and similar below C extends keyof JSX.IntrinsicElements | React.ComponentType<any>,
( T extends object,
strings: TemplateStringsArray | string[], O extends object = {},
...interpolations: Interpolation<ThemedStyledProps<P, T>>[] A extends keyof any = never
): StyledComponentClass<P, T, O>; > extends ThemedStyledFunctionBase<C, T, O, A> {
withConfig(config: any): any;
new <U>( // tslint:enable:unified-signatures
strings: TemplateStringsArray | string[],
...interpolations: Interpolation<ThemedStyledProps<P & U, T>>[]
): StyledComponentClass<P & U, T, O & U>;
// adding "withConfig" for transpileTemplateLiterals
withConfig(config: any): ThemedStyledFunction<P, T, O>;
}
export interface ThemedCssFunction<T> {
// adding "| string[]" for transpileTemplateLiterals and similar below
(
strings: TemplateStringsArray | string[],
...interpolations: SimpleInterpolation[]
): InterpolationValue[];
<P>(
strings: TemplateStringsArray | string[],
...interpolations: Interpolation<ThemedStyledProps<P, T>>[]
): FlattenInterpolation<ThemedStyledProps<P, T>>[];
} }
interface ThemedStyledComponentsModule<T> { interface ThemedStyledComponentsModule<T> {
keyframes( keyframes(
strings: TemplateStringsArray | string[], strings: TemplateStringsArray | string[],
...interpolations: SimpleInterpolation[] ...interpolations: SimpleInterpolation[]
): string; ): Keyframes;
injectGlobal(
strings: TemplateStringsArray | string[],
...interpolations: SimpleInterpolation[]
): void;
} }
} }

View File

@ -17,12 +17,18 @@ const nodeExternals = require('webpack-node-externals')({
}); });
const VERSION = JSON.stringify(require('./package.json').version); const VERSION = JSON.stringify(require('./package.json').version);
const REVISION = JSON.stringify( let REVISION;
try {
REVISION = JSON.stringify(
require('child_process') require('child_process')
.execSync('git rev-parse --short HEAD') .execSync('git rev-parse --short HEAD')
.toString() .toString()
.trim(), .trim(),
); );
} catch (e) {
console.error('Skipping REDOC_REVISION');
}
const BANNER = `ReDoc - OpenAPI/Swagger-generated API Reference Documentation const BANNER = `ReDoc - OpenAPI/Swagger-generated API Reference Documentation
------------------------------------------------------------- -------------------------------------------------------------
@ -106,7 +112,7 @@ export default (env: { standalone?: boolean } = {}, { mode }) => ({
}, },
}, },
], ],
exclude: ['node_modules'], exclude: [/node_modules/],
}, },
{ {
test: /node_modules\/(swagger2openapi|reftools|oas-resolver|oas-kit-common|oas-schema-walker)\/.*\.js$/, test: /node_modules\/(swagger2openapi|reftools|oas-resolver|oas-kit-common|oas-schema-walker)\/.*\.js$/,

5400
yarn.lock

File diff suppressed because it is too large Load Diff