Merge commit 'cf6b03c2ae1ff7cb8e2a81b215daae20db6da32a' into releases

This commit is contained in:
RedocBot 2016-10-31 19:42:43 +00:00 committed by travis@localhost
commit d8224f95a4
69 changed files with 911 additions and 640 deletions

View File

@ -94,6 +94,9 @@ For npm:
## Configuration ## Configuration
### Security Definition location
You can inject Security Definitions widget into any place of your specification `description`. Check out details [here](docs/security-definitions-injection.md).
### Swagger vendor extensions ### Swagger vendor extensions
ReDoc makes use of the following [vendor extensions](http://swagger.io/specification/#vendorExtensions): ReDoc makes use of the following [vendor extensions](http://swagger.io/specification/#vendorExtensions):
* [`x-logo`](docs/redoc-vendor-extensions.md#x-logo) - is used to specify API logo * [`x-logo`](docs/redoc-vendor-extensions.md#x-logo) - is used to specify API logo
@ -132,11 +135,7 @@ Redoc.init('http://petstore.swagger.io/v2/swagger.json', {
`cd ReDoc` `cd ReDoc`
- Install dependencies - Install dependencies
`npm install` `npm install`
- *(Temporary step, will be obsolete after fixing #97)* Compile CSS - _(optional)_ Replace `demo/swagger.yaml` with your own schema
```bash
npm run build:sass
```
- _(optional)_ Replace `demo/swagger.json` with your own schema
- Start the server - Start the server
`npm start` `npm start`
- Open `http://localhost:9000` - Open `http://localhost:9000`

View File

@ -1,5 +1,6 @@
const webpack = require('webpack'); const webpack = require('webpack');
const ForkCheckerPlugin = require('awesome-typescript-loader').ForkCheckerPlugin; const ForkCheckerPlugin = require('awesome-typescript-loader').ForkCheckerPlugin;
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);
@ -62,6 +63,28 @@ module.exports = {
exclude: [ exclude: [
/node_modules/ /node_modules/
] ]
}, {
enforce: 'pre',
test: /\.ts$/,
exclude: [
/node_modules/
],
loader: StringReplacePlugin.replace({
replacements: [
{
pattern: /styleUrls:\s*\[\s*'([\w\.\/-]*)\.css'\s*\][\s,]*$/m,
replacement: function (match, p1, offset, string) {
return `styleUrls: ['${p1}.scss'],`;
}
},
{
pattern: /(\.\/components\/Redoc\/redoc-initial-styles\.css)/m,
replacement: function (match, p1, offset, string) {
return p1.replace('.css', '.scss');
}
}
]
})
}, { }, {
test: /\.ts$/, test: /\.ts$/,
loaders: [ loaders: [
@ -70,13 +93,16 @@ module.exports = {
], ],
exclude: [/\.(spec|e2e)\.ts$/] exclude: [/\.(spec|e2e)\.ts$/]
}, { }, {
test: /lib[\\\/].*\.css$/, test: /lib[\\\/].*\.scss$/,
loaders: ['raw-loader'], loaders: ['raw-loader', "sass"],
exclude: [/redoc-initial-styles\.css$/] exclude: [/redoc-initial-styles\.scss$/]
}, {
test: /\.scss$/,
loaders: ['style', 'css?-import', "sass"],
exclude: [/lib[\\\/](?!.*redoc-initial-styles).*\.scss$/]
}, { }, {
test: /\.css$/, test: /\.css$/,
loaders: ['style', 'css?-import'], loaders: ['style', 'css?-import'],
exclude: [/lib[\\\/](?!.*redoc-initial-styles).*\.css$/]
}, { }, {
test: /\.html$/, test: /\.html$/,
loader: 'raw-loader' loader: 'raw-loader'
@ -97,6 +123,8 @@ module.exports = {
'AOT': IS_PRODUCTION 'AOT': IS_PRODUCTION
}), }),
new ForkCheckerPlugin() new ForkCheckerPlugin(),
new StringReplacePlugin()
], ],
} }

View File

@ -12,18 +12,6 @@ const BANNER =
const IS_MODULE = process.env.IS_MODULE != null; const IS_MODULE = process.env.IS_MODULE != null;
const TS_RULE = {
test: /\.ts$/,
loader: 'awesome-typescript-loader',
exclude: /(node_modules)/,
};
//
// if (IS_MODULE) {
// TS_RULE.query = {
// noEmitHelpers: false
// }
// }
const config = { const config = {
context: root(), context: root(),
devtool: 'source-map', devtool: 'source-map',
@ -70,7 +58,14 @@ const config = {
exclude: [ exclude: [
/node_modules/ /node_modules/
] ]
}, TS_RULE, { }, {
test: /node_modules\/.*\.ngfactory\.ts$/,
loader: 'awesome-typescript-loader'
}, {
test: /\.ts$/,
loader: 'awesome-typescript-loader',
exclude: /(node_modules)/,
}, {
test: /lib[\\\/].*\.css$/, test: /lib[\\\/].*\.css$/,
loaders: ['raw-loader'], loaders: ['raw-loader'],
exclude: [/redoc-initial-styles\.css$/] exclude: [/redoc-initial-styles\.css$/]

View File

@ -26,7 +26,6 @@ info:
This API features Cross-Origin Resource Sharing (CORS) implemented in compliance with [W3C spec](https://www.w3.org/TR/cors/). This API features Cross-Origin Resource Sharing (CORS) implemented in compliance with [W3C spec](https://www.w3.org/TR/cors/).
And that allows cross-domain communication from the browser. And that allows cross-domain communication from the browser.
All responses have a wildcard same-origin which makes them completely public and accessible to everyone, including any code on any site. All responses have a wildcard same-origin which makes them completely public and accessible to everyone, including any code on any site.
version: 1.0.0 version: 1.0.0
title: Swagger Petstore title: Swagger Petstore
termsOfService: 'http://swagger.io/terms/' termsOfService: 'http://swagger.io/terms/'

View File

@ -49,7 +49,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 #### x-traitTag [DEPRECATED]
| 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

@ -0,0 +1,19 @@
# Injection security definitions
You can inject Security Definitions widget into any place of your specification `description`:
```markdown
...
# Authorization
Some description
<!-- ReDoc-Inject: <security-definitions> -->
...
```
Inject instruction is wrapped into HTML comment so it is **visible only in ReDoc**. It won't be visible e.g. in SwaggerUI.
# Default behavior
If injection tag is not found in the description it will be appended to the end
of description under `Authentication` header.
If `Authentication` header is already present in the description, Security Definitions won't be inserted and rendered at all.

13
lib/app.module.ts Normal file
View File

@ -0,0 +1,13 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RedocModule } from './redoc.module';
import { Redoc } from './components/index';
@NgModule({
imports: [ BrowserModule, RedocModule ],
bootstrap: [ Redoc ],
exports: [ Redoc ]
})
export class AppModule {
}

View File

@ -1,6 +1,6 @@
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { RedocModule } from './redoc.module'; import { AppModule } from './app.module';
export function bootstrapRedoc() { export function bootstrapRedoc() {
return platformBrowserDynamic().bootstrapModule(RedocModule); return platformBrowserDynamic().bootstrapModule(AppModule);
} }

View File

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

View File

@ -17,5 +17,5 @@
<span *ngIf="!info.license.url"> {{info.license.name}} </span> <span *ngIf="!info.license.url"> {{info.license.name}} </span>
</span> </span>
</p> </p>
<p *ngIf="info.description" class="redoc-markdown-block" [innerHtml]="info['x-redoc-html-description'] | safe"> </p> <dynamic-ng2-viewer [html]="info['x-redoc-html-description']"></dynamic-ng2-viewer>
</div> </div>

View File

@ -11,7 +11,7 @@ import {
} from '@angular/core/testing'; } from '@angular/core/testing';
import { ApiInfo } from './api-info'; import { ApiInfo } from './api-info';
import { SpecManager } from '../../utils/SpecManager'; import { SpecManager } from '../../utils/spec-manager';
describe('Redoc components', () => { describe('Redoc components', () => {
describe('ApiInfo Component', () => { describe('ApiInfo Component', () => {

View File

@ -12,7 +12,7 @@ import { OptionsService, MenuService } 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, private menuServ: MenuService) { constructor(specMgr: SpecManager, private optionsService: OptionsService) {
super(specMgr); super(specMgr);
} }

View File

@ -10,7 +10,7 @@ import {
} from '@angular/core/testing'; } from '@angular/core/testing';
import { ApiLogo } from './api-logo'; import { ApiLogo } from './api-logo';
import { SpecManager } from '../../utils/SpecManager'; import { SpecManager } from '../../utils/spec-manager';
describe('Redoc components', () => { describe('Redoc components', () => {

View File

@ -5,7 +5,7 @@ import { Component, ElementRef, ViewContainerRef, OnDestroy, Input,
import { JsonSchema } from './json-schema'; import { JsonSchema } from './json-schema';
import { OptionsService } from '../../services/options.service'; import { OptionsService } from '../../services/options.service';
import { SpecManager } from '../../utils/SpecManager'; import { SpecManager } from '../../utils/spec-manager';
var cache = {}; var cache = {};
@ -38,7 +38,7 @@ export class JsonSchemaLazy implements OnDestroy, AfterViewInit {
var componentFactory = this.resolver.resolveComponentFactory(JsonSchema); var componentFactory = this.resolver.resolveComponentFactory(JsonSchema);
let contextInjector = this.location.parentInjector; let contextInjector = this.location.parentInjector;
let compRef = this.location.createComponent(componentFactory, null, contextInjector, null); let compRef = this.location.createComponent(componentFactory, null, contextInjector, null);
this.initComponent(compRef.instance); this.projectComponentInputs(compRef.instance);
this._renderer.setElementAttribute(compRef.location.nativeElement, 'class', this.location.element.nativeElement.className); this._renderer.setElementAttribute(compRef.location.nativeElement, 'class', this.location.element.nativeElement.className);
compRef.changeDetectorRef.detectChanges(); compRef.changeDetectorRef.detectChanges();
this.loaded = true; this.loaded = true;
@ -58,7 +58,6 @@ export class JsonSchemaLazy implements OnDestroy, AfterViewInit {
this.pointer = this.normalizePointer(); this.pointer = this.normalizePointer();
if (cache[this.pointer]) { if (cache[this.pointer]) {
let compRef = cache[this.pointer]; let compRef = cache[this.pointer];
setTimeout( ()=> {
let $element = compRef.location.nativeElement; let $element = compRef.location.nativeElement;
// skip caching view with descendant schemas // skip caching view with descendant schemas
@ -69,13 +68,12 @@ export class JsonSchemaLazy implements OnDestroy, AfterViewInit {
} }
insertAfter($element.cloneNode(true), this.elementRef.nativeElement); insertAfter($element.cloneNode(true), this.elementRef.nativeElement);
this.loaded = true; this.loaded = true;
});
} else { } else {
cache[this.pointer] = this._loadAfterSelf(); cache[this.pointer] = this._loadAfterSelf();
} }
} }
initComponent(instance:JsonSchema) { projectComponentInputs(instance:JsonSchema) {
Object.assign(instance, this); Object.assign(instance, this);
} }

View File

@ -10,7 +10,7 @@ import { getChildDebugElement } from '../../../tests/helpers';
import { JsonSchema } from './json-schema'; import { JsonSchema } from './json-schema';
import { SpecManager } from '../../utils/SpecManager';; import { SpecManager } from '../../utils/spec-manager';;
describe('Redoc components', () => { describe('Redoc components', () => {
beforeEach(() => { beforeEach(() => {

View File

@ -10,7 +10,7 @@ import {
import { getChildDebugElement } from '../../../tests/helpers'; import { getChildDebugElement } from '../../../tests/helpers';
import { Method } from './method'; import { Method } from './method';
import { SpecManager } from '../../utils/SpecManager';; import { SpecManager } from '../../utils/spec-manager';;
describe('Redoc components', () => { describe('Redoc components', () => {
beforeEach(() => { beforeEach(() => {

View File

@ -11,7 +11,7 @@ import { getChildDebugElement } from '../../../tests/helpers';
import { MethodsList } from './methods-list'; import { MethodsList } from './methods-list';
import { SpecManager } from '../../utils/SpecManager'; import { SpecManager } from '../../utils/spec-manager';
describe('Redoc components', () => { describe('Redoc components', () => {
beforeEach(() => { beforeEach(() => {

View File

@ -8,8 +8,11 @@ redoc.loading {
} }
@keyframes rotate { @keyframes rotate {
0% {transform: rotate(0deg)} 0% {
100% {transform: rotate(360deg)} transform: rotate(0deg); }
100% {
transform: rotate(360deg);
}
} }
redoc.loading:before { redoc.loading:before {
@ -51,20 +54,3 @@ redoc.loading:after {
redoc.loading-remove:before, redoc.loading-remove:after { redoc.loading-remove:before, redoc.loading-remove:after {
opacity: 0; opacity: 0;
} }
.redoc-error {
padding: 20px;
text-align: center;
color: #cc0000;
> h2 {
color: #cc0000;
font-size: 40px;
}
}
.redoc-error-details {
max-width: 750px;
margin: 0 auto;
font-size: 18px;
}

View File

@ -1,4 +1,8 @@
<div class="redoc-wrap" *ngIf="specLoaded"> <div class="redoc-error" *ngIf="error">
<h1>Oops... ReDoc failed to render this spec</h1>
<div class='redoc-error-details'>{{error.message}}</div>
</div>
<div class="redoc-wrap" *ngIf="specLoaded && !error">
<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> <api-logo> </api-logo>
<side-menu> </side-menu> <side-menu> </side-menu>

View File

@ -256,3 +256,19 @@ footer {
font-weight: bold; font-weight: bold;
} }
} }
.redoc-error {
padding: 20px;
text-align: center;
color: #cc0000;
> h2 {
color: #cc0000;
font-size: 40px;
}
}
.redoc-error-details {
max-width: 750px;
margin: 0 auto;
font-size: 18px;
}

View File

@ -11,7 +11,7 @@ import {
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { Redoc } from './redoc'; import { Redoc } from './redoc';
import { SpecManager } from '../../utils/SpecManager'; import { SpecManager } from '../../utils/spec-manager';
import { OptionsService } from '../../services/index'; import { OptionsService } from '../../services/index';
let optsMgr:OptionsService; let optsMgr:OptionsService;
@ -46,123 +46,6 @@ describe('Redoc components', () => {
fixture.destroy(); fixture.destroy();
}); });
}); });
// describe('Redoc init', () => {
// let dom = new BrowserDomAdapter();
// let elem;
// beforeEach(() => {
// elem = dom.createElement('redoc');
// dom.defaultDoc().body.appendChild(elem);
// });
//
// afterEach(() => {
// dom.defaultDoc().body.removeChild(elem);
// });
//
// it('should return promise', () => {
// let res = Redoc.init().catch(() => {/**/});
// res.should.be.instanceof(Promise);
// });
//
// it('should hide loading animation and display message in case of error', async(() => {
// spyOn(Redoc, 'hideLoadingAnimation').and.callThrough();
// spyOn(Redoc, 'displayError').and.callThrough();
// let res = Redoc.init();
// return res.catch(() => {
// expect(Redoc.hideLoadingAnimation).toHaveBeenCalled();
// expect(Redoc.displayError).toHaveBeenCalled();
// });
// }));
//
// //skip because of PhantomJS crashes on this testcase
// xit('should init redoc', (done) => {
// var node = document.createElement('redoc');
// document.body.appendChild(node);
// let res = Redoc.init('/tests/schemas/extended-petstore.yml');
// res.then(() => { done(); }, () => {
// done.fail('Error handler should not been called');
// });
// });
// });
//
// describe('Redoc destroy', () => {
// let builder;
// let fixture;
// let element;
// let dom;
// let destroySpy;
//
// beforeEach(async(inject([SpecManager, OptionsService, BrowserDomAdapter],
// ( specMgr, opts, _dom) => {
//
// optsMgr = opts;
// dom = _dom;
// return specMgr.load('/tests/schemas/extended-petstore.yml');
// })));
//
// beforeEach(() => {
// fixture = TestBed.createComponent(TestAppComponent);
// element = getChildDebugElement(fixture.debugElement, 'methods-list').nativeElement;
// destroySpy = jasmine.createSpy('spy');
// Redoc.appRef = <ComponentRef<any>>{
// destroy: destroySpy
// };
// fixture.detectChanges();
// });
//
// afterEach(()=> {
// fixture.destroy();
// Redoc.appRef = null;
// });
//
// it('should call componentRef.destroy', () => {
// Redoc.destroy();
// expect(destroySpy).toHaveBeenCalled();
// });
//
// it('should create new host element', () => {
// element.parentElement.removeChild(element);
// Redoc.destroy();
// expect(dom.query('redoc')).not.toBeNull();
// dom.query('redoc').should.not.be.equal(element);
// });
//
// it('should set to null appRef', () => {
// Redoc.destroy();
// expect(Redoc.appRef).toBeNull();
// });
// });
//
// describe('Redoc autoInit', () => {
// const testURL = 'testurl';
// let dom = new BrowserDomAdapter();
// //let redocInitSpy;
// let elem: HTMLElement;
//
// beforeEach(() => {
// spyOn(Redoc, 'init').and.stub();
// elem = dom.createElement('redoc');
// dom.defaultDoc().body.appendChild(elem);
// dom.setAttribute(elem, 'spec-url', testURL);
// });
//
// it('should call Redoc.init with url from param spec-url', () => {
// Redoc.autoInit();
// expect(Redoc.init).toHaveBeenCalled();
// expect((<jasmine.Spy>Redoc.init).calls.argsFor(0)).toEqual([testURL]);
// });
//
// it('should not call Redoc.init when spec-url param is not provided', () => {
// dom.removeAttribute(elem, 'spec-url');
// Redoc.autoInit();
// expect(Redoc.init).not.toHaveBeenCalled();
// });
//
// afterEach(() => {
// (<jasmine.Spy>Redoc.init).and.callThrough();
// dom.defaultDoc().body.removeChild(elem);
// });
// });
}); });
/** Test component that contains a Redoc. */ /** Test component that contains a Redoc. */

View File

@ -1,15 +1,23 @@
'use strict'; 'use strict';
import { ElementRef, ComponentRef, ChangeDetectorRef, Input, import { ElementRef,
Component, OnInit, ChangeDetectionStrategy} from '@angular/core'; ComponentRef,
ChangeDetectorRef,
Input,
Component,
OnInit,
ChangeDetectionStrategy,
HostBinding
} from '@angular/core';
import { BrowserDomAdapter as DOM } from '../../utils/browser-adapter'; import { BrowserDomAdapter as DOM } from '../../utils/browser-adapter';
import { BaseComponent } from '../base'; import { BaseComponent } from '../base';
import * as detectScollParent from 'scrollparent'; import * as detectScollParent from 'scrollparent';
import { SpecManager } from '../../utils/SpecManager'; import { SpecManager } from '../../utils/spec-manager';
import { OptionsService, RedocEventsService } from '../../services/index'; import { OptionsService, Hash, MenuService, AppStateService } from '../../services/index';
import { CustomErrorHandler } from '../../utils/';
@Component({ @Component({
selector: 'redoc', selector: 'redoc',
@ -18,45 +26,29 @@ import { OptionsService, RedocEventsService } from '../../services/index';
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class Redoc extends BaseComponent implements OnInit { export class Redoc extends BaseComponent implements OnInit {
static appRef: ComponentRef<any>;
static _preOptions: any; static _preOptions: any;
specLoaded: boolean;
public options: any;
private element: any; private element: any;
error: any;
specLoaded: boolean;
options: any;
@Input() specUrl: string; @Input() specUrl: string;
@HostBinding('class.loading') specLoading: boolean = false;
@HostBinding('class.loading-remove') specLoadingRemove: boolean = false;
// TODO: refactor in separate component/service constructor(
showLoadingAnimation() { specMgr: SpecManager,
DOM.addClass(this.element, 'loading'); optionsMgr: OptionsService,
} elementRef: ElementRef,
private changeDetector: ChangeDetectorRef,
hideLoadingAnimation() { private appState: AppStateService
DOM.addClass(this.element, 'loading-remove'); ) {
setTimeout(() => {
DOM.removeClass(this.element, 'loading-remove');
DOM.removeClass(this.element, 'loading');
}, 400);
}
static displayError(err, elem?) {
let redocEl = elem || DOM.query('redoc');
if (!redocEl) return;
let heading = 'Oops... ReDoc failed to render this spec';
let details = err.message;
let erroHtml = `<div class="redoc-error">
<h1>${heading}</h1>
<div class='redoc-error-details'>${details}</div>`;
redocEl.innerHTML = erroHtml;
}
constructor(specMgr: SpecManager, optionsMgr:OptionsService, elementRef:ElementRef,
public events:RedocEventsService, private changeDetector: ChangeDetectorRef) {
super(specMgr); super(specMgr);
// merge options passed before init // merge options passed before init
optionsMgr.options = Redoc._preOptions || {}; optionsMgr.options = Redoc._preOptions || {};
this.element = elementRef.nativeElement; this.element = elementRef.nativeElement;
//parse options (top level component doesn't support inputs) //parse options (top level component doesn't support inputs)
optionsMgr.parseOptions( this.element ); optionsMgr.parseOptions( this.element );
@ -66,22 +58,46 @@ export class Redoc extends BaseComponent implements OnInit {
this.options = optionsMgr.options; this.options = optionsMgr.options;
} }
hideLoadingAnimation() {
this.specLoadingRemove = true;
setTimeout(() => {
this.specLoadingRemove = true;
this.specLoading = false;
}, 400);
}
load() { load() {
this.showLoadingAnimation(); this.specMgr.load(this.options.specUrl).catch(err => {
SpecManager.instance().load(this.options.specUrl).then(() => {
this.specLoaded = true;
this.changeDetector.markForCheck();
//this.changeDetector.detectChanges();
this.events.bootstrapped.next({});
this.hideLoadingAnimation();
}).catch((err) => {
this.hideLoadingAnimation();
Redoc.displayError(err, this.element);
throw err; throw err;
}) });
this.specMgr.spec.subscribe((spec) => {
if (!spec) {
this.specLoading = true;
this.specLoaded = false;
} else {
this.specLoaded = true;
this.hideLoadingAnimation();
this.changeDetector.markForCheck();
}
});
} }
ngOnInit() { ngOnInit() {
this.appState.error.subscribe(_err => {
if (!_err) return;
if (this.specLoading) {
this.specLoaded = true;
this.hideLoadingAnimation();
}
this.error = _err;
this.changeDetector.markForCheck();
setTimeout(() => {
this.changeDetector.detectChanges()
});
})
if (this.specUrl) { if (this.specUrl) {
this.options.specUrl = this.specUrl; this.options.specUrl = this.specUrl;
} }

View File

@ -1,12 +1,14 @@
'use strict'; 'use strict';
import { Component, ViewChildren, QueryList, EventEmitter, Input, import { Component, ViewChildren, QueryList, Input,
ChangeDetectionStrategy, OnInit, HostBinding } from '@angular/core'; ChangeDetectionStrategy, OnInit, HostBinding } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { BaseComponent, SpecManager } from '../base'; import { BaseComponent, SpecManager } from '../base';
import JsonPointer from '../../utils/JsonPointer'; import JsonPointer from '../../utils/JsonPointer';
import { Tabs } from '../../shared/components/index'; import { Tabs } from '../../shared/components/index';
import { RedocEventsService } from '../../services/index'; import { AppStateService } from '../../services/index';
@Component({ @Component({
selector: 'request-samples', selector: 'request-samples',
@ -21,18 +23,17 @@ export class RequestSamples extends BaseComponent implements OnInit {
@HostBinding('attr.hidden') hidden; @HostBinding('attr.hidden') hidden;
childTabs: Tabs; childTabs: Tabs;
selectedLang: EventEmitter<any>; selectedLang: Subject<any>;
samples: Array<any>; samples: Array<any>;
constructor(specMgr:SpecManager, public events:RedocEventsService) { constructor(specMgr:SpecManager, public appState:AppStateService) {
super(specMgr); super(specMgr);
this.selectedLang = this.events.samplesLanguageChanged; this.selectedLang = this.appState.samplesLanguage;
} }
changeLangNotify(lang) { changeLangNotify(lang) {
this.events.samplesLanguageChanged.next(lang); this.selectedLang.next(lang);
} }
init() { init() {

View File

@ -0,0 +1,40 @@
<div class="security-definition" *ngFor="let def of defs">
<h2 class="sharable-header" attr.section="section/Authentication/{{def.name}}">
<a class="share-link" href="#section/Authentication/{{def.name}}"></a>{{def.name}}</h2>
<div [innerHTML]="def.details.description | marked"></div>
<div class="redoc-markdown-block"> <!-- apply md styles to table -->
<table class="details">
<tr>
<th> Security scheme type: </th>
<td> {{def.details._displayType}} </td>
</tr>
<tr *ngIf="def.details.type === 'apiKey'">
<th> {{def.details.in}} parameter name:</th>
<td> {{def.details.name}} </td>
</tr>
<template [ngIf]="def.details.type === 'oauth2'">
<tr>
<th> OAuth2 Flow</th>
<td> {{def.details.flow}} </td>
</tr>
<tr *ngIf="def.details.flow === 'implicit' || def.details.flow === 'accessCode'">
<th> Authorization URL </th>
<td> {{def.details.authorizationUrl}} </td>
</tr>
<tr *ngIf="def.details.flow !== 'implicit'">
<th> Token URL </th>
<td> {{def.details.tokenUrl}} </td>
</tr>
</template>
</table>
<template [ngIf]="def.details.type === 'oauth2'">
<h3> OAuth2 Scopes </h3>
<table class="scopes">
<tr *ngFor="let scopeName of def.details.scopes | keys">
<th> {{scopeName}} </th>
<td> {{def.details.scopes[scopeName]}} </td>
</tr>
</table>
</template>
</div>
</div>

View File

@ -0,0 +1,36 @@
@import '../../shared/styles/variables';
.security-definition:not(:last-of-type) {
border-bottom: 1px solid rgba($text-color, .3);
padding-bottom: 20px;
}
h2, h3 {
color: $secondary-color;
}
:host h2 {
padding-top: 40px;
}
h3 {
margin: 1em 0;
font-size: 1em;
}
:host .redoc-markdown-block table {
margin-top: 20px;
}
table.details th, table.details td {
font-weight: bold;
width: 200px;
max-width: 50%;
}
table.details th {
text-align: left;
padding: 6px;
text-transform: capitalize;
font-weight: normal;
}

View File

@ -0,0 +1,50 @@
'use strict';
import { Component, ChangeDetectionStrategy, OnInit, HostListener } from '@angular/core';
import { SpecManager, BaseComponent } from '../base';
import { ComponentParser } from '../../services/component-parser.service';
const AUTH_TYPES = {
'oauth2': 'OAuth2',
'apiKey': 'API Key',
'basic': 'Basic Authorization'
}
@Component({
selector: 'security-definitions',
styleUrls: ['./security-definitions.css'],
templateUrl: './security-definitions.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SecurityDefinitions extends BaseComponent implements OnInit {
info: any = {};
specUrl: String;
defs: any[];
static insertTagIntoDescription(md:string) {
if (ComponentParser.contains(md, 'security-definitions')) return md;
if (/^#\s?Authentication\s*$/mi.test(md)) return md;
return md + '\n# Authentication \n' + ComponentParser.build('security-definitions');
}
constructor(specMgr:SpecManager) {
super(specMgr);
}
init() {
this.componentSchema = this.componentSchema.securityDefinitions;
this.defs = Object.keys(this.componentSchema).map(name => {
let details = this.componentSchema[name];
details._displayType = AUTH_TYPES[details.type];
return {
name,
details
}
});
}
ngOnInit() {
this.preinit();
}
}

View File

@ -13,7 +13,7 @@ import { TestBed } from '@angular/core/testing';
import { MethodsList, SideMenu } from '../index'; import { MethodsList, SideMenu } from '../index';
import { SpecManager } from '../../utils/SpecManager'; import { SpecManager } from '../../utils/spec-manager';
let testOptions; let testOptions;

View File

@ -103,7 +103,6 @@ export class SideMenu extends BaseComponent implements OnInit {
destroy() { destroy() {
this.scrollService.unbind(); this.scrollService.unbind();
this.hash.unbind();
} }
ngOnInit() { ngOnInit() {

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { SpecManager } from '../utils/SpecManager'; import { SpecManager } from '../utils/spec-manager';
import { BaseComponent } from '../components/base'; import { BaseComponent } from '../components/base';
describe('Redoc components', () => { describe('Redoc components', () => {

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { OnInit, OnDestroy } from '@angular/core'; import { OnInit, OnDestroy } from '@angular/core';
import { SpecManager } from '../utils/SpecManager'; import { SpecManager } from '../utils/spec-manager';
export { SpecManager }; export { SpecManager };

View File

@ -13,13 +13,14 @@ import { SideMenu } from './SideMenu/side-menu';
import { MethodsList } from './MethodsList/methods-list'; import { MethodsList } from './MethodsList/methods-list';
import { Method } from './Method/method'; import { Method } from './Method/method';
import { Warnings } from './Warnings/warnings'; import { Warnings } from './Warnings/warnings';
import { SecurityDefinitions } from './SecurityDefinitions/security-definitions';
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 ResponsesSamples, SchemaSample, SideMenu, MethodsList, Method, Warnings, Redoc, SecurityDefinitions
]; ];
export { ApiInfo, ApiLogo, JsonSchema, JsonSchemaLazy, ParamsList, RequestSamples, ResponsesList, export { ApiInfo, ApiLogo, JsonSchema, JsonSchemaLazy, ParamsList, RequestSamples, ResponsesList,
ResponsesSamples, SchemaSample, SideMenu, MethodsList, Method, Warnings, Redoc } ResponsesSamples, SchemaSample, SideMenu, MethodsList, Method, Warnings, Redoc, SecurityDefinitions }

View File

@ -3,7 +3,7 @@ import './components/Redoc/redoc-initial-styles.css';
import { enableProdMode } from '@angular/core'; import { enableProdMode } from '@angular/core';
import { Redoc } from './components/index'; import { Redoc } from './components/index';
import { SpecManager } from './utils/SpecManager'; import { SpecManager } from './utils/spec-manager';
import { BrowserDomAdapter as DOM } from './utils/browser-adapter'; import { BrowserDomAdapter as DOM } from './utils/browser-adapter';
import { disableDebugTools } from '@angular/platform-browser'; import { disableDebugTools } from '@angular/platform-browser';
@ -34,7 +34,7 @@ export function init(specUrl:string, options:any = {}) {
moduleRef = appRef; moduleRef = appRef;
console.log('ReDoc initialized!'); console.log('ReDoc initialized!');
}).catch(err => { }).catch(err => {
Redoc.displayError(err); //Redoc.displayError(err);
throw err; throw err;
}); });
}; };

View File

@ -1,26 +1,40 @@
import { NgModule } from '@angular/core'; import { NgModule, ErrorHandler } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser'; import { CommonModule } from '@angular/common';
import { Redoc, REDOC_DIRECTIVES } from './components/index'; import { Redoc, SecurityDefinitions, REDOC_DIRECTIVES } from './components/index';
import { REDOC_COMMON_DIRECTIVES } from './shared/components/index'; import { REDOC_COMMON_DIRECTIVES, DynamicNg2Wrapper } from './shared/components/index';
import { REDOC_PIPES } from './utils/pipes'; import { REDOC_PIPES, KeysPipe } from './utils/pipes';
import { CustomErrorHandler } from './utils/'
import { OptionsService, RedocEventsService, MenuService, import {
ScrollService, Hash, WarningsService } from './services/index'; OptionsService,
import { SpecManager } from './utils/SpecManager'; MenuService,
ScrollService,
Hash,
WarningsService,
AppStateService,
ComponentParser,
ContentProjector,
COMPONENT_PARSER_ALLOWED } from './services/';
import { SpecManager } from './utils/spec-manager';
@NgModule({ @NgModule({
imports: [ BrowserModule ], imports: [ CommonModule ],
declarations: [ REDOC_DIRECTIVES, REDOC_COMMON_DIRECTIVES, REDOC_PIPES ], declarations: [ REDOC_DIRECTIVES, REDOC_COMMON_DIRECTIVES, REDOC_PIPES ],
bootstrap: [ Redoc ], bootstrap: [ Redoc ],
entryComponents: [ SecurityDefinitions, DynamicNg2Wrapper ],
providers: [ providers: [
SpecManager, SpecManager,
RedocEventsService,
ScrollService, ScrollService,
Hash, Hash,
MenuService, MenuService,
WarningsService, WarningsService,
OptionsService OptionsService,
AppStateService,
ComponentParser,
ContentProjector,
{ provide: ErrorHandler, useClass: CustomErrorHandler },
{ provide: COMPONENT_PARSER_ALLOWED, useValue: { 'security-definitions': SecurityDefinitions} }
], ],
exports: [Redoc] exports: [Redoc]
}) })

View File

@ -0,0 +1,11 @@
'use strict';
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
@Injectable()
export class AppStateService {
samplesLanguage = new Subject<string>();
error = new BehaviorSubject<any>(null);
}

View File

@ -1,8 +1,9 @@
'use strict'; 'use strict';
var isSupported = document.queryCommandSupported && document.queryCommandSupported('copy');
export class Clipboard { export class Clipboard {
static isSupported():boolean { static isSupported():boolean {
return document.queryCommandSupported && document.queryCommandSupported('copy'); return isSupported;
} }
static selectElement(element:any):void { static selectElement(element:any):void {

View File

@ -0,0 +1,88 @@
'use strict';
import {
Injectable,
Renderer,
ComponentRef,
Type,
Injector,
Inject,
ComponentFactoryResolver
} from '@angular/core';
type NodesOrComponents = HTMLElement | ComponentRef<any>;
export const COMPONENT_PARSER_ALLOWED = 'COMPONENT_PARSER_ALLOWED';
const COMPONENT_REGEXP = '^\\s*<!-- ReDoc-Inject:\\s+?{component}\\s+?-->\\s*$';
@Injectable()
export class ComponentParser {
private renderer: Renderer;
private allowedComponents: any;
static contains(content: string, componentSelector: string) {
let regexp = new RegExp(COMPONENT_REGEXP.replace('{component}', `<${componentSelector}.*>`), 'mi');
return regexp.test(content);
}
static build(componentSelector) {
return `<!-- ReDoc-Inject: <${componentSelector}> -->`;
}
constructor(
private resolver: ComponentFactoryResolver,
@Inject(COMPONENT_PARSER_ALLOWED) allowedComponents
) {
this.allowedComponents = allowedComponents;
}
setRenderer(_renderer: Renderer) {
this.renderer = _renderer;
}
splitIntoNodesOrComponents(content: string, injector: Injector):NodesOrComponents[] {
let componentDefs = [];
let match;
let anyCompRegexp = new RegExp(COMPONENT_REGEXP.replace('{component}', '(.*?)'), 'gmi');
while (match = anyCompRegexp.exec(content)) {
componentDefs.push(match[1]);
}
let splitCompRegexp = new RegExp(COMPONENT_REGEXP.replace('{component}', '.*?'), 'mi');
let htmlParts = content.split(splitCompRegexp);
let res = [];
for (let i = 0; i < htmlParts.length; i++) {
let node = this.renderer.createElement(null, 'div');
this.renderer.setElementProperty(node, 'innerHTML', htmlParts[i]);
if (htmlParts[i]) res.push(node);
if (componentDefs[i]) {
let componentRef = this.createComponentByHtml(componentDefs[i], injector);
res.push(componentRef);
}
}
return res;
}
createComponentByHtml(htmlTag: string, injector:Injector):ComponentRef<any>| null {
let {componentType, options} = this._parseHtml(htmlTag);
if (!componentType) return null;
let factory = this.resolver.resolveComponentFactory(componentType);
return factory.create(injector);
}
private _parseHtml(htmlTag: string):{componentType: Type<any> | null, options: any} {
// TODO: for now only primitive parsing by tagname
let match = /<([\w_-]+).*?>/.exec(htmlTag);
if (match.length <= 1) return { componentType: null, options: null };
let componentName = match[1];
let componentType = this.allowedComponents[componentName];
// TODO parse options
let options = {};
return {
componentType,
options
};
}
}

View File

@ -0,0 +1,39 @@
'use strict';
import {
Injectable,
ComponentFactory,
ComponentRef,
ViewContainerRef
} from '@angular/core';
@Injectable()
export class ContentProjector {
instantiateAndProject<T>(componentFactory: ComponentFactory<T>,
parentView:ViewContainerRef, projectedNodesOrComponents: any[]):ComponentRef<T> {
let contextInjector = parentView.parentInjector;
let projectedNodes = [];
let componentRefs:ComponentRef<any>[] = [];
for (let i=0; i < projectedNodesOrComponents.length; i++) {
let nodeOrCompRef = projectedNodesOrComponents[i];
if (nodeOrCompRef instanceof ComponentRef) {
projectedNodes.push(nodeOrCompRef.location.nativeElement);
componentRefs.push(nodeOrCompRef);
} else {
projectedNodes.push(nodeOrCompRef);
}
}
let parentCompRef = parentView.createComponent(componentFactory, null, contextInjector, [projectedNodes]);
let appElement = (<any>parentView)._element;
appElement.nestedViews = appElement.nestedViews || [];
for (let i=0; i < componentRefs.length; i++) {
let compRef = componentRefs[i];
appElement.nestedViews.push((<any>compRef.hostView).internalView);
(<any>compRef.hostView).internalView.addToContentChildren(appElement);
}
return parentCompRef;
}
}

View File

@ -1,8 +0,0 @@
'use strict';
import { EventEmitter, Output } from '@angular/core';
export class RedocEventsService {
@Output() bootstrapped = new EventEmitter();
@Output() samplesLanguageChanged = new EventEmitter();
}

View File

@ -1,25 +1,22 @@
'use strict'; 'use strict';
import {
inject
} from '@angular/core/testing';
import { RedocEventsService } from './events.service';
import { Hash } from './hash.service'; import { Hash } from './hash.service';
import { SpecManager } from '../utils/spec-manager';
describe('Hash Service', () => { describe('Hash Service', () => {
let events = new RedocEventsService(); let specMgr = new SpecManager();
let hashService; let hashService;
beforeEach(() => { beforeEach(inject([Hash], (_hash) => hashService = _hash));
hashService = new Hash(events);
});
afterEach(() => {
hashService.unbind();
});
it('should trigger changed event after ReDoc bootstrapped', (done) => { it('should trigger changed event after ReDoc bootstrapped', (done) => {
spyOn(hashService.changed, 'next').and.callThrough(); spyOn(hashService.value, 'next').and.callThrough();
events.bootstrapped.next({}); specMgr.spec.next({});
setTimeout(() => { setTimeout(() => {
expect(hashService.changed.next).toHaveBeenCalled(); expect(hashService.value.next).toHaveBeenCalled();
done(); done();
}); });
}); });

View File

@ -1,32 +1,31 @@
'use strict'; 'use strict';
import { Injectable, EventEmitter, Output } from '@angular/core'; import { Injectable } from '@angular/core';
import { BrowserDomAdapter as DOM } from '../utils/browser-adapter'; import { PlatformLocation } from '@angular/common';
import { global } from '@angular/core/src/facade/lang';
import { RedocEventsService } from './events.service'; import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { SpecManager } from '../utils/spec-manager';
@Injectable() @Injectable()
export class Hash { export class Hash {
@Output() changed = new EventEmitter(); public value = new BehaviorSubject<string>('');
private _cancel: any; constructor(private specMgr: SpecManager, private location: PlatformLocation) {
constructor(private events:RedocEventsService) {
this.bind(); this.bind();
events.bootstrapped.subscribe(() => this.changed.next(this.hash)); this.specMgr.spec.subscribe((spec) => {
} if (!spec) return;
setTimeout(() => {
get hash() { this.value.next(this.hash);
return DOM.getLocation().hash; });
}
bind() {
this._cancel = DOM.onAndCancel(global, 'hashchange', (evt) => {
this.changed.next(this.hash);
evt.preventDefault();
}); });
} }
unbind() { get hash() {
this._cancel(); return this.location.hash;
}
bind() {
this.location.onHashChange(() => {
this.value.next(this.hash);
});
} }
} }

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
export * from './events.service'; export * from './app-state.service';
export * from './options.service'; export * from './options.service';
export * from './menu.service'; export * from './menu.service';
export * from './scroll.service'; export * from './scroll.service';
@ -8,3 +8,6 @@ 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 './component-parser.service';
export * from './content-projector.service';

View File

@ -9,7 +9,7 @@ import {
import { MenuService } from './menu.service'; import { MenuService } from './menu.service';
import { Hash } from './hash.service'; import { Hash } from './hash.service';
import { ScrollService } from './scroll.service'; import { ScrollService } from './scroll.service';
import { SpecManager } from '../utils/SpecManager';; import { SpecManager } from '../utils/spec-manager';;
describe('Menu service', () => { describe('Menu service', () => {
beforeEach(() => { beforeEach(() => {
@ -36,56 +36,59 @@ describe('Menu service', () => {
it('should run hashScroll when hash changed', (done) => { it('should run hashScroll when hash changed', (done) => {
spyOn(menu, 'hashScroll').and.callThrough(); spyOn(menu, 'hashScroll').and.callThrough();
hashService.changed.subscribe(() => { hashService.value.subscribe((hash) => {
if (!hash) return;
expect(menu.hashScroll).toHaveBeenCalled(); expect(menu.hashScroll).toHaveBeenCalled();
menu.hashScroll.and.callThrough(); menu.hashScroll.and.callThrough();
done(); done();
}); });
hashService.changed.next(); hashService.value.next('nonFalsy');
}); });
it('should scroll to method when location hash is present [jp]', (done) => { it('should scroll to method when location hash is present [jp]', (done) => {
let hash = '#tag/pet/paths/~1pet~1findByStatus/get'; let hash = '#tag/pet/paths/~1pet~1findByStatus/get';
spyOn(menu, 'hashScroll').and.callThrough(); spyOn(menu, 'hashScroll').and.callThrough();
spyOn(window, 'scrollTo').and.stub(); spyOn(window, 'scrollTo').and.stub();
hashService.changed.subscribe(() => { hashService.value.subscribe((hash) => {
if (!hash) return;
expect(menu.hashScroll).toHaveBeenCalled(); expect(menu.hashScroll).toHaveBeenCalled();
let scrollY = (<jasmine.Spy>window.scrollTo).calls.argsFor(0)[1]; let scrollY = (<jasmine.Spy>window.scrollTo).calls.argsFor(0)[1];
expect(scrollY).toBeGreaterThan(0); expect(scrollY).toBeGreaterThan(0);
(<jasmine.Spy>window.scrollTo).and.callThrough(); (<jasmine.Spy>window.scrollTo).and.callThrough();
done(); done();
}); });
hashService.changed.next(hash); hashService.value.next(hash);
}); });
it('should scroll to method when location hash is present [operation]', (done) => { it('should scroll to method when location hash is present [operation]', (done) => {
let hash = '#operation/getPetById'; let hash = '#operation/getPetById';
spyOn(menu, 'hashScroll').and.callThrough(); spyOn(menu, 'hashScroll').and.callThrough();
spyOn(window, 'scrollTo').and.stub(); spyOn(window, 'scrollTo').and.stub();
hashService.changed.subscribe(() => { hashService.value.subscribe((hash) => {
if (!hash) return;
expect(menu.hashScroll).toHaveBeenCalled(); expect(menu.hashScroll).toHaveBeenCalled();
let scrollY = (<jasmine.Spy>window.scrollTo).calls.argsFor(0)[1]; let scrollY = (<jasmine.Spy>window.scrollTo).calls.argsFor(0)[1];
expect(scrollY).toBeGreaterThan(0); expect(scrollY).toBeGreaterThan(0);
done(); done();
}); });
hashService.changed.next(hash); hashService.value.next(hash);
}); });
it('should select next/prev menu item when scrolled down/up', () => { it('should select next/prev menu item when scrolled down/up', () => {
scroll.$scrollParent = document.querySelector('#parent'); scroll.$scrollParent = document.querySelector('#parent');
menu.activeCatIdx.should.be.equal(0); menu.activeCatIdx.should.be.equal(0);
menu.activeMethodIdx.should.be.equal(-1); menu.activeMethodIdx.should.be.equal(-1);
let elTop = menu.getCurrentMethodEl().getBoundingClientRect().bottom; let nextElTop = menu.getRelativeCatOrItem(1).getBoundingClientRect().top;
scroll.$scrollParent.scrollTop = elTop + 1; scroll.$scrollParent.scrollTop = nextElTop + 1;
//simulate scroll down //simulate scroll down
spyOn(scroll, 'scrollY').and.returnValue(elTop + 2); spyOn(scroll, 'scrollY').and.returnValue(nextElTop + 10);
menu.scrollUpdate(true); menu.scrollUpdate(true);
menu.activeCatIdx.should.be.equal(1); menu.activeCatIdx.should.be.equal(1);
scroll.scrollY.and.returnValue(elTop - 2); scroll.scrollY.and.returnValue(nextElTop - 2);
scroll.$scrollParent.scrollTop = elTop - 1; scroll.$scrollParent.scrollTop = nextElTop - 1;
menu.scrollUpdate(false); menu.scrollUpdate(false);
menu.activeCatIdx.should.be.equal(0); menu.activeCatIdx.should.be.equal(0);
}); });
@ -95,6 +98,7 @@ describe('Menu service', () => {
selector: 'test-app', selector: 'test-app',
template: template:
`<div id='parent' style='height: 500px; overflow:auto'> `<div id='parent' style='height: 500px; overflow:auto'>
<api-info></api-info>
<methods-list></methods-list> <methods-list></methods-list>
</div>` </div>`
}) })

View File

@ -2,7 +2,7 @@
import { Injectable, EventEmitter } from '@angular/core'; import { Injectable, EventEmitter } from '@angular/core';
import { ScrollService, INVIEW_POSITION } from './scroll.service'; import { ScrollService, INVIEW_POSITION } from './scroll.service';
import { Hash } from './hash.service'; import { Hash } from './hash.service';
import { SpecManager } from '../utils/SpecManager'; import { SpecManager } from '../utils/spec-manager';
import { SchemaHelper, MenuCategory } from './schema-helper.service'; import { SchemaHelper, MenuCategory } from './schema-helper.service';
const CHANGE = { const CHANGE = {
@ -30,7 +30,7 @@ export class MenuService {
this.changeActive(CHANGE.INITIAL); this.changeActive(CHANGE.INITIAL);
this.hash.changed.subscribe((hash) => { this.hash.value.subscribe((hash) => {
this.hashScroll(hash); this.hashScroll(hash);
}); });
} }
@ -41,10 +41,15 @@ export class MenuService {
let $activeMethodHost = this.getCurrentMethodEl(); let $activeMethodHost = this.getCurrentMethodEl();
if (!$activeMethodHost) return; if (!$activeMethodHost) return;
var elementInViewPos = this.scrollService.getElementPos($activeMethodHost); var elementInViewPos = this.scrollService.getElementPos($activeMethodHost);
if(isScrolledDown && elementInViewPos === INVIEW_POSITION.BELLOW) { if(isScrolledDown) {
//&& elementInViewPos === INVIEW_POSITION.BELLOW
let $nextEl = this.getRelativeCatOrItem(1);
let nextInViewPos = this.scrollService.getElementPos($nextEl, true);
if (elementInViewPos === INVIEW_POSITION.BELLOW && nextInViewPos === INVIEW_POSITION.ABOVE) {
stable = this.changeActive(CHANGE.NEXT); stable = this.changeActive(CHANGE.NEXT);
continue; continue;
} }
}
if(!isScrolledDown && elementInViewPos === INVIEW_POSITION.ABOVE ) { if(!isScrolledDown && elementInViewPos === INVIEW_POSITION.ABOVE ) {
stable = this.changeActive(CHANGE.BACK); stable = this.changeActive(CHANGE.BACK);
continue; continue;
@ -53,6 +58,25 @@ export class MenuService {
} }
} }
getRelativeCatOrItem(offset: number = 0) {
let ptr, cat;
cat = this.categories[this.activeCatIdx];
if (cat.methods.length === 0) {
ptr = null;
cat = this.categories[this.activeCatIdx + Math.sign(offset)] || cat;
} else {
let cat = this.categories[this.activeCatIdx];
let idx = this.activeMethodIdx + offset;
if ((idx >= cat.methods.length - 1) || idx < 0) {
cat = this.categories[this.activeCatIdx + Math.sign(offset)] || cat;
idx = offset > 0 ? -1 : cat.methods.length - 1;
}
ptr = cat.methods[idx] && cat.methods[idx].pointer;
}
return this.getMethodElByPtr(ptr, cat.id);
}
getCurrentMethodEl() { getCurrentMethodEl() {
return this.getMethodElByPtr(this.activeMethodPtr, return this.getMethodElByPtr(this.activeMethodPtr,
this.categories[this.activeCatIdx].id); this.categories[this.activeCatIdx].id);
@ -141,11 +165,13 @@ export class MenuService {
let ptr = decodeURIComponent(hash.substr(namespace.length + 1)); let ptr = decodeURIComponent(hash.substr(namespace.length + 1));
if (namespace === 'operation') { if (namespace === 'operation') {
$el = this.getMethodElByOperId(ptr); $el = this.getMethodElByOperId(ptr);
} else { } else if (namespace === 'tag') {
let sectionId = ptr.split('/')[0]; let sectionId = ptr.split('/')[0];
ptr = ptr.substr(sectionId.length) || null; ptr = ptr.substr(sectionId.length) || null;
sectionId = namespace + (sectionId ? '/' + sectionId : ''); sectionId = namespace + (sectionId ? '/' + sectionId : '');
$el = this.getMethodElByPtr(ptr, sectionId); $el = this.getMethodElByPtr(ptr, sectionId);
} else {
$el = this.getMethodElByPtr(null, namespace + '/' + ptr);
} }
if ($el) this.scrollService.scrollTo($el); if ($el) this.scrollService.scrollTo($el);

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { isFunction, isString } from '@angular/core/src/facade/lang'; import { isFunction, isString } from '../utils/helpers';
import { BrowserDomAdapter as DOM } from '../utils/browser-adapter'; import { BrowserDomAdapter as DOM } from '../utils/browser-adapter';
const defaults = { const defaults = {

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { SchemaHelper } from './schema-helper.service'; import { SchemaHelper } from './schema-helper.service';
import { SpecManager } from '../utils/SpecManager'; import { SpecManager } from '../utils/spec-manager';
describe('Spec Helper', () => { describe('Spec Helper', () => {
describe('buildMenuTree method', () => { describe('buildMenuTree method', () => {

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { JsonPointer } from '../utils/JsonPointer'; import { JsonPointer } from '../utils/JsonPointer';
import { SpecManager } from '../utils/SpecManager'; import { SpecManager } from '../utils/spec-manager';
import { methods as swaggerMethods, keywordTypes } from '../utils/swagger-defs'; import { methods as swaggerMethods, keywordTypes } from '../utils/swagger-defs';
import { WarningsService } from './warnings.service'; import { WarningsService } from './warnings.service';
import * as slugify from 'slugify'; import * as slugify from 'slugify';
@ -14,6 +14,7 @@ export interface MenuMethod {
active: boolean; active: boolean;
summary: string; summary: string;
tag: string; tag: string;
pointer: string;
} }
export interface MenuCategory { export interface MenuCategory {

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { SchemaNormalizer } from './schema-normalizer.service'; import { SchemaNormalizer } from './schema-normalizer.service';
import { SpecManager } from '../utils/SpecManager';; import { SpecManager } from '../utils/spec-manager';;
describe('Spec Helper', () => { describe('Spec Helper', () => {
let specMgr:SpecManager = new SpecManager(); let specMgr:SpecManager = new SpecManager();

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { SpecManager } from '../utils/SpecManager'; import { SpecManager } from '../utils/spec-manager';
import { JsonPointer } from '../utils/JsonPointer'; import { JsonPointer } from '../utils/JsonPointer';
import { defaults } from '../utils/helpers'; import { defaults } from '../utils/helpers';
import { WarningsService } from './warnings.service'; import { WarningsService } from './warnings.service';

View File

@ -18,7 +18,6 @@ export class ScrollService {
private prevOffsetY: number; private prevOffsetY: number;
private _cancel:any; private _cancel:any;
constructor(optionsService:OptionsService) { constructor(optionsService:OptionsService) {
//events.bootstrapped.subscribe(() => this.hashScroll());
this.scrollYOffset = () => optionsService.options.scrollYOffset(); this.scrollYOffset = () => optionsService.options.scrollYOffset();
this.$scrollParent = optionsService.options.$scrollParent; this.$scrollParent = optionsService.options.$scrollParent;
this.scroll = new EventEmitter(); this.scroll = new EventEmitter();
@ -30,12 +29,14 @@ export class ScrollService {
} }
/* returns 1 if element if above the view, 0 if in view and -1 below the view */ /* returns 1 if element if above the view, 0 if in view and -1 below the view */
getElementPos($el) { getElementPos($el, inverted=false) {
if (Math.floor($el.getBoundingClientRect().top) > this.scrollYOffset()) { let scrollYOffset = this.scrollYOffset();
let mul = inverted ? -1 : 1;
if (mul*Math.floor($el.getBoundingClientRect().top) > mul*scrollYOffset) {
return INVIEW_POSITION.ABOVE; return INVIEW_POSITION.ABOVE;
} }
if ($el.getBoundingClientRect().bottom <= this.scrollYOffset()) { if (mul*$el.getBoundingClientRect().bottom <= mul*scrollYOffset) {
return INVIEW_POSITION.BELLOW; return INVIEW_POSITION.BELLOW;
} }
return INVIEW_POSITION.INVIEW; return INVIEW_POSITION.INVIEW;

View File

@ -0,0 +1,47 @@
'use strict';
import {
Component,
EventEmitter,
Output,
Input,
OnInit,
ViewContainerRef,
ComponentFactoryResolver,
Renderer
} from '@angular/core';
import {
ComponentParser,
ContentProjector
} from '../../../services/';
@Component({
selector: 'dynamic-ng2-viewer',
template: ''
})
export class DynamicNg2Viewer implements OnInit {
@Input() html: string;
constructor(
private view: ViewContainerRef,
private projector: ContentProjector,
private parser: ComponentParser,
private resolver: ComponentFactoryResolver,
private renderer: Renderer) {
}
ngOnInit() {
this.parser.setRenderer(this.renderer);
let nodesOrComponents = this.parser.splitIntoNodesOrComponents(this.html, this.view.injector);
let wrapperFactory = this.resolver.resolveComponentFactory(DynamicNg2Wrapper);
let ref = this.projector.instantiateAndProject(wrapperFactory, this.view, nodesOrComponents);
ref.changeDetectorRef.markForCheck();
}
}
@Component({
selector: 'dynamic-ng2-wrapper',
template: '<ng-content></ng-content>'
})
export class DynamicNg2Wrapper {}

View File

@ -5,9 +5,10 @@ import { Tabs, Tab } from './Tabs/tabs';
import { Zippy } from './Zippy/zippy'; import { Zippy } from './Zippy/zippy';
import { CopyButton } from './CopyButton/copy-button.directive'; import { CopyButton } from './CopyButton/copy-button.directive';
import { SelectOnClick } from './SelectOnClick/select-on-click.directive'; import { SelectOnClick } from './SelectOnClick/select-on-click.directive';
import { DynamicNg2Viewer, DynamicNg2Wrapper } from './DynamicNg2Viewer/dynamic-ng2-viewer.component';
export const REDOC_COMMON_DIRECTIVES = [ export const REDOC_COMMON_DIRECTIVES = [
DropDown, StickySidebar, Tabs, Tab, Zippy, CopyButton, SelectOnClick DropDown, StickySidebar, Tabs, Tab, Zippy, CopyButton, SelectOnClick, DynamicNg2Viewer, DynamicNg2Wrapper
]; ];
export { DropDown, StickySidebar, Tabs, Tab, Zippy, CopyButton, SelectOnClick } export { DropDown, StickySidebar, Tabs, Tab, Zippy, CopyButton, SelectOnClick, DynamicNg2Viewer, DynamicNg2Wrapper }

View File

@ -1,6 +1,5 @@
'use strict'; 'use strict';
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
//import { isBlank } from '@angular/core/src/facade/lang';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
function isBlank(obj) { function isBlank(obj) {

View File

@ -1,10 +0,0 @@
/*eslint no-unused-vars: 0*/
/*eslint strict: 0*/
var $buoop = { vs: {i:9, f:25, o:12.1, s:7}, c:2 };
function $buo_f(){
var e = document.createElement('script');
e.src = '//browser-update.org/update.min.js';
document.body.appendChild(e);
}
try {document.addEventListener('DOMContentLoaded', $buo_f, false);}
catch(e){window['attachEvent']('onload', $buo_f);}

View File

@ -0,0 +1,13 @@
import { ErrorHandler, Injectable } from '@angular/core';
import { AppStateService } from '../services/app-state.service';
@Injectable()
export class CustomErrorHandler extends ErrorHandler {
constructor(private appState: AppStateService) {
super(true);
}
handleError(error) {
this.appState.error.next(error && error.rejection || error);
super.handleError(error);
}
}

View File

@ -1,103 +0,0 @@
'use strict';
var Remarkable = require('remarkable');
var md = new Remarkable({
html: true,
linkify: true,
breaks: false,
typographer: false,
highlight: function (str, lang) {
if (lang === 'json')
lang = 'js';
var grammar = Prism.languages[lang];
if (!grammar)
return str;
return Prism.highlight(str, grammar);
}
});
function renderMd(rawText, headersHandler) {
var _origRule;
if (headersHandler) {
_origRule = {
open: md.renderer.rules.heading_open,
close: md.renderer.rules.heading_close
};
md.renderer.rules.heading_open = function (tokens, idx) {
if (tokens[idx].hLevel !== 1) {
return _origRule.open(tokens, idx);
}
else {
return headersHandler.open(tokens, idx);
}
};
md.renderer.rules.heading_close = function (tokens, idx) {
if (tokens[idx].hLevel !== 1) {
return _origRule.close(tokens, idx);
}
else {
return headersHandler.close(tokens, idx);
}
};
}
var res = md.render(rawText);
if (headersHandler) {
md.renderer.rules.heading_open = _origRule.open;
md.renderer.rules.heading_close = _origRule.close;
}
return res;
}
exports.renderMd = renderMd;
function statusCodeType(statusCode) {
if (statusCode < 100 || statusCode > 599) {
throw new Error('invalid HTTP code');
}
var res = 'success';
if (statusCode >= 300 && statusCode < 400) {
res = 'redirect';
}
else if (statusCode >= 400) {
res = 'error';
}
else if (statusCode < 200) {
res = 'info';
}
return res;
}
exports.statusCodeType = statusCodeType;
function defaults(target, src) {
var props = Object.keys(src);
var index = -1, length = props.length;
while (++index < length) {
var key = props[index];
if (target[key] === undefined) {
target[key] = src[key];
}
}
return target;
}
exports.defaults = defaults;
function safePush(obj, prop, val) {
if (!obj[prop])
obj[prop] = [];
obj[prop].push(val);
}
exports.safePush = safePush;
function throttle(fn, threshhold, scope) {
threshhold = threshhold || 250;
var last, deferTimer;
return function () {
var context = scope || this;
var now = +new Date, args = arguments;
if (last && now < last + threshhold) {
clearTimeout(deferTimer);
deferTimer = setTimeout(function () {
last = now;
fn.apply(context, args);
}, threshhold);
}
else {
last = now;
fn.apply(context, args);
}
};
}
exports.throttle = throttle;

View File

@ -1,58 +1,19 @@
'use strict'; 'use strict';
import * as Remarkable from 'remarkable';
declare var Prism: any;
const md = new Remarkable({ export function stringify(obj:any) {
html: true, return JSON.stringify(obj);
linkify: true,
breaks: false,
typographer: false,
highlight: (str, lang) => {
if (lang === 'json') lang = 'js';
let grammar = Prism.languages[lang];
//fallback to clike
if (!grammar) return str;
return Prism.highlight(str, grammar);
}
});
interface HeadersHandler {
open: Function;
close: Function;
} }
export function renderMd(rawText:string, headersHandler?:HeadersHandler) { export function isString(str:any) {
let _origRule; return typeof str === 'string';
if (headersHandler) {
_origRule = {
open: md.renderer.rules.heading_open,
close: md.renderer.rules.heading_close
};
md.renderer.rules.heading_open = (tokens, idx) => {
if (tokens[idx].hLevel !== 1 ) {
return _origRule.open(tokens, idx);
} else {
return headersHandler.open(tokens, idx);
}
};
md.renderer.rules.heading_close = (tokens, idx) => {
if (tokens[idx].hLevel !== 1 ) {
return _origRule.close(tokens, idx);
} else {
return headersHandler.close(tokens, idx);
}
};
} }
let res = md.render(rawText); export function isFunction(func: any) {
return typeof func === 'function';
if (headersHandler) {
md.renderer.rules.heading_open = _origRule.open;
md.renderer.rules.heading_close = _origRule.close;
} }
return res; export function isBlank(obj: any): boolean {
return obj == undefined;
} }
export function statusCodeType(statusCode) { export function statusCodeType(statusCode) {

3
lib/utils/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './custom-error-handler';
export * from './helpers';
export * from './md-renderer';

90
lib/utils/md-renderer.ts Normal file
View File

@ -0,0 +1,90 @@
'use strict';
import { Injectable } from '@angular/core';
import * as slugify from 'slugify';
import * as Remarkable from 'remarkable';
declare var Prism: any;
const md = new Remarkable({
html: true,
linkify: true,
breaks: false,
typographer: false,
highlight: (str, lang) => {
if (lang === 'json') lang = 'js';
let grammar = Prism.languages[lang];
//fallback to clike
if (!grammar) return str;
return Prism.highlight(str, grammar);
}
});
interface HeadersHandler {
open: Function;
close: Function;
}
@Injectable()
export class MdRenderer {
public firstLevelHeadings: string[] = [];
private _origRules:any = {};
private _preProcessors:Function[] = [];
constructor(private raw: boolean = false) {
}
addPreprocessor(p: Function) {
this._preProcessors.push(p);
}
saveOrigRules() {
this._origRules.open = md.renderer.rules.heading_open;
this._origRules.close = md.renderer.rules.heading_close;
}
restoreOrigRules() {
md.renderer.rules.heading_open = this._origRules.open;
md.renderer.rules.heading_close = this._origRules.close;
}
headingOpenRule(tokens, idx) {
if (tokens[idx].hLevel !== 1 ) {
return this._origRules.open(tokens, idx);
} else {
let content = tokens[idx + 1].content;
this.firstLevelHeadings.push(content);
let contentSlug = slugify(content);
return `<h${tokens[idx].hLevel} section="section/${contentSlug}">` +
`<a class="share-link" href="#section/${contentSlug}"></a>`;
}
}
headingCloseRule(tokens, idx) {
if (tokens[idx].hLevel !== 1 ) {
return this._origRules.close(tokens, idx);
} else {
return `</h${tokens[idx].hLevel}>\n`;
}
}
renderMd(rawText:string) {
if (!this.raw) {
this.saveOrigRules();
md.renderer.rules.heading_open = this.headingOpenRule.bind(this);
md.renderer.rules.heading_close = this.headingCloseRule.bind(this);
}
let text = rawText;
for (let i=0; i<this._preProcessors.length; i++) {
text = this._preProcessors[i](text);
}
let res = md.render(text);
if (!this.raw) {
this.restoreOrigRules();
}
return res;
}
}

View File

@ -2,9 +2,9 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
import { isString, stringify, isBlank } from '@angular/core/src/facade/lang'; import { isString, stringify, isBlank } from './helpers';
import JsonPointer from './JsonPointer'; import JsonPointer from './JsonPointer';
import { renderMd } from './helpers'; import { MdRenderer } from './';
import { JsonFormatter } from './JsonFormatterPipe'; import { JsonFormatter } from './JsonFormatterPipe';
declare var Prism: any; declare var Prism: any;
@ -28,23 +28,12 @@ export class KeysPipe implements PipeTransform {
transform(value) { transform(value) {
if (isBlank(value)) return value; if (isBlank(value)) return value;
if (typeof value !== 'object') { if (typeof value !== 'object') {
throw new InvalidPipeArgumentException(ValuesPipe, value); throw new InvalidPipeArgumentException(KeysPipe, value);
} }
return Object.keys(value); return Object.keys(value);
} }
} }
@Pipe({ name: 'values' })
export class ValuesPipe implements PipeTransform {
transform(value) {
if (isBlank(value)) return value;
if (typeof value !== 'object') {
throw new InvalidPipeArgumentException(ValuesPipe, value);
}
return Object.keys(value).map(key => value[key]);
}
}
@Pipe({ name: 'jsonPointerEscape' }) @Pipe({ name: 'jsonPointerEscape' })
export class JsonPointerEscapePipe implements PipeTransform { export class JsonPointerEscapePipe implements PipeTransform {
transform(value:string) { transform(value:string) {
@ -58,7 +47,10 @@ export class JsonPointerEscapePipe implements PipeTransform {
@Pipe({ name: 'marked' }) @Pipe({ name: 'marked' })
export class MarkedPipe implements PipeTransform { export class MarkedPipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) {} renderer: MdRenderer;
constructor(private sanitizer: DomSanitizer) {
this.renderer = new MdRenderer(true);
}
transform(value:string) { transform(value:string) {
if (isBlank(value)) return value; if (isBlank(value)) return value;
if (!isString(value)) { if (!isString(value)) {
@ -66,7 +58,7 @@ export class MarkedPipe implements PipeTransform {
} }
return this.sanitizer.bypassSecurityTrustHtml( return this.sanitizer.bypassSecurityTrustHtml(
`<span class="redoc-markdown-block">${renderMd(value)}</span>` `<span class="redoc-markdown-block">${this.renderer.renderMd(value)}</span>`
); );
} }
} }
@ -125,5 +117,5 @@ export class EncodeURIComponentPipe implements PipeTransform {
} }
export const REDOC_PIPES = [ export const REDOC_PIPES = [
JsonPointerEscapePipe, MarkedPipe, SafePipe, PrismPipe, EncodeURIComponentPipe, JsonFormatter JsonPointerEscapePipe, MarkedPipe, SafePipe, PrismPipe, EncodeURIComponentPipe, JsonFormatter, KeysPipe
]; ];

View File

@ -2,13 +2,16 @@
import * as JsonSchemaRefParser from 'json-schema-ref-parser'; import * as JsonSchemaRefParser from 'json-schema-ref-parser';
import { JsonPointer } from './JsonPointer'; import { JsonPointer } from './JsonPointer';
import { renderMd, safePush } from './helpers';
import * as slugify from 'slugify';
import { parse as urlParse, resolve as urlResolve } from 'url'; import { parse as urlParse, resolve as urlResolve } from 'url';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { MdRenderer } from './md-renderer';
export class SpecManager { export class SpecManager {
public _schema: any = {}; public _schema: any = {};
public apiUrl: string; public apiUrl: string;
public spec = new BehaviorSubject<any|null>(null);
private _instance: any; private _instance: any;
private _url: string; private _url: string;
@ -24,17 +27,19 @@ export class SpecManager {
SpecManager.prototype._instance = this; SpecManager.prototype._instance = this;
} }
load(url) { load(urlOrObject: string|Object) {
this.schema = null;
let promise = new Promise((resolve, reject) => { let promise = new Promise((resolve, reject) => {
this._schema = {}; JsonSchemaRefParser.bundle(urlOrObject, {http: {withCredentials: false}})
JsonSchemaRefParser.bundle(url, {http: {withCredentials: false}})
.then(schema => { .then(schema => {
try { if (typeof urlOrObject === 'string') {
this._url = url; this._url = urlOrObject;
}
this._schema = schema; this._schema = schema;
try {
this.init(); this.init();
resolve(this._schema); resolve(this._schema);
this.spec.next(this._schema);
} catch(err) { } catch(err) {
reject(err); reject(err);
} }
@ -70,24 +75,25 @@ export class SpecManager {
} }
preprocess() { preprocess() {
this._schema.info['x-redoc-html-description'] = renderMd( this._schema.info.description, { let mdRender = new MdRenderer();
open: (tokens, idx) => { if (!this._schema.info.description) this._schema.info.description = '';
let content = tokens[idx + 1].content; if (this._schema.securityDefinitions) {
safePush(this._schema.info, 'x-redoc-markdown-headers', content); let SecurityDefinitions = require('../components/').SecurityDefinitions;
content = slugify(content); mdRender.addPreprocessor(SecurityDefinitions.insertTagIntoDescription);
return `<h${tokens[idx].hLevel} section="section/${content}">` +
`<a class="share-link" href="#section/${content}"></a>`;
},
close: (tokens, idx) => {
return `</h${tokens[idx].hLevel}>`;
} }
}); this._schema.info['x-redoc-html-description'] = mdRender.renderMd(this._schema.info.description);
this._schema.info['x-redoc-markdown-headers'] = mdRender.firstLevelHeadings;
} }
get schema() { get schema() {
return this._schema; return this._schema;
} }
set schema(val:any) {
this._schema = val;
this.spec.next(this._schema);
}
byPointer(pointer) { byPointer(pointer) {
let res = null; let res = null;
try { try {
@ -148,6 +154,9 @@ 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;

View File

@ -1,7 +1,7 @@
{ {
"name": "redoc", "name": "redoc",
"description": "Swagger-generated API Reference Documentation", "description": "Swagger-generated API Reference Documentation",
"version": "1.4.1", "version": "1.5.0",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/Rebilly/ReDoc" "url": "git://github.com/Rebilly/ReDoc"
@ -45,22 +45,22 @@
"author": "Roman Hotsiy", "author": "Roman Hotsiy",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@angular/common": "^2.1.0", "@angular/common": "^2.1.2",
"@angular/compiler": "^2.1.0", "@angular/compiler": "^2.1.2",
"@angular/compiler-cli": "^2.1.0", "@angular/compiler-cli": "^2.1.2",
"@angular/core": "^2.1.0", "@angular/core": "^2.1.2",
"@angular/platform-browser": "^2.1.0", "@angular/platform-browser": "^2.1.2",
"@angular/platform-browser-dynamic": "^2.1.0", "@angular/platform-browser-dynamic": "^2.1.2",
"@angular/platform-server": "^2.1.0", "@angular/platform-server": "^2.1.2",
"@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",
"angular2-template-loader": "^0.5.0", "angular2-template-loader": "^0.6.0",
"awesome-typescript-loader": "^2.2.1", "awesome-typescript-loader": "2.2.4",
"branch-release": "^1.0.3", "branch-release": "^1.0.3",
"chalk": "^1.1.3", "chalk": "^1.1.3",
"codelyzer": "^1.0.0-beta.2", "codelyzer": "^1.0.0-beta.3",
"copy-webpack-plugin": "^3.0.1", "copy-webpack-plugin": "^3.0.1",
"core-js": "^2.4.1", "core-js": "^2.4.1",
"coveralls": "^2.11.9", "coveralls": "^2.11.9",
@ -82,15 +82,17 @@
"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": "^1.8.0",
"node-sass": "^3.8.0", "node-sass": "^3.10.1",
"phantomjs-prebuilt": "^2.1.7", "phantomjs-prebuilt": "^2.1.7",
"protractor": "^4.0.4", "protractor": "^4.0.10",
"raw-loader": "^0.5.1", "raw-loader": "^0.5.1",
"rxjs": "^5.0.0-beta.12", "rxjs": "5.0.0-beta.12",
"sass-loader": "^4.0.2",
"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",
"style-loader": "^0.13.1", "style-loader": "^0.13.1",
"ts-helpers": "^1.1.1", "ts-helpers": "^1.1.1",
"tslint": "^3.15.1", "tslint": "^3.15.1",
@ -113,12 +115,12 @@
"stream-http": "^2.3.1" "stream-http": "^2.3.1"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/common": "^2.1.0", "@angular/common": "^2.1.2",
"@angular/compiler": "^2.1.0", "@angular/compiler": "^2.1.2",
"@angular/core": "^2.1.0", "@angular/core": "^2.1.2",
"@angular/platform-browser": "^2.1.0", "@angular/platform-browser": "^2.1.2",
"@angular/platform-browser-dynamic": "^2.1.0", "@angular/platform-browser-dynamic": "^2.1.2",
"@angular/platform-server": "^2.1.0", "@angular/platform-server": "^2.1.2",
"core-js": "^2.4.1", "core-js": "^2.4.1",
"rxjs": "5.0.0-beta.12", "rxjs": "5.0.0-beta.12",
"zone.js": "^0.6.25" "zone.js": "^0.6.25"

View File

@ -14,7 +14,7 @@
<script> <script>
window.redocError = null; window.redocError = null;
/* init redoc */ /* init redoc */
var url = window.location.search.substr(5) || 'http://rebilly.github.io/SwaggerTemplateRepo/swagger.json'; var url = window.location.search.substr(5) || 'http://rebilly.github.io/ReDoc/swagger.yaml';
Redoc.init(decodeURIComponent(url), {disableLazySchemas: true, suppressWarnings: true}).then(function() {}, function(err) { Redoc.init(decodeURIComponent(url), {disableLazySchemas: true, suppressWarnings: true}).then(function() {}, function(err) {
window.redocError = err; window.redocError = err;
}); });

View File

@ -6,6 +6,13 @@ const eachNth = require('./helpers').eachNth;
const URL = 'index.html'; const URL = 'index.html';
function waitForInit() {
var EC = protractor.ExpectedConditions;
var $apiInfo = $('api-info');
var $errorMessage = $('.redoc-error')
browser.wait(EC.or(EC.visibilityOf($apiInfo), EC.visibilityOf($errorMessage)), 60000);
}
function basicTests(swaggerUrl, title) { function basicTests(swaggerUrl, title) {
describe(`Basic suite for ${title}`, () => { describe(`Basic suite for ${title}`, () => {
let specUrl = URL; let specUrl = URL;
@ -15,6 +22,7 @@ function basicTests(swaggerUrl, title) {
beforeEach((done) => { beforeEach((done) => {
browser.get(specUrl); browser.get(specUrl);
waitForInit();
fixFFTest(done); fixFFTest(done);
}); });
@ -22,11 +30,14 @@ function basicTests(swaggerUrl, title) {
verifyNoBrowserErrors(); verifyNoBrowserErrors();
}); });
it('should init redoc without errors', () => { it('should init redoc without errors', (done) => {
let $redoc = $('redoc'); let $redoc = $('redoc');
expect($redoc.isPresent()).toBe(true); expect($redoc.isPresent()).toBe(true);
setTimeout(() => {
let $methods = $$('method'); let $methods = $$('method');
expect($methods.count()).toBeGreaterThan(0); expect($methods.count()).toBeGreaterThan(0);
done();
});
}); });
}); });
} }
@ -39,6 +50,7 @@ describe('Scroll sync', () => {
beforeEach((done) => { beforeEach((done) => {
browser.get(specUrl); browser.get(specUrl);
waitForInit();
fixFFTest(done); fixFFTest(done);
}); });
@ -64,6 +76,7 @@ describe('Language tabs sync', () => {
beforeEach((done) => { beforeEach((done) => {
browser.get(specUrl); browser.get(specUrl);
waitForInit();
fixFFTest(done); fixFFTest(done);
}); });

View File

@ -1,7 +1,6 @@
--- ---
swagger: "2.0" swagger: "2.0"
info: info:
description: "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters."
version: "1.0.0" version: "1.0.0"
title: "Swagger Petstore" title: "Swagger Petstore"
termsOfService: "http://swagger.io/terms/" termsOfService: "http://swagger.io/terms/"
@ -12,13 +11,8 @@
license: license:
name: "Apache 2.0" name: "Apache 2.0"
url: "http://www.apache.org/licenses/LICENSE-2.0.html" url: "http://www.apache.org/licenses/LICENSE-2.0.html"
host: "petstore.swagger.io"
basePath: "/v2"
tags:
-
name: "Pagination"
x-traitTag: true
description: |- description: |-
# Pagination
Sometimes you just can't get enough. For this reason, we've provided a convenient way to access more data in Sometimes you just can't get enough. For this reason, we've provided a convenient way to access more data in
any request for sequential data. Simply call the url in the next_url parameter and we'll respond with the next any request for sequential data. Simply call the url in the next_url parameter and we'll respond with the next
set of data. set of data.
@ -37,13 +31,12 @@
Simply set this to the number of items you'd like to receive. Note that the default values Simply set this to the number of items you'd like to receive. Note that the default values
should be fine for most applications - but if you decide to increase this number there is a maximum value should be fine for most applications - but if you decide to increase this number there is a maximum value
defined on each endpoint. defined on each endpoint.
externalDocs:
description: "Find out more" # JSONP
url: "http://swagger.io" This is a sample server Petstore server.
- You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on
name: "JSONP" [irc.freenode.net, #swagger](http://swagger.io/irc/).
x-traitTag: true For this sample, you can use the api key `special-key` to test the authorization filters.
description: |-
If you're writing an AJAX application, and you'd like to wrap our response with a callback, If you're writing an AJAX application, and you'd like to wrap our response with a callback,
all you have to do is specify a callback parameter with any API call: all you have to do is specify a callback parameter with any API call:
@ -59,9 +52,9 @@
``` ```
> Example of markdown blockquote > Example of markdown blockquote
externalDocs: host: "petstore.swagger.io"
description: "Find out more" basePath: "/v2"
url: "http://swagger.io" tags:
- -
name: "pet" name: "pet"
description: "Everything about your Pets" description: "Everything about your Pets"

View File

@ -19,14 +19,17 @@ require('zone.js/dist/jasmine-patch');
require('../lib/vendor'); require('../lib/vendor');
var TestBed = require('@angular/core/testing').TestBed; var TestBed = require('@angular/core/testing').TestBed;
var ErrorHandler = require('@angular/core').ErrorHandler;
var BrowserDynamicTestingModule = require('@angular/platform-browser-dynamic/testing').BrowserDynamicTestingModule; var BrowserDynamicTestingModule = require('@angular/platform-browser-dynamic/testing').BrowserDynamicTestingModule;
var platformBrowserDynamicTesting = require('@angular/platform-browser-dynamic/testing').platformBrowserDynamicTesting; var platformBrowserDynamicTesting = require('@angular/platform-browser-dynamic/testing').platformBrowserDynamicTesting;
var SpecManager = require('../lib/utils/spec-manager').SpecManager;
var services = require('../lib/services/index'); var services = require('../lib/services/index');
var SpecManager = require('../lib/utils/SpecManager').SpecManager;
var BrowserDomAdapter = require('@angular/platform-browser/src/browser/browser_adapter').BrowserDomAdapter;
var REDOC_PIPES = require('../lib/utils/pipes').REDOC_PIPES; var REDOC_PIPES = require('../lib/utils/pipes').REDOC_PIPES;
var REDOC_COMMON_DIRECTIVES = require('../lib/shared/components/index').REDOC_COMMON_DIRECTIVES; var sharedComponents = require('../lib/shared/components/');
var REDOC_DIRECTIVES = require('../lib/components/index').REDOC_DIRECTIVES; var REDOC_COMMON_DIRECTIVES = sharedComponents.REDOC_COMMON_DIRECTIVES;
var components = require('../lib/components/');
var REDOC_DIRECTIVES = components.REDOC_DIRECTIVES;
TestBed.initTestEnvironment( TestBed.initTestEnvironment(
BrowserDynamicTestingModule, BrowserDynamicTestingModule,
@ -36,18 +39,25 @@ TestBed.initTestEnvironment(
beforeEach(function() { beforeEach(function() {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
BrowserDomAdapter,
SpecManager, SpecManager,
BrowserDomAdapter, services.AppStateService,
services.RedocEventsService,
services.ScrollService, services.ScrollService,
services.Hash, services.Hash,
services.MenuService, services.MenuService,
services.WarningsService, services.WarningsService,
services.OptionsService services.OptionsService,
services.ComponentParser,
services.ContentProjector,
{ provide: ErrorHandler, useClass: services.CustomErrorHandler },
{ provide: services.COMPONENT_PARSER_ALLOWED, useValue: { 'security-definitions': components.SecurityDefinitions }}
], ],
declarations: [REDOC_PIPES, REDOC_DIRECTIVES, REDOC_COMMON_DIRECTIVES] declarations: [REDOC_PIPES, REDOC_DIRECTIVES, REDOC_COMMON_DIRECTIVES]
}); });
TestBed.overrideModule(BrowserDynamicTestingModule, {
set: {
entryComponents: [ sharedComponents.DynamicNg2Wrapper, components.SecurityDefinitions ]
},
});
}); });

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { SpecManager } from '../../lib/utils/SpecManager'; import { SpecManager } from '../../lib/utils/spec-manager';
describe('Utils', () => { describe('Utils', () => {
describe('Schema manager', () => { describe('Schema manager', () => {
let specMgr; let specMgr;
@ -75,9 +75,9 @@ describe('Utils', () => {
describe('byPointer method', () => { describe('byPointer method', () => {
it('should return correct schema part', ()=> { it('should return correct schema part', ()=> {
let part = specMgr.byPointer('/tags/3'); let part = specMgr.byPointer('/tags/0');
part.should.be.deepEqual(specMgr.schema.tags[3]); part.should.be.deepEqual(specMgr.schema.tags[0]);
part.should.be.equal(specMgr.schema.tags[3]); part.should.be.equal(specMgr.schema.tags[0]);
}); });
it('should return null for incorrect pointer', ()=> { it('should return null for incorrect pointer', ()=> {

View File

@ -1,11 +1,11 @@
'use strict'; 'use strict';
import {KeysPipe, ValuesPipe, JsonPointerEscapePipe, MarkedPipe} from '../../lib/utils/pipes'; import {KeysPipe, JsonPointerEscapePipe, MarkedPipe} from '../../lib/utils/pipes';
describe('Pipes', () => { describe('Pipes', () => {
describe('KeysPipe and ValuesPipe', () => { describe('KeysPipe', () => {
let obj; let obj;
var keysPipe, valuesPipe; var keysPipe;
beforeEach(() => { beforeEach(() => {
obj = { obj = {
@ -14,7 +14,6 @@ describe('Pipes', () => {
c: 3 c: 3
}; };
keysPipe = new KeysPipe(); keysPipe = new KeysPipe();
valuesPipe = new ValuesPipe();
}); });
describe('KeysPipe transform', () => { describe('KeysPipe transform', () => {
@ -28,22 +27,6 @@ describe('Pipes', () => {
(() => keysPipe.transform('45')).should.throw(); (() => keysPipe.transform('45')).should.throw();
}); });
it('should not throw on blank input', () => {
(() => valuesPipe.transform()).should.not.throw();
});
});
describe('KeysPipe transform', () => {
it('should return values', () => {
var val = valuesPipe.transform(obj);
val.should.be.deepEqual([1, 2, 3]);
});
it('should not support other objects', () => {
(() => valuesPipe.transform(45)).should.throw();
(() => valuesPipe.transform('45')).should.throw();
});
it('should not throw on blank input', () => { it('should not throw on blank input', () => {
(() => keysPipe.transform()).should.not.throw(); (() => keysPipe.transform()).should.not.throw();
}); });

View File

@ -15,7 +15,7 @@
"should", "should",
"requirejs" "requirejs"
], ],
"noEmitHelpers": false "noEmitHelpers": true
}, },
"compileOnSave": false, "compileOnSave": false,
"exclude": [ "exclude": [

View File

@ -38,12 +38,21 @@
"directive-selector-type": [true, "attribute"], "directive-selector-type": [true, "attribute"],
"component-selector-type": [true, "element"], "component-selector-type": [true, "element"],
"component-selector-name": [true, "kebab-case"],
"directive-selector-name": [true, "camelCase"],
"use-input-property-decorator": true, "use-input-property-decorator": true,
"use-output-property-decorator": true, "use-output-property-decorator": true,
"use-host-property-decorator": true, "use-host-property-decorator": true,
"no-input-rename": true, "no-input-rename": true,
"no-output-rename": true, "no-output-rename": true,
"pipe-naming": [true, "camelCase"],
"import-destructuring-spacing": true,
"use-life-cycle-interface": true, "use-life-cycle-interface": true,
"use-pipe-transform-interface": true "use-pipe-transform-interface": true,
"templates-use-public": true,
"no-access-missing-member": true,
"invoke-injectable": true,
"no-forward-ref": true,
"no-attribute-parameter-decorator": true
} }
} }