Merge commit '8df06dcc19700e954febcd6b5373051e182c6ab2' into releases

This commit is contained in:
RedocBot 2017-02-02 21:54:32 +00:00 committed by travis@localhost
commit 45d16b8173
87 changed files with 7033 additions and 582 deletions

1
.gitignore vendored
View File

@ -33,6 +33,7 @@ lib/**/*.shim.ngstyle.ts
/dist /dist
/demo/build /demo/build
.tmp .tmp
compiled
/coverage /coverage
.ghpages-tmp .ghpages-tmp
stats.json stats.json

View File

@ -1,6 +1,6 @@
.DS_Store .DS_Store
**/.* **/.*
.tmp compiled
node_modules node_modules
jspm_packages jspm_packages

View File

@ -1,6 +1,6 @@
language: node_js language: node_js
node_js: node_js:
- '4.0' - '6'
branches: branches:
except: except:
- releases - releases
@ -22,9 +22,7 @@ env:
- secure: apiavCfCQngL9Een1m7MIXMf3bqO3rY4YY59TMBl/yFKi80CEsHPHhgVUkl6hC+aM5PeBt/vgjh37rHMX31j/pcSZ4Z8SO/4Bwr36iHfhSxSEuAQog8P07qWqH7wYYWGIVmF682stgl0fYF+GN92sx/6edFVzsWVECf2G7imtICKSTbhKGm3Dhn2JwGnhD7eyfgZ33omgiaswumdu0xABoXDfqSZR+16fC4Ap5rhv3fXO9ndvRNy1STn376nT+my6e86UrQL4aS/S+HNHgIe1BUs+5cOp6Jgw6t0ie7phY0EAiECsRxy9K4e3Dctv9m6+Wma4+vy65MS0zGyrqey6oyV4l827sCOjrD1qcqc9bX6FlMSouVoNfE4ZjINNAbgigTaiLSoDSPcf5I5smkkM2ezzFOMSZwZxNdaNL2LKb97vc8m/ZUkv0sKZyT7oqVL7aJweEivsSHj5l2KR8Z7XrVB1y2eI6GvyTSa/d+CL4dSRzjh8+IRN047YBrdTKD5IkdT0upfoBu14WPUfFmLKxX+iMCslXRWb6kwojhrWNYmZvL65KRAzJ6+eIPDG/W5QUOpYyYT77bLlBQjVo6NmVvl9v3HMECq9CHH0ivKFBGPiKMOx7cJkTax3FuyznOW2WCXB9kTb5Zk9toaiNlSp9L6ll/h2Eyxa6n6sWUgmmM= - secure: apiavCfCQngL9Een1m7MIXMf3bqO3rY4YY59TMBl/yFKi80CEsHPHhgVUkl6hC+aM5PeBt/vgjh37rHMX31j/pcSZ4Z8SO/4Bwr36iHfhSxSEuAQog8P07qWqH7wYYWGIVmF682stgl0fYF+GN92sx/6edFVzsWVECf2G7imtICKSTbhKGm3Dhn2JwGnhD7eyfgZ33omgiaswumdu0xABoXDfqSZR+16fC4Ap5rhv3fXO9ndvRNy1STn376nT+my6e86UrQL4aS/S+HNHgIe1BUs+5cOp6Jgw6t0ie7phY0EAiECsRxy9K4e3Dctv9m6+Wma4+vy65MS0zGyrqey6oyV4l827sCOjrD1qcqc9bX6FlMSouVoNfE4ZjINNAbgigTaiLSoDSPcf5I5smkkM2ezzFOMSZwZxNdaNL2LKb97vc8m/ZUkv0sKZyT7oqVL7aJweEivsSHj5l2KR8Z7XrVB1y2eI6GvyTSa/d+CL4dSRzjh8+IRN047YBrdTKD5IkdT0upfoBu14WPUfFmLKxX+iMCslXRWb6kwojhrWNYmZvL65KRAzJ6+eIPDG/W5QUOpYyYT77bLlBQjVo6NmVvl9v3HMECq9CHH0ivKFBGPiKMOx7cJkTax3FuyznOW2WCXB9kTb5Zk9toaiNlSp9L6ll/h2Eyxa6n6sWUgmmM=
addons: addons:
sauce_connect: true sauce_connect: true
cache: cache: yarn
directories:
- node_modules
before_install: if [[ `npm -v` != 3* ]]; then npm i -g npm@3; fi before_install: if [[ `npm -v` != 3* ]]; then npm i -g npm@3; fi
before_script: before_script:
- npm run e2e-server > /dev/null & # kill e2e server - npm run e2e-server > /dev/null & # kill e2e server
@ -32,7 +30,7 @@ before_script:
after_script: after_script:
- kill %1 # kill e2e server - kill %1 # kill e2e server
before_deploy: before_deploy:
- if [[ ! -z "$TRAVIS_TAG" ]]; then npm run build:prod-module; fi - if [[ ! -z "$TRAVIS_TAG" ]]; then npm run build:prod; fi
deploy: deploy:
- provider: npm - provider: npm
skip_cleanup: true skip_cleanup: true

View File

@ -1,3 +1,18 @@
# 1.7.0 (2017-01-06)
### Features/Improvements
* Add support for grouping items in menu via [`x-tagGroups`](https://github.com/Rebilly/ReDoc/blob/master/docs/redoc-vendor-extensions.md#x-taggroups)
* Support inherited discriminator (only one at the moment)
* Add support for second-level headings from Markdown docs (by [@jaingaurav](https://github.com/jaingaurav))
### Bug fixes
* Fix response list for shared schemas (fixes [#177](https://github.com/Rebilly/ReDoc/issues/177))
* Fix right panel overlaps site-footer
# 1.6.4 (2016-12-28)
### Bug fixes
* Fix crash on MS Edge (fixes [#166](https://github.com/Rebilly/ReDoc/issues/166))
* Uncomment animation after upgrade to the latest ng2 (resolves [#162](https://github.com/Rebilly/ReDoc/issues/162))
# 1.6.3 (2016-12-19) # 1.6.3 (2016-12-19)
### Bug fixes ### Bug fixes
* Disable side-menu animation (workaround for [#162](https://github.com/Rebilly/ReDoc/issues/162)) * Disable side-menu animation (workaround for [#162](https://github.com/Rebilly/ReDoc/issues/162))

View File

@ -1,7 +1,7 @@
# ReDoc # ReDoc
**OpenAPI/Swagger-generated API Reference Documentation** **OpenAPI/Swagger-generated API Reference Documentation**
[![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) [![Tested on APIs.guru](http://api.apis.guru/badges/tested_on.svg)](https://APIs.guru) [![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) [![Stories in Ready](https://badge.waffle.io/Rebilly/ReDoc.png?label=ready&title=Ready)](https://waffle.io/Rebilly/ReDoc) [![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) [![Tested on APIs.guru](http://api.apis.guru/badges/tested_on.svg)](https://APIs.guru) [![Dependency Status](https://gemnasium.com/badges/github.com/Rebilly/ReDoc.svg)](https://gemnasium.com/github.com/Rebilly/ReDoc) [![Stories in Ready](https://badge.waffle.io/Rebilly/ReDoc.png?label=ready&title=Ready)](https://waffle.io/Rebilly/ReDoc)
[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/Rebilly/redoc.svg)](http://isitmaintained.com/project/Rebilly/redoc "Average time to resolve an issue") [![Percentage of issues still open](http://isitmaintained.com/badge/open/REBILLY/REDOC.svg)](http://isitmaintained.com/project/REBILLY/REDOC "Percentage of issues still open") [![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/Rebilly/redoc.svg)](http://isitmaintained.com/project/Rebilly/redoc "Average time to resolve an issue") [![Percentage of issues still open](http://isitmaintained.com/badge/open/REBILLY/REDOC.svg)](http://isitmaintained.com/project/REBILLY/REDOC "Percentage of issues still open")
@ -17,15 +17,18 @@
## Features ## Features
- Extremely easy deployment - Extremely easy deployment
- Its free and open-source project under MIT license - The widest OpenAPI features support (yes, it supports even `discriminator`) <br>
- The widest OpenAPI features support (yes, it supports even `discriminator`) ![](docs/images/discriminator-demo.gif)
- Neat **interactive** documentation for nested objects - Neat **interactive** documentation for nested objects <br>
![](docs/images/nested-demo.gif)
<img src="http://i.imgur.com/260gaV4.png" width="500"> - Code samples support (via vendor extension) <br>
![](docs/images/code-samples-demo.gif)
- Code samples support (via vendor extension) - Progressive loading with `lazy-rendering` options <br>
![](docs/images/progressive-loading-demo.gif)
- Responsive three-panel design with menu/scrolling synchronization - Responsive three-panel design with menu/scrolling synchronization
- Integrate API introduction into side menu - ReDoc takes advantage of markdown headings from OpenAPI description field. It pulls them into side menu and also supports deep linking. - Integrate API Introduction into side menu - ReDoc takes advantage of markdown headings from OpenAPI description field. It pulls them into side menu and also supports deep linking.
- High-level grouping in side-menu via [`x-tagGroups`](docs/redoc-vendor-extensions.md#x-tagGroups) vendor extension
- Multiple ReDoc instances on single page ([example](demo/examples/multiple-apis/index.html))
## Roadmap ## Roadmap
- [x] performance optimizations - [x] performance optimizations
@ -38,7 +41,7 @@
We host the latest and all the previous ReDoc releases on GitHub Pages-based **CDN**: We host the latest and all the previous ReDoc releases on GitHub Pages-based **CDN**:
- particular release, e.g. `v1.2.0`: https://rebilly.github.io/ReDoc/releases/v1.2.0/redoc.min.js - particular release, e.g. `v1.2.0`: https://rebilly.github.io/ReDoc/releases/v1.2.0/redoc.min.js
- `v1.x.x` release: https://rebilly.github.io/ReDoc/releases/v1.x.x/redoc.min.js - `v1.x.x` release: https://rebilly.github.io/ReDoc/releases/v1.x.x/redoc.min.js
- `latest` release: https://rebilly.github.io/ReDoc/releases/latest/redoc.min.js **[not for production]** - `latest` release: https://rebilly.github.io/ReDoc/releases/latest/redoc.min.js this file is updated with each release of ReDoc and may introduce breaking changes. **Not recommended to use in production.** Use particular release or `v1.x.x`.
## Deployment ## Deployment

View File

@ -26,6 +26,7 @@
"**/.*", "**/.*",
"node_modules", "node_modules",
"tests", "tests",
"compiled",
"lib", "lib",
"demo", "demo",
"build", "build",

View File

133
build/webpack.common.js Normal file
View File

@ -0,0 +1,133 @@
const webpack = require('webpack');
const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin;
const StringReplacePlugin = require("string-replace-webpack-plugin");
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
const ngcWebpack = require('ngc-webpack');
const VERSION = JSON.stringify(require('../package.json').version);
const root = require('./helpers').root;
module.exports = function (options) {
const conf = {
performance: { hints: false },
output: {
path: root('dist'),
filename: '[name].js',
sourceMapFilename: '[name].[id].map',
chunkFilename: '[id].chunk.js'
},
resolve: {
extensions: ['.ts', '.js', '.json', '.css'],
alias: {
http: 'stream-http',
https: 'stream-http'
}
},
externals: {
'jquery': 'jquery',
'esprima': 'esprima' // optional dep of ys-yaml not needed for redoc
},
module: {
exprContextCritical: false,
rules: [
{
enforce: 'pre',
test: /\.ts$/,
exclude: [
/node_modules/
],
loader: StringReplacePlugin.replace({
replacements: [
{
pattern: /styleUrls:\s*\[\s*'([\w\.\/-]*)\.css'\s*\][\s,]*$/gm,
replacement: function (match, p1, offset, string) {
return `styleUrls: ['${p1}.scss'],`;
}
},
{
pattern: /(\.\/components\/Redoc\/redoc-initial-styles\.css)/gm,
replacement: function (match, p1, offset, string) {
return p1.replace('.css', '.scss');
}
}
]
})
},
{
enforce: 'pre',
test: /\.js$/,
loader: 'source-map-loader',
exclude: [
/node_modules/
]
},
{
test: /\.json$/,
use: 'json-loader'
},
{
test: /lib[\\\/].*\.css$/,
loaders: ['raw-loader'],
exclude: [/redoc-initial-styles\.css$/]
}, {
test: /\.css$/,
loaders: ['style-loader', 'css-loader?-import'],
exclude: [/lib[\\\/](?!.*redoc-initial-styles).*\.css$/]
},
{
test: /lib[\\\/].*\.scss$/,
loaders: ['raw-loader', "sass-loader"],
exclude: [/redoc-initial-styles\.scss$/]
},
{
test: /\.scss$/,
loaders: ['style-loader', 'css-loader?-import', "sass-loader"],
exclude: [/lib[\\\/](?!.*redoc-initial-styles).*\.scss$/]
},
{
test: /\.html$/,
loader: 'raw-loader'
}
],
},
plugins: [
new CheckerPlugin(),
new webpack.DefinePlugin({
'IS_PRODUCTION': options.IS_PRODUCTION,
'LIB_VERSION': VERSION,
'AOT': options.AOT
}),
new StringReplacePlugin()
],
node: {
global: true,
crypto: 'empty',
fs: 'empty',
process: true,
module: false,
clearImmediate: false,
setImmediate: false
}
};
if (options.AOT) {
conf.plugins.push(
new ngcWebpack.NgcWebpackPlugin({
disable: !options.AOT,
tsConfig: root('tsconfig.webpack.json'),
resourceOverride: root('build/resource-override.js')
})
);
}
return conf;
}

View File

@ -1,41 +1,24 @@
const webpack = require('webpack'); const webpack = require('webpack');
const ForkCheckerPlugin = require('awesome-typescript-loader').ForkCheckerPlugin; const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin;
const StringReplacePlugin = require("string-replace-webpack-plugin"); const StringReplacePlugin = require("string-replace-webpack-plugin");
const root = require('./helpers').root; const root = require('./helpers').root;
const VERSION = JSON.stringify(require('../package.json').version); const VERSION = JSON.stringify(require('../package.json').version);
const IS_PRODUCTION = process.env.NODE_ENV === "production"; const IS_PRODUCTION = process.env.NODE_ENV === "production";
// TODO Refactor common parts of config
module.exports = { const webpackMerge = require('webpack-merge'); // used to merge webpack configs
const commonConfig = require('./webpack.common.js');
module.exports = webpackMerge(commonConfig({
IS_PRODUCTION: process.env.NODE_ENV === "production",
AOT: false
}), {
devtool: '#inline-source-map', devtool: '#inline-source-map',
performance: { hints: false },
resolve: {
extensions: ['.ts', '.js', '.json', '.css'],
alias: {
http: 'stream-http',
https: 'stream-http'
}
},
externals: {
'jquery': 'jquery',
'esprima': 'esprima' // optional dep of ys-yaml not needed for redoc
},
node: {
fs: "empty",
crypto: "empty",
global: true,
process: true,
module: false,
clearImmediate: false,
setImmediate: false
},
entry: { entry: {
'redoc': './lib/index.ts', 'polyfills': './lib/polyfills.ts',
'vendor': './lib/vendor.ts', 'vendor': './lib/vendor.ts',
'polyfills': './lib/polyfills.ts' 'redoc': './lib/index.ts',
}, },
devServer: { devServer: {
contentBase: root('demo'), contentBase: root('demo'),
watchContentBase: true, watchContentBase: true,
@ -47,87 +30,22 @@ module.exports = {
hot: false, hot: false,
stats: 'errors-only' stats: 'errors-only'
}, },
output: {
path: root('dist'),
filename: '[name].js',
sourceMapFilename: '[name].[id].map',
chunkFilename: '[id].chunk.js',
// devtoolModuleFilenameTemplate: "[resource-path]",
// devtoolFallbackModuleFilenameTemplate: "[resource-path]?[hash]",
},
module: { module: {
exprContextCritical: false, rules: [
rules: [{ {
enforce: 'pre', test: /\.ts$/,
test: /\.js$/, use: [
loader: 'source-map-loader', 'awesome-typescript-loader?{configFileName: "tsconfig.webpack.json"}',
exclude: [ 'angular2-template-loader',
/node_modules/ ],
] exclude: [/\.(spec|e2e)\.ts$/]
}, { },
enforce: 'pre', ]
test: /\.ts$/,
exclude: [
/node_modules/
],
loader: StringReplacePlugin.replace({
replacements: [
{
pattern: /styleUrls:\s*\[\s*'([\w\.\/-]*)\.css'\s*\][\s,]*$/gm,
replacement: function (match, p1, offset, string) {
return `styleUrls: ['${p1}.scss'],`;
}
},
{
pattern: /(\.\/components\/Redoc\/redoc-initial-styles\.css)/gm,
replacement: function (match, p1, offset, string) {
return p1.replace('.css', '.scss');
}
}
]
})
}, {
test: /\.ts$/,
loaders: [
'awesome-typescript-loader',
'angular2-template-loader'
],
exclude: [/\.(spec|e2e)\.ts$/]
}, {
test: /lib[\\\/].*\.scss$/,
loaders: ['raw-loader', "sass-loader"],
exclude: [/redoc-initial-styles\.scss$/]
}, {
test: /\.scss$/,
loaders: ['style-loader', 'css-loader?-import', "sass-loader"],
exclude: [/lib[\\\/](?!.*redoc-initial-styles).*\.scss$/]
}, {
test: /\.css$/,
loaders: ['style-loader', 'css-loader?-import'],
}, {
test: /\.html$/,
loader: 'raw-loader'
}]
}, },
plugins: [ plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.optimize.CommonsChunkPlugin({ new webpack.optimize.CommonsChunkPlugin({
name: ['vendor', 'polyfills'], name: ['vendor', 'polyfills'],
minChunks: Infinity minChunks: Infinity
}), })
]
new webpack.DefinePlugin({ })
'IS_PRODUCTION': IS_PRODUCTION,
'LIB_VERSION': VERSION,
'AOT': IS_PRODUCTION
}),
new ForkCheckerPlugin(),
new StringReplacePlugin()
],
}

View File

@ -11,31 +11,15 @@ const BANNER =
const IS_MODULE = process.env.IS_MODULE != null; const IS_MODULE = process.env.IS_MODULE != null;
const config = { const webpackMerge = require('webpack-merge'); // used to merge webpack configs
context: root(), const commonConfig = require('./webpack.common.js');
devtool: 'source-map',
performance: { hints: false }, const config = webpackMerge(commonConfig({
IS_PRODUCTION: true,
AOT: true
}), {
devtool: 'source-map',
resolve: {
extensions: ['.ts', '.js', '.json', '.css'],
alias: {
http: 'stream-http',
https: 'stream-http'
}
},
externals: {
'jquery': 'jquery',
'esprima': 'esprima' // optional dep of ys-yaml not needed for redoc
},
node: {
fs: "empty",
crypto: "empty",
global: true,
process: true,
module: false,
clearImmediate: false,
setImmediate: false
},
entry: { entry: {
'redoc': IS_MODULE ? ['./lib/vendor.ts', './lib/redoc.module.ts'] : ['./lib/polyfills.ts', './lib/vendor.ts', './lib/index.ts'] 'redoc': IS_MODULE ? ['./lib/vendor.ts', './lib/redoc.module.ts'] : ['./lib/polyfills.ts', './lib/vendor.ts', './lib/index.ts']
}, },
@ -48,34 +32,18 @@ const config = {
libraryTarget: 'umd', libraryTarget: 'umd',
umdNamedDefine: true umdNamedDefine: true
}, },
module: { module: {
exprContextCritical: false, rules: [
rules: [{ {
enforce: 'pre', test: /\.ts$/,
test: /\.js$/, use: [
loader: 'source-map-loader', 'awesome-typescript-loader?{configFileName: "tsconfig.webpack.json"}',
exclude: [ 'angular2-template-loader',
/node_modules/ ],
] exclude: [/\.(spec|e2e)\.ts$/]
}, { }
test: /node_modules\/.*\.ngfactory\.ts$/, ]
loader: 'awesome-typescript-loader'
}, {
test: /\.ts$/,
loader: 'awesome-typescript-loader',
exclude: /(node_modules)/,
}, {
test: /lib[\\\/].*\.css$/,
loaders: ['raw-loader'],
exclude: [/redoc-initial-styles\.css$/]
}, {
test: /\.css$/,
loaders: ['style-loader', 'css-loader?-import'],
exclude: [/lib[\\\/](?!.*redoc-initial-styles).*\.css$/]
}]
}, },
plugins: [ plugins: [
new webpack.LoaderOptionsPlugin({ new webpack.LoaderOptionsPlugin({
minimize: true, minimize: true,
@ -84,7 +52,8 @@ const config = {
new webpack.optimize.UglifyJsPlugin({ new webpack.optimize.UglifyJsPlugin({
compress: { compress: {
warnings: false, warnings: false,
screw_ie8: true screw_ie8: true,
negate_iife: false // for lazy v8
}, },
mangle: { screw_ie8 : true }, mangle: { screw_ie8 : true },
output: { output: {
@ -92,14 +61,9 @@ const config = {
}, },
sourceMap: true sourceMap: true
}), }),
new webpack.BannerPlugin(BANNER), new webpack.BannerPlugin(BANNER)
new webpack.DefinePlugin({ ]
'IS_PRODUCTION': true, })
'LIB_VERSION': VERSION,
'AOT': true
})
],
}
if (IS_MODULE) { if (IS_MODULE) {
config.externals = { config.externals = {
@ -114,17 +78,6 @@ if (IS_MODULE) {
'rxjs': 'rxjs', 'rxjs': 'rxjs',
'zone.js/dist/zone': 'zone.js/dist/zone' 'zone.js/dist/zone': 'zone.js/dist/zone'
}; };
config.module.rules.push({
test: /\.ts$/,
loader: 'angular2-template-loader',
exclude: [/\.(spec|e2e)\.ts$/]
});
config.module.rules.push({
test: /\.html$/,
loader: 'raw-loader'
});
} }
module.exports = config; module.exports = config;

View File

@ -1,73 +1,31 @@
const webpack = require('webpack'); const webpack = require('webpack');
const root = require('./helpers').root; const root = require('./helpers').root;
const VERSION = JSON.stringify(require('../package.json').version);
const webpackMerge = require('webpack-merge'); // used to merge webpack configs
const commonConfig = require('./webpack.common.js');
module.exports = { module.exports = webpackMerge(commonConfig({
IS_PRODUCTION: true,
AOT: false
}), {
devtool: 'inline-source-map', devtool: 'inline-source-map',
performance: { hints: false },
resolve: {
extensions: ['.ts', '.js', '.json', '.css'],
alias: {
http: 'stream-http',
https: 'stream-http'
}
},
externals: {
'jquery': 'jquery',
'esprima': 'esprima' // optional dep of ys-yaml not needed for redoc
},
node: {
fs: "empty",
crypto: "empty",
global: true,
process: true,
module: false,
clearImmediate: false,
setImmediate: false
},
output: {
path: root('dist'),
filename: '[name].js',
sourceMapFilename: '[name].map',
chunkFilename: '[id].chunk.js'
},
module: { module: {
exprContextCritical: false, exprContextCritical: false,
rules: [{ rules: [
enforce: 'pre', {
test: /\.js$/,
loader: 'source-map-loader',
exclude: [
/node_modules/
]
},{
test: /\.ts$/, test: /\.ts$/,
loaders: [ use: 'awesome-typescript-loader'
'awesome-typescript-loader' },
] {
}, {
test: /\.ts$/, test: /\.ts$/,
loaders: [ use: [
'angular2-template-loader' 'angular2-template-loader',
], ],
exclude: [/\.(spec|e2e)\.ts$/] exclude: [/\.(spec|e2e)\.ts$/]
}, { },
test: /lib[\\\/].*\.css$/, {
loaders: ['raw-loader'],
exclude: [/redoc-initial-styles\.css$/]
}, {
test: /\.css$/,
loaders: ['style-loader', 'css-loader?-import'],
exclude: [/lib[\\\/](?!.*redoc-initial-styles).*\.css$/]
}, {
test: /\.html$/,
loader: 'raw-loader'
}, {
/** /**
* Instruments JS files with Istanbul for subsequent code coverage reporting. * Instruments JS files with Istanbul for subsequent code coverage reporting.
* Instrument only testing sources. * Instrument only testing sources.
@ -85,11 +43,6 @@ module.exports = {
}, },
plugins: [ plugins: [
new webpack.DefinePlugin({
'IS_PRODUCTION': false,
'LIB_VERSION': VERSION,
'AOT': 'false'
}),
new webpack.LoaderOptionsPlugin({ new webpack.LoaderOptionsPlugin({
test: /\.ts$/, test: /\.ts$/,
sourceMap: false, sourceMap: false,
@ -106,4 +59,4 @@ module.exports = {
/(?:[^\\\/]*(?:[\\\/]|$))*[^\\\/]*\.css$/ // ignore css files /(?:[^\\\/]*(?:[\\\/]|$))*[^\\\/]*\.css$/ // ignore css files
]) ])
], ],
} })

View File

@ -0,0 +1,79 @@
<!DOCTYPE html>
<html>
<head>
<title>ReDoc Demo: Multiple apis</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
margin: 0;
padding-top: 40px;
}
nav {
position: fixed;
top: 0;
width: 100%;
z-index: 100;
}
ul#links_container {
margin: 0;
padding: 0;
background-color: #0033a0;
}
li {
display: inline-block;
padding: 10px;
color: white;
cursor: pointer;
}
</style>
</head>
<body>
<!-- Top navigation placeholder -->
<nav>
<ul id="links_container">
</ul>
</nav>
<redoc scroll-y-offset="body > nav"></redoc>
<script src="https://rebilly.github.io/ReDoc/releases/v1.x.x/redoc.min.js"> </script>
<script>
// list of APIS
var apis = [
{
name: 'PetStore',
url: 'https://rebilly.github.io/ReDoc/swagger.yaml'
},
{
name: 'Instagram',
url: 'https://api.apis.guru/v2/specs/instagram.com/1.0.0/swagger.yaml'
},
{
name: 'Google Calendar',
url: 'https://api.apis.guru/v2/specs/googleapis.com/calendar/v3/swagger.yaml'
}
];
// initially render first API
Redoc.init(apis[0].url);
function onClick() {
var url = this.getAttribute('data-link');
Redoc.init(url);
}
// dynamically building navigation items
var $list = document.getElementById('links_container');
apis.forEach(function(api) {
var $listitem = document.createElement('li');
$listitem.setAttribute('data-link', api.url);
$listitem.innerText = api.name;
$listitem.addEventListener('click', onClick);
$list.appendChild($listitem);
});
</script>
</body>
</html>

View File

@ -51,10 +51,10 @@
var $specInput = document.getElementById('spec-input'); var $specInput = document.getElementById('spec-input');
// $specInput.addEventListener('value-changed', function(e) { $specInput.addEventListener('value-changed', function(e) {
// schemaUrlInput.value = e.detail.value; schemaUrlInput.value = e.detail.value;
// location.search = updateQueryStringParameter(location.search, 'url', schemaUrlInput.value); location.search = updateQueryStringParameter(location.search, 'url', schemaUrlInput.value);
// }); });
function selectItem() { function selectItem() {
let value = this.innerText.trim(); let value = this.innerText.trim();

View File

@ -676,6 +676,13 @@ definitions:
description: Category name description: Category name
type: string type: string
minLength: 1 minLength: 1
sub:
description: Test Sub Category
type: object
properties:
prop1:
type: string
description: Dumb Property
xml: xml:
name: Category name: Category
Dog: Dog:
@ -747,9 +754,6 @@ definitions:
- photoUrls - photoUrls
discriminator: petType discriminator: petType
properties: properties:
petType:
description: Type of a pet
type: string
id: id:
description: Pet ID description: Pet ID
allOf: allOf:
@ -786,6 +790,9 @@ definitions:
- available - available
- pending - pending
- sold - sold
petType:
description: Type of a pet
type: string
xml: xml:
name: Pet name: Pet
Tag: Tag:

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

BIN
docs/images/nested-demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

View File

@ -100,7 +100,7 @@ info:
### Tag Object vendor extensions ### Tag Object vendor extensions
Extends OpenAPI [Tag Object](http://swagger.io/specification/#tagObject) Extends OpenAPI [Tag Object](http://swagger.io/specification/#tagObject)
#### x-traitTag [DEPRECATED] #### x-traitTag
| Field Name | Type | Description | | 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-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) |

View File

@ -9,13 +9,13 @@ module.exports = function (config) {
}, },
coverageReporter: { coverageReporter: {
dir: 'coverage/', type: 'in-memory'
reporters: [ },
{type: 'html'},
{type: 'lcov'}, remapCoverageReporter: {
{type: 'json'}, 'text-summary': null,
{type: 'text-summary'} 'text-lcov': './coverage/lcov.info',
] 'html': './coverage/html'
}, },
webpack: testWebpackConfig, webpack: testWebpackConfig,
webpackMiddleware: { webpackMiddleware: {
@ -42,7 +42,7 @@ module.exports = function (config) {
}, },
colors: true, colors: true,
singleRun: true, singleRun: true,
reporters: travis ? ['mocha', 'coverage', 'coveralls'] : ['mocha', 'coverage'], reporters: travis ? ['mocha', 'coverage', 'remap-coverage', 'coveralls'] : ['mocha', 'coverage', 'remap-coverage'],
browsers: ['PhantomJS'], browsers: ['PhantomJS'],

View File

@ -1,5 +1,5 @@
import { platformBrowser } from '@angular/platform-browser'; import { platformBrowser } from '@angular/platform-browser';
import { AppModuleNgFactory } from './app.module.ngfactory'; import { AppModuleNgFactory } from '../compiled/lib/app.module.ngfactory';
export function bootstrapRedoc() { export function bootstrapRedoc() {
return platformBrowser().bootstrapModuleFactory(AppModuleNgFactory); return platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);

View File

@ -16,6 +16,7 @@
<a *ngIf="info.license.url" href="{{info.license.url}}"> {{info.license.name}} </a> <a *ngIf="info.license.url" href="{{info.license.url}}"> {{info.license.name}} </a>
<span *ngIf="!info.license.url"> {{info.license.name}} </span> <span *ngIf="!info.license.url"> {{info.license.name}} </span>
</span> </span>
<redoc-externalDocs [docs]="componentSchema.externalDocs"></redoc-externalDocs>
</p> </p>
<span class="redoc-markdown-block"> <span class="redoc-markdown-block">
<dynamic-ng2-viewer [html]="info['x-redoc-html-description']"></dynamic-ng2-viewer> <dynamic-ng2-viewer [html]="info['x-redoc-html-description']"></dynamic-ng2-viewer>

View File

@ -1,7 +1,7 @@
'use strict'; 'use strict';
import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core'; import { Component, ChangeDetectionStrategy, OnInit, ElementRef } from '@angular/core';
import { SpecManager, BaseComponent } from '../base'; import { SpecManager, BaseComponent } from '../base';
import { OptionsService } from '../../services/index'; import { OptionsService, Marker } from '../../services/index';
@Component({ @Component({
selector: 'api-info', selector: 'api-info',
@ -12,8 +12,13 @@ import { OptionsService } from '../../services/index';
export class ApiInfo extends BaseComponent implements OnInit { export class ApiInfo extends BaseComponent implements OnInit {
info: any = {}; info: any = {};
specUrl: String; specUrl: String;
constructor(specMgr: SpecManager, private optionsService: OptionsService) { constructor(specMgr: SpecManager,
private optionsService: OptionsService,
elRef: ElementRef,
marker: Marker
) {
super(specMgr); super(specMgr);
marker.addElement(elRef.nativeElement);
} }
init() { init() {

View File

@ -0,0 +1,18 @@
'use strict';
import { Component, Input, ChangeDetectionStrategy, OnInit } from '@angular/core';
import { BaseComponent, SpecManager } from '../base';
@Component({
selector: 'redoc-externalDocs',
template: `<a *ngIf="docs" [href]="docs.url" [innerHtml]="docs.description | marked"></a>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ExternalDocs implements OnInit {
@Input() docs;
ngOnInit() {
if (this.docs && !this.docs.description) {
this.docs.description = 'External Docs';
}
}
}

View File

@ -72,6 +72,7 @@ $sub-schema-offset: ($bullet-size / 2) + $bullet-margin;
} }
.param-type { .param-type {
font-weight: normal; font-weight: normal;
word-break: break-all;
&.array::before, &.tuple::before { &.array::before, &.tuple::before {
color: $black; color: $black;
font-weight: $base-font-weight; font-weight: $base-font-weight;

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { Component, ElementRef, ViewContainerRef, OnDestroy, Input, import { Component, ElementRef, ViewContainerRef, OnDestroy, OnInit, Input,
AfterViewInit, ComponentFactoryResolver, Renderer } from '@angular/core'; AfterViewInit, ComponentFactoryResolver, Renderer } from '@angular/core';
import { JsonSchema } from './json-schema'; import { JsonSchema } from './json-schema';
@ -15,8 +15,9 @@ var cache = {};
template: '', template: '',
styles: [':host { display:none }'] styles: [':host { display:none }']
}) })
export class JsonSchemaLazy implements OnDestroy, AfterViewInit { export class JsonSchemaLazy implements OnDestroy, OnInit, AfterViewInit {
@Input() pointer: string; @Input() pointer: string;
@Input() absolutePointer: string;
@Input() auto: boolean; @Input() auto: boolean;
@Input() isRequestSchema: boolean; @Input() isRequestSchema: boolean;
@Input() final: boolean = false; @Input() final: boolean = false;
@ -63,7 +64,8 @@ export class JsonSchemaLazy implements OnDestroy, AfterViewInit {
// skip caching view with descendant schemas // skip caching view with descendant schemas
// as it needs attached controller // as it needs attached controller
if (!this.disableLazy && (compRef.instance.hasDescendants || compRef.instance._hasSubSchemas)) { let hasDescendants = compRef.instance.descendants && compRef.instance.descendants.length;
if (!this.disableLazy && (hasDescendants || compRef.instance._hasSubSchemas)) {
this._loadAfterSelf(); this._loadAfterSelf();
return; return;
} }
@ -78,6 +80,10 @@ export class JsonSchemaLazy implements OnDestroy, AfterViewInit {
Object.assign(instance, this); Object.assign(instance, this);
} }
ngOnInit() {
if (!this.absolutePointer) this.absolutePointer = this.pointer;
}
ngAfterViewInit() { ngAfterViewInit() {
if (!this.auto && !this.disableLazy) return; if (!this.auto && !this.disableLazy) return;
this.loadCached(); this.loadCached();

View File

@ -34,6 +34,7 @@
<div class="tuple-item"> <div class="tuple-item">
<span class="tuple-item-index"> [{{idx}}]: </span> <span class="tuple-item-index"> [{{idx}}]: </span>
<json-schema class="nested-schema" [pointer]="item._pointer" <json-schema class="nested-schema" [pointer]="item._pointer"
[absolutePointer]="item._pointer"
[nestOdd]="!nestOdd" [isRequestSchema]="isRequestSchema"> [nestOdd]="!nestOdd" [isRequestSchema]="isRequestSchema">
</json-schema> </json-schema>
</div> </div>
@ -52,12 +53,12 @@
'discriminator': prop.isDiscriminator, 'discriminator': prop.isDiscriminator,
'complex': prop._pointer, 'complex': prop._pointer,
'additional': prop._additional, 'additional': prop._additional,
'expanded': subSchema.visible 'expanded': subSchema.open
}"> }">
<td class="param-name"> <td class="param-name">
<span class="param-name-wrap" (click)="subSchema.toggle()"> <span class="param-name-wrap" (click)="subSchema.toggle()">
<span class="param-name-content"> <span class="param-name-content">
{{prop._name}} {{prop.name}}
<span class="param-name-enumvalue" [hidden]="!prop._enumItem"> {{prop._enumItem?.val | json}} </span> <span class="param-name-enumvalue" [hidden]="!prop._enumItem"> {{prop._enumItem?.val | json}} </span>
</span> </span>
<svg *ngIf="prop._pointer" xmlns="http://www.w3.org/2000/svg" version="1.1" x="0" y="0" viewBox="0 0 24 24" xml:space="preserve"> <svg *ngIf="prop._pointer" xmlns="http://www.w3.org/2000/svg" version="1.1" x="0" y="0" viewBox="0 0 24 24" xml:space="preserve">
@ -83,7 +84,7 @@
</div> </div>
<div class="param-description" [innerHtml]="prop.description | marked"></div> <div class="param-description" [innerHtml]="prop.description | marked"></div>
<div class="discriminator-info" *ngIf="prop.isDiscriminator"> <div class="discriminator-info" *ngIf="prop.isDiscriminator">
<drop-down (change)="selectDescendant($event)"> <drop-down (change)="selectDescendantByIdx($event)" [active]="activeDescendant.idx">
<option *ngFor="let descendant of descendants; let i=index" <option *ngFor="let descendant of descendants; let i=index"
[value]="i" [attr.selected]="descendant.active ? '' : null" >{{descendant.name}}</option> [value]="i" [attr.selected]="descendant.active ? '' : null" >{{descendant.name}}</option>
</drop-down> </drop-down>
@ -92,9 +93,9 @@
</tr> </tr>
<tr class="param-schema" [ngClass]="{'last': last}" [hidden]="!prop._pointer"> <tr class="param-schema" [ngClass]="{'last': last}" [hidden]="!prop._pointer">
<td colspan="2"> <td colspan="2">
<zippy #subSchema title="Expand" [headless]="true" (open)="lazySchema.load()" [visible]="autoExpand"> <zippy [attr.disabled]="prop.name" #subSchema title="Expand" [headless]="true" (openChange)="lazySchema.load()" [(open)]="prop.expanded">
<json-schema-lazy #lazySchema [auto]="autoExpand" class="nested-schema" [pointer]="prop._pointer" <json-schema-lazy #lazySchema [auto]="prop.expanded" class="nested-schema" [pointer]="prop._pointer"
[nestOdd]="!nestOdd" [isRequestSchema]="isRequestSchema"> [nestOdd]="!nestOdd" [isRequestSchema]="isRequestSchema" absolutePointer="{{absolutePointer}}/properties/{{prop.name}}">
</json-schema-lazy> </json-schema-lazy>
</zippy> </zippy>
</td> </td>

View File

@ -1,9 +1,19 @@
'use strict'; 'use strict';
import { Component, Input, Renderer, ElementRef, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { Component,
Input,
Renderer,
ElementRef,
OnInit,
ChangeDetectionStrategy,
ChangeDetectorRef
} from '@angular/core';
import { BaseComponent, SpecManager } from '../base'; import { BaseSearchableComponent, SpecManager } from '../base';
import { SchemaNormalizer, SchemaHelper } from '../../services/index'; import { SchemaNormalizer, SchemaHelper, AppStateService } from '../../services/';
import { JsonPointer, DescendantInfo } from '../../utils/';
import { Zippy } from '../../shared/components';
import { JsonSchemaLazy } from './json-schema-lazy';
@Component({ @Component({
selector: 'json-schema', selector: 'json-schema',
@ -11,8 +21,9 @@ import { SchemaNormalizer, SchemaHelper } from '../../services/index';
styleUrls: ['./json-schema.css'], styleUrls: ['./json-schema.css'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class JsonSchema extends BaseComponent implements OnInit { export class JsonSchema extends BaseSearchableComponent implements OnInit {
@Input() pointer: string; @Input() pointer: string;
@Input() absolutePointer: string;
@Input() final: boolean = false; @Input() final: boolean = false;
@Input() nestOdd: boolean; @Input() nestOdd: boolean;
@Input() childFor: string; @Input() childFor: string;
@ -20,16 +31,20 @@ export class JsonSchema extends BaseComponent implements OnInit {
schema: any = {}; schema: any = {};
activeDescendant:any = {}; activeDescendant:any = {};
hasDescendants: boolean = false; discriminator: string = null;
_hasSubSchemas: boolean = false; _hasSubSchemas: boolean = false;
properties: any; properties: any;
_isArray: boolean; _isArray: boolean;
normalizer: SchemaNormalizer; normalizer: SchemaNormalizer;
autoExpand = false; descendants: DescendantInfo[];
descendants: any;
constructor(specMgr:SpecManager, private _renderer: Renderer, private _elementRef: ElementRef) { constructor(
super(specMgr); specMgr:SpecManager,
app: AppStateService,
private _renderer: Renderer,
private cdr: ChangeDetectorRef,
private _elementRef: ElementRef) {
super(specMgr, app);
this.normalizer = new SchemaNormalizer(specMgr); this.normalizer = new SchemaNormalizer(specMgr);
} }
@ -37,8 +52,11 @@ export class JsonSchema extends BaseComponent implements OnInit {
return this.schema._pointer || this.pointer; return this.schema._pointer || this.pointer;
} }
selectDescendant(idx) { selectDescendantByIdx(idx) {
let activeDescendant = this.descendants[idx]; this.selectDescendant(this.descendants[idx]);
}
selectDescendant(activeDescendant: DescendantInfo) {
if (!activeDescendant || activeDescendant.active) return; if (!activeDescendant || activeDescendant.active) return;
this.descendants.forEach(d => { this.descendants.forEach(d => {
d.active = false; d.active = false;
@ -51,13 +69,13 @@ export class JsonSchema extends BaseComponent implements OnInit {
this.schema = this.normalizer.normalize(this.schema, this.normPointer, this.schema = this.normalizer.normalize(this.schema, this.normPointer,
{resolved: true}); {resolved: true});
this.preprocessSchema(); this.preprocessSchema();
this.activeDescendant = activeDescendant;
} }
initDescendants() { initDescendants() {
this.descendants = this.specMgr.findDerivedDefinitions(this.normPointer, this.schema); this.descendants = this.specMgr.findDerivedDefinitions(this.normPointer, this.schema);
if (!this.descendants.length) return; if (!this.descendants.length) return;
this.hasDescendants = true; let discriminator = this.discriminator = this.schema.discriminator || this.schema['x-extendedDiscriminator'];
let discriminator = this.schema.discriminator || this.schema['x-extendedDiscriminator'];
let discrProperty = this.schema.properties && let discrProperty = this.schema.properties &&
this.schema.properties[discriminator]; this.schema.properties[discriminator];
if (discrProperty && discrProperty.enum) { if (discrProperty && discrProperty.enum) {
@ -72,12 +90,15 @@ export class JsonSchema extends BaseComponent implements OnInit {
}).sort((a, b) => { }).sort((a, b) => {
return enumOrder[a.name] > enumOrder[b.name] ? 1 : -1; return enumOrder[a.name] > enumOrder[b.name] ? 1 : -1;
}); });
this.descendants.forEach((d, idx) => d.idx = idx);
} }
this.selectDescendant(0); this.selectDescendantByIdx(0);
} }
init() { init() {
if (!this.pointer) return; if (!this.pointer) return;
if (!this.absolutePointer) this.absolutePointer = this.pointer;
this.schema = this.componentSchema; this.schema = this.componentSchema;
if (!this.schema) { if (!this.schema) {
throw new Error(`Can't load component schema at ${this.pointer}`); throw new Error(`Can't load component schema at ${this.pointer}`);
@ -88,6 +109,7 @@ export class JsonSchema extends BaseComponent implements OnInit {
this.schema = this.normalizer.normalize(this.schema, this.normPointer, {resolved: true}); this.schema = this.normalizer.normalize(this.schema, this.normPointer, {resolved: true});
this.schema = SchemaHelper.unwrapArray(this.schema, this.normPointer); this.schema = SchemaHelper.unwrapArray(this.schema, this.normPointer);
this._isArray = this.schema._isArray; this._isArray = this.schema._isArray;
this.absolutePointer += (this._isArray ? '/items' : '');
this.initDescendants(); this.initDescendants();
this.preprocessSchema(); this.preprocessSchema();
} }
@ -97,11 +119,12 @@ export class JsonSchema extends BaseComponent implements OnInit {
if (!this.schema.isTrivial) { if (!this.schema.isTrivial) {
SchemaHelper.preprocessProperties(this.schema, this.normPointer, { SchemaHelper.preprocessProperties(this.schema, this.normPointer, {
childFor: this.childFor childFor: this.childFor,
discriminator: this.discriminator
}); });
} }
this.properties = this.schema._properties; this.properties = this.schema._properties || [];
if (this.isRequestSchema) { if (this.isRequestSchema) {
this.properties = this.properties && this.properties.filter(prop => !prop.readOnly); this.properties = this.properties && this.properties.filter(prop => !prop.readOnly);
} }
@ -114,7 +137,9 @@ export class JsonSchema extends BaseComponent implements OnInit {
return (propSchema && propSchema.type === 'object' && propSchema._pointer); return (propSchema && propSchema.type === 'object' && propSchema._pointer);
}); });
this.autoExpand = this.properties && this.properties.length === 1; if (this.properties.length === 1) {
this.properties[0].expanded = true;
}
} }
applyStyling() { applyStyling() {
@ -127,6 +152,46 @@ export class JsonSchema extends BaseComponent implements OnInit {
return item.name + (item._pointer || ''); return item.name + (item._pointer || '');
} }
trackByIdx(idx: number, _: any): number {
return idx;
}
findDescendantWithField(fieldName: string): DescendantInfo {
let res: DescendantInfo;
for (let descendantInfo of this.descendants) {
let schema = this.specMgr.getDescendant(descendantInfo, this.schema);
this.normalizer.reset();
schema = this.normalizer.normalize(schema, this.normPointer,
{resolved: true});
if (schema.properties && schema.properties[fieldName]) {
res = descendantInfo;
break;
};
};
return res;
}
ensureSearchIsShown(ptr: string) {
if (ptr.startsWith(this.absolutePointer)) {
let props = this.properties;
if (!props) return;
let relative = JsonPointer.relative(this.absolutePointer, ptr);
let propName;
if (relative.length > 1 && relative[0] === 'properties') {
propName = relative[1];
}
let prop = props.find(p => p.name === propName);
if (!prop) {
let d = this.findDescendantWithField(propName);
this.selectDescendant(d);
prop = this.properties.find(p => p.name === propName);
}
if (prop && !prop.isTrivial) prop.expanded = true;
this.cdr.markForCheck();
this.cdr.detectChanges();
}
}
ngOnInit() { ngOnInit() {
this.preinit(); this.preinit();
} }

View File

@ -0,0 +1,21 @@
:host {
position: fixed;
top: 0;
left: 0;
right: 0;
display: block;
height: 5px;
z-index: 100;
}
span {
display: block;
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: attr(progress percentage);
background-color: #5f7fc3;
transition: right 0.2s linear;
}

View File

@ -0,0 +1,58 @@
'use strict';
import {
Component
} from '@angular/core';
import {
ComponentFixture,
inject,
fakeAsync,
tick,
TestBed,
} from '@angular/core/testing';
import { getChildDebugElement } from '../../../tests/helpers';
import { LoadingBar } from './loading-bar';
describe('Redoc components', () => {
describe('Loading Bar', () => {
let component: LoadingBar;
it('should init component', () => {
let fixture = TestBed.createComponent(LoadingBar);
component = fixture.componentInstance;
fixture.detectChanges();
should.exist(component);
component.progress.should.be.equal(0);
component.display.should.be.equal('block');
});
it('should hide itself in 500ms if progress is 100', fakeAsync(() => {
TestBed.configureTestingModule({ declarations: [ TestAppComponent ] });
let fixture = TestBed.createComponent(TestAppComponent);
let parentComp = fixture.componentInstance;
component = getChildDebugElement(fixture.debugElement, 'loading-bar').componentInstance;
// need to pass update through parent component as ngOnChanges is run only for view changes
parentComp.progress = 50;
fixture.detectChanges();
parentComp.progress = 100;
fixture.detectChanges();
component.display.should.be.equal('block');
tick(500);
component.display.should.be.equal('none');
}));
});
});
/** Test component that contains an ApiInfo. */
@Component({
selector: 'test-app',
template:
`<loading-bar [progress]="progress"></loading-bar>`
})
class TestAppComponent {
progress = 0;
}

View File

@ -6,29 +6,7 @@ import { Input, HostBinding, Component, OnChanges } from '@angular/core';
template: ` template: `
<span [style.width]='progress + "%"'> </span> <span [style.width]='progress + "%"'> </span>
`, `,
styles: [` styleUrls: ['loading-bar.scss']
:host {
position: fixed;
top: 0;
left: 0;
right: 0;
display: block;
height: 5px;
z-index: 100;
}
span {
display: block;
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: attr(progress percentage);
background-color: #5f7fc3;
transition: right 0.2s linear;
}
`]
}) })
export class LoadingBar implements OnChanges { export class LoadingBar implements OnChanges {
@Input() progress:number = 0; @Input() progress:number = 0;

View File

@ -9,6 +9,7 @@
<p *ngIf="method.info.description" class="method-description" <p *ngIf="method.info.description" class="method-description"
[innerHtml]="method.info.description | marked"> [innerHtml]="method.info.description | marked">
</p> </p>
<redoc-externalDocs [docs]="method.externalDocs"></redoc-externalDocs>
<params-list pointer="{{pointer}}/parameters"> </params-list> <params-list pointer="{{pointer}}/parameters"> </params-list>
<responses-list pointer="{{pointer}}/responses"> </responses-list> <responses-list pointer="{{pointer}}/responses"> </responses-list>
</div> </div>

View File

@ -17,6 +17,10 @@ interface MethodInfo {
bodyParam: any; bodyParam: any;
summary: any; summary: any;
anchor: any; anchor: any;
externalDocs: {
url: string;
description?: string;
}
} }
@Component({ @Component({
@ -50,7 +54,8 @@ export class Method extends BaseComponent implements OnInit {
bodyParam: this.findBodyParam(), bodyParam: this.findBodyParam(),
summary: SchemaHelper.methodSummary(this.componentSchema), summary: SchemaHelper.methodSummary(this.componentSchema),
apiUrl: this.getBaseUrl(), apiUrl: this.getBaseUrl(),
anchor: this.buildAnchor() anchor: this.buildAnchor(),
externalDocs: this.componentSchema.externalDocs
}; };
} }
@ -77,7 +82,7 @@ export class Method extends BaseComponent implements OnInit {
} }
findBodyParam() { findBodyParam() {
let pathParams = this.specMgr.getMethodParams(this.pointer, true); let pathParams = this.specMgr.getMethodParams(this.pointer);
let bodyParam = pathParams.find(param => param.in === 'body'); let bodyParam = pathParams.find(param => param.in === 'body');
return bodyParam; return bodyParam;
} }

View File

@ -3,6 +3,7 @@
<div class="tag-info" *ngIf="tag.name"> <div class="tag-info" *ngIf="tag.name">
<h1 class="sharable-header"> <a class="share-link" href="#{{tag.id}}"></a>{{tag.name}} </h1> <h1 class="sharable-header"> <a class="share-link" href="#{{tag.id}}"></a>{{tag.name}} </h1>
<p *ngIf="tag.description" [innerHtml]="tag.description | marked"> </p> <p *ngIf="tag.description" [innerHtml]="tag.description | marked"> </p>
<redoc-externalDocs [docs]="tag.metadata.externalDocs"></redoc-externalDocs>
</div> </div>
<method *lazyFor="let methodItem of tag.items; let ready = ready;" <method *lazyFor="let methodItem of tag.items; let ready = ready;"
[hidden]="!ready" [pointer]="methodItem.metadata.pointer" [hidden]="!ready" [pointer]="methodItem.metadata.pointer"

View File

@ -27,12 +27,11 @@ export class ParamsList extends BaseComponent implements OnInit {
init() { init() {
this.params = []; this.params = [];
let paramsList = this.specMgr.getMethodParams(this.pointer, true); let paramsList = this.specMgr.getMethodParams(this.pointer);
paramsList = paramsList.map(paramSchema => { paramsList = paramsList.map(paramSchema => {
let propPointer = paramSchema._pointer; let propPointer = paramSchema._pointer;
if (paramSchema.in === 'body') return paramSchema; if (paramSchema.in === 'body') return paramSchema;
paramSchema._name = paramSchema.name;
return SchemaHelper.preprocess(paramSchema, propPointer, this.pointer); return SchemaHelper.preprocess(paramSchema, propPointer, this.pointer);
}); });

View File

@ -8,7 +8,10 @@
<div class="background-actual"> </div> <div class="background-actual"> </div>
</div> </div>
<div class="menu-content" sticky-sidebar [scrollParent]="options.$scrollParent" [scrollYOffset]="options.scrollYOffset"> <div class="menu-content" sticky-sidebar [scrollParent]="options.$scrollParent" [scrollYOffset]="options.scrollYOffset">
<api-logo> </api-logo> <div class="menu-header">
<api-logo> </api-logo>
<redoc-search> </redoc-search>
</div>
<side-menu> </side-menu> <side-menu> </side-menu>
</div> </div>
<div class="api-content"> <div class="api-content">

View File

@ -40,12 +40,17 @@
.menu-content { .menu-content {
overflow: hidden; overflow: hidden;
display: flex;
flex-direction: column;
}
side-menu {
overflow-y: auto;
} }
[sticky-sidebar] { [sticky-sidebar] {
width: $side-bar-width; width: $side-bar-width;
background-color: $side-bar-bg-color; background-color: $side-bar-bg-color;
overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
transform: translateZ(0); transform: translateZ(0);
z-index: 75; z-index: 75;

View File

@ -15,7 +15,7 @@ import { BaseComponent } from '../base';
import * as detectScollParent from 'scrollparent'; import * as detectScollParent from 'scrollparent';
import { SpecManager } from '../../utils/spec-manager'; import { SpecManager } from '../../utils/spec-manager';
import { OptionsService, Hash, AppStateService, SchemaHelper } from '../../services/index'; import { SearchService, OptionsService, Hash, AppStateService, SchemaHelper } from '../../services/';
import { LazyTasksService } from '../../shared/components/LazyFor/lazy-for'; import { LazyTasksService } from '../../shared/components/LazyFor/lazy-for';
@Component({ @Component({

View File

@ -1,6 +1,6 @@
<h2 class="responses-list-header" *ngIf="responses.length"> Responses </h2> <h2 class="responses-list-header" *ngIf="responses.length"> Responses </h2>
<zippy *ngFor="let response of responses;trackBy:trackByCode" [title]="response.code + ' ' + response.description | marked" <zippy *ngFor="let response of responses;trackBy:trackByCode" [title]="response.code + ' ' + response.description | marked"
[type]="response.type" [visible]="response.expanded" [empty]="response.empty" (open)="lazySchema.load()"> [type]="response.type" [(open)]="response.expanded" [empty]="response.empty" (openChange)="lazySchema.load()">
<div *ngIf="response.headers" class="response-headers"> <div *ngIf="response.headers" class="response-headers">
<header> <header>
Headers Headers
@ -20,6 +20,7 @@
<header *ngIf="response.schema"> <header *ngIf="response.schema">
Response Schema Response Schema
</header> </header>
<json-schema-lazy [auto]="response.expanded" #lazySchema pointer="{{response.schema ? response.pointer + '/schema' : null}}"> <json-schema-lazy [auto]="response.expanded" #lazySchema
pointer="{{response.schema ? response.pointer + '/schema' : null}}">
</json-schema-lazy> </json-schema-lazy>
</zippy> </zippy>

View File

@ -1,10 +1,16 @@
'use strict'; 'use strict';
import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { Component,
import { BaseComponent, SpecManager } from '../base'; Input,
OnInit,
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef
} from '@angular/core';
import { BaseSearchableComponent, SpecManager } from '../base';
import JsonPointer from '../../utils/JsonPointer'; import JsonPointer from '../../utils/JsonPointer';
import { statusCodeType } from '../../utils/helpers'; import { statusCodeType } from '../../utils/helpers';
import { OptionsService } from '../../services/index'; import { OptionsService, AppStateService } from '../../services/index';
import { SchemaHelper } from '../../services/schema-helper.service'; import { SchemaHelper } from '../../services/schema-helper.service';
function isNumeric(n) { function isNumeric(n) {
@ -17,14 +23,18 @@ function isNumeric(n) {
styleUrls: ['./responses-list.css'], styleUrls: ['./responses-list.css'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ResponsesList extends BaseComponent implements OnInit { export class ResponsesList extends BaseSearchableComponent implements OnInit {
@Input() pointer:string; @Input() pointer:string;
responses: Array<any>; responses: Array<any>;
options: any; options: any;
constructor(specMgr:SpecManager, optionsMgr:OptionsService) { constructor(specMgr:SpecManager,
super(specMgr); optionsMgr:OptionsService,
app: AppStateService,
private cdr: ChangeDetectorRef
) {
super(specMgr, app);
this.options = optionsMgr.options; this.options = optionsMgr.options;
} }
@ -50,6 +60,7 @@ export class ResponsesList extends BaseComponent implements OnInit {
resp.code = respCode; resp.code = respCode;
resp.type = statusCodeType(resp.code); resp.type = statusCodeType(resp.code);
resp.expanded = false;
if (this.options.expandResponses) { if (this.options.expandResponses) {
if (this.options.expandResponses === 'all' || this.options.expandResponses.has(respCode.toString())) { if (this.options.expandResponses === 'all' || this.options.expandResponses.has(respCode.toString())) {
resp.expanded = true; resp.expanded = true;
@ -74,6 +85,17 @@ export class ResponsesList extends BaseComponent implements OnInit {
return el.code; return el.code;
} }
ensureSearchIsShown(ptr: string) {
if (ptr.startsWith(this.pointer)) {
let code = JsonPointer.relative(this.pointer, ptr)[0];
if (code && this.componentSchema[code]) {
this.componentSchema[code].expanded = true;
this.cdr.markForCheck();
this.cdr.detectChanges();
}
}
}
ngOnInit() { ngOnInit() {
this.preinit(); this.preinit();
} }

View File

@ -0,0 +1,13 @@
<div class="search-input-wrap">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
<path d="M968.2,849.4L667.3,549c83.9-136.5,66.7-317.4-51.7-435.6C477.1-25,252.5-25,113.9,113.4c-138.5,138.3-138.5,362.6,0,501C219.2,730.1,413.2,743,547.6,666.5l301.9,301.4c43.6,43.6,76.9,14.9,104.2-12.4C981,928.3,1011.8,893,968.2,849.4z M524.5,522c-88.9,88.7-233,88.7-321.8,0c-88.9-88.7-88.9-232.6,0-321.3c88.9-88.7,233-88.7,321.8,0C613.4,289.4,613.4,433.3,524.5,522z"/>
</svg>
<input #search (keyup)="update($event, search.value)" [value]="searchTerm" placeholder="Search">
</div>
<ul class="search-results" [hidden]="!items.length">
<li class="result" *ngFor="let item of items"
ngClass="menu-item-depth-{{item.menuItem.depth}} {{item.menuItem.ready ? '' : 'disabled'}}"
(click)="clickSearch(item)">
{{item.menuItem.name}}
</li>
</ul>

View File

@ -0,0 +1,72 @@
@import '../../shared/styles/variables';
:host {
display: block;
margin: 10px 0;
}
.search-input-wrap {
padding: 0 20px;
> svg {
width: 13px;
height: 27px;
display: inline-block;
position: absolute;
path {
fill: lighten($text-color, 20%);
}
}
}
input {
width: 100%;
box-sizing: border-box;
padding: 5px 5px 5px 20px;
border: 0;
border-bottom: 1px solid darken($side-bar-bg-color, 10%);
font-weight: bold;
font-size: 13px;
color: $text-color;
background-color: transparent;
outline: none;
}
.search-results {
margin: 10px 0 0;
list-style: none;
padding: 10px 0;
background-color: darken($side-bar-bg-color, 5%);
max-height: 100px;
overflow-y: auto;
border-bottom: 1px solid darken($side-bar-bg-color, 10%);
border-top: 1px solid darken($side-bar-bg-color, 10%);
min-height: 150px;
max-height: 250px;
> li {
display: block;
cursor: pointer;
font-family: Montserrat, sans-serif;
font-size: 13px;
padding: 5px 20px;
&:hover {
background-color: darken($side-bar-bg-color, 10%);
}
}
li.menu-item-depth-1 {
color: #0033a0;
text-transform: uppercase;
}
> li.disabled {
cursor: default;
color: lighten($text-color, 60%);
}
}

View File

@ -0,0 +1,88 @@
'use strict';
import { Component, ChangeDetectionStrategy, ChangeDetectorRef, OnInit, HostBinding } from '@angular/core';
import { Marker, SearchService, MenuService, MenuItem } from '../../services/';
import { throttle } from '../../utils/';
@Component({
selector: 'redoc-search',
styleUrls: ['./redoc-search.css'],
templateUrl: './redoc-search.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class RedocSearch implements OnInit {
logo:any = {};
items: { menuItem: MenuItem, pointers: string[] }[] = [];
searchTerm = '';
throttledSearch: Function;
_subscription;
constructor(
cdr: ChangeDetectorRef,
private marker: Marker,
public search: SearchService,
public menu: MenuService) {
this._subscription = menu.changed.subscribe(() => {
cdr.markForCheck();
cdr.detectChanges();
});
this.throttledSearch = throttle(() => {
this.updateSearch();
cdr.markForCheck();
cdr.detectChanges();
}, 300, this);
}
init() {
this.search.indexAll();
}
update(event:KeyboardEvent, val) {
if (event && event.keyCode === 27) { // escape
this.searchTerm = '';
} else {
this.searchTerm = val;
}
this.throttledSearch();
}
updateSearch() {
if (!this.searchTerm || this.searchTerm.length < 2) {
this.items = [];
this.marker.unmark();
return;
}
let searchRes = this.search.search(this.searchTerm);
this.items = Object.keys(searchRes).map(id => ({
menuItem: this.menu.getItemById(id),
pointers: searchRes[id].map(el => el.pointer)
})).filter(res => !!res.menuItem);
this.items.sort((a, b) => {
if (a.menuItem.depth > b.menuItem.depth) return 1;
else if (a.menuItem.depth < b.menuItem.depth) return -1;
else return 0;
});
this.marker.mark(this.searchTerm);
}
clickSearch(item) {
this.search.ensureSearchVisible(
item.pointers
);
this.marker.remark();
this.menu.activate(item.menuItem.flatIdx);
this.menu.scrollToActive();
}
ngOnInit() {
this.init();
}
destroy() {
this._subscription.unsubscribe();
}
}

View File

@ -1,12 +1,10 @@
<div #mobile class="mobile-nav" (click)="toggleMobileNav()"> <div #mobile class="mobile-nav" (click)="toggleMobileNav()">
<span class="menu-header"> API Reference: </span>
<span class="selected-item-info"> <span class="selected-item-info">
<span class="selected-tag"> {{activeCatCaption}} </span> <span class="selected-tag"> {{activeCatCaption}} </span>
<span class="selected-endpoint">{{activeItemCaption}}</span> <span class="selected-endpoint">{{activeItemCaption}}</span>
</span> </span>
</div> </div>
<div #desktop id="resources-nav"> <div #desktop id="resources-nav">
<h5 class="menu-header"> API reference </h5>
<ul class="menu-root"> <ul class="menu-root">
<side-menu-items [items]="menuItems" (activate)="activateAndScroll($event)"></side-menu-items> <side-menu-items [items]="menuItems" (activate)="activateAndScroll($event)"></side-menu-items>
</ul> </ul>

View File

@ -11,13 +11,6 @@ ul.menu-root {
padding: 0; padding: 0;
} }
.menu-header {
text-transform: uppercase;
color: $headers-color;
padding: 0 $side-menu-item-hpadding;
margin: 10px 0;
}
.mobile-nav { .mobile-nav {
display: none; display: none;
height: 3em; height: 3em;
@ -39,15 +32,6 @@ ul.menu-root {
float: right; float: right;
vertical-align: middle; vertical-align: middle;
} }
.menu-header {
padding: 0 10px 0 20px;
font-size: 0.95em;
@media (max-width: $mobile-menu-compact-breakpoint) {
display: none;
}
}
} }
@media (max-width: $side-menu-mobile-breakpoint) { @media (max-width: $side-menu-mobile-breakpoint) {
@ -61,10 +45,6 @@ ul.menu-root {
transition: all 0.3s ease; transition: all 0.3s ease;
} }
#resources-nav .menu-header {
display: none;
}
.menu-subitems { .menu-subitems {
height: auto; height: auto;
} }

View File

@ -2,14 +2,14 @@
import { getChildDebugElement } from '../../../tests/helpers'; import { getChildDebugElement } from '../../../tests/helpers';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { OptionsService } from '../../services/index'; import { OptionsService, MenuItem } from '../../services/index';
import { import {
inject, inject,
async async
} from '@angular/core/testing'; } from '@angular/core/testing';
import { TestBed } from '@angular/core/testing'; import { TestBed, ComponentFixture } from '@angular/core/testing';
import { MethodsList, SideMenu } from '../index'; import { MethodsList, SideMenu } from '../index';
@ -23,8 +23,8 @@ describe('Redoc components', () => {
}); });
describe('SideMenu Component', () => { describe('SideMenu Component', () => {
let builder; let builder;
let component; let component: SideMenu;
let fixture; let fixture: ComponentFixture<TestAppComponent>;
let specMgr; let specMgr;
beforeEach(inject([SpecManager, OptionsService], beforeEach(inject([SpecManager, OptionsService],
@ -53,8 +53,34 @@ describe('Redoc components', () => {
}); });
it('should init component and component data', () => { it('should init component and component data', () => {
expect(component).not.toBeNull(); should.exist(component);
expect(component.data).not.toBeNull(); });
it('should clear active item and cat captions on change to null', () => {
component.activeCatCaption = 'test';
component.activeItemCaption = 'test';
component.changed(null);
component.activeCatCaption.should.be.equal('');
component.activeItemCaption.should.be.equal('');
});
it('should set active item and cat captions on change event', () => {
let parentItem: MenuItem = {
id: 'id',
name: 'Item'
};
component.changed(parentItem);
component.activeCatCaption.should.be.equal(parentItem.name);
component.activeItemCaption.should.be.equal('');
let childItem: MenuItem = {
id: 'id2',
name: 'Child',
parent: parentItem
};
component.changed(childItem);
component.activeCatCaption.should.be.equal(parentItem.name);
component.activeItemCaption.should.be.equal(childItem.name);
}); });
}); });
}); });

View File

@ -5,7 +5,7 @@ import { Component, EventEmitter, Input, Output, ElementRef, ChangeDetectorRef,
//import { global } from '@angular/core/src/facade/lang'; //import { global } from '@angular/core/src/facade/lang';
import { trigger, state, animate, transition, style } from '@angular/core'; import { trigger, state, animate, transition, style } from '@angular/core';
import { BaseComponent, SpecManager } from '../base'; import { BaseComponent, SpecManager } from '../base';
import { ScrollService, MenuService, OptionsService, MenuItem } from '../../services/'; import { ScrollService, MenuService, OptionsService, MenuItem, Marker} from '../../services/';
import { BrowserDomAdapter as DOM } from '../../utils/browser-adapter'; import { BrowserDomAdapter as DOM } from '../../utils/browser-adapter';
const global = window; const global = window;
@ -55,7 +55,7 @@ export class SideMenu extends BaseComponent implements OnInit, OnDestroy {
constructor(specMgr:SpecManager, elementRef:ElementRef, constructor(specMgr:SpecManager, elementRef:ElementRef,
private scrollService:ScrollService, private menuService:MenuService, private scrollService:ScrollService, private menuService:MenuService,
optionsService:OptionsService, private detectorRef:ChangeDetectorRef) { optionsService:OptionsService, private detectorRef:ChangeDetectorRef, private marker:Marker) {
super(specMgr); super(specMgr);
this.$element = elementRef.nativeElement; this.$element = elementRef.nativeElement;
@ -64,7 +64,8 @@ export class SideMenu extends BaseComponent implements OnInit, OnDestroy {
this.options = optionsService.options; this.options = optionsService.options;
this.menuService.changed.subscribe((evt) => this.changed(evt)); this.menuService.changedActiveItem.subscribe((evt) => this.changed(evt));
this.menuService.changed.subscribe((evt) => this.detectorRef.detectChanges());
} }
changed(item) { changed(item) {
@ -147,4 +148,7 @@ export class SideMenu extends BaseComponent implements OnInit, OnDestroy {
ngOnInit() { ngOnInit() {
this.preinit(); this.preinit();
} }
ngAfterViewInit() {
}
} }

View File

@ -1,26 +1,11 @@
'use strict'; 'use strict';
import { OnInit, OnDestroy } from '@angular/core'; import { OnInit, OnDestroy } from '@angular/core';
import { SpecManager } from '../utils/spec-manager'; import { SpecManager } from '../utils/spec-manager';
import { AppStateService } from '../services/app-state.service';
import { Subscription } from 'rxjs/Subscription';
export { SpecManager }; export { SpecManager };
function snapshot(obj) {
if(obj == undefined || typeof(obj) !== 'object') {
return obj;
}
var temp = new obj.constructor();
for(var key in obj) {
if (obj.hasOwnProperty(key)) {
temp[key] = snapshot(obj[key]);
}
}
return temp;
}
/** /**
* Generic Component * Generic Component
* @class * @class
@ -65,3 +50,35 @@ export class BaseComponent implements OnInit, OnDestroy {
// emtpy // emtpy
} }
} }
export abstract class BaseSearchableComponent extends BaseComponent implements OnDestroy {
searchSubscription: Subscription;
constructor(public specMgr: SpecManager, public app: AppStateService) {
super(specMgr);
}
subscribeForSearch() {
this.searchSubscription = this.app.searchContainingPointers.subscribe(ptrs => {
for (let i = 0; i < ptrs.length; ++i) {
if (ptrs[i]) this.ensureSearchIsShown(ptrs[i]);
}
});
}
preinit() {
super.preinit();
this.subscribeForSearch();
}
ngOnDestroy() {
if (this.searchSubscription) {
this.searchSubscription.unsubscribe();
}
}
/**
+ Used to destroy component
* @abstract
*/
abstract ensureSearchIsShown(ptr: string);
}

View File

@ -15,15 +15,17 @@ import { Method } from './Method/method';
import { Warnings } from './Warnings/warnings'; import { Warnings } from './Warnings/warnings';
import { SecurityDefinitions } from './SecurityDefinitions/security-definitions'; import { SecurityDefinitions } from './SecurityDefinitions/security-definitions';
import { LoadingBar } from './LoadingBar/loading-bar'; import { LoadingBar } from './LoadingBar/loading-bar';
import { RedocSearch } from './Search/redoc-search';
import { ExternalDocs } from './ExternalDocs/external-docs';
import { Redoc } from './Redoc/redoc'; import { Redoc } from './Redoc/redoc';
export const REDOC_DIRECTIVES = [ export const REDOC_DIRECTIVES = [
ApiInfo, ApiLogo, JsonSchema, JsonSchemaLazy, ParamsList, RequestSamples, ResponsesList, ApiInfo, ApiLogo, JsonSchema, JsonSchemaLazy, ParamsList, RequestSamples, ResponsesList,
ResponsesSamples, SchemaSample, SideMenu, MethodsList, Method, Warnings, Redoc, SecurityDefinitions, ResponsesSamples, SchemaSample, SideMenu, MethodsList, Method, Warnings, Redoc, SecurityDefinitions,
LoadingBar, SideMenuItems LoadingBar, SideMenuItems, RedocSearch, ExternalDocs
]; ];
export { ApiInfo, ApiLogo, JsonSchema, JsonSchemaLazy, ParamsList, RequestSamples, ResponsesList, export { ApiInfo, ApiLogo, JsonSchema, JsonSchemaLazy, ParamsList, RequestSamples, ResponsesList,
ResponsesSamples, SchemaSample, SideMenu, MethodsList, Method, Warnings, Redoc, SecurityDefinitions, ResponsesSamples, SchemaSample, SideMenu, MethodsList, Method, Warnings, Redoc, SecurityDefinitions,
LoadingBar, SideMenuItems } LoadingBar, SideMenuItems, RedocSearch, ExternalDocs }

View File

@ -16,6 +16,8 @@ import {
AppStateService, AppStateService,
ComponentParser, ComponentParser,
ContentProjector, ContentProjector,
Marker,
SearchService,
COMPONENT_PARSER_ALLOWED } from './services/'; COMPONENT_PARSER_ALLOWED } from './services/';
import { SpecManager } from './utils/spec-manager'; import { SpecManager } from './utils/spec-manager';
@ -34,7 +36,9 @@ import { SpecManager } from './utils/spec-manager';
AppStateService, AppStateService,
ComponentParser, ComponentParser,
ContentProjector, ContentProjector,
SearchService,
LazyTasksService, LazyTasksService,
Marker,
{ provide: APP_ID, useValue: 'redoc' }, { provide: APP_ID, useValue: 'redoc' },
{ provide: ErrorHandler, useClass: CustomErrorHandler }, { provide: ErrorHandler, useClass: CustomErrorHandler },
{ provide: COMPONENT_PARSER_ALLOWED, useValue: { 'security-definitions': SecurityDefinitions} } { provide: COMPONENT_PARSER_ALLOWED, useValue: { 'security-definitions': SecurityDefinitions} }

View File

@ -11,6 +11,8 @@ export class AppStateService {
loading = new Subject<boolean>(); loading = new Subject<boolean>();
initialized = new BehaviorSubject<any>(false); initialized = new BehaviorSubject<any>(false);
searchContainingPointers = new BehaviorSubject<string|null[]>([]);
startLoading() { startLoading() {
this.loading.next(true); this.loading.next(true);
} }

View File

@ -0,0 +1,56 @@
'use strict';
import { Clipboard } from './clipboard.service';
describe('Clipboard Service', () => {
let el:Node;
let copiedText = null;
function createEl(html) {
let tmpDiv = document.createElement('div');
tmpDiv.innerHTML = html;
document.body.appendChild(tmpDiv);
return tmpDiv.lastChild;
}
beforeEach(() => {
spyOn(Clipboard, 'copySelected').and.callFake(() => {
copiedText = window.getSelection().toString();
return true;
});
});
afterEach(() => {
copiedText = null;
if (el && el.parentNode) el.parentNode.removeChild(el);
(<jasmine.Spy>Clipboard.copySelected).and.callThrough();
});
it('selectElement should select element text', () => {
el = createEl('<div>Test</div>');
Clipboard.selectElement(el);
let selected = window.getSelection().toString();
selected.should.be.equal('Test');
});
it('deselect should clear selection', () => {
el = createEl('<div>Test</div>');
Clipboard.selectElement(el);
let selected = window.getSelection().toString();
selected.should.be.equal('Test');
Clipboard.deselect();
window.getSelection().toString().should.be.equal('');
});
it('copyElement should copy and deselect', () => {
el = createEl('<div>Test</div>');
Clipboard.copyElement(el);
copiedText.should.be.equal('Test');
window.getSelection().toString().should.be.equal('');
});
it('copyCustom should copy custom text', () => {
Clipboard.copyCustom('Custom text');
copiedText.should.be.equal('Custom text');
});
});

View File

@ -8,6 +8,8 @@ export * from './hash.service';
export * from './schema-normalizer.service'; export * from './schema-normalizer.service';
export * from './schema-helper.service'; export * from './schema-helper.service';
export * from './warnings.service'; export * from './warnings.service';
export * from './search.service';
export * from './component-parser.service'; export * from './component-parser.service';
export * from './content-projector.service'; export * from './content-projector.service';
export * from './marker.service';

View File

@ -0,0 +1,87 @@
import { Injectable } from '@angular/core';
import * as Mark from 'mark.js';
import { MenuService } from './menu.service';
const ROLL_LEN = 5;
@Injectable()
export class Marker {
permInstances = [];
rolledInstances = new Array(ROLL_LEN);
term: string;
currIdx = -1;
constructor(private menu: MenuService) {
menu.changedActiveItem.subscribe(() => {
this.roll();
});
}
addElement(el: Element) {
this.permInstances.push(new Mark(el));
}
newMarkerAtMenuItem(idx:number) {
let context = this.menu.getEl(idx);
if (this.menu.isTagOrGroupItem(idx)) {
context = this.menu.getTagInfoEl(idx);
}
let newInst = context && new Mark(context);
if (newInst && this.term) {
newInst.mark(this.term);
}
return newInst;
}
roll() {
let newIdx = this.menu.activeIdx;
let diff = newIdx - this.currIdx;
this.currIdx = newIdx;
if (diff < 0) {
diff = - diff;
for (let i=0; i < Math.min(diff, ROLL_LEN); i++) {
let prevInst = this.rolledInstances.pop();
if(prevInst) prevInst.unmark();
let idx = newIdx - Math.floor(ROLL_LEN/2) + i;
let newMark = this.newMarkerAtMenuItem(idx);
this.rolledInstances.unshift(newMark);
}
} else {
for (let i=0; i < Math.min(diff, ROLL_LEN); i++) {
let oldInst = this.rolledInstances.shift();
if (oldInst) oldInst.unmark();
let idx = newIdx + Math.floor(ROLL_LEN/2) - i;
let newMark = this.newMarkerAtMenuItem(idx);
this.rolledInstances.push(newMark);
}
}
}
mark(term: string) {
this.term = term || null;
this.remark();
}
remark() {
for (let marker of this.permInstances) {
if (marker) {
marker.unmark();
if (this.term) marker.mark(this.term);
}
}
for (let marker of this.rolledInstances) {
if (marker) {
marker.unmark();
if (this.term) marker.mark(this.term);
}
}
}
unmark() {
this.term = null;
this.remark();
}
}

View File

@ -11,7 +11,7 @@ import { Hash } from './hash.service';
import { LazyTasksService } from '../shared/components/LazyFor/lazy-for'; import { LazyTasksService } from '../shared/components/LazyFor/lazy-for';
import { ScrollService } from './scroll.service'; import { ScrollService } from './scroll.service';
import { SchemaHelper } from './schema-helper.service'; import { SchemaHelper } from './schema-helper.service';
import { SpecManager } from '../utils/spec-manager';; import { SpecManager } from '../utils/spec-manager';
describe('Menu service', () => { describe('Menu service', () => {
beforeEach(() => { beforeEach(() => {

View File

@ -8,7 +8,7 @@ import { SpecManager } from '../utils/spec-manager';
import { SchemaHelper } from './schema-helper.service'; import { SchemaHelper } from './schema-helper.service';
import { AppStateService } from './app-state.service'; import { AppStateService } from './app-state.service';
import { LazyTasksService } from '../shared/components/LazyFor/lazy-for'; import { LazyTasksService } from '../shared/components/LazyFor/lazy-for';
import { JsonPointer } from '../utils/JsonPointer'; import { JsonPointer, MarkdownHeading, StringMap } from '../utils/';
import * as slugify from 'slugify'; import * as slugify from 'slugify';
@ -34,7 +34,7 @@ export interface MenuItem {
active?: boolean; active?: boolean;
ready?: boolean; ready?: boolean;
level?: number; depth?: number;
flatIdx?: number; flatIdx?: number;
metadata?: any; metadata?: any;
@ -44,6 +44,7 @@ export interface MenuItem {
@Injectable() @Injectable()
export class MenuService { export class MenuService {
changed: EventEmitter<any> = new EventEmitter(); changed: EventEmitter<any> = new EventEmitter();
changedActiveItem: EventEmitter<any> = new EventEmitter();
items: MenuItem[]; items: MenuItem[];
activeIdx: number = -1; activeIdx: number = -1;
@ -51,6 +52,7 @@ export class MenuService {
private _flatItems: MenuItem[]; private _flatItems: MenuItem[];
private _hashSubscription: Subscription; private _hashSubscription: Subscription;
private _scrollSubscription: Subscription; private _scrollSubscription: Subscription;
private _progressSubscription: Subscription;
private _tagsWithMethods: any; private _tagsWithMethods: any;
constructor( constructor(
@ -70,6 +72,12 @@ export class MenuService {
this._hashSubscription = this.hash.value.subscribe((hash) => { this._hashSubscription = this.hash.value.subscribe((hash) => {
this.onHashChange(hash); this.onHashChange(hash);
}); });
this._progressSubscription = this.tasks.loadProgress.subscribe(progress => {
if (progress === 100) {
this.makeSureLastItemsEnabled();
}
});
} }
get flatItems():MenuItem[] { get flatItems():MenuItem[] {
@ -87,7 +95,7 @@ export class MenuService {
idx = item.parent.flatIdx; idx = item.parent.flatIdx;
} }
// check if previous items can be enabled // check if previous items§ can be enabled
let prevItem = this.flatItems[idx -= 1]; let prevItem = this.flatItems[idx -= 1];
while(prevItem && (!prevItem.metadata || !prevItem.items)) { while(prevItem && (!prevItem.metadata || !prevItem.items)) {
prevItem.ready = true; prevItem.ready = true;
@ -97,6 +105,15 @@ export class MenuService {
this.changed.next(); this.changed.next();
} }
makeSureLastItemsEnabled() {
let lastIdx = this.flatItems.length - 1;
let item = this.flatItems[lastIdx];
while(item && (!item.metadata || !item.items)) {
item.ready = true;
item = this.flatItems[lastIdx -= 1];
}
}
onScroll(isScrolledDown) { onScroll(isScrolledDown) {
let stable = false; let stable = false;
while(!stable) { while(!stable) {
@ -135,6 +152,7 @@ export class MenuService {
getEl(flatIdx:number):Element { getEl(flatIdx:number):Element {
if (flatIdx < 0) return null; if (flatIdx < 0) return null;
if (flatIdx > this.flatItems.length - 1) return null;
let currentItem = this.flatItems[flatIdx]; let currentItem = this.flatItems[flatIdx];
if (!currentItem) return; if (!currentItem) return;
if (currentItem.isGroup) currentItem = this.flatItems[flatIdx + 1]; if (currentItem.isGroup) currentItem = this.flatItems[flatIdx + 1];
@ -156,6 +174,18 @@ export class MenuService {
return selector ? document.querySelector(selector) : null; return selector ? document.querySelector(selector) : null;
} }
isTagOrGroupItem(flatIdx: number):boolean {
let item = this.flatItems[flatIdx];
return item && (item.isGroup || (item.metadata && item.metadata.type === 'tag'));
}
getTagInfoEl(flatIdx: number):Element {
if (!this.isTagOrGroupItem(flatIdx)) return null;
let el = this.getEl(flatIdx);
return el && el.querySelector('.tag-info');
}
getCurrentEl():Element { getCurrentEl():Element {
return this.getEl(this.activeIdx); return this.getEl(this.activeIdx);
} }
@ -186,7 +216,7 @@ export class MenuService {
cItem.parent.active = true; cItem.parent.active = true;
cItem = cItem.parent; cItem = cItem.parent;
} }
this.changed.next(item); this.changedActiveItem.next(item);
} }
changeActive(offset = 1):boolean { changeActive(offset = 1):boolean {
@ -236,40 +266,35 @@ export class MenuService {
addMarkdownItems() { addMarkdownItems() {
let schema = this.specMgr.schema; let schema = this.specMgr.schema;
for (let header of (<Array<string>>(schema.info && schema.info['x-redoc-markdown-headers'] || []))) { let headings:StringMap<MarkdownHeading> = schema.info && schema.info['x-redoc-markdown-headers'] || {};
let id = 'section/' + slugify(header); Object.keys(headings).forEach(h => {
let heading = headings[h];
let id = 'section/' + heading.id;
let item = { let item = {
name: header, name: heading.title,
id: id, id: id,
items: null items: null
}; };
item.items = this.getMarkdownSubheaders(item); item.items = this.getMarkdownSubheaders(item, heading);
this.items.push(item); this.items.push(item);
} });
} }
getMarkdownSubheaders(parent: MenuItem):MenuItem[] { getMarkdownSubheaders(parent: MenuItem, parentHeading: MarkdownHeading):MenuItem[] {
let res = []; let res = [];
let schema = this.specMgr.schema; Object.keys(parentHeading.children || {}).forEach(h => {
for (let subheader of (<Array<string>>(schema.info && schema.info['x-redoc-markdown-subheaders'] || []))) { let heading = parentHeading.children[h];
let parts = subheader.split('/'); let id = 'section/' + heading.id;
let header = parts[0];
if (parent.name !== header) {
continue;
}
let name = parts[1];
let id = parent.id + '/' + slugify(name);
let subItem = { let subItem = {
name: name, name: heading.title,
id: id, id: id,
parent: parent parent: parent
}; };
res.push(subItem); res.push(subItem);
} });
return res; return res;
} }
@ -332,7 +357,7 @@ export class MenuService {
name: tag['x-displayName'] || tag.name, name: tag['x-displayName'] || tag.name,
id: id, id: id,
description: tag.description, description: tag.description,
metadata: { type: 'tag' }, metadata: { type: 'tag', externalDocs: tag.externalDocs },
parent: parent, parent: parent,
items: null items: null
}; };
@ -403,6 +428,10 @@ export class MenuService {
return res; return res;
} }
getItemById(id: string):MenuItem {
return this.flatItems.find(item => item.id === id || item.id === `section/${id}`);
}
destroy() { destroy() {
this._hashSubscription.unsubscribe(); this._hashSubscription.unsubscribe();
this._scrollSubscription.unsubscribe(); this._scrollSubscription.unsubscribe();

View File

@ -7,6 +7,7 @@ import * as slugify from 'slugify';
interface PropertyPreprocessOptions { interface PropertyPreprocessOptions {
childFor: string; childFor: string;
skipReadOnly?: boolean; skipReadOnly?: boolean;
discriminator?: string;
} }
// global var for this module // global var for this module
@ -214,14 +215,13 @@ export class SchemaHelper {
let propPointer = propertySchema._pointer || let propPointer = propertySchema._pointer ||
JsonPointer.join(pointer, ['properties', propName]); JsonPointer.join(pointer, ['properties', propName]);
propertySchema = SchemaHelper.preprocess(propertySchema, propPointer); propertySchema = SchemaHelper.preprocess(propertySchema, propPointer);
propertySchema._name = propName; propertySchema.name = propName;
// stop endless discriminator recursion // stop endless discriminator recursion
if (propertySchema._pointer === opts.childFor) { if (propertySchema._pointer === opts.childFor) {
propertySchema._pointer = null; propertySchema._pointer = null;
} }
propertySchema._required = !!requiredMap[propName]; propertySchema._required = !!requiredMap[propName];
propertySchema.isDiscriminator = (schema.discriminator === propName propertySchema.isDiscriminator = opts.discriminator === propName;
|| schema['x-extendedDiscriminator'] === propName);
return propertySchema; return propertySchema;
}); });
@ -244,7 +244,7 @@ export class SchemaHelper {
var addProps = schema.additionalProperties; var addProps = schema.additionalProperties;
let ptr = addProps._pointer || JsonPointer.join(pointer, ['additionalProperties']); let ptr = addProps._pointer || JsonPointer.join(pointer, ['additionalProperties']);
let res = SchemaHelper.preprocess(addProps, ptr); let res = SchemaHelper.preprocess(addProps, ptr);
res._name = '<Additional Properties> *'; res.name = '<Additional Properties> *';
return res; return res;
} }

View File

@ -27,6 +27,7 @@ export class SchemaNormalizer {
let hasPtr = !!schema.$ref; let hasPtr = !!schema.$ref;
if (opts.resolved && !hasPtr) this._dereferencer.visit(ptr); if (opts.resolved && !hasPtr) this._dereferencer.visit(ptr);
if (opts.childFor) this._dereferencer.visit(opts.childFor);
if (schema['x-redoc-normalized']) return schema; if (schema['x-redoc-normalized']) return schema;
let res = SchemaWalker.walk(schema, ptr, (subSchema, ptr) => { let res = SchemaWalker.walk(schema, ptr, (subSchema, ptr) => {
let resolved = this._dereferencer.dereference(subSchema, ptr); let resolved = this._dereferencer.dereference(subSchema, ptr);
@ -38,6 +39,7 @@ export class SchemaNormalizer {
return resolved; return resolved;
}); });
if (opts.resolved && !hasPtr) this._dereferencer.exit(ptr); if (opts.resolved && !hasPtr) this._dereferencer.exit(ptr);
if (opts.childFor) this._dereferencer.exit(opts.childFor);
res['x-redoc-normalized'] = true; res['x-redoc-normalized'] = true;
return res; return res;
} }
@ -94,7 +96,7 @@ class SchemaWalker {
} }
} }
class AllOfMerger { export class AllOfMerger {
static merge(into, schemas) { static merge(into, schemas) {
into['x-derived-from'] = []; into['x-derived-from'] = [];
for (let i=0; i < schemas.length; i++) { for (let i=0; i < schemas.length; i++) {
@ -113,6 +115,7 @@ class AllOfMerger {
defaults(into, subSchema); defaults(into, subSchema);
subSchema._pointer = tmpPtr; subSchema._pointer = tmpPtr;
} }
into.discriminator = null;
into.allOf = null; into.allOf = null;
} }
@ -196,7 +199,6 @@ class SchemaDereferencer {
dereference(schema: Reference, pointer:string):any { dereference(schema: Reference, pointer:string):any {
if (!schema || !schema.$ref) return schema; if (!schema || !schema.$ref) return schema;
window['derefCount'] = window['derefCount'] ? window['derefCount'] + 1 : 1;
let $ref = schema.$ref; let $ref = schema.$ref;
let resolved = this._spec.byPointer($ref); let resolved = this._spec.byPointer($ref);
if (!this._refCouner.visited($ref)) { if (!this._refCouner.visited($ref)) {

View File

@ -0,0 +1,225 @@
import { Injectable } from '@angular/core';
import { AppStateService } from './app-state.service';
import { SchemaNormalizer } from './schema-normalizer.service';
import { JsonPointer, groupBy, SpecManager, StringMap, snapshot, MarkdownHeading } from '../utils/';
import { methods as swaggerMethods } from '../utils/swagger-defs';
import * as slugify from 'slugify';
import {
SwaggerSpec,
SwaggerOperation,
SwaggerSchema,
SwaggerBodyParameter,
SwaggerResponse
} from '../utils/swagger-typings';
import * as lunr from 'lunr';
interface IndexElement {
menuId: string;
title: string;
body: string;
pointer: string;
}
const index = lunr(function () {
this.field('title', {boost: 1.5});
this.field('body');
this.ref('pointer');
});
const store:StringMap<IndexElement> = {};
@Injectable()
export class SearchService {
normalizer: SchemaNormalizer;
constructor(private app: AppStateService, private spec: SpecManager) {
this.normalizer = new SchemaNormalizer(spec);
}
ensureSearchVisible(containingPointers: string|null[]) {
this.app.searchContainingPointers.next(containingPointers);
}
indexAll() {
console.time('Indexing');
this.indexPaths(this.spec.schema);
this.indexTags(this.spec.schema);
this.indexDescriptionHeadings(this.spec.schema.info['x-redoc-markdown-headers']);
console.time('Indexing end');
}
search(q):StringMap<IndexElement[]> {
var items = {};
const res:IndexElement[] = index.search(q).map(res => {
items[res.menuId] = res;
return store[res.ref];
});
const grouped = groupBy(res, 'menuId');
return grouped;
}
index(element: IndexElement) {
// don't reindex same pointers (for discriminator)
if (store[element.pointer]) return;
index.add(element);
store[element.pointer] = element;
}
indexDescriptionHeadings(headings:StringMap<MarkdownHeading>) {
if (!headings) return;
Object.keys(headings).forEach(k => {
let heading = headings[k];
this.index({
menuId: heading.id,
title: heading.title,
body: heading.content,
pointer: '/heading/' + heading.id
});
this.indexDescriptionHeadings(heading.children);
});
}
indexTags(swagger:SwaggerSpec) {
let tags = swagger.tags;
if (!tags) return;
for (let tag of tags) {
if (tag['x-traitTag']) continue;
let id = `tag/${slugify(tag.name)}`;
this.index({
menuId: id,
title: tag.name,
body: tag.description,
pointer: id
});
}
}
indexPaths(swagger:SwaggerSpec) {
const paths = swagger.paths;
const basePtr = '#/paths';
Object.keys(paths).forEach(path => {
let opearations = paths[path];
Object.keys(opearations).forEach(verb => {
if (!swaggerMethods.has(verb)) return;
const opearation = opearations[verb];
const ptr = JsonPointer.join(basePtr, [path, verb]);
this.indexOperation(opearation, ptr);
});
});
}
indexOperation(operation:SwaggerOperation, operationPointer:string) {
this.index({
pointer: operationPointer,
menuId: operationPointer,
title: operation.summary,
body: operation.description
});
this.indexOperationResponses(operation, operationPointer);
this.indexOperationParameters(operation, operationPointer);
}
indexOperationParameters(operation: SwaggerOperation, operationPointer: string) {
const parameters = this.spec.getMethodParams(operationPointer);
if (!parameters) return;
for (let i=0; i<parameters.length; ++i) {
const param = parameters[i];
const paramPointer = JsonPointer.join(operationPointer, ['parameters', i]);
this.index({
pointer: paramPointer,
menuId: operationPointer,
title: param.in === 'body' ? '' : param.name,
body: param.description
});
if (param.in === 'body') {
this.normalizer.reset();
this.indexSchema((<SwaggerBodyParameter>param).schema,
'', JsonPointer.join(paramPointer, ['schema']), operationPointer);
}
}
}
indexOperationResponses(operation:SwaggerOperation, operationPtr:string) {
const responses = operation.responses;
if (!responses) return;
Object.keys(responses).forEach(code => {
const resp = responses[code];
const respPtr = JsonPointer.join(operationPtr, ['responses', code]);
this.index({
pointer: respPtr,
menuId: operationPtr,
title: code,
body: resp.description
});
if (resp.schema) {
this.normalizer.reset();
this.indexSchema(resp.schema, '', JsonPointer.join(respPtr, 'schema'), operationPtr);
}
if (resp.headers) {
this.indexOperationResponseHeaders(resp, respPtr, operationPtr);
}
});
}
indexOperationResponseHeaders(response: SwaggerResponse, responsePtr: string, operationPtr: string, ) {
let headers = response.headers || [];
Object.keys(headers).forEach(headerName => {
let header = headers[headerName];
this.index({
pointer: `${responsePtr}/${headerName}`,
menuId: operationPtr,
title: headerName,
body: header.description
});
});
}
indexSchema(_schema:SwaggerSchema, name: string, absolutePointer: string,
menuPointer: string, parent?: string) {
if (!_schema) return;
let schema = _schema;
let title = name;
schema = this.normalizer.normalize(schema, schema._pointer || absolutePointer, { childFor: parent });
let body = schema.description; // TODO: defaults, examples, etc...
if (schema.type === 'array') {
this.indexSchema(schema.items, title, JsonPointer.join(absolutePointer, ['items']), menuPointer);
return;
}
if (schema.discriminator) {
let derived = this.spec.findDerivedDefinitions(schema._pointer, schema);
for (let defInfo of derived ) {
let subSpec = this.spec.getDescendant(defInfo, schema);
this.indexSchema(snapshot(subSpec), '', absolutePointer, menuPointer, schema._pointer);
}
}
if (schema.type === 'string' && schema.enum) {
body += ' ' + schema.enum.join(' ');
}
if (!parent) {
// redoc doesn't display top level descriptions and titles
this.index({
pointer: absolutePointer,
menuId: menuPointer,
title,
body
});
}
if (schema.properties) {
Object.keys(schema.properties).forEach(propName => {
let propPtr = JsonPointer.join(absolutePointer, ['properties', propName]);
this.indexSchema(schema.properties[propName], propName, propPtr, menuPointer);
});
}
}
}

View File

@ -0,0 +1,3 @@
<select (change)=onChange($event.target.value)>
<ng-content></ng-content>
</select>

View File

@ -1,19 +1,16 @@
'use strict'; 'use strict';
import { Component, EventEmitter, ElementRef, Output, AfterContentInit } from '@angular/core'; import { Component, EventEmitter, ElementRef, Output, Input, AfterContentInit, OnChanges } from '@angular/core';
import * as DropKick from 'dropkickjs'; import * as DropKick from 'dropkickjs';
@Component({ @Component({
selector: 'drop-down', selector: 'drop-down',
template: ` templateUrl: 'drop-down.html',
<select (change)=onChange($event.target.value)>
<ng-content></ng-content>
</select>
`,
styleUrls: ['./drop-down.css'] styleUrls: ['./drop-down.css']
}) })
export class DropDown implements AfterContentInit { export class DropDown implements AfterContentInit, OnChanges {
@Output() change = new EventEmitter(); @Output() change = new EventEmitter();
@Input() active: string;
elem: any; elem: any;
inst: any; inst: any;
constructor(elem:ElementRef) { constructor(elem:ElementRef) {
@ -28,6 +25,12 @@ export class DropDown implements AfterContentInit {
this.change.next(value); this.change.next(value);
} }
ngOnChanges(ch) {
if (ch.active.currentValue) {
this.inst && this.inst.select(ch.active.currentValue);
}
}
destroy() { destroy() {
this.inst.dispose(); this.inst.dispose();
} }

View File

@ -38,11 +38,11 @@ describe('Common components', () => {
expect(component.stickBottom).not.toHaveBeenCalled(); expect(component.stickBottom).not.toHaveBeenCalled();
}); });
it('should stick to the top on the next VM tick', (done) => { it('should stick to the top on the next animation frame', (done) => {
spyOn(component, 'stick').and.callThrough(); spyOn(component, 'stick').and.callThrough();
spyOn(component, 'stickBottom').and.callThrough(); spyOn(component, 'stickBottom').and.callThrough();
fixture.detectChanges(); fixture.detectChanges();
setTimeout(() => { requestAnimationFrame(() => {
expect(component.stick).toHaveBeenCalled(); expect(component.stick).toHaveBeenCalled();
expect(component.stickBottom).toHaveBeenCalled(); expect(component.stickBottom).toHaveBeenCalled();
done(); done();

View File

@ -0,0 +1,3 @@
<div class="tab-wrap" [ngClass]="{'active': active}">
<ng-content></ng-content>
</div>

View File

@ -0,0 +1,10 @@
:host {
display: block;
}
.tab-wrap {
display: none;
}
.tab-wrap.active {
display: block;
}

View File

@ -0,0 +1,5 @@
<ul>
<li *ngFor="let tab of tabs" [ngClass]="{active: tab.active}" (click)="selectTab(tab)"
class="tab-{{tab.tabStatus}}">{{tab.tabTitle}}</li>
</ul>
<ng-content></ng-content>

View File

@ -5,13 +5,7 @@ import { ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core';
@Component({ @Component({
selector: 'tabs', selector: 'tabs',
template: ` templateUrl: 'tabs.html',
<ul>
<li *ngFor="let tab of tabs" [ngClass]="{active: tab.active}" (click)="selectTab(tab)"
class="tab-{{tab.tabStatus}}">{{tab.tabTitle}}</li>
</ul>
<ng-content></ng-content>
`,
styleUrls: ['tabs.css'], styleUrls: ['tabs.css'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
@ -63,23 +57,8 @@ export class Tabs implements OnInit {
@Component({ @Component({
selector: 'tab', selector: 'tab',
template: ` templateUrl: 'tab.html',
<div class="tab-wrap" [ngClass]="{'active': active}"> styleUrls: ['tab.css']
<ng-content></ng-content>
</div>
`,
styles: [`
:host {
display: block;
}
.tab-wrap {
display: none;
}
.tab-wrap.active {
display: block;
}`
]
}) })
export class Tab { export class Tab {
@Input() active: boolean = false; @Input() active: boolean = false;

View File

@ -1,4 +1,4 @@
<div class="zippy zippy-{{type}}" [ngClass]="{'zippy-empty': empty, 'zippy-hidden': !visible}"> <div class="zippy zippy-{{type}}" [ngClass]="{'zippy-empty': empty, 'zippy-hidden': !open}">
<div *ngIf='!headless' class="zippy-title" (click)="toggle()"> <div *ngIf='!headless' class="zippy-title" (click)="toggle()">
<span class="zippy-indicator"> <span class="zippy-indicator">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0" y="0" viewBox="0 0 24 24" xml:space="preserve"> <svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0" y="0" viewBox="0 0 24 24" xml:space="preserve">

View File

@ -16,7 +16,7 @@ describe('Common components', () => {
}); });
describe('Zippy Component', () => { describe('Zippy Component', () => {
let builder; let builder;
let component; let component: Zippy;
let nativeElement; let nativeElement;
let fixture; let fixture;
@ -33,13 +33,13 @@ describe('Common components', () => {
it('should init component defaults', () => { it('should init component defaults', () => {
component.empty.should.be.false(); component.empty.should.be.false();
component.visible.should.be.false(); component.open.should.be.false();
component.type.should.be.equal('general'); component.type.should.be.equal('general');
}); });
it('should init properties from dom params', () => { it('should init properties from dom params', () => {
fixture.detectChanges(); fixture.detectChanges();
component.visible.should.be.true(); component.open.should.be.true();
component.empty.should.be.true(); component.empty.should.be.true();
component.title.should.be.equal('Zippy'); component.title.should.be.equal('Zippy');
component.type.should.be.equal('test'); component.type.should.be.equal('test');
@ -54,7 +54,7 @@ describe('Common components', () => {
it('should open and close zippy', (done) => { it('should open and close zippy', (done) => {
fixture.detectChanges(); fixture.detectChanges();
component.empty = false; component.empty = false;
component.visible = true; component.open = true;
fixture.detectChanges(); fixture.detectChanges();
let testComponent = fixture.debugElement.componentInstance; let testComponent = fixture.debugElement.componentInstance;
@ -62,13 +62,13 @@ describe('Common components', () => {
let titleEl = nativeElement.querySelector('.zippy-title'); let titleEl = nativeElement.querySelector('.zippy-title');
mouseclick(titleEl); mouseclick(titleEl);
fixture.detectChanges(); fixture.detectChanges();
component.visible.should.be.false(); component.open.should.be.false();
testComponent.opened.should.be.false(); testComponent.opened.should.be.false();
mouseclick(titleEl); mouseclick(titleEl);
fixture.detectChanges(); fixture.detectChanges();
setTimeout(() => { setTimeout(() => {
component.visible.should.be.true(); component.open.should.be.true();
testComponent.opened.should.be.true(); testComponent.opened.should.be.true();
testComponent.clickCount.should.be.equal(2); testComponent.clickCount.should.be.equal(2);
done(); done();
@ -95,21 +95,17 @@ describe('Common components', () => {
@Component({ @Component({
selector: 'test-app', selector: 'test-app',
template: template:
`<zippy title="Zippy" type="test" [visible]="true" [empty]="true" (open)="open()" (close)="close()">test</zippy>` `<zippy title="Zippy" type="test" [open]="true" [empty]="true" (openChange)="open($event)">test</zippy>`
}) })
class TestApp { class TestApp {
opened: boolean; opened: boolean;
clickCount: number; clickCount: number;
constructor() { constructor() {
this.opened = false; this.opened = false;
this.clickCount = 0; this.clickCount = -1; // initial change detection
} }
open() { open(val) {
this.opened = true; this.opened = val;
this.clickCount++;
}
close() {
this.opened = false;
this.clickCount++; this.clickCount++;
} }
} }

View File

@ -1,26 +1,29 @@
'use strict'; 'use strict';
import { Component, EventEmitter, Output, Input } from '@angular/core'; import { Component, EventEmitter, Output, Input, OnChanges } from '@angular/core';
@Component({ @Component({
selector: 'zippy', selector: 'zippy',
templateUrl: './zippy.html', templateUrl: './zippy.html',
styleUrls: ['./zippy.css'] styleUrls: ['./zippy.css']
}) })
export class Zippy { export class Zippy implements OnChanges {
@Input() type = 'general'; @Input() type = 'general';
@Input() visible = false;
@Input() empty = false; @Input() empty = false;
@Input() title; @Input() title;
@Input() headless: boolean = false; @Input() headless: boolean = false;
@Output() open = new EventEmitter(); @Input() open = false;
@Output() close = new EventEmitter(); @Output() openChange = new EventEmitter();
toggle() { toggle() {
this.visible = !this.visible; this.open = !this.open;
if (this.empty) return; if (this.empty) return;
if (this.visible) { this.openChange.emit(this.open);
this.open.next({}); }
} else {
this.close.next({}); ngOnChanges(ch) {
if (ch.open.currentValue === true) {
this.openChange.emit(ch.open.currentValue);
} }
} }
} }

View File

@ -35,6 +35,20 @@ export class JsonPointer {
return JsonPointerLib.compile(tokens.slice(0, tokens.length - level)); return JsonPointerLib.compile(tokens.slice(0, tokens.length - level));
} }
/**
* returns relative path tokens
* @example
* // returns ['subpath']
* JsonPointerHelper.relative('/path/0', '/path/0/subpath')
* // returns ['foo', 'subpath']
* JsonPointerHelper.relative('/path', '/path/foo/subpath')
*/
static relative(from, to):string[] {
let fromTokens = JsonPointer.parse(from);
let toTokens = JsonPointer.parse(to);
return toTokens.slice(fromTokens.length);
}
/** /**
* overridden JsonPointer original parse to take care of prefixing '#' symbol * overridden JsonPointer original parse to take care of prefixing '#' symbol
* that is not valid JsonPointer * that is not valid JsonPointer

View File

@ -14,16 +14,6 @@ export class BrowserDomAdapter {
return () => { el.removeEventListener(evt, listener, false); }; return () => { el.removeEventListener(evt, listener, false); };
} }
static addClass(element: any /** TODO #9100 */, className: string) { element.classList.add(className); }
static removeClass(element: any /** TODO #9100 */, className: string) {
element.classList.remove(className);
}
static hasClass(element: any /** TODO #9100 */, className: string): boolean {
return element.classList.contains(className);
}
static attributeMap(element: any /** TODO #9100 */): Map<string, string> { static attributeMap(element: any /** TODO #9100 */): Map<string, string> {
var res = new Map<string, string>(); var res = new Map<string, string>();
var elAttrs = element.attributes; var elAttrs = element.attributes;
@ -59,15 +49,5 @@ export class BrowserDomAdapter {
return element.getAttribute(attribute); return element.getAttribute(attribute);
} }
static setAttribute(element: any /** TODO #9100 */, name: string, value: string) {
element.setAttribute(name, value);
}
static removeAttribute(element: any /** TODO #9100 */, attribute: string) {
element.removeAttribute(attribute);
}
static getLocation(): Location { return window.location; }
static defaultDoc(): HTMLDocument { return document; } static defaultDoc(): HTMLDocument { return document; }
} }

View File

@ -1,5 +1,9 @@
'use strict'; 'use strict';
export interface StringMap<T> {
[key: string]: T;
}
export function stringify(obj:any) { export function stringify(obj:any) {
return JSON.stringify(obj); return JSON.stringify(obj);
} }
@ -16,6 +20,18 @@ export function isBlank(obj: any): boolean {
return obj == undefined; return obj == undefined;
} }
const hasOwnProperty = Object.prototype.hasOwnProperty;
export function groupBy<T>(array: T[], key:string):StringMap<T[]> {
return array.reduce<StringMap<T[]>>(function(res, value) {
if (hasOwnProperty.call(res, value[key])) {
res[value[key]].push(value);
} else {
res[value[key]] = [value];
}
return res;
}, {});
}
export function statusCodeType(statusCode) { export function statusCodeType(statusCode) {
if (statusCode < 100 || statusCode > 599) { if (statusCode < 100 || statusCode > 599) {
throw new Error('invalid HTTP code'); throw new Error('invalid HTTP code');
@ -78,3 +94,19 @@ export function throttle(fn, threshhold, scope) {
export const isSafari = Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0 export const isSafari = Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0
|| (function (p) { return p.toString() === '[object SafariRemoteNotification]'; })(!window['safari'] || (function (p) { return p.toString() === '[object SafariRemoteNotification]'; })(!window['safari']
|| safari.pushNotification); || safari.pushNotification);
export function snapshot(obj) {
if(obj == undefined || typeof(obj) !== 'object') {
return obj;
}
var temp = new obj.constructor();
for(var key in obj) {
if (obj.hasOwnProperty(key)) {
temp[key] = snapshot(obj[key]);
}
}
return temp;
}

View File

@ -1,3 +1,6 @@
export * from './custom-error-handler'; export * from './custom-error-handler';
export * from './helpers'; export * from './helpers';
export * from './md-renderer'; export * from './md-renderer';
export * from './spec-manager';
export { default as JsonPointer } from './JsonPointer';

View File

@ -0,0 +1,24 @@
'use strict';
import { MdRenderer } from '../../lib/utils/md-renderer';
describe('Utils', () => {
describe('Markdown renderer', () => {
let mdRender: MdRenderer;
beforeEach(() => {
mdRender = new MdRenderer();
});
it('should return a level-1 heading even though only level-2 is present', () => {
mdRender.renderMd('## Sub Intro');
Object.keys(mdRender.headings).length.should.be.equal(1);
should.exist(mdRender.headings['Sub-Intro']);
});
it('should return a level-2 heading as a child of level-1', () => {
mdRender.renderMd('# Introduction \n ## Sub Intro');
Object.keys(mdRender.headings).length.should.be.equal(1);
should.exist(mdRender.headings['Introduction']);
should.exist(mdRender.headings['Introduction'].children);
Object.keys(mdRender.headings['Introduction'].children).length.should.be.equal(1);
});
});
});

View File

@ -3,6 +3,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import * as slugify from 'slugify'; import * as slugify from 'slugify';
import * as Remarkable from 'remarkable'; import * as Remarkable from 'remarkable';
import { StringMap } from './';
declare var Prism: any; declare var Prism: any;
const md = new Remarkable({ const md = new Remarkable({
@ -13,17 +14,23 @@ const md = new Remarkable({
highlight: (str, lang) => { highlight: (str, lang) => {
if (lang === 'json') lang = 'js'; if (lang === 'json') lang = 'js';
let grammar = Prism.languages[lang]; let grammar = Prism.languages[lang];
//fallback to clike // fallback to click
if (!grammar) return str; if (!grammar) return str;
return Prism.highlight(str, grammar); return Prism.highlight(str, grammar);
} }
}); });
export interface MarkdownHeading {
title?: string;
id: string;
content?: string;
children?: StringMap<MarkdownHeading>;
}
@Injectable() @Injectable()
export class MdRenderer { export class MdRenderer {
public firstLevelHeadings: string[] = []; public headings: StringMap<MarkdownHeading> = {};
public secondLevelHeadings: string[] = []; currentTopHeading: MarkdownHeading;
public currentHeading: string = null;
private _origRules:any = {}; private _origRules:any = {};
private _preProcessors:Function[] = []; private _preProcessors:Function[] = [];
@ -45,21 +52,70 @@ export class MdRenderer {
md.renderer.rules.heading_close = this._origRules.close; md.renderer.rules.heading_close = this._origRules.close;
} }
saveHeading(title: string, parent:MarkdownHeading = {id:null, children: this.headings}) :MarkdownHeading {
let id = slugify(title);
if (parent && parent.id) id = `${parent.id}/${id}`;
parent.children = parent.children || {};
parent.children[id] = {
title,
id
};
return parent.children[id];
}
flattenHeadings(container: StringMap<MarkdownHeading>): MarkdownHeading[] {
if (!container) return [];
let res = [];
Object.keys(container).forEach(k => {
let heading = container[k];
res.push(heading);
res.push(...this.flattenHeadings(heading.children));
});
return res;
}
attachHeadingsContent(rawText:string) {
const buildRegexp = heading => new RegExp(
`<h\\d section="section/${heading.id}">`
);
const tmpEl = document.createElement('DIV');
const html2Str = html => {
tmpEl.innerHTML = html;
return tmpEl.innerText;
};
let flatHeadings = this.flattenHeadings(this.headings);
if (flatHeadings.length < 1) return;
let prevHeading = flatHeadings[0];
let prevPos = rawText.search(buildRegexp(prevHeading));
for (let i=1; i < flatHeadings.length; i++) {
let heading = flatHeadings[i];
let currentPos = rawText.substr(prevPos + 1).search(buildRegexp(heading)) + prevPos + 1;
prevHeading.content = html2Str(rawText.substring(prevPos, currentPos));
prevHeading = heading;
prevPos = currentPos;
}
prevHeading.content = html2Str(rawText.substring(prevPos));
}
headingOpenRule(tokens, idx) { headingOpenRule(tokens, idx) {
if (tokens[idx].hLevel > 2 ) { if (tokens[idx].hLevel > 2 ) {
return this._origRules.open(tokens, idx); return this._origRules.open(tokens, idx);
} else { } else {
let content = tokens[idx + 1].content; let content = tokens[idx + 1].content;
if (tokens[idx].hLevel === 1 ) { if (tokens[idx].hLevel === 1 ) {
this.firstLevelHeadings.push(content); this.currentTopHeading = this.saveHeading(content);;
this.currentHeading = content; let id = this.currentTopHeading.id;
let contentSlug = slugify(content); return `<h${tokens[idx].hLevel} section="section/${id}">` +
return `<h${tokens[idx].hLevel} section="section/${contentSlug}">` + `<a class="share-link" href="#section/${id}"></a>`;
`<a class="share-link" href="#section/${contentSlug}"></a>`;
} else if (tokens[idx].hLevel === 2 ) { } else if (tokens[idx].hLevel === 2 ) {
this.secondLevelHeadings.push(this.currentHeading + `/` + content); let heading = this.saveHeading(content, this.currentTopHeading);
let contentSlug = slugify(this.currentHeading) + `/` + slugify(content); let contentSlug = `${heading.id}`;
return `<h${tokens[idx].hLevel} section="section/${contentSlug}">` + return `<h${tokens[idx].hLevel} section="section/${heading.id}">` +
`<a class="share-link" href="#section/${contentSlug}"></a>`; `<a class="share-link" href="#section/${contentSlug}"></a>`;
} }
} }
@ -87,6 +143,8 @@ export class MdRenderer {
let res = md.render(text); let res = md.render(text);
this.attachHeadingsContent(res);
if (!this.raw) { if (!this.raw) {
this.restoreOrigRules(); this.restoreOrigRules();
} }

View File

@ -7,10 +7,19 @@ import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { MdRenderer } from './md-renderer'; import { MdRenderer } from './md-renderer';
import { SwaggerOperation, SwaggerParameter } from './swagger-typings';
function getDiscriminator(obj) { function getDiscriminator(obj) {
return obj.discriminator || obj['x-extendedDiscriminator']; return obj.discriminator || obj['x-extendedDiscriminator'];
} }
export interface DescendantInfo {
$ref: string;
name: string;
active?: boolean;
idx?: number;
}
export class SpecManager { export class SpecManager {
public _schema: any = {}; public _schema: any = {};
public apiUrl: string; public apiUrl: string;
@ -75,8 +84,7 @@ export class SpecManager {
mdRender.addPreprocessor(SecurityDefinitions.insertTagIntoDescription); mdRender.addPreprocessor(SecurityDefinitions.insertTagIntoDescription);
} }
this._schema.info['x-redoc-html-description'] = mdRender.renderMd(this._schema.info.description); this._schema.info['x-redoc-html-description'] = mdRender.renderMd(this._schema.info.description);
this._schema.info['x-redoc-markdown-headers'] = mdRender.firstLevelHeadings; this._schema.info['x-redoc-markdown-headers'] = mdRender.headings;
this._schema.info['x-redoc-markdown-subheaders'] = mdRender.secondLevelHeadings;
} }
get schema() { get schema() {
@ -114,9 +122,9 @@ export class SpecManager {
return obj; return obj;
} }
getMethodParams(methodPtr, resolveRefs) { getMethodParams(methodPtr:string):SwaggerParameter[] {
/* inject JsonPointer into array elements */ /* inject JsonPointer into array elements */
function injectPointers(array, root) { function injectPointers(array:SwaggerParameter[], root) {
if (!Array.isArray(array)) { if (!Array.isArray(array)) {
throw new Error(`parameters must be an array. Got ${typeof array} at ${root}`); throw new Error(`parameters must be an array. Got ${typeof array} at ${root}`);
} }
@ -133,17 +141,16 @@ export class SpecManager {
//get path params //get path params
let pathParamsPtr = JsonPointer.join(JsonPointer.dirName(methodPtr), ['parameters']); let pathParamsPtr = JsonPointer.join(JsonPointer.dirName(methodPtr), ['parameters']);
let pathParams = this.byPointer(pathParamsPtr) || []; let pathParams:SwaggerParameter[] = this.byPointer(pathParamsPtr) || [];
let methodParamsPtr = JsonPointer.join(methodPtr, ['parameters']); let methodParamsPtr = JsonPointer.join(methodPtr, ['parameters']);
let methodParams = this.byPointer(methodParamsPtr) || []; let methodParams:SwaggerParameter[] = this.byPointer(methodParamsPtr) || [];
pathParams = injectPointers(pathParams, pathParamsPtr); pathParams = injectPointers(pathParams, pathParamsPtr);
methodParams = injectPointers(methodParams, methodParamsPtr); methodParams = injectPointers(methodParams, methodParamsPtr);
if (resolveRefs) { // resolve references
methodParams = this.resolveRefs(methodParams); methodParams = this.resolveRefs(methodParams);
pathParams = this.resolveRefs(pathParams); pathParams = this.resolveRefs(pathParams);
}
return methodParams.concat(pathParams); return methodParams.concat(pathParams);
} }
@ -155,21 +162,18 @@ export class SpecManager {
description: tag.description, description: tag.description,
'x-traitTag': tag['x-traitTag'] || false 'x-traitTag': tag['x-traitTag'] || false
}; };
if (tag['x-traitTag']) {
console.warn(`x-traitTag (${tag.name}) is deprecated since v1.5.0 and will be removed in the future`);
}
} }
return tagsMap; return tagsMap;
} }
findDerivedDefinitions(defPointer, schema) { findDerivedDefinitions(defPointer: string, schema): DescendantInfo[] {
let definition = schema || this.byPointer(defPointer); let definition = schema || this.byPointer(defPointer);
if (!definition) throw new Error(`Can't load schema at ${defPointer}`); if (!definition) throw new Error(`Can't load schema at ${defPointer}`);
if (!definition.discriminator && !definition['x-extendedDiscriminator']) return []; if (!definition.discriminator && !definition['x-extendedDiscriminator']) return [];
let globalDefs = this._schema.definitions || {}; let globalDefs = this._schema.definitions || {};
let res = []; let res:DescendantInfo[] = [];
let extendedDiscriminatorProp = definition['x-extendedDiscriminator']; let extendedDiscriminatorProp = definition['x-extendedDiscriminator'];
for (let defName of Object.keys(globalDefs)) { for (let defName of Object.keys(globalDefs)) {
let def = globalDefs[defName]; let def = globalDefs[defName];
@ -206,7 +210,7 @@ export class SpecManager {
return res; return res;
} }
getDescendant(descendant, componentSchema) { getDescendant(descendant:DescendantInfo, componentSchema:any) {
let res; let res;
if (!getDiscriminator(componentSchema) && componentSchema.allOf) { if (!getDiscriminator(componentSchema) && componentSchema.allOf) {
// discriminator inherited from parents // discriminator inherited from parents

View File

@ -0,0 +1,25 @@
import {
Operation,
Parameter,
Schema,
BodyParameter,
HeaderParameter,
QueryParameter,
FormDataParameter,
Spec,
Response
} from '@types/swagger-schema-official';
interface RedocInjectedPointer {
_pointer?: string;
}
export interface SwaggerOperation extends Operation, RedocInjectedPointer {};
export interface SwaggerBodyParameter extends BodyParameter, RedocInjectedPointer {}
export interface SwaggerHeaderParameter extends HeaderParameter, RedocInjectedPointer {}
export interface SwaggerQueryParameter extends QueryParameter, RedocInjectedPointer {}
export interface SwaggerFormDataParameter extends FormDataParameter, RedocInjectedPointer {}
export type SwaggerParameter = SwaggerBodyParameter | SwaggerHeaderParameter | SwaggerQueryParameter | SwaggerFormDataParameter;
export interface SwaggerSchema extends Schema, RedocInjectedPointer {};
export { Spec as SwaggerSpec, Response as SwaggerResponse };

View File

@ -6,6 +6,7 @@ declare module "scrollparent"
declare module "slugify" declare module "slugify"
declare module "url" declare module "url"
declare module "json-pointer"; declare module "json-pointer";
declare module "mark.js";
declare module "*.css" { declare module "*.css" {
const content: string; const content: string;

View File

@ -1,13 +1,13 @@
{ {
"name": "redoc", "name": "redoc",
"description": "Swagger-generated API Reference Documentation", "description": "Swagger-generated API Reference Documentation",
"version": "1.7.0", "version": "1.8.0",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/Rebilly/ReDoc" "url": "git://github.com/Rebilly/ReDoc"
}, },
"engines": { "engines": {
"node": ">=4.0.0", "node": ">=6.9",
"npm": ">=3.0.0" "npm": ">=3.0.0"
}, },
"main": "dist/redoc.min.js", "main": "dist/redoc.min.js",
@ -15,14 +15,16 @@
"test": "npm run lint && node ./build/run_tests.js", "test": "npm run lint && node ./build/run_tests.js",
"branch-release": "git reset --hard && branch-release", "branch-release": "git reset --hard && branch-release",
"lint": "tslint -e \"lib/**/*{ngfactory|css.shim}.ts\" lib/**/*.ts", "lint": "tslint -e \"lib/**/*{ngfactory|css.shim}.ts\" lib/**/*.ts",
"unit": "npm run build:sass && karma start", "unit": "karma start",
"e2e": "npm run build:prod && npm run e2e-copy && npm run webdriver && protractor", "e2e": "npm run build:prod && npm run e2e-copy && npm run webdriver && protractor",
"deploy": "node ./build/prepare_deploy.js && deploy-to-gh-pages --update demo", "deploy": "node ./build/prepare_deploy.js && deploy-to-gh-pages --update demo",
"ngc": "ngc -p .", "ngc": "ngc -p .",
"clean:dist": "npm run rimraf -- dist/",
"clean:aot": "npm run rimraf -- .tmp compiled lib/**/*.css",
"rimraf": "rimraf",
"webpack:prod": "webpack --config build/webpack.prod.js --profile --bail", "webpack:prod": "webpack --config build/webpack.prod.js --profile --bail",
"build:sass": "node-sass -q -o lib lib", "build:sass": "node-sass -q -o lib lib",
"build:prod": "npm run build:sass && npm run ngc && npm run webpack:prod", "build:prod": "npm run clean:aot && npm run build:sass && npm run webpack:prod",
"build:prod-module": "npm run build:sass && npm run ngc && npm run webpack:prod && IS_MODULE=true npm run webpack:prod",
"build-dist": "npm run build:prod", "build-dist": "npm run build:prod",
"stats": "webpack --config build/webpack.prod.js --json > stats.json", "stats": "webpack --config build/webpack.prod.js --json > stats.json",
"start": "webpack-dev-server --config build/webpack.dev.js --content-base demo", "start": "webpack-dev-server --config build/webpack.dev.js --content-base demo",
@ -45,19 +47,20 @@
"author": "Roman Hotsiy", "author": "Roman Hotsiy",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@angular/common": "^2.4.0", "@angular/common": "^2.4.5",
"@angular/compiler": "^2.4.0", "@angular/compiler": "^2.4.5",
"@angular/compiler-cli": "^2.4.0", "@angular/compiler-cli": "^2.4.5",
"@angular/core": "^2.4.0", "@angular/core": "^2.4.5",
"@angular/platform-browser": "^2.4.0", "@angular/platform-browser": "^2.4.5",
"@angular/platform-browser-dynamic": "^2.4.0", "@angular/platform-browser-dynamic": "^2.4.5",
"@angular/platform-server": "^2.4.0", "@angular/platform-server": "^2.4.5",
"@types/core-js": "^0.9.31", "@types/core-js": "^0.9.31",
"@types/jasmine": "^2.2.32", "@types/jasmine": "^2.2.32",
"@types/requirejs": "^2.1.26", "@types/requirejs": "^2.1.26",
"@types/should": "^8.1.28", "@types/should": "^8.1.28",
"@types/swagger-schema-official": "^2.0.0",
"angular2-template-loader": "^0.6.0", "angular2-template-loader": "^0.6.0",
"awesome-typescript-loader": "^2.2.4", "awesome-typescript-loader": "~3.0.0-beta.17",
"branch-release": "^1.0.3", "branch-release": "^1.0.3",
"chalk": "^1.1.3", "chalk": "^1.1.3",
"codelyzer": "^2.0.0-beta.4", "codelyzer": "^2.0.0-beta.4",
@ -66,38 +69,42 @@
"css-loader": "^0.26.0", "css-loader": "^0.26.0",
"deploy-to-gh-pages": "^1.1.2", "deploy-to-gh-pages": "^1.1.2",
"http-server": "^0.9.0", "http-server": "^0.9.0",
"istanbul-instrumenter-loader": "^0.2.0", "istanbul-instrumenter-loader": "^1.2.0",
"jasmine-core": "^2.4.1", "jasmine-core": "^2.4.1",
"jasmine-spec-reporter": "^2.4.0", "jasmine-spec-reporter": "^3.1.0",
"karma": "^1.2.0", "karma": "^1.4.1",
"karma-chrome-launcher": "^2.0.0", "karma-chrome-launcher": "^2.0.0",
"karma-coverage": "github:douglasduteil/karma-coverage#next", "karma-coverage": "^1.1.1",
"karma-coveralls": "^1.1.2", "karma-coveralls": "^1.1.2",
"karma-jasmine": "^1.0.2", "karma-jasmine": "^1.0.2",
"karma-mocha-reporter": "^2.0.0", "karma-mocha-reporter": "^2.0.0",
"karma-phantomjs-launcher": "^1.0.0", "karma-phantomjs-launcher": "^1.0.0",
"karma-phantomjs-shim": "^1.1.2", "karma-phantomjs-shim": "^1.1.2",
"karma-remap-coverage": "^0.1.4",
"karma-should": "^1.0.0", "karma-should": "^1.0.0",
"karma-sinon": "^1.0.4", "karma-sinon": "^1.0.4",
"karma-sourcemap-loader": "^0.3.7", "karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^1.8.0", "karma-webpack": "^2.0.1",
"node-sass": "^4.1.1", "ngc-webpack": "^1.2.0",
"node-sass": "^4.5.0",
"phantomjs-prebuilt": "^2.1.7", "phantomjs-prebuilt": "^2.1.7",
"protractor": "^4.0.10", "protractor": "^5.1.0",
"raw-loader": "^0.5.1", "raw-loader": "^0.5.1",
"rxjs": "^5.0.1", "rimraf": "^2.5.4",
"sass-loader": "^4.0.2", "rxjs": "^5.1.0",
"sass-loader": "^4.1.1",
"shelljs": "^0.7.0", "shelljs": "^0.7.0",
"should": "^11.1.0", "should": "^11.1.0",
"sinon": "^1.17.2", "sinon": "^1.17.2",
"source-map-loader": "^0.1.5", "source-map-loader": "^0.1.5",
"string-replace-webpack-plugin": "0.0.4", "string-replace-webpack-plugin": "0.0.5",
"style-loader": "^0.13.1", "style-loader": "^0.13.1",
"ts-helpers": "^1.1.1", "ts-helpers": "^1.1.1",
"tslint": "^4.0.2", "tslint": "^4.3.1",
"typescript": "2.0.9", "typescript": "^2.1.5",
"webpack": "^2.1.0-beta.28", "webpack": "^2.2.1",
"webpack-dev-server": "^2.1.0-beta.12", "webpack-dev-server": "^2.2.0-rc.0",
"webpack-merge": "^2.6.1",
"zone.js": "^0.7.2" "zone.js": "^0.7.2"
}, },
"dependencies": { "dependencies": {
@ -105,20 +112,22 @@
"hint.css": "^2.3.2", "hint.css": "^2.3.2",
"json-pointer": "^0.6.0", "json-pointer": "^0.6.0",
"json-schema-ref-parser": "^3.1.2", "json-schema-ref-parser": "^3.1.2",
"lunr": "^0.7.2",
"mark.js": "github:julmot/mark.js",
"openapi-sampler": "^0.3.3", "openapi-sampler": "^0.3.3",
"prismjs": "^1.5.1", "prismjs": "^1.5.1",
"remarkable": "^1.6.2", "remarkable": "^1.6.2",
"scrollparent": "^1.0.0", "scrollparent": "^1.0.0",
"slugify": "^1.0.2", "slugify": "^1.0.2",
"stream-http": "^2.5.0" "stream-http": "^2.6.1"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/common": "^2.4.0", "@angular/common": "^2.4.5",
"@angular/compiler": "^2.4.0", "@angular/compiler": "^2.4.5",
"@angular/core": "^2.4.0", "@angular/core": "^2.4.5",
"@angular/platform-browser": "^2.4.0", "@angular/platform-browser": "^2.4.5",
"@angular/platform-browser-dynamic": "^2.4.0", "@angular/platform-browser-dynamic": "^2.4.5",
"@angular/platform-server": "^2.4.0", "@angular/platform-server": "^2.4.5",
"core-js": "^2.4.1", "core-js": "^2.4.1",
"rxjs": "^5.0.1", "rxjs": "^5.0.1",
"zone.js": "^0.7.2" "zone.js": "^0.7.2"

View File

@ -7,7 +7,7 @@ let config = {
baseUrl: 'http://localhost:3000', baseUrl: 'http://localhost:3000',
framework: 'jasmine2', framework: 'jasmine2',
onPrepare: function() { onPrepare: function() {
var SpecReporter = require('jasmine-spec-reporter'); var SpecReporter = require('jasmine-spec-reporter').SpecReporter;
// add jasmine spec reporter // add jasmine spec reporter
jasmine.getEnv().addReporter(new SpecReporter({displaySpecDuration: true})); jasmine.getEnv().addReporter(new SpecReporter({displaySpecDuration: true}));
// load APIs.guru list // load APIs.guru list

View File

@ -99,10 +99,15 @@ function eachNth(obj, n) {
return res; return res;
} }
function getInnerHtml(locator) {
return browser.executeScript("return arguments[0].innerHTML;", $(locator));
}
module.exports = { module.exports = {
loadJson: loadJson, loadJson: loadJson,
verifyNoBrowserErrors: verifyNoBrowserErrors, verifyNoBrowserErrors: verifyNoBrowserErrors,
scrollToEl: scrollToEl, scrollToEl: scrollToEl,
fixFFTest: fixFFTest, fixFFTest: fixFFTest,
eachNth: eachNth eachNth: eachNth,
getInnerHtml: getInnerHtml
} }

View File

@ -3,6 +3,7 @@ const verifyNoBrowserErrors = require('./helpers').verifyNoBrowserErrors;
const scrollToEl = require('./helpers').scrollToEl; const scrollToEl = require('./helpers').scrollToEl;
const fixFFTest = require('./helpers').fixFFTest; const fixFFTest = require('./helpers').fixFFTest;
const eachNth = require('./helpers').eachNth; const eachNth = require('./helpers').eachNth;
const getInnerHtml = require('./helpers').getInnerHtml;
const URL = 'index.html'; const URL = 'index.html';
@ -56,17 +57,17 @@ describe('Scroll sync', () => {
it('should update active menu entries on page scroll forwards', () => { it('should update active menu entries on page scroll forwards', () => {
scrollToEl('[section="tag/store"]').then(() => { scrollToEl('[section="tag/store"]').then(() => {
expect($('.menu-item.active > .menu-item-header').getInnerHtml()).toContain('store'); expect(getInnerHtml('.menu-item.menu-item-depth-1.active > .menu-item-header')).toContain('store');
expect($('.selected-tag').getInnerHtml()).toContain('store'); expect(getInnerHtml('.selected-tag')).toContain('store');
}); });
}); });
it('should update active menu entries on page scroll backwards', () => { it('should update active menu entries on page scroll backwards', () => {
scrollToEl('[operation-id="getPetById"]').then(() => { scrollToEl('[operation-id="getPetById"]').then(() => {
expect($('.menu-item.menu-item-depth-1.active .menu-item-header').getInnerHtml()).toContain('pet'); expect(getInnerHtml('.menu-item.menu-item-depth-1.active .menu-item-header')).toContain('pet');
expect($('.selected-tag').getInnerHtml()).toContain('pet'); expect(getInnerHtml('.selected-tag')).toContain('pet');
expect($('.menu-item.menu-item-depth-2.active .menu-item-header').getInnerHtml()).toContain('Find pet by ID'); expect(getInnerHtml('.menu-item.menu-item-depth-2.active .menu-item-header')).toContain('Find pet by ID');
expect($('.selected-endpoint').getInnerHtml()).toContain('Find pet by ID'); expect(getInnerHtml('.selected-endpoint')).toContain('Find pet by ID');
}); });
}); });
}); });

View File

@ -48,6 +48,8 @@ beforeEach(function() {
services.OptionsService, services.OptionsService,
services.ComponentParser, services.ComponentParser,
services.ContentProjector, services.ContentProjector,
services.Marker,
services.SearchService,
{ provide: sharedComponents.LazyTasksService, useClass: sharedComponents.LazyTasksServiceSync }, { provide: sharedComponents.LazyTasksService, useClass: sharedComponents.LazyTasksServiceSync },
{ provide: ErrorHandler, useClass: services.CustomErrorHandler }, { provide: ErrorHandler, useClass: services.CustomErrorHandler },
{ provide: services.COMPONENT_PARSER_ALLOWED, useValue: { 'security-definitions': components.SecurityDefinitions }} { provide: services.COMPONENT_PARSER_ALLOWED, useValue: { 'security-definitions': components.SecurityDefinitions }}
@ -61,15 +63,6 @@ beforeEach(function() {
}); });
}); });
// afterEach(function() {
// TestBed.resetTestingModule();
// });
// afterEach(function() {
// TestBed.resetTestEnvironment();
// })
var testContext = require.context('..', true, /\.spec\.ts/); var testContext = require.context('..', true, /\.spec\.ts/);
/* /*

View File

@ -15,7 +15,7 @@ describe('Utils', () => {
it('load should reject promise for invalid url', (done)=> { it('load should reject promise for invalid url', (done)=> {
specMgr.load('/nonexisting/schema.json').then(() => { specMgr.load('/nonexisting/schema.json').then(() => {
throw new Error('Succees handler should not be called'); throw new Error('Success handler should not be called');
}, () => { }, () => {
done(); done();
}); });
@ -122,13 +122,13 @@ describe('Utils', () => {
}); });
it('should accept pointer directly to parameters', () => { it('should accept pointer directly to parameters', () => {
let params = specMgr.getMethodParams('/paths/test1/get/parameters', true); let params = specMgr.getMethodParams('/paths/test1/get/parameters');
expect(params).not.toBeNull(); expect(params).not.toBeNull();
params.length.should.be.equal(2); params.length.should.be.equal(2);
}); });
it('should resolve path params from Parameters Definitions Object', () => { it('should resolve path params from Parameters Definitions Object', () => {
let params = specMgr.getMethodParams('/paths/test2/get', true); let params = specMgr.getMethodParams('/paths/test2/get');
params.length.should.be.equal(2); params.length.should.be.equal(2);
params[0].name.should.be.equal('methodParam'); params[0].name.should.be.equal('methodParam');
params[1].name.should.be.equal('extParam'); params[1].name.should.be.equal('extParam');
@ -136,14 +136,14 @@ describe('Utils', () => {
}); });
it('should resolve method params from Parameters Definitions Object', () => { it('should resolve method params from Parameters Definitions Object', () => {
let params = specMgr.getMethodParams('/paths/test3/get', true); let params = specMgr.getMethodParams('/paths/test3/get');
params.length.should.be.equal(1); params.length.should.be.equal(1);
params[0].name.should.be.equal('extParam'); params[0].name.should.be.equal('extParam');
params[0]._pointer.should.be.equal('#/parameters/extparam'); params[0]._pointer.should.be.equal('#/parameters/extparam');
}); });
it('should throw for parameters other than array', () => { it('should throw for parameters other than array', () => {
let func = () => specMgr.getMethodParams('/paths/test4/get', true); let func = () => specMgr.getMethodParams('/paths/test4/get');
expect(func).toThrow(); expect(func).toThrow();
}); });
}); });

View File

@ -6,7 +6,6 @@
"target": "es5", "target": "es5",
"noImplicitAny": false, "noImplicitAny": false,
"sourceMap": true, "sourceMap": true,
"outDir": ".tmp",
"pretty": true, "pretty": true,
"moduleResolution": "node", "moduleResolution": "node",
"types": [ "types": [
@ -20,7 +19,6 @@
"compileOnSave": false, "compileOnSave": false,
"exclude": [ "exclude": [
"node_modules", "node_modules",
".tmp",
"dist" "dist"
], ],
"awesomeTypescriptLoaderOptions": { "awesomeTypescriptLoaderOptions": {

41
tsconfig.webpack.json Normal file
View File

@ -0,0 +1,41 @@
{
"compilerOptions": {
"target": "es5",
"module": "es2015",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"noEmit": true,
"noEmitHelpers": true,
"strictNullChecks": false,
"baseUrl": "./src",
"paths": {
},
"lib": [
"es2015",
"dom"
],
"types": [
"node"
]
},
"exclude": [
"node_modules",
"dist",
"**/*.spec.ts",
"**/*.e2e.ts"
],
"awesomeTypescriptLoaderOptions": {
"forkChecker": true,
"useWebpackText": true
},
"angularCompilerOptions": {
"genDir": "./compiled",
"skipMetadataEmit": true
},
"compileOnSave": false,
"buildOnSave": false,
"atom": { "rewriteTsconfig": false }
}

5402
yarn.lock Normal file

File diff suppressed because it is too large Load Diff