merge master forker

This commit is contained in:
Yurov Dmitry 2019-05-08 13:00:17 +03:00
commit 4452eb1b75
49 changed files with 7033 additions and 2620 deletions

View File

@ -1,3 +1,59 @@
# [2.0.0-rc.4](https://github.com/Rebilly/ReDoc/compare/v2.0.0-rc.3...v2.0.0-rc.4) (2019-03-15)
### Bug Fixes
* move swagger2openapi to deps because of missing transitive deps ([ed9b878](https://github.com/Rebilly/ReDoc/commit/ed9b878))
### Features
* display requestBody description [#833](https://github.com/Rebilly/ReDoc/issues/833) ([#838](https://github.com/Rebilly/ReDoc/issues/838)) ([56ca371](https://github.com/Rebilly/ReDoc/commit/56ca371))
# [2.0.0-rc.3](https://github.com/Rebilly/ReDoc/compare/v2.0.0-rc.2...v2.0.0-rc.3) (2019-03-15)
### Bug Fixes
* add extra deref step for anyOf/oneOf variants ([d81b631](https://github.com/Rebilly/ReDoc/commit/d81b631)), closes [#810](https://github.com/Rebilly/ReDoc/issues/810)
* duplicate keys in request samples ([3ce5bff](https://github.com/Rebilly/ReDoc/commit/3ce5bff)), closes [#815](https://github.com/Rebilly/ReDoc/issues/815)
* escape backslashes in string literals ([#823](https://github.com/Rebilly/ReDoc/issues/823)) ([70faca1](https://github.com/Rebilly/ReDoc/commit/70faca1)), closes [#822](https://github.com/Rebilly/ReDoc/issues/822)
* escape quotes in string values ([0473165](https://github.com/Rebilly/ReDoc/commit/0473165)), closes [#882](https://github.com/Rebilly/ReDoc/issues/882)
* pin lunr version in ReDoc ([178ff4c](https://github.com/Rebilly/ReDoc/commit/178ff4c)), closes [#844](https://github.com/Rebilly/ReDoc/issues/844)
* set last section min-height ([4dd79cd](https://github.com/Rebilly/ReDoc/commit/4dd79cd)), closes [#820](https://github.com/Rebilly/ReDoc/issues/820)
### Features
* support externalValue for examples ([2cdfcd2](https://github.com/Rebilly/ReDoc/commit/2cdfcd2)), closes [#551](https://github.com/Rebilly/ReDoc/issues/551) [#840](https://github.com/Rebilly/ReDoc/issues/840)
* **cli:** Add templateOptions param to pass additional data to custom template ([#792](https://github.com/Rebilly/ReDoc/issues/792)) ([4e8ee03](https://github.com/Rebilly/ReDoc/commit/4e8ee03))
# [2.0.0-rc.2](https://github.com/Rebilly/ReDoc/compare/v2.0.0-rc.1...v2.0.0-rc.2) (2019-01-27)
### Bug Fixes
* make padding for md code blocks and code samples consistent ([007752d](https://github.com/Rebilly/ReDoc/commit/007752d))
* make syntax highlighting for md js code blocks same as for payload samples ([d197c0f](https://github.com/Rebilly/ReDoc/commit/d197c0f))
* Only display API version if present ([#773](https://github.com/Rebilly/ReDoc/issues/773)) ([fb3cb36](https://github.com/Rebilly/ReDoc/commit/fb3cb36))
# [2.0.0-rc.1](https://github.com/Rebilly/ReDoc/compare/v2.0.0-rc.0...v2.0.0-rc.1) (2019-01-17)
### Bug Fixes
* allow docker container serving under non-root URLs ([#731](https://github.com/Rebilly/ReDoc/issues/731)) ([cfb6f0f](https://github.com/Rebilly/ReDoc/commit/cfb6f0f)), closes [#730](https://github.com/Rebilly/ReDoc/issues/730)
* make example/defaults badge consistent with code blocks ([fa39ce4](https://github.com/Rebilly/ReDoc/commit/fa39ce4))
* pattern constrain spacing ([c7436f2](https://github.com/Rebilly/ReDoc/commit/c7436f2))
* sidebar navigation issues when scrollYOffset is float number ([c04f387](https://github.com/Rebilly/ReDoc/commit/c04f387)), closes [#748](https://github.com/Rebilly/ReDoc/issues/748)
# [2.0.0-rc.0](https://github.com/Rebilly/ReDoc/compare/v2.0.0-alpha.41...v2.0.0-rc.0) (2018-11-27)

View File

@ -162,10 +162,10 @@ Also you can pass options:
```js
<RedocStandalone
specUrl="http://rebilly.github.io/RebillyAPI/swagger.json"
specUrl="http://rebilly.github.io/RebillyAPI/openapi.json"
options={{
nativeScrollbars: true,
theme: { colors: { main: '#dd5522' } },
theme: { colors: { primary { main: '#dd5522' } } },
}}
/>
```
@ -176,7 +176,7 @@ You can also specify `onLoaded` callback which will be called each time Redoc ha
```js
<RedocStandalone
specUrl="http://rebilly.github.io/RebillyAPI/swagger.json"
specUrl="http://rebilly.github.io/RebillyAPI/openapi.json"
onLoaded={error => {
if (!error) {
console.log('Yay!');

1
cli

@ -1 +0,0 @@
Subproject commit a16c4a85125a95d45864a6f03fbdcc8892eb4874

1
cli/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/node_modules

4
cli/.npmignore Normal file
View File

@ -0,0 +1,4 @@
*/
!index.js
!package.json
!README.md

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 []

21
cli/README.md Normal file
View File

@ -0,0 +1,21 @@
# redoc-cli
**[ReDoc](https://github.com/Rebilly/ReDoc)'s Command Line Interface**
## Installation
You can use redoc cli by installing `redoc-cli` globally or using [npx](https://medium.com/@maybekatz/introducing-npx-an-npm-package-runner-55f7d4bd282b).
## Usage
Two following commands are available:
- `redoc-cli serve [spec]` - starts the server with `spec` rendered with ReDoc. Supports SSR mode (`--ssr`) and can watch the spec (`--watch`)
- `redoc-cli bundle [spec]` - bundles spec and ReDoc into **zero-dependency** HTML file.
Some examples:
- Bundle with main color changed to `orange`: <br> `$ redoc-cli bundle [spec] --options.theme.colors.primary.main=orange`
- Serve with `nativeScrollbars` option set to true: <br> `$ redoc-cli serve [spec] --options.nativeScrollbars`
- Bundle using custom template (check [default template](https://github.com/Rebilly/ReDoc/blob/master/cli/template.hbs) for reference): <br> `$ redoc-cli bundle [spec] -t custom.hbs`
For more details run `redoc-cli --help`.

264
cli/index.js Normal file
View File

@ -0,0 +1,264 @@
#!/usr/bin/env node
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const React = require("react");
const server_1 = require("react-dom/server");
const styled_components_1 = require("styled-components");
const handlebars_1 = require("handlebars");
const http_1 = require("http");
const path_1 = require("path");
const zlib = require("zlib");
// @ts-ignore
const redoc_1 = require("redoc");
const chokidar_1 = require("chokidar");
const fs_1 = require("fs");
const mkdirp = require("mkdirp");
const YargsParser = require("yargs");
const BUNDLES_DIR = path_1.dirname(require.resolve('redoc'));
/* tslint:disable-next-line */
YargsParser.command('serve <spec>', 'start the server', yargs => {
yargs.positional('spec', {
describe: 'path or URL to your spec',
});
yargs.option('s', {
alias: 'ssr',
describe: 'Enable server-side rendering',
type: 'boolean',
});
yargs.option('p', {
alias: 'port',
type: 'number',
default: 8080,
});
yargs.option('w', {
alias: 'watch',
type: 'boolean',
});
yargs.demandOption('spec');
return yargs;
}, (argv) => __awaiter(this, void 0, void 0, function* () {
const config = {
ssr: argv.ssr,
watch: argv.watch,
templateFileName: argv.template,
redocOptions: argv.options || {},
};
try {
yield serve(argv.port, argv.spec, config);
}
catch (e) {
handleError(e);
}
}))
.command('bundle <spec>', 'bundle spec into zero-dependency HTML-file', 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',
default: 'ReDoc documentation',
});
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;
}, (argv) => __awaiter(this, void 0, void 0, function* () {
const config = {
ssr: true,
output: argv.o,
cdn: argv.cdn,
title: argv.title,
templateFileName: argv.template,
redocOptions: argv.options || {},
};
try {
yield bundle(argv.spec, config);
}
catch (e) {
handleError(e);
}
}))
.demandCommand()
.options('t', {
alias: 'template',
describe: 'Path to handlebars page template, see https://git.io/vh8fP for the example ',
type: 'string',
})
.options('options', {
describe: 'ReDoc options, use dot notation, e.g. options.nativeScrollbars',
}).argv;
function serve(port, pathToSpec, options = {}) {
return __awaiter(this, void 0, void 0, function* () {
let spec = yield redoc_1.loadAndBundleSpec(pathToSpec);
let pageHTML = yield getPageHTML(spec, pathToSpec, options);
const server = http_1.createServer((request, response) => {
console.time('GET ' + request.url);
if (request.url === '/redoc.standalone.js') {
respondWithGzip(fs_1.createReadStream(path_1.join(BUNDLES_DIR, 'redoc.standalone.js'), 'utf8'), request, response, {
'Content-Type': 'application/javascript',
});
}
else if (request.url === '/') {
respondWithGzip(pageHTML, request, response);
}
else if (request.url === '/spec.json') {
const specStr = JSON.stringify(spec, null, 2);
respondWithGzip(specStr, request, response, {
'Content-Type': 'application/json',
});
}
else {
response.writeHead(404);
response.write('Not found');
response.end();
}
console.timeEnd('GET ' + request.url);
});
console.log();
server.listen(port, () => console.log(`Server started: http://127.0.0.1:${port}`));
if (options.watch && fs_1.existsSync(pathToSpec)) {
const pathToSpecDirectory = path_1.resolve(path_1.dirname(pathToSpec));
const watchOptions = {
ignored: /(^|[\/\\])\../,
};
const watcher = chokidar_1.watch(pathToSpecDirectory, watchOptions);
const log = console.log.bind(console);
watcher
.on('change', (path) => __awaiter(this, void 0, void 0, function* () {
log(`${path} changed, updating docs`);
try {
spec = yield redoc_1.loadAndBundleSpec(pathToSpec);
pageHTML = yield getPageHTML(spec, pathToSpec, options);
log('Updated successfully');
}
catch (e) {
console.error('Error while updating: ', e.message);
}
}))
.on('error', error => console.error(`Watcher error: ${error}`))
.on('ready', () => log(`👀 Watching ${pathToSpecDirectory} for changes...`));
}
});
}
function bundle(pathToSpec, options = {}) {
return __awaiter(this, void 0, void 0, function* () {
const start = Date.now();
const spec = yield redoc_1.loadAndBundleSpec(pathToSpec);
const pageHTML = yield getPageHTML(spec, pathToSpec, Object.assign({}, options, { ssr: true }));
mkdirp.sync(path_1.dirname(options.output));
fs_1.writeFileSync(options.output, pageHTML);
const sizeInKiB = Math.ceil(Buffer.byteLength(pageHTML) / 1024);
const time = Date.now() - start;
console.log(`\n🎉 bundled successfully in: ${options.output} (${sizeInKiB} KiB) [⏱ ${time / 1000}s]`);
});
}
function getPageHTML(spec, pathToSpec, { ssr, cdn, title, templateFileName, redocOptions = {} }) {
return __awaiter(this, void 0, void 0, function* () {
let html;
let css;
let state;
let redocStandaloneSrc;
if (ssr) {
console.log('Prerendering docs');
const specUrl = redocOptions.specUrl || (isURL(pathToSpec) ? pathToSpec : undefined);
const store = yield redoc_1.createStore(spec, specUrl, redocOptions);
const sheet = new styled_components_1.ServerStyleSheet();
html = server_1.renderToString(sheet.collectStyles(React.createElement(redoc_1.Redoc, { store })));
css = sheet.getStyleTags();
state = yield store.toJS();
if (!cdn) {
redocStandaloneSrc = fs_1.readFileSync(path_1.join(BUNDLES_DIR, 'redoc.standalone.js'));
}
}
templateFileName = templateFileName ? templateFileName : path_1.join(__dirname, './template.hbs');
const template = handlebars_1.compile(fs_1.readFileSync(templateFileName).toString());
return template({
redocHTML: `
<div id="redoc">${(ssr && html) || ''}</div>
<script>
${(ssr && `const __redoc_state = ${sanitizeJSONString(JSON.stringify(state))};`) || ''}
var container = document.getElementById('redoc');
Redoc.${ssr
? 'hydrate(__redoc_state, container);'
: `init("spec.json", ${JSON.stringify(redocOptions)}, container)`};
</script>`,
redocHead: ssr
? (cdn
? '<script src="https://unpkg.com/redoc@next/bundles/redoc.standalone.js"></script>'
: `<script>${redocStandaloneSrc}</script>`) + css
: '<script src="redoc.standalone.js"></script>',
title,
});
});
}
// credits: https://stackoverflow.com/a/9238214/1749888
function respondWithGzip(contents, request, response, headers = {}) {
let compressedStream;
const acceptEncoding = request.headers['accept-encoding'] || '';
if (acceptEncoding.match(/\bdeflate\b/)) {
response.writeHead(200, Object.assign({}, headers, { 'content-encoding': 'deflate' }));
compressedStream = zlib.createDeflate();
}
else if (acceptEncoding.match(/\bgzip\b/)) {
response.writeHead(200, Object.assign({}, headers, { 'content-encoding': 'gzip' }));
compressedStream = zlib.createGzip();
}
else {
response.writeHead(200, headers);
if (typeof contents === 'string') {
response.write(contents);
response.end();
}
else {
contents.pipe(response);
}
return;
}
if (typeof contents === 'string') {
compressedStream.write(contents);
compressedStream.pipe(response);
compressedStream.end();
return;
}
else {
contents.pipe(compressedStream).pipe(response);
}
}
function isURL(str) {
return /^(https?:)\/\//m.test(str);
}
function sanitizeJSONString(str) {
return escapeClosingScriptTag(escapeUnicode(str));
}
// see http://www.thespanner.co.uk/2011/07/25/the-json-specification-is-now-wrong/
function escapeClosingScriptTag(str) {
return str.replace(/<\/script>/g, '<\\/script>');
}
// see http://www.thespanner.co.uk/2011/07/25/the-json-specification-is-now-wrong/
function escapeUnicode(str) {
return str.replace(/\u2028|\u2029/g, m => '\\u202' + (m === '\u2028' ? '8' : '9'));
}
function handleError(error) {
console.error(error.stack);
process.exit(1);
}

314
cli/index.ts Normal file
View File

@ -0,0 +1,314 @@
#!/usr/bin/env node
/* tslint:disable:no-implicit-dependencies */
import * as React from 'react';
import { renderToString } from 'react-dom/server';
import { ServerStyleSheet } from 'styled-components';
import { compile } from 'handlebars';
import { createServer, IncomingMessage, ServerResponse } from 'http';
import { dirname, join, resolve } from 'path';
import * as zlib from 'zlib';
// @ts-ignore
import { createStore, loadAndBundleSpec, Redoc } from 'redoc';
import { watch } from 'chokidar';
import { createReadStream, existsSync, readFileSync, ReadStream, writeFileSync } from 'fs';
import * as mkdirp from 'mkdirp';
import * as YargsParser from 'yargs';
interface Options {
ssr?: boolean;
watch?: boolean;
cdn?: boolean;
output?: string;
title?: string;
templateFileName?: string;
redocOptions?: any;
}
const BUNDLES_DIR = dirname(require.resolve('redoc'));
/* tslint:disable-next-line */
YargsParser.command(
'serve <spec>',
'start the server',
yargs => {
yargs.positional('spec', {
describe: 'path or URL to your spec',
});
yargs.option('s', {
alias: 'ssr',
describe: 'Enable server-side rendering',
type: 'boolean',
});
yargs.option('p', {
alias: 'port',
type: 'number',
default: 8080,
});
yargs.option('w', {
alias: 'watch',
type: 'boolean',
});
yargs.demandOption('spec');
return yargs;
},
async argv => {
const config = {
ssr: argv.ssr,
watch: argv.watch,
templateFileName: argv.template,
redocOptions: argv.options || {},
};
try {
await serve(argv.port, argv.spec, config);
} catch (e) {
handleError(e);
}
},
)
.command(
'bundle <spec>',
'bundle spec into zero-dependency HTML-file',
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',
default: 'ReDoc documentation',
});
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 => {
const config = {
ssr: true,
output: argv.o,
cdn: argv.cdn,
title: argv.title,
templateFileName: argv.template,
redocOptions: argv.options || {},
};
try {
await bundle(argv.spec, config);
} catch (e) {
handleError(e);
}
},
)
.demandCommand()
.options('t', {
alias: 'template',
describe: 'Path to handlebars page template, see https://git.io/vh8fP for the example ',
type: 'string',
})
.options('options', {
describe: 'ReDoc options, use dot notation, e.g. options.nativeScrollbars',
}).argv;
async function serve(port: any, pathToSpec: any, options: any = {}) {
let spec = await loadAndBundleSpec(pathToSpec);
let pageHTML = await getPageHTML(spec, pathToSpec, options);
const server = createServer((request, response) => {
console.time('GET ' + request.url);
if (request.url === '/redoc.standalone.js') {
respondWithGzip(
createReadStream(join(BUNDLES_DIR, 'redoc.standalone.js'), 'utf8'),
request,
response,
{
'Content-Type': 'application/javascript',
},
);
} else if (request.url === '/') {
respondWithGzip(pageHTML, request, response);
} else if (request.url === '/spec.json') {
const specStr = JSON.stringify(spec, null, 2);
respondWithGzip(specStr, request, response, {
'Content-Type': 'application/json',
});
} else {
response.writeHead(404);
response.write('Not found');
response.end();
}
console.timeEnd('GET ' + request.url);
});
console.log();
server.listen(port, () => console.log(`Server started: http://127.0.0.1:${port}`));
if (options.watch && existsSync(pathToSpec)) {
const pathToSpecDirectory = resolve(dirname(pathToSpec));
const watchOptions = {
ignored: /(^|[\/\\])\../,
};
const watcher = watch(pathToSpecDirectory, watchOptions);
const log = console.log.bind(console);
watcher
.on('change', async path => {
log(`${path} changed, updating docs`);
try {
spec = await loadAndBundleSpec(pathToSpec);
pageHTML = await getPageHTML(spec, pathToSpec, options);
log('Updated successfully');
} catch (e) {
console.error('Error while updating: ', e.message);
}
})
.on('error', error => console.error(`Watcher error: ${error}`))
.on('ready', () => log(`👀 Watching ${pathToSpecDirectory} for changes...`));
}
}
async function bundle(pathToSpec, options: any = {}) {
const start = Date.now();
const spec = await loadAndBundleSpec(pathToSpec);
const pageHTML = await getPageHTML(spec, pathToSpec, { ...options, ssr: true });
mkdirp.sync(dirname(options.output!));
writeFileSync(options.output!, pageHTML);
const sizeInKiB = Math.ceil(Buffer.byteLength(pageHTML) / 1024);
const time = Date.now() - start;
console.log(
`\n🎉 bundled successfully in: ${options.output!} (${sizeInKiB} KiB) [⏱ ${time / 1000}s]`,
);
}
async function getPageHTML(
spec: any,
pathToSpec: string,
{ ssr, cdn, title, templateFileName, redocOptions = {} }: Options,
) {
let html;
let css;
let state;
let redocStandaloneSrc;
if (ssr) {
console.log('Prerendering docs');
const specUrl = redocOptions.specUrl || (isURL(pathToSpec) ? pathToSpec : undefined);
const store = await createStore(spec, specUrl, redocOptions);
const sheet = new ServerStyleSheet();
html = renderToString(sheet.collectStyles(React.createElement(Redoc, { store })));
css = sheet.getStyleTags();
state = await store.toJS();
if (!cdn) {
redocStandaloneSrc = readFileSync(join(BUNDLES_DIR, 'redoc.standalone.js'));
}
}
templateFileName = templateFileName ? templateFileName : join(__dirname, './template.hbs');
const template = compile(readFileSync(templateFileName).toString());
return template({
redocHTML: `
<div id="redoc">${(ssr && html) || ''}</div>
<script>
${(ssr && `const __redoc_state = ${sanitizeJSONString(JSON.stringify(state))};`) || ''}
var container = document.getElementById('redoc');
Redoc.${
ssr
? 'hydrate(__redoc_state, container);'
: `init("spec.json", ${JSON.stringify(redocOptions)}, container)`
};
</script>`,
redocHead: ssr
? (cdn
? '<script src="https://unpkg.com/redoc@next/bundles/redoc.standalone.js"></script>'
: `<script>${redocStandaloneSrc}</script>`) + css
: '<script src="redoc.standalone.js"></script>',
title,
});
}
// credits: https://stackoverflow.com/a/9238214/1749888
function respondWithGzip(
contents: string | ReadStream,
request: IncomingMessage,
response: ServerResponse,
headers = {},
) {
let compressedStream;
const acceptEncoding = (request.headers['accept-encoding'] as string) || '';
if (acceptEncoding.match(/\bdeflate\b/)) {
response.writeHead(200, { ...headers, 'content-encoding': 'deflate' });
compressedStream = zlib.createDeflate();
} else if (acceptEncoding.match(/\bgzip\b/)) {
response.writeHead(200, { ...headers, 'content-encoding': 'gzip' });
compressedStream = zlib.createGzip();
} else {
response.writeHead(200, headers);
if (typeof contents === 'string') {
response.write(contents);
response.end();
} else {
contents.pipe(response);
}
return;
}
if (typeof contents === 'string') {
compressedStream.write(contents);
compressedStream.pipe(response);
compressedStream.end();
return;
} else {
contents.pipe(compressedStream).pipe(response);
}
}
function isURL(str: string): boolean {
return /^(https?:)\/\//m.test(str);
}
function sanitizeJSONString(str: string) {
return escapeClosingScriptTag(escapeUnicode(str));
}
// see http://www.thespanner.co.uk/2011/07/25/the-json-specification-is-now-wrong/
function escapeClosingScriptTag(str) {
return str.replace(/<\/script>/g, '<\\/script>');
}
// see http://www.thespanner.co.uk/2011/07/25/the-json-specification-is-now-wrong/
function escapeUnicode(str) {
return str.replace(/\u2028|\u2029/g, m => '\\u202' + (m === '\u2028' ? '8' : '9'));
}
function handleError(error: Error) {
console.error(error.stack);
process.exit(1);
}

35
cli/package.json Normal file
View File

@ -0,0 +1,35 @@
{
"name": "redoc-cli",
"version": "0.7.0",
"description": "ReDoc's Command Line Interface",
"main": "index.js",
"bin": "index.js",
"repository": "https://github.com/Rebilly/ReDoc",
"author": "Roman Hotsiy <gotsijroman@gmail.com>",
"license": "MIT",
"dependencies": {
"chokidar": "^2.0.4",
"handlebars": "^4.0.11",
"isarray": "^2.0.4",
"mkdirp": "^0.5.1",
"mobx": "^4.2.0",
"react": "^16.6.3",
"react-dom": "^16.6.3",
"redoc": "github:BusinessDuck/ReDoc#master",
"styled-components": "^4.1.1",
"tslib": "^1.9.3",
"yargs": "^12.0.5"
},
"scripts": {
"ci-publish": "ci-publish"
},
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@types/chokidar": "^1.7.5",
"@types/handlebars": "^4.0.39",
"@types/mkdirp": "^0.5.2",
"ci-publish": "^1.3.1"
}
}

23
cli/template.hbs Normal file
View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8" />
<title>{{title}}</title>
<!-- needed for adaptive design -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
padding: 0;
margin: 0;
}
</style>
{{{redocHead}}}
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
</head>
<body>
{{{redocHTML}}}
</body>
</html>

3415
cli/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -5,13 +5,13 @@
Serve remote spec by URL:
docker run -it --rm -p 80:80 \
-e SPEC_URL='http://localhost:8000/swagger.yaml' redoc
-e SPEC_URL='http://localhost:8000/swagger.yaml' redocly/redoc
Serve local file:
docker run -it --rm -p 80:80 \
-v $(PWD)/demo/swagger.yaml:/usr/share/nginx/html/swagger.yaml \
-e SPEC_URL=swagger.yaml redoc
-v $(pwd)/demo/swagger.yaml:/usr/share/nginx/html/swagger.yaml \
-e SPEC_URL=swagger.yaml redocly/redoc
## Runtime configuration options
@ -23,4 +23,4 @@ Serve local file:
## Build
docker build -t redoc .
docker build -t redocly/redoc .

View File

@ -6,6 +6,6 @@ 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|%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|80|${PORT}|g" /etc/nginx/nginx.conf
sed -i -e "s|\(listen\s*\) [0-9]*|\1 ${PORT}|g" /etc/nginx/nginx.conf
exec nginx -g 'daemon off;'

2
custom.d.ts vendored
View File

@ -18,6 +18,8 @@ declare module '*.css' {
declare var __REDOC_VERSION__: string;
declare var __REDOC_REVISION__: string;
declare var reactHotLoaderGlobal: any;
interface Element {
scrollIntoViewIfNeeded(centerIfNeeded?: boolean): void;
}

View File

@ -77,6 +77,12 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json'],
alias:
mode !== 'production'
? {
'react-dom': '@hot-loader/react-dom',
}
: {},
},
node: {
@ -105,7 +111,6 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
loader: 'css-loader',
options: {
sourceMap: true,
minimize: true,
},
},
},

View File

@ -132,14 +132,7 @@ Extends OpenAPI [Tag Object](http://swagger.io/specification/#tagObject)
#### x-traitTag
| Field Name | Type | Description |
| :------------- | :------: | :---------- |
| x-traitTag | boolean | In Swagger two operations can have multiply tags. This property distinguish between tags that are used to group operations (default) from tags that are used to mark operation with certain trait (`true` value) |
#### x-displayName
| Field Name | Type | Description |
| :------------- | :------: | :---------- |
| x-displayName | string | Define the text that is used for this tag in the menu and in section headings |
| x-traitTag | boolean | In Swagger two operations can have multiple tags. This property distinguishes between tags that are used to group operations (default) from tags that are used to mark operation with certain trait (`true` value) |
###### Usage in Redoc
Tags that have `x-traitTag` set to `true` are listed in side-menu but don't have any subitems (operations). Tag `description` is rendered as well.
@ -161,6 +154,12 @@ description: Pagination description (can use markdown syntax)
x-traitTag: true
```
#### x-displayName
| Field Name | Type | Description |
| :------------- | :------: | :---------- |
| x-displayName | string | Defines the text that is used for this tag in the menu and in section headings |
### Operation Object vendor extensions
Extends OpenAPI [Operation Object](http://swagger.io/specification/#operationObject)
#### x-code-samples

View File

@ -1,6 +1,6 @@
{
"name": "redoc",
"version": "2.0.0-rc.0",
"version": "2.0.0-rc.4",
"description": "ReDoc",
"repository": {
"type": "git",
@ -52,106 +52,107 @@
"docker:build": "docker build -f config/docker/Dockerfile -t redoc ."
},
"devDependencies": {
"@babel/core": "7.1.6",
"@babel/plugin-syntax-decorators": "7.1.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-syntax-jsx": "7.0.0",
"@babel/plugin-syntax-typescript": "7.1.5",
"@cypress/webpack-preprocessor": "4.0.2",
"@babel/core": "7.3.4",
"@babel/plugin-syntax-decorators": "7.2.0",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-syntax-jsx": "7.2.0",
"@babel/plugin-syntax-typescript": "7.3.3",
"@cypress/webpack-preprocessor": "4.0.3",
"@hot-loader/react-dom": "^16.8.4",
"@types/chai": "4.1.7",
"@types/dompurify": "^0.0.32",
"@types/enzyme": "^3.1.15",
"@types/enzyme-to-json": "^1.5.2",
"@types/jest": "^23.3.9",
"@types/enzyme": "^3.9.0",
"@types/enzyme-to-json": "^1.5.3",
"@types/jest": "^24.0.11",
"@types/json-pointer": "^1.0.30",
"@types/lodash": "^4.14.118",
"@types/lunr": "^2.1.6",
"@types/mark.js": "^8.11.1",
"@types/marked": "^0.4.2",
"@types/prismjs": "^1.6.4",
"@types/prop-types": "^15.5.6",
"@types/react": "^16.7.7",
"@types/react-dom": "^16.0.10",
"@types/lodash": "^4.14.122",
"@types/lunr": "^2.3.2",
"@types/mark.js": "^8.11.3",
"@types/marked": "^0.6.3",
"@types/prismjs": "^1.9.1",
"@types/prop-types": "^15.7.0",
"@types/react": "^16.8.7",
"@types/react-dom": "^16.8.2",
"@types/react-hot-loader": "^4.1.0",
"@types/react-tabs": "^2.3.0",
"@types/styled-components": "^4.1.1",
"@types/react-tabs": "^2.3.1",
"@types/styled-components": "^4.1.12",
"@types/tapable": "1.0.4",
"@types/webpack": "^4.4.19",
"@types/webpack-env": "^1.13.0",
"@types/yargs": "^12.0.1",
"babel-loader": "8.0.4",
"babel-plugin-styled-components": "^1.9.0",
"@types/webpack": "^4.4.25",
"@types/webpack-env": "^1.13.9",
"@types/yargs": "^12.0.9",
"babel-loader": "8.0.5",
"babel-plugin-styled-components": "^1.10.0",
"beautify-benchmark": "^0.2.4",
"bundlesize": "^0.17.0",
"conventional-changelog-cli": "^2.0.11",
"copy-webpack-plugin": "^4.6.0",
"core-js": "^2.5.7",
"coveralls": "^3.0.2",
"css-loader": "^1.0.1",
"cypress": "~3.1.2",
"bundlesize": "^0.17.1",
"conventional-changelog-cli": "^2.0.12",
"copy-webpack-plugin": "^5.0.0",
"core-js": "^2.6.5",
"coveralls": "^3.0.3",
"css-loader": "^2.1.1",
"cypress": "~3.1.5",
"deploy-to-gh-pages": "^1.3.6",
"enzyme": "^3.7.0",
"enzyme-adapter-react-16": "^1.7.0",
"enzyme-to-json": "^3.3.4",
"fork-ts-checker-webpack-plugin": "0.5.0",
"enzyme": "^3.9.0",
"enzyme-adapter-react-16": "^1.10.0",
"enzyme-to-json": "^3.3.5",
"fork-ts-checker-webpack-plugin": "1.0.0",
"html-webpack-plugin": "^3.1.0",
"jest": "^23.6.0",
"license-checker": "^24.0.1",
"jest": "^24.3.1",
"license-checker": "^25.0.1",
"lodash": "^4.17.11",
"mobx": "^4.3.1",
"prettier": "^1.15.2",
"prettier": "^1.16.4",
"prettier-eslint": "^8.8.2",
"puppeteer": "^1.10.0",
"puppeteer": "^1.13.0",
"raf": "^3.4.1",
"react": "^16.6.3",
"react-dom": "^16.6.3",
"rimraf": "^2.6.2",
"react": "^16.8.4",
"react-dom": "^16.8.4",
"rimraf": "^2.6.3",
"shelljs": "^0.8.3",
"source-map-loader": "^0.2.4",
"style-loader": "^0.23.1",
"styled-components": "^4.1.1",
"swagger2openapi": "^3.2.14",
"ts-jest": "23.10.5",
"ts-loader": "5.3.1",
"ts-node": "^7.0.1",
"tslint": "^5.11.0",
"styled-components": "^4.1.3",
"ts-jest": "24.0.0",
"ts-loader": "5.3.3",
"ts-node": "^8.0.3",
"tslint": "^5.13.1",
"tslint-react": "^3.4.0",
"typescript": "^3.1.6",
"webpack": "^4.26.1",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.10",
"typescript": "^3.3.3333",
"webpack": "^4.29.6",
"webpack-cli": "^3.2.3",
"webpack-dev-server": "^3.2.1",
"webpack-node-externals": "^1.6.0",
"workerize-loader": "^1.0.4",
"yaml-js": "^0.2.3"
},
"peerDependencies": {
"mobx": "^4.2.0",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"styled-components": "^4.0.1"
"mobx": "^4.2.0 || ^5.0.0",
"react": "^16.8.4",
"react-dom": "^16.8.4",
"styled-components": "^4.1.1"
},
"dependencies": {
"classnames": "^2.2.6",
"decko": "^1.2.0",
"dompurify": "^1.0.7",
"dompurify": "^1.0.10",
"eventemitter3": "^3.0.0",
"json-pointer": "^0.6.0",
"json-schema-ref-parser": "^6.0.1",
"lunr": "^2.3.2",
"json-schema-ref-parser": "^6.1.0",
"lunr": "2.3.6",
"mark.js": "^8.11.1",
"marked": "^0.5.2",
"memoize-one": "^4.0.0",
"mobx-react": "^5.2.5",
"marked": "^0.6.1",
"memoize-one": "^5.0.0",
"mobx-react": "^5.4.3",
"openapi-sampler": "1.0.0-beta.14",
"perfect-scrollbar": "^1.4.0",
"polished": "^2.0.2",
"polished": "^3.0.3",
"prismjs": "^1.15.0",
"prop-types": "^15.6.2",
"react-dropdown": "^1.6.2",
"react-hot-loader": "^4.3.5",
"react-tabs": "^2.0.0",
"slugify": "^1.3.1",
"prop-types": "^15.7.2",
"react-dropdown": "^1.6.4",
"react-hot-loader": "^4.8.0",
"react-tabs": "^3.0.0",
"slugify": "^1.3.4",
"stickyfill": "^1.1.1",
"swagger2openapi": "^5.2.3",
"tslib": "^1.9.3"
},
"bundlesize": [
@ -161,7 +162,9 @@
}
],
"jest": {
"setupTestFrameworkScriptFile": "<rootDir>/src/setupTests.ts",
"setupFilesAfterEnv": [
"<rootDir>/src/setupTests.ts"
],
"preset": "ts-jest",
"collectCoverageFrom": [
"src/**/*.{ts,tsx}"

View File

@ -18,7 +18,7 @@ export const ClickablePropertyNameCell = styled(PropertyNameCell)`
export const FieldLabel = styled.span`
vertical-align: middle;
font-size: 0.929em;
font-size: ${({ theme }) => theme.typography.code.fontSize};
line-height: 20px;
`;
@ -58,9 +58,16 @@ export const PatternLabel = styled(FieldLabel)`
color: #3195a6;
&::before,
&::after {
content: '/';
font-weight: bold;
}
&::before {
content: ' /';
}
&::after {
content: '/ ';
}
`;
export const ExampleValue = styled(FieldLabel)`
@ -72,6 +79,8 @@ export const ExampleValue = styled(FieldLabel)`
margin: ${theme.spacing.unit}px;
padding: 0 ${theme.spacing.unit}px;
border: 1px solid ${transparentize(0.9, theme.colors.text.primary)};
font-family: ${theme.typography.code.fontFamily};
color: ${theme.typography.code.color};
}`};
& + & {
margin-left: 0;
@ -79,6 +88,8 @@ export const ExampleValue = styled(FieldLabel)`
${extensionsHook('ExampleValue')};
`;
export const ExtensionValue = styled(ExampleValue)``;
export const ConstraintItem = styled(FieldLabel)`
border-radius: 2px;
${({ theme }) => `

View File

@ -7,14 +7,15 @@ const headerFontSize = {
};
export const headerCommonMixin = level => css`
font-family: ${props => props.theme.typography.headings.fontFamily};
font-family: ${({ theme }) => theme.typography.headings.fontFamily};
font-weight: ${({ theme }) => theme.typography.headings.fontWeight};
font-size: ${headerFontSize[level]};
line-height: ${({ theme }) => theme.typography.headings.lineHeight};
`;
export const H1 = styled.h1`
${headerCommonMixin(1)};
color: ${props => props.theme.colors.primary.main};
color: ${({ theme }) => theme.colors.primary.main};
${extensionsHook('H1')};
`;

View File

@ -12,6 +12,14 @@ export const Section = styled.div.attrs(props => ({
}))<{ underlined?: boolean }>`
padding: ${props => props.theme.spacing.sectionVertical}px 0;
&:last-child {
min-height: calc(100vh + 1px);
}
& > &:last-child {
min-height: initial;
}
${media.lessThan('medium', true)`
padding: 0;
`}

View File

@ -1,4 +1,5 @@
import styled from '../styled-components';
import { PrismDiv } from './PrismDiv';
export const SampleControls = styled.div`
opacity: 0.4;
@ -21,3 +22,12 @@ export const SampleControlsWrap = styled.div`
opacity: 1;
}
`;
export const StyledPre = styled(PrismDiv.withComponent('pre'))`
font-family: ${props => props.theme.typography.code.fontFamily};
font-size: ${props => props.theme.typography.code.fontSize};
overflow-x: auto;
margin: 0;
white-space: ${({ theme }) => (theme.typography.code.wrap ? 'pre-wrap' : 'pre')};
`;

View File

@ -61,9 +61,13 @@ export const Tabs = styled(ReactTabs)`
border-radius: 4px;
& > div,
& > pre {
padding: 20px;
padding: ${props => props.theme.spacing.unit * 4}px;
margin: 0;
}
& > div > pre {
padding: 0;
}
}
`;
@ -94,8 +98,7 @@ export const SmallTabs = styled(Tabs)`
> .react-tabs__tab-panel {
& > div,
& > pre {
padding: 10px 0;
margin: 0;
padding: ${props => props.theme.spacing.unit * 2} 0;
}
}
`;

View File

@ -70,12 +70,18 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
)) ||
null;
const version =
(info.version && (
<span>({info.version})</span>
)) ||
null;
return (
<Section>
<Row>
<MiddlePanel className="api-info">
<ApiHeader>
{info.title} <span>({info.version})</span>
{info.title} {version}
</ApiHeader>
{!hideDownloadButton && (
<p>

View File

@ -10,6 +10,7 @@ export const LogoImgEl = styled.img`
export const LogoWrap = styled.div`
text-align: center;
padding: ${props => props.theme.logo.gutter};
`;
const Link = styled.a`

View File

@ -8,7 +8,7 @@ export const OperationEndpointWrap = styled.div`
`;
export const ServerRelativeURL = styled.span`
font-family: ${props => props.theme.typography.headings.fontFamily};
font-family: ${props => props.theme.typography.code.fontFamily};
margin-left: 10px;
text-overflow: ellipsis;
`;

View File

@ -1,4 +1,7 @@
import * as React from 'react';
import { ExtensionValue, FieldLabel } from '../../common-elements/fields';
import styled from '../../styled-components';
import { OptionsContext } from '../OptionsProvider';
@ -6,14 +9,9 @@ import { OptionsContext } from '../OptionsProvider';
import { StyledMarkdownBlock } from '../Markdown/styled.elements';
const Extension = styled(StyledMarkdownBlock)`
opacity: 0.9;
margin: 2px 0;
`;
const ExtensionLable = styled.span`
font-style: italic;
`;
export interface ExtensionsProps {
extensions: {
[k: string]: any;
@ -22,15 +20,18 @@ export interface ExtensionsProps {
export class Extensions extends React.PureComponent<ExtensionsProps> {
render() {
const exts = this.props.extensions;
return (
<OptionsContext.Consumer>
{options => (
<>
{options.showExtensions &&
Object.keys(this.props.extensions).map(key => (
Object.keys(exts).map(key => (
<Extension key={key}>
<ExtensionLable>{key}</ExtensionLable>:{' '}
<code>{JSON.stringify(this.props.extensions[key])}</code>
<FieldLabel> {key.substring(2)}: </FieldLabel>{' '}
<ExtensionValue>
{typeof exts[key] === 'string' ? exts[key] : JSON.stringify(exts[key])}
</ExtensionValue>
</Extension>
))}
</>

View File

@ -4,11 +4,12 @@ import { ParametersGroup } from './ParametersGroup';
import { UnderlinedHeader } from '../../common-elements';
import { MediaContentModel } from '../../services';
import { FieldModel, RequestBodyModel } from '../../services/models';
import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch';
import { Schema } from '../Schema';
import { MediaContentModel } from '../../services';
import { Markdown } from '../Markdown/Markdown';
function safePush(obj, prop, item) {
if (!obj[prop]) {
@ -45,13 +46,15 @@ export class Parameters extends React.PureComponent<ParametersProps> {
const bodyContent = body && body.content;
const bodyDescription = body && body.description;
return (
<div>
<>
{paramsPlaces.map(place => (
<ParametersGroup key={place} place={place} parameters={paramsMap[place]} />
))}
{bodyContent && <BodyContent content={bodyContent} />}
</div>
{bodyContent && <BodyContent content={bodyContent} description={bodyDescription} />}
</>
);
}
}
@ -64,12 +67,17 @@ function DropdownWithinHeader(props) {
);
}
function BodyContent(props: { content: MediaContentModel }): JSX.Element {
const { content } = props;
function BodyContent(props: { content: MediaContentModel; description?: string }): JSX.Element {
const { content, description } = props;
return (
<MediaTypesSwitch content={content} renderDropdown={DropdownWithinHeader}>
{({ schema }) => {
return <Schema skipReadOnly={true} key="schema" schema={schema} />;
return (
<>
{description !== undefined && <Markdown source={description} />}
<Schema skipReadOnly={true} key="schema" schema={schema} />
</>
);
}}
</MediaTypesSwitch>
);

View File

@ -0,0 +1,52 @@
import * as React from 'react';
import { StyledPre } from '../../common-elements/samples';
import { ExampleModel } from '../../services/models';
import { isJsonLike, langFromMime } from '../../utils';
import { JsonViewer } from '../JsonViewer/JsonViewer';
import { SourceCodeWithCopy } from '../SourceCode/SourceCode';
import { ExampleValue } from './ExampleValue';
import { useExternalExample } from './exernalExampleHook';
export interface ExampleProps {
example: ExampleModel;
mimeType: string;
}
export function Example({ example, mimeType }: ExampleProps) {
if (example.value === undefined && example.externalValueUrl) {
return <ExternalExample example={example} mimeType={mimeType} />;
} else {
return <ExampleValue value={example.value} mimeType={mimeType} />;
}
}
export function ExternalExample({ example, mimeType }: ExampleProps) {
let value = useExternalExample(example, mimeType);
if (value === undefined) {
return <span>Loading...</span>;
}
if (value instanceof Error) {
console.log(value);
return (
<StyledPre>
Error loading external example: <br />
<a className={'token string'} href={example.externalValueUrl} target="_blank">
{example.externalValueUrl}
</a>
</StyledPre>
);
}
if (isJsonLike(mimeType)) {
return <JsonViewer data={value} />;
} else {
if (typeof value === 'object') {
// just in case example was cached as json but used as non-json
value = JSON.stringify(value, null, 2);
}
return <SourceCodeWithCopy lang={langFromMime(mimeType)} source={value} />;
}
}

View File

@ -0,0 +1,18 @@
import * as React from 'react';
import { isJsonLike, langFromMime } from '../../utils/openapi';
import { JsonViewer } from '../JsonViewer/JsonViewer';
import { SourceCodeWithCopy } from '../SourceCode/SourceCode';
export interface ExampleValueProps {
value: any;
mimeType: string;
}
export function ExampleValue({ value, mimeType }: ExampleValueProps) {
if (isJsonLike(mimeType)) {
return <JsonViewer data={value} />;
} else {
return <SourceCodeWithCopy lang={langFromMime(mimeType)} source={value} />;
}
}

View File

@ -2,11 +2,9 @@ import * as React from 'react';
import { SmallTabs, Tab, TabList, TabPanel } from '../../common-elements';
import { MediaTypeModel } from '../../services/models';
import { JsonViewer } from '../JsonViewer/JsonViewer';
import { SourceCodeWithCopy } from '../SourceCode/SourceCode';
import { NoSampleLabel } from './styled.elements';
import { isJsonLike, langFromMime } from '../../utils';
import { Example } from './Example';
import { NoSampleLabel } from './styled.elements';
export interface PayloadSamplesProps {
mediaType: MediaTypeModel;
@ -18,13 +16,6 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps> {
const mimeType = this.props.mediaType.name;
const noSample = <NoSampleLabel>No sample</NoSampleLabel>;
const sampleView = isJsonLike(mimeType)
? sample => <JsonViewer data={sample} />
: sample =>
(sample !== undefined && (
<SourceCodeWithCopy lang={langFromMime(mimeType)} source={sample} />
)) ||
noSample;
const examplesNames = Object.keys(examples);
if (examplesNames.length === 0) {
@ -39,13 +30,19 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps> {
))}
</TabList>
{examplesNames.map(name => (
<TabPanel key={name}>{sampleView(examples[name].value)}</TabPanel>
<TabPanel key={name}>
<Example example={examples[name]} mimeType={mimeType} />
</TabPanel>
))}
</SmallTabs>
);
} else {
const name = examplesNames[0];
return <div>{sampleView(examples[name].value)}</div>;
return (
<div>
<Example example={examples[name]} mimeType={mimeType} />
</div>
);
}
}
}

View File

@ -0,0 +1,34 @@
import { useEffect, useRef, useState } from 'react';
import { ExampleModel } from '../../services/models/Example';
export function useExternalExample(example: ExampleModel, mimeType: string) {
const [, setIsLoading] = useState(true); // to trigger component reload
const value = useRef<any>(undefined);
const prevRef = useRef<ExampleModel | undefined>(undefined);
if (prevRef.current !== example) {
value.current = undefined;
}
prevRef.current = example;
useEffect(
() => {
const load = async () => {
setIsLoading(true);
try {
value.current = await example.getExternalValue(mimeType);
} catch (e) {
value.current = e;
}
setIsLoading(false);
};
load();
},
[example, mimeType],
);
return value.current;
}

View File

@ -1,19 +1,8 @@
import * as React from 'react';
import { highlight } from '../../utils';
import { SampleControls, SampleControlsWrap } from '../../common-elements';
import { SampleControls, SampleControlsWrap, StyledPre } from '../../common-elements';
import { CopyButtonWrapper } from '../../common-elements/CopyButtonWrapper';
import { PrismDiv } from '../../common-elements/PrismDiv';
import styled from '../../styled-components';
const StyledPre = styled(PrismDiv.withComponent('pre'))`
font-family: ${props => props.theme.typography.code.fontFamily};
font-size: ${props => props.theme.typography.code.fontSize};
overflow-x: auto;
margin: 0;
white-space: ${({ theme }) => (theme.typography.code.wrap ? 'pre-wrap' : 'pre')};
`;
export interface SourceCodeProps {
source: string;

View File

@ -8,7 +8,6 @@ export * from './Schema/';
export * from './SearchBox/SearchBox';
export * from './Operation/Operation';
export * from './Loading/Loading';
export * from './RedocStandalone';
export * from './JsonViewer';
export * from './Markdown/Markdown';
export { StyledMarkdownBlock } from './Markdown/styled.elements';

View File

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

View File

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

View File

@ -118,7 +118,7 @@ export class MarkdownRenderer {
.trim();
}
headingRule = (text: string, level: number, raw: string) => {
headingRule = (text: string, level: number, raw: string, slugger: marked.Slugger) => {
if (level === 1) {
this.currentTopHeading = this.saveHeading(text, level);
} else if (level === 2) {
@ -129,7 +129,7 @@ export class MarkdownRenderer {
this.currentTopHeading && this.currentTopHeading.id,
);
}
return this.originalHeadingRule(text, level, raw);
return this.originalHeadingRule(text, level, raw, slugger);
};
renderMd(rawText: string, extractHeadings: boolean = false): string {

View File

@ -43,7 +43,7 @@ export class MenuBuilder {
const items: ContentItemModel[] = [];
const tagsMap = MenuBuilder.getTagsWithOperations(spec);
items.push(...MenuBuilder.addMarkdownItems(spec.info.description || '', options));
if (spec['x-tagGroups']) {
if (spec['x-tagGroups'] && spec['x-tagGroups'].length > 0) {
items.push(
...MenuBuilder.getTagGroupsItems(parser, undefined, spec['x-tagGroups'], tagsMap, options),
);

View File

@ -66,7 +66,8 @@ export class ScrollService {
}
element.scrollIntoView();
if (this._scrollParent && this._scrollParent.scrollBy) {
(this._scrollParent.scrollBy as any)(0, -this.options.scrollYOffset());
// adding 1 account rounding errors in case scrollYOffset is float-number
(this._scrollParent.scrollBy as any)(0, -this.options.scrollYOffset() + 1);
}
}

View File

@ -1,14 +1,64 @@
import { OpenAPIExample, Referenced } from '../../types';
import { resolve as urlResolve } from 'url';
import { OpenAPIEncoding, OpenAPIExample, Referenced } from '../../types';
import { isFormUrlEncoded, isJsonLike, urlFormEncodePayload } from '../../utils/openapi';
import { OpenAPIParser } from '../OpenAPIParser';
const externalExamplesCache: { [url: string]: Promise<any> } = {};
export class ExampleModel {
value: any;
summary?: string;
description?: string;
externalValue?: string;
externalValueUrl?: string;
constructor(parser: OpenAPIParser, infoOrRef: Referenced<OpenAPIExample>) {
Object.assign(this, parser.deref(infoOrRef));
constructor(
parser: OpenAPIParser,
infoOrRef: Referenced<OpenAPIExample>,
mime: string,
encoding?: { [field: string]: OpenAPIEncoding },
) {
const example = parser.deref(infoOrRef);
this.value = example.value;
this.summary = example.summary;
this.description = example.description;
if (example.externalValue) {
this.externalValueUrl = urlResolve(parser.specUrl || '', example.externalValue);
}
parser.exitRef(infoOrRef);
if (isFormUrlEncoded(mime) && this.value && typeof this.value === 'object') {
this.value = urlFormEncodePayload(this.value, encoding);
}
}
getExternalValue(mimeType: string): Promise<any> {
if (!this.externalValueUrl) {
return Promise.resolve(undefined);
}
if (externalExamplesCache[this.externalValueUrl]) {
return externalExamplesCache[this.externalValueUrl];
}
externalExamplesCache[this.externalValueUrl] = fetch(this.externalValueUrl).then(res => {
return res.text().then(txt => {
if (!res.ok) {
return Promise.reject(new Error(txt));
}
if (isJsonLike(mimeType)) {
try {
return JSON.parse(txt);
} catch (e) {
return txt;
}
} else {
return txt;
}
});
});
return externalExamplesCache[this.externalValueUrl];
}
}

View File

@ -1,6 +1,6 @@
import * as Sampler from 'openapi-sampler';
import { OpenAPIExample, OpenAPIMediaType } from '../../types';
import { OpenAPIMediaType } from '../../types';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { SchemaModel } from './Schema';
@ -9,7 +9,7 @@ import { OpenAPIParser } from '../OpenAPIParser';
import { ExampleModel } from './Example';
export class MediaTypeModel {
examples?: { [name: string]: OpenAPIExample };
examples?: { [name: string]: ExampleModel };
schema?: SchemaModel;
name: string;
isRequestType: boolean;
@ -30,10 +30,18 @@ export class MediaTypeModel {
this.schema = info.schema && new SchemaModel(parser, info.schema, '', options);
this.onlyRequiredInSamples = options.onlyRequiredInSamples;
if (info.examples !== undefined) {
this.examples = mapValues(info.examples, example => new ExampleModel(parser, example));
this.examples = mapValues(
info.examples,
example => new ExampleModel(parser, example, name, info.encoding),
);
} else if (info.example !== undefined) {
this.examples = {
default: new ExampleModel(parser, { value: info.example }),
default: new ExampleModel(
parser,
{ value: parser.shalowDeref(info.example) },
name,
info.encoding,
),
};
} else if (isJsonLike(name)) {
this.generateExample(parser, info);
@ -49,29 +57,31 @@ export class MediaTypeModel {
if (this.schema && this.schema.oneOf) {
this.examples = {};
for (const subSchema of this.schema.oneOf) {
const sample = Sampler.sample(
subSchema.rawSchema,
samplerOptions,
parser.spec,
);
const sample = Sampler.sample(subSchema.rawSchema, samplerOptions, parser.spec);
if (this.schema.discriminatorProp && typeof sample === 'object' && sample) {
sample[this.schema.discriminatorProp] = subSchema.title;
}
this.examples[subSchema.title] = {
value: sample,
};
this.examples[subSchema.title] = new ExampleModel(
parser,
{
value: sample,
},
this.name,
info.encoding,
);
}
} else if (this.schema) {
this.examples = {
default: new ExampleModel(parser, {
value: Sampler.sample(
info.schema,
samplerOptions,
parser.spec,
),
}),
default: new ExampleModel(
parser,
{
value: Sampler.sample(info.schema, samplerOptions, parser.spec),
},
this.name,
info.encoding,
),
};
}
}

View File

@ -161,7 +161,15 @@ export class SchemaModel {
private initOneOf(oneOf: OpenAPISchema[], parser: OpenAPIParser) {
this.oneOf = oneOf!.map((variant, idx) => {
const merged = parser.mergeAllOf(variant, this.pointer + '/oneOf/' + idx);
const derefVariant = parser.deref(variant);
const merged = parser.mergeAllOf(derefVariant, this.pointer + '/oneOf/' + idx);
// try to infer title
const title =
isNamedDefinition(variant.$ref) && !merged.title
? JsonPointer.baseName(variant.$ref)
: merged.title;
const schema = new SchemaModel(
parser,
@ -169,12 +177,14 @@ export class SchemaModel {
{
// variant may already have allOf so merge it to not get overwritten
...merged,
title,
allOf: [{ ...this.schema, oneOf: undefined, anyOf: undefined }],
} as OpenAPISchema,
this.pointer + '/oneOf/' + idx,
this.options,
);
parser.exitRef(variant);
// each oneOf should be independent so exiting all the parent refs
// otherwise it will cause false-positive recursive detection
parser.exitParents(merged);

View File

@ -101,6 +101,7 @@ const defaultTheme: ThemeInterface = {
headings: {
fontFamily: 'Arial',
fontWeight: '400',
lineHeight: '1.6em',
},
code: {
fontSize: '13px',
@ -135,6 +136,7 @@ const defaultTheme: ThemeInterface = {
logo: {
maxHeight: ({ menu }) => menu.width,
maxWidth: ({ menu }) => menu.width,
gutter: '2px',
},
rightPanel: {
backgroundColor: '#ffffff',
@ -162,7 +164,7 @@ export function resolveTheme(theme: ThemeInterface): ResolvedThemeInterface {
counter++;
if (counter > 1000) {
throw new Error(
`Theme probably contains cirucal dependency at ${currentPath}: ${val.toString()}`,
`Theme probably contains circular dependency at ${currentPath}: ${val.toString()}`,
);
}
@ -277,6 +279,7 @@ export interface ResolvedThemeInterface {
headings: {
fontFamily: string;
fontWeight: string;
lineHeight: string;
};
links: {
@ -303,6 +306,7 @@ export interface ResolvedThemeInterface {
logo: {
maxHeight: string;
maxWidth: string;
gutter: string;
};
rightPanel: {
backgroundColor: string;

View File

@ -21,6 +21,30 @@ import 'prismjs/components/prism-swift.js';
const DEFAULT_LANG = 'clike';
Prism.languages.insertBefore(
'javascript',
'string',
{
'property string': {
pattern: /([{,]\s*)"(?:\\.|[^\\"\r\n])*"(?=\s*:)/i,
lookbehind: true,
},
} as any,
undefined as any,
);
Prism.languages.insertBefore(
'javascript',
'punctuation',
{
property: {
pattern: /([{,]\s*)[a-z]\w*(?=\s*:)/i,
lookbehind: true,
},
},
undefined as any,
);
/**
* map language names to Prism.js names
*/
@ -49,5 +73,5 @@ export function highlight(source: string, lang: string = DEFAULT_LANG): string {
if (!grammar) {
grammar = Prism.languages[mapLang(lang)];
}
return Prism.highlight(source, grammar);
return Prism.highlight(source, grammar, lang);
}

View File

@ -21,6 +21,10 @@ function htmlEncode(t) {
: '';
}
function escapeForStringLiteral(str: string) {
return str.replace(/([\\"])/g, '\\$1');
}
function decorateWithSpan(value, className) {
return '<span class="' + className + '">' + htmlEncode(value) + '</span>';
}
@ -53,11 +57,11 @@ function valueToHTML(value) {
'<a href="' +
value +
'">' +
htmlEncode(value) +
htmlEncode(escapeForStringLiteral(value)) +
'</a>' +
decorateWithSpan('"', 'token string');
} else {
output += decorateWithSpan('"' + value + '"', 'token string');
output += decorateWithSpan('"' + escapeForStringLiteral(value) + '"', 'token string');
}
} else if (valueType === 'boolean') {
output += decorateWithSpan(value, 'token boolean');

View File

@ -2,6 +2,7 @@ import { dirname } from 'path';
import { OpenAPIParser } from '../services/OpenAPIParser';
import {
OpenAPIEncoding,
OpenAPIMediaType,
OpenAPIOperation,
OpenAPIParameter,
@ -130,6 +131,101 @@ export function isJsonLike(contentType: string): boolean {
return contentType.search(/json/i) !== -1;
}
export function isFormUrlEncoded(contentType: string): boolean {
return contentType === 'application/x-www-form-urlencoded';
}
function formEncodeField(fieldVal: any, fieldName: string, explode: boolean): string {
if (!fieldVal || !fieldVal.length) {
return fieldName + '=';
}
if (Array.isArray(fieldVal)) {
if (explode) {
return fieldVal.map(val => `${fieldName}=${val}`).join('&');
} else {
return fieldName + '=' + fieldVal.map(val => val.toString()).join(',');
}
} else if (typeof fieldVal === 'object') {
if (explode) {
return Object.keys(fieldVal)
.map(k => `${k}=${fieldVal[k]}`)
.join('&');
} else {
return (
fieldName +
'=' +
Object.keys(fieldVal)
.map(k => `${k},${fieldVal[k]}`)
.join(',')
);
}
} else {
return fieldName + '=' + fieldVal.toString();
}
}
function delimitedEncodeField(fieldVal: any, fieldName: string, delimeter: string): string {
if (Array.isArray(fieldVal)) {
return fieldVal.map(v => v.toString()).join(delimeter);
} else if (typeof fieldVal === 'object') {
return Object.keys(fieldVal)
.map(k => `${k}${delimeter}${fieldVal[k]}`)
.join(delimeter);
} else {
return fieldName + '=' + fieldVal.toString();
}
}
function deepObjectEncodeField(fieldVal: any, fieldName: string): string {
if (Array.isArray(fieldVal)) {
console.warn('deepObject style cannot be used with array value:' + fieldVal.toString());
return '';
} else if (typeof fieldVal === 'object') {
return Object.keys(fieldVal)
.map(k => `${fieldName}[${k}]=${fieldVal[k]}`)
.join('&');
} else {
console.warn('deepObject style cannot be used with non-object value:' + fieldVal.toString());
return '';
}
}
/*
* Should be used only for url-form-encoded body payloads
* To be used for parmaters should be extended with other style values
*/
export function urlFormEncodePayload(
payload: object,
encoding: { [field: string]: OpenAPIEncoding } = {},
) {
if (Array.isArray(payload)) {
throw new Error('Payload must have fields: ' + payload.toString());
} else {
return Object.keys(payload)
.map(fieldName => {
const fieldVal = payload[fieldName];
const { style = 'form', explode = true } = encoding[fieldName] || {};
switch (style) {
case 'form':
return formEncodeField(fieldVal, fieldName, explode);
break;
case 'spaceDelimited':
return delimitedEncodeField(fieldVal, fieldName, '%20');
case 'pipeDelimited':
return delimitedEncodeField(fieldVal, fieldName, '|');
case 'deepObject':
return deepObjectEncodeField(fieldVal, fieldName);
default:
// TODO implement rest of styles for path parameters
console.warn('Incorrect or unsupported encoding style: ' + style);
return '';
}
})
.join('&');
}
}
export function langFromMime(contentType: string): string {
if (contentType.search(/xml/i) !== -1) {
return 'xml';

View File

@ -18,4 +18,15 @@ declare module 'styled-components' {
...interpolations: SimpleInterpolation[]
): Keyframes;
}
export interface BaseThemedCssFunction<T extends object> {
<P extends object>(
first:
| TemplateStringsArray
| CSSObject
| InterpolationFunction<ThemedStyledProps<P, T>>
| string[],
...interpolations: Array<Interpolation<ThemedStyledProps<P, T>>>
): FlattenInterpolation<ThemedStyledProps<P, T>>;
}
}

View File

@ -134,7 +134,6 @@ export default (env: { standalone?: boolean } = {}, { mode }) => ({
loader: 'css-loader',
options: {
sourceMap: false,
minimize: true,
},
},
},

4794
yarn.lock

File diff suppressed because it is too large Load Diff