Merge branch 'master' into external-docs

This commit is contained in:
Matthias Mohr 2018-08-29 15:35:21 +02:00 committed by GitHub
commit 72d120f422
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 1522 additions and 1213 deletions

View File

@ -1,3 +1,48 @@
<a name="2.0.0-alpha.38"></a>
# [2.0.0-alpha.38](https://github.com/Rebilly/ReDoc/compare/v2.0.0-alpha.37...v2.0.0-alpha.38) (2018-08-24)
### Bug Fixes
* addd indent to array schema internals ([865f3ce](https://github.com/Rebilly/ReDoc/commit/865f3ce))
* fix oneOf/anyOf titles ([39b930d](https://github.com/Rebilly/ReDoc/commit/39b930d)), closes [#618](https://github.com/Rebilly/ReDoc/issues/618) [#621](https://github.com/Rebilly/ReDoc/issues/621)
<a name="2.0.0-alpha.37"></a>
# [2.0.0-alpha.37](https://github.com/Rebilly/ReDoc/compare/v2.0.0-alpha.36...v2.0.0-alpha.37) (2018-08-22)
### Bug Fixes
* do not inherit title in allOf ([720e282](https://github.com/Rebilly/ReDoc/commit/720e282)), closes [#601](https://github.com/Rebilly/ReDoc/issues/601)
* fix crash on empty media object ([fb21212](https://github.com/Rebilly/ReDoc/commit/fb21212)), closes [#608](https://github.com/Rebilly/ReDoc/issues/608)
* make http badges font-based instead of inline png ([5d84bd4](https://github.com/Rebilly/ReDoc/commit/5d84bd4))
* use correct parent section for security definition ([f903406](https://github.com/Rebilly/ReDoc/commit/f903406))
<a name="2.0.0-alpha.36"></a>
# [2.0.0-alpha.36](https://github.com/Rebilly/ReDoc/compare/v2.0.0-alpha.35...v2.0.0-alpha.36) (2018-08-11)
### Bug Fixes
* broken rendering of code blocks with language in markdown ([8218a26](https://github.com/Rebilly/ReDoc/commit/8218a26))
* broken rendering of headings with regexp characters ([e660517](https://github.com/Rebilly/ReDoc/commit/e660517))
<a name="2.0.0-alpha.35"></a>
# [2.0.0-alpha.35](https://github.com/Rebilly/ReDoc/compare/v2.0.0-alpha.34...v2.0.0-alpha.35) (2018-08-09)
### Bug Fixes
* crash on any backticks code block without lang specified ([58ae668](https://github.com/Rebilly/ReDoc/commit/58ae668))
* fix auth requirements font size ([d13fe13](https://github.com/Rebilly/ReDoc/commit/d13fe13))
<a name="2.0.0-alpha.34"></a> <a name="2.0.0-alpha.34"></a>
# [2.0.0-alpha.34](https://github.com/Rebilly/ReDoc/compare/v2.0.0-alpha.33...v2.0.0-alpha.34) (2018-08-08) # [2.0.0-alpha.34](https://github.com/Rebilly/ReDoc/compare/v2.0.0-alpha.33...v2.0.0-alpha.34) (2018-08-08)

View File

@ -14,7 +14,7 @@ Two following commands are available:
Some examples: Some examples:
- Bundle with main color changed to `orange`: <br> `$ redoc-cli bundle [spec] --options.theme.colors.main=orange` - Bundle with main color changed to `orange`: <br> `$ redoc-cli bundle [spec] --options.theme.colors.primary.main=orange`
- Serve with `nativeScrollbars` option set to true: <br> `$ redoc-cli serve [spec] --options.nativeScrollbars` - Serve with `nativeScrollbars` option set to true: <br> `$ redoc-cli serve [spec] --options.nativeScrollbars`
- Bundle using custom template (check [default template](https://github.com/Rebilly/ReDoc/blob/master/cli/template.hbs) for reference): <br> `$ redoc-cli bundle [spec] -t custom.hbs` - Bundle using custom template (check [default template](https://github.com/Rebilly/ReDoc/blob/master/cli/template.hbs) for reference): <br> `$ redoc-cli bundle [spec] -t custom.hbs`

View File

@ -1,6 +1,6 @@
{ {
"name": "redoc-cli", "name": "redoc-cli",
"version": "0.6.1", "version": "0.6.2",
"description": "ReDoc's Command Line Interface", "description": "ReDoc's Command Line Interface",
"main": "index.js", "main": "index.js",
"bin": "index.js", "bin": "index.js",
@ -12,11 +12,11 @@
"isarray": "^2.0.4", "isarray": "^2.0.4",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"mobx": "^4.2.0", "mobx": "^4.2.0",
"react": "^16.3.2", "react": "^16.4.2",
"react-dom": "^16.3.2", "react-dom": "^16.4.2",
"redoc": "^2.0.0-alpha.29", "redoc": "^2.0.0-alpha.37",
"tslib": "^1.9.0", "tslib": "^1.9.3",
"yargs": "^11.0.0" "yargs": "^12.0.1"
}, },
"scripts": { "scripts": {
"ci-publish": "ci-publish" "ci-publish": "ci-publish"
@ -25,7 +25,7 @@
"access": "public" "access": "public"
}, },
"devDependencies": { "devDependencies": {
"@types/handlebars": "^4.0.36", "@types/handlebars": "^4.0.39",
"@types/mkdirp": "^0.5.2", "@types/mkdirp": "^0.5.2",
"ci-publish": "^1.3.1" "ci-publish": "^1.3.1"
} }

View File

@ -29,8 +29,11 @@ const tsLoader = env => ({
const babelLoader = mode => ({ const babelLoader = mode => ({
loader: 'babel-loader', loader: 'babel-loader',
options: { options: {
generatorOpts: {
decoratorsBeforeExport: true,
},
plugins: compact([ plugins: compact([
'@babel/plugin-syntax-typescript', ['@babel/plugin-syntax-typescript', { isTSX: true }],
['@babel/plugin-syntax-decorators', { legacy: true }], ['@babel/plugin-syntax-decorators', { legacy: true }],
'@babel/plugin-syntax-jsx', '@babel/plugin-syntax-jsx',
mode !== 'production' ? 'react-hot-loader/babel' : undefined, mode !== 'production' ? 'react-hot-loader/babel' : undefined,
@ -113,6 +116,7 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
instance: 'ts2js-transpiler-only', instance: 'ts2js-transpiler-only',
compilerOptions: { compilerOptions: {
allowJs: true, allowJs: true,
declaration: false,
}, },
}, },
}, },

View File

@ -12,13 +12,14 @@ describe('Menu', () => {
it('should sync active menu items while scroll', () => { it('should sync active menu items while scroll', () => {
cy.contains('h1', 'Introduction') cy.contains('h1', 'Introduction')
.scrollIntoView() .scrollIntoView()
.get('[role=menuitem].active:not(.-depth0)') .get('[role=menuitem].active')
.should('have.text', 'Introduction'); .should('have.text', 'Introduction');
cy.contains('h2', 'Add a new pet to the store') cy.contains('h2', 'Add a new pet to the store')
.scrollIntoView() .scrollIntoView()
.get('[role=menuitem].active:not(.-depth0)') .wait(100)
.should('have.length', 2) .get('[role=menuitem].active')
.children()
.last() .last()
.should('have.text', 'Add a new pet to the store') .should('have.text', 'Add a new pet to the store')
.should('be.visible'); .should('be.visible');

View File

@ -1,6 +1,6 @@
{ {
"name": "redoc", "name": "redoc",
"version": "2.0.0-alpha.34", "version": "2.0.0-alpha.38",
"description": "ReDoc", "description": "ReDoc",
"repository": { "repository": {
"type": "git", "type": "git",
@ -51,71 +51,71 @@
"license-check": "license-checker --production --onlyAllow 'MIT;ISC;Apache-2.0;BSD;BSD-2-Clause;BSD-3-Clause' --summary" "license-check": "license-checker --production --onlyAllow 'MIT;ISC;Apache-2.0;BSD;BSD-2-Clause;BSD-3-Clause' --summary"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.0.0-beta.47", "@babel/core": "7.0.0-rc.2",
"@babel/plugin-syntax-decorators": "7.0.0-beta.47", "@babel/plugin-syntax-decorators": "7.0.0-rc.2",
"@babel/plugin-syntax-jsx": "7.0.0-beta.47", "@babel/plugin-syntax-jsx": "7.0.0-rc.2",
"@babel/plugin-syntax-typescript": "7.0.0-beta.47", "@babel/plugin-syntax-typescript": "7.0.0-rc.2",
"@cypress/webpack-preprocessor": "2.0.1", "@cypress/webpack-preprocessor": "2.0.1",
"@types/chai": "4.1.4", "@types/chai": "4.1.4",
"@types/dompurify": "^0.0.31", "@types/dompurify": "^0.0.31",
"@types/enzyme": "^3.1.11", "@types/enzyme": "^3.1.13",
"@types/enzyme-to-json": "^1.5.0", "@types/enzyme-to-json": "^1.5.2",
"@types/jest": "^23.1.6", "@types/jest": "^23.3.1",
"@types/json-pointer": "^1.0.30", "@types/json-pointer": "^1.0.30",
"@types/lodash": "^4.14.112", "@types/lodash": "^4.14.116",
"@types/lunr": "^2.1.5", "@types/lunr": "^2.1.6",
"@types/mark.js": "^8.11.1", "@types/mark.js": "^8.11.1",
"@types/marked": "^0.4.0", "@types/marked": "^0.4.1",
"@types/prismjs": "^1.6.4", "@types/prismjs": "^1.6.4",
"@types/prop-types": "^15.5.3", "@types/prop-types": "^15.5.5",
"@types/react": "^16.4.6", "@types/react": "^16.4.11",
"@types/react-dom": "^16.0.6", "@types/react-dom": "^16.0.7",
"@types/react-hot-loader": "^4.1.0", "@types/react-hot-loader": "^4.1.0",
"@types/react-tabs": "^1.0.2", "@types/react-tabs": "^1.0.5",
"@types/tapable": "1.0.4", "@types/tapable": "1.0.4",
"@types/webpack": "^4.4.6", "@types/webpack": "^4.4.11",
"@types/webpack-env": "^1.13.0", "@types/webpack-env": "^1.13.0",
"@types/yargs": "^11.1.0", "@types/yargs": "^11.1.1",
"babel-loader": "8.0.0-beta.2", "babel-loader": "8.0.0-beta.2",
"babel-plugin-styled-components": "^1.5.1", "babel-plugin-styled-components": "^1.5.1",
"beautify-benchmark": "^0.2.4", "beautify-benchmark": "^0.2.4",
"bundlesize": "^0.17.0", "bundlesize": "^0.17.0",
"conventional-changelog-cli": "^2.0.1", "conventional-changelog-cli": "^2.0.5",
"copy-webpack-plugin": "^4.5.2", "copy-webpack-plugin": "^4.5.2",
"core-js": "^2.5.7", "core-js": "^2.5.7",
"coveralls": "^3.0.2", "coveralls": "^3.0.2",
"css-loader": "^1.0.0", "css-loader": "^1.0.0",
"cypress": "~3.0.2", "cypress": "~3.1.0",
"deploy-to-gh-pages": "^1.3.6", "deploy-to-gh-pages": "^1.3.6",
"enzyme": "^3.1.1", "enzyme": "^3.4.4",
"enzyme-adapter-react-16": "^1.0.4", "enzyme-adapter-react-16": "^1.2.0",
"enzyme-to-json": "^3.3.4", "enzyme-to-json": "^3.3.4",
"fork-ts-checker-webpack-plugin": "^0.4.3", "fork-ts-checker-webpack-plugin": "0.4.3",
"html-webpack-plugin": "^3.1.0", "html-webpack-plugin": "^3.1.0",
"jest": "^23.4.1", "jest": "^23.5.0",
"license-checker": "^20.1.0", "license-checker": "^20.2.0",
"lodash": "^4.17.10", "lodash": "^4.17.10",
"mobx": "^4.3.1", "mobx": "^4.3.1",
"prettier": "^1.13.7", "prettier": "^1.14.2",
"prettier-eslint": "^8.8.2", "prettier-eslint": "^8.8.2",
"puppeteer": "^1.6.0", "puppeteer": "^1.7.0",
"raf": "^3.4.0", "raf": "^3.4.0",
"react": "^16.4.1", "react": "^16.4.2",
"react-dom": "^16.4.1", "react-dom": "^16.4.2",
"rimraf": "^2.6.2", "rimraf": "^2.6.2",
"shelljs": "^0.8.1", "shelljs": "^0.8.1",
"source-map-loader": "^0.2.1", "source-map-loader": "^0.2.4",
"style-loader": "^0.21.0", "style-loader": "^0.22.1",
"swagger2openapi": "^3.2.8", "swagger2openapi": "^3.2.8",
"ts-jest": "^23.0.0", "ts-jest": "23.0.1",
"ts-loader": "4.4.2", "ts-loader": "4.5.0",
"ts-node": "^7.0.0", "ts-node": "^7.0.1",
"tslint": "^5.7.0", "tslint": "^5.11.0",
"tslint-react": "^3.4.0", "tslint-react": "^3.4.0",
"typescript": "^3.0.0-dev.20180712", "typescript": "^3.0.1",
"webpack": "^4.16.1", "webpack": "^4.17.1",
"webpack-cli": "^3.0.8", "webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.1", "webpack-dev-server": "^3.1.5",
"webpack-node-externals": "^1.6.0", "webpack-node-externals": "^1.6.0",
"workerize-loader": "^1.0.3", "workerize-loader": "^1.0.3",
"yaml-js": "^0.2.3" "yaml-js": "^0.2.3"
@ -128,26 +128,26 @@
"dependencies": { "dependencies": {
"classnames": "^2.2.6", "classnames": "^2.2.6",
"decko": "^1.2.0", "decko": "^1.2.0",
"dompurify": "^1.0.6", "dompurify": "^1.0.7",
"eventemitter3": "^3.0.0", "eventemitter3": "^3.0.0",
"json-pointer": "^0.6.0", "json-pointer": "^0.6.0",
"json-schema-ref-parser": "^5.1.1", "json-schema-ref-parser": "^5.1.2",
"lunr": "^2.3.0", "lunr": "^2.3.2",
"mark.js": "^8.11.1", "mark.js": "^8.11.1",
"marked": "0.3.18", "marked": "0.3.18",
"memoize-one": "^4.0.0", "memoize-one": "^4.0.0",
"mobx-react": "^5.2.3", "mobx-react": "^5.2.5",
"openapi-sampler": "1.0.0-beta.13", "openapi-sampler": "1.0.0-beta.14",
"perfect-scrollbar": "^1.4.0", "perfect-scrollbar": "^1.4.0",
"polished": "^1.9.3", "polished": "^2.0.2",
"prismjs": "^1.15.0", "prismjs": "^1.15.0",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react-dropdown": "^1.3.0", "react-dropdown": "^1.6.2",
"react-hot-loader": "^4.3.3", "react-hot-loader": "^4.3.5",
"react-tabs": "^2.0.0", "react-tabs": "^2.0.0",
"slugify": "^1.2.1", "slugify": "^1.3.1",
"stickyfill": "^1.1.1", "stickyfill": "^1.1.1",
"styled-components": "^3.3.3", "styled-components": "^3.4.5",
"tslib": "^1.9.3" "tslib": "^1.9.3"
}, },
"resolutions": { "resolutions": {
@ -162,7 +162,7 @@
], ],
"jest": { "jest": {
"transform": { "transform": {
"^.+\\.tsx?$": "<rootDir>/node_modules/ts-jest/preprocessor.js" "^.+\\.tsx?$": "ts-jest"
}, },
"setupTestFrameworkScriptFile": "<rootDir>/src/setupTests.ts", "setupTestFrameworkScriptFile": "<rootDir>/src/setupTests.ts",
"testPathIgnorePatterns": [ "testPathIgnorePatterns": [

View File

@ -7,8 +7,8 @@ export const PrismDiv = styled.div`
code[class*='language-'], code[class*='language-'],
pre[class*='language-'] { pre[class*='language-'] {
color: white; /* color: white;
background: none; background: none; */
text-shadow: 0 -0.1em 0.2em black; text-shadow: 0 -0.1em 0.2em black;
text-align: left; text-align: left;
white-space: pre; white-space: pre;

View File

@ -93,7 +93,7 @@ export const StyledDropdown = withProps<DropdownProps>(styled(Dropdown))`
} }
` as StyledComponentClass<any, DropdownProps>; ` as StyledComponentClass<any, DropdownProps>;
export const SimpleDropdown = StyledDropdown.extend` export const SimpleDropdown = styled(StyledDropdown)`
margin-left: 10px; margin-left: 10px;
text-transform: none; text-transform: none;
font-size: 0.929em; font-size: 0.929em;

View File

@ -54,11 +54,11 @@ export const PropertyCell = styled.td`
} }
`; `;
export const PropertyCellWithInner = PropertyCell.extend` export const PropertyCellWithInner = styled(PropertyCell)`
padding: 0; padding: 0;
`; `;
export const PropertyNameCell = withProps<{ kind?: string }>(PropertyCell.extend)` export const PropertyNameCell = withProps<{ kind?: string }>(styled(PropertyCell))`
vertical-align: top; vertical-align: top;
line-height: 20px; line-height: 20px;
white-space: nowrap; white-space: nowrap;

View File

@ -4,7 +4,7 @@ import styled, { extensionsHook } from '../styled-components';
import { PropertyNameCell } from './fields-layout'; import { PropertyNameCell } from './fields-layout';
import { ShelfIcon } from './shelfs'; import { ShelfIcon } from './shelfs';
export const ClickablePropertyNameCell = PropertyNameCell.extend` export const ClickablePropertyNameCell = styled(PropertyNameCell)`
cursor: pointer; cursor: pointer;
${ShelfIcon} { ${ShelfIcon} {
@ -22,21 +22,21 @@ export const FieldLabel = styled.span`
line-height: 20px; line-height: 20px;
`; `;
export const TypePrefix = FieldLabel.extend` export const TypePrefix = styled(FieldLabel)`
color: ${props => transparentize(0.2, props.theme.schema.typeNameColor)}; color: ${props => transparentize(0.2, props.theme.schema.typeNameColor)};
`; `;
export const TypeName = FieldLabel.extend` export const TypeName = styled(FieldLabel)`
color: ${props => props.theme.schema.typeNameColor}; color: ${props => props.theme.schema.typeNameColor};
`; `;
export const TypeTitle = FieldLabel.extend` export const TypeTitle = styled(FieldLabel)`
color: ${props => props.theme.schema.typeTitleColor}; color: ${props => props.theme.schema.typeTitleColor};
`; `;
export const TypeFormat = TypeName; export const TypeFormat = TypeName;
export const RequiredLabel = FieldLabel.withComponent('div').extend` export const RequiredLabel = styled(FieldLabel.withComponent('div'))`
color: ${props => props.theme.schema.requireLabelColor}; color: ${props => props.theme.schema.requireLabelColor};
font-size: ${props => props.theme.schema.labelsTextSize}; font-size: ${props => props.theme.schema.labelsTextSize};
font-weight: normal; font-weight: normal;
@ -44,17 +44,17 @@ export const RequiredLabel = FieldLabel.withComponent('div').extend`
line-height: 1; line-height: 1;
`; `;
export const RecursiveLabel = FieldLabel.extend` export const RecursiveLabel = styled(FieldLabel)`
color: ${({ theme }) => theme.colors.warning.main}; color: ${({ theme }) => theme.colors.warning.main};
font-size: 13px; font-size: 13px;
`; `;
export const NullableLabel = FieldLabel.extend` export const NullableLabel = styled(FieldLabel)`
color: #3195a6; color: #3195a6;
font-size: 13px; font-size: 13px;
`; `;
export const PatternLabel = FieldLabel.extend` export const PatternLabel = styled(FieldLabel)`
color: #3195a6; color: #3195a6;
&::before, &::before,
&::after { &::after {
@ -63,7 +63,7 @@ export const PatternLabel = FieldLabel.extend`
} }
`; `;
export const ExampleValue = FieldLabel.extend` export const ExampleValue = styled(FieldLabel)`
border-radius: 2px; border-radius: 2px;
${({ theme }) => ` ${({ theme }) => `
background-color: ${transparentize(0.95, theme.colors.text.primary)}; background-color: ${transparentize(0.95, theme.colors.text.primary)};
@ -79,7 +79,7 @@ export const ExampleValue = FieldLabel.extend`
${extensionsHook('ExampleValue')}; ${extensionsHook('ExampleValue')};
`; `;
export const ConstraintItem = FieldLabel.extend` export const ConstraintItem = styled(FieldLabel)`
border-radius: 2px; border-radius: 2px;
${({ theme }) => ` ${({ theme }) => `
background-color: ${transparentize(0.95, theme.colors.primary.light)}; background-color: ${transparentize(0.95, theme.colors.primary.light)};

View File

@ -1,5 +1,10 @@
import * as React from 'react';
import { StoreConsumer } from '../components/StoreBuilder';
import styled, { css } from '../styled-components'; import styled, { css } from '../styled-components';
import { HistoryService } from '../services';
// tslint:disable-next-line // tslint:disable-next-line
export const linkifyMixin = className => css` export const linkifyMixin = className => css`
${className} { ${className} {
@ -27,6 +32,42 @@ export const linkifyMixin = className => css`
} }
`; `;
export const ShareLink = styled.a` const isModifiedEvent = event =>
!!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
export class Link extends React.Component<{ to: string; className?: string; children?: any }> {
navigate = (history: HistoryService, event) => {
if (
!event.defaultPrevented && // onClick prevented default
event.button === 0 && // ignore everything but left clicks
!isModifiedEvent(event) // ignore clicks with modifier keys
) {
event.preventDefault();
history.replace(this.props.to);
}
};
render() {
return (
<StoreConsumer>
{store => (
<a
className={this.props.className}
href={store!.menu.history.linkForId(this.props.to)}
onClick={this.navigate.bind(this, store!.menu.history)}
>
{this.props.children}
</a>
)}
</StoreConsumer>
);
}
}
const StyledShareLink = styled(Link)`
${linkifyMixin('&')}; ${linkifyMixin('&')};
`; `;
export function ShareLink(props: { to: string }) {
return <StyledShareLink to={props.to} />;
}

View File

@ -1,32 +1,58 @@
import styled, { media } from '../styled-components'; import { SECTION_ATTR } from '../services/MenuStore';
import styled, { media, withProps } from '../styled-components';
export const MiddlePanel = styled.div` export const MiddlePanel = styled.div`
width: calc(100% - ${props => props.theme.rightPanel.width}); width: calc(100% - ${props => props.theme.rightPanel.width});
padding: ${props => props.theme.spacing.unit * 8}px; padding: 0 ${props => props.theme.spacing.unit * 8}px;
${media.lessThan('medium')` ${media.lessThan('medium')`
width: 100%; width: 100%;
`}; `};
`; `;
export const Section = withProps<{ underlined?: boolean }>(
styled.div.attrs({
[SECTION_ATTR]: props => props.id,
} as any),
)`
padding: ${props => props.theme.spacing.unit * 8}px 0;
${props =>
(props.underlined &&
`
position: relative;
&:not(:last-of-type):after {
position: absolute;
bottom: 0;
width: 100%;
display: block;
content: '';
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
}
`) ||
''}
`;
export const RightPanel = styled.div` export const RightPanel = styled.div`
width: ${props => props.theme.rightPanel.width}; width: ${props => props.theme.rightPanel.width};
color: #fafbfc; color: #fafbfc;
background-color: ${props => props.theme.rightPanel.backgroundColor}; background-color: ${props => props.theme.rightPanel.backgroundColor};
padding: ${props => props.theme.spacing.unit * 8}px; padding: 0 ${props => props.theme.spacing.unit * 8}px;
${media.lessThan('medium')` ${media.lessThan('medium')`
width: 100%; width: 100%;
`}; `};
`; `;
export const DarkRightPanel = RightPanel.extend` export const DarkRightPanel = styled(RightPanel)`
background-color: ${props => props.theme.rightPanel.backgroundColor}; background-color: ${props => props.theme.rightPanel.backgroundColor};
`; `;
export const Row = styled.div` export const Row = styled.div`
display: flex; display: flex;
width: 100%; width: 100%;
padding: 0;
${media.lessThan('medium')` ${media.lessThan('medium')`
flex-direction: column; flex-direction: column;

View File

@ -2,6 +2,8 @@ import * as React from 'react';
import PerfectScrollbarType, * as PerfectScrollbarNamespace from 'perfect-scrollbar'; import PerfectScrollbarType, * as PerfectScrollbarNamespace from 'perfect-scrollbar';
import psStyles from 'perfect-scrollbar/css/perfect-scrollbar.css'; import psStyles from 'perfect-scrollbar/css/perfect-scrollbar.css';
import { OptionsContext } from '../components/OptionsProvider';
import styled, { injectGlobal } from '../styled-components'; import styled, { injectGlobal } from '../styled-components';
/* /*
@ -18,11 +20,13 @@ const StyledScrollWrapper = styled.div`
position: relative; position: relative;
`; `;
export class PerfectScrollbar extends React.Component<{ export interface PerfectScrollbarProps {
options?: PerfectScrollbarType.Options; options?: PerfectScrollbarType.Options;
className?: string; className?: string;
updateFn: (fn) => void; updateFn?: (fn) => void;
}> { }
export class PerfectScrollbar extends React.Component<PerfectScrollbarProps> {
private _container: HTMLElement; private _container: HTMLElement;
private inst: PerfectScrollbarType; private inst: PerfectScrollbarType;
@ -49,7 +53,9 @@ export class PerfectScrollbar extends React.Component<{
render() { render() {
const { children, className, updateFn } = this.props; const { children, className, updateFn } = this.props;
updateFn(this.componentDidUpdate.bind(this)); if (updateFn) {
updateFn(this.componentDidUpdate.bind(this));
}
return ( return (
<StyledScrollWrapper className={`scrollbar-container ${className}`} innerRef={this.handleRef}> <StyledScrollWrapper className={`scrollbar-container ${className}`} innerRef={this.handleRef}>
@ -58,3 +64,26 @@ export class PerfectScrollbar extends React.Component<{
); );
} }
} }
export function PerfectScrollbarWrap(
props: PerfectScrollbarProps & { children: JSX.Element[] | JSX.Element },
) {
return (
<OptionsContext.Consumer>
{options =>
!options.nativeScrollbars ? (
<PerfectScrollbar {...props}>{props.children}</PerfectScrollbar>
) : (
<div
style={{
overflow: 'auto',
msOverflowStyle: '-ms-autohiding-scrollbar',
}}
>
{props.children}
</div>
)
}
</OptionsContext.Consumer>
);
}

View File

@ -64,7 +64,7 @@ export const Tabs = styled(ReactTabs)`
} }
`; `;
export const SmallTabs = Tabs.extend` export const SmallTabs = styled(Tabs)`
> ul { > ul {
display: block; display: block;
> li { > li {

View File

@ -1,22 +0,0 @@
import * as React from 'react';
import { MiddlePanel, Row } from '../../common-elements/';
import { Markdown } from '../Markdown/Markdown';
export interface ApiDescriptionProps {
description: string;
}
export class ApiDescription extends React.PureComponent<ApiDescriptionProps> {
render() {
const { description } = this.props;
return (
<Row>
<MiddlePanel>
<Markdown source={description} />
</MiddlePanel>
</Row>
);
}
}

View File

@ -3,9 +3,9 @@ import * as React from 'react';
import { AppStore } from '../../services/AppStore'; import { AppStore } from '../../services/AppStore';
import { MiddlePanel, Row } from '../../common-elements/';
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation'; import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
import { MiddlePanel, Row, Section } from '../../common-elements/';
import { Markdown } from '../Markdown/Markdown';
import { StyledMarkdownBlock } from '../Markdown/styled.elements'; import { StyledMarkdownBlock } from '../Markdown/styled.elements';
import { import {
ApiHeader, ApiHeader,
@ -71,41 +71,44 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
null; null;
return ( return (
<Row> <Section>
<MiddlePanel className="api-info"> <Row>
<ApiHeader> <MiddlePanel className="api-info">
{info.title} <span>({info.version})</span> <ApiHeader>
</ApiHeader> {info.title} <span>({info.version})</span>
{!hideDownloadButton && ( </ApiHeader>
<p> {!hideDownloadButton && (
Download OpenAPI specification: <p>
<DownloadButton Download OpenAPI specification:
download={downloadFilename} <DownloadButton
target="_blank" download={downloadFilename}
href={downloadLink} target="_blank"
onClick={this.handleDownloadClick} href={downloadLink}
> onClick={this.handleDownloadClick}
Download >
</DownloadButton> Download
</p> </DownloadButton>
)} </p>
<StyledMarkdownBlock> )}
{((info.license || info.contact || info.termsOfService) && ( <StyledMarkdownBlock>
<InfoSpanBoxWrap> {((info.license || info.contact || info.termsOfService) && (
<InfoSpanBox> <InfoSpanBoxWrap>
{email} {website} {license} {terms} <InfoSpanBox>
</InfoSpanBox> {email} {website} {license} {terms}
</InfoSpanBoxWrap> </InfoSpanBox>
)) || </InfoSpanBoxWrap>
null} )) ||
</StyledMarkdownBlock> null}
{externalDocs && ( </StyledMarkdownBlock>
<p> <Markdown source={store.spec.info.description} />
<ExternalDocumentation externalDocs={externalDocs} /> {externalDocs && (
</p> <p>
)} <ExternalDocumentation externalDocs={externalDocs} />
</MiddlePanel> </p>
</Row> )}
</MiddlePanel>
</Row>
</Section>
); );
} }
} }

View File

@ -1,2 +1 @@
export { ApiDescription } from './ApiDescription';
export { ApiInfo } from './ApiInfo'; export { ApiInfo } from './ApiInfo';

View File

@ -5,7 +5,7 @@ const delimiterWidth = 15;
export const ApiInfoWrap = MiddlePanel; export const ApiInfoWrap = MiddlePanel;
export const ApiHeader = H1.extend` export const ApiHeader = styled(H1)`
margin-top: 0; margin-top: 0;
margin-bottom: 0.5em; margin-bottom: 0.5em;

View File

@ -1,48 +1,29 @@
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import * as React from 'react'; import * as React from 'react';
import { SECTION_ATTR } from '../../services/MenuStore'; import { AdvancedMarkdown } from '../Markdown/AdvancedMarkdown';
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation'; import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
import { Markdown } from '../Markdown/Markdown';
import { H1, H2, MiddlePanel, Row, ShareLink } from '../../common-elements'; import { H1, H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements';
import { MDXComponentMeta } from '../../services/MarkdownRenderer';
import { ContentItemModel } from '../../services/MenuBuilder'; import { ContentItemModel } from '../../services/MenuBuilder';
import { GroupModel, OperationModel } from '../../services/models'; import { GroupModel, OperationModel } from '../../services/models';
import { Operation } from '../Operation/Operation'; import { Operation } from '../Operation/Operation';
import { SecurityDefs } from '../SecuritySchemes/SecuritySchemes';
import { StoreConsumer } from '../StoreBuilder';
@observer @observer
export class ContentItems extends React.Component<{ export class ContentItems extends React.Component<{
items: ContentItemModel[]; items: ContentItemModel[];
allowedMdComponents?: Dict<MDXComponentMeta>;
}> { }> {
static defaultProps = {
allowedMdComponents: {
'security-definitions': {
component: SecurityDefs,
propsSelector: _store => ({
securitySchemes: _store!.spec.securitySchemes,
}),
},
},
};
render() { render() {
const items = this.props.items; const items = this.props.items;
if (items.length === 0) { if (items.length === 0) {
return null; return null;
} }
return items.map(item => ( return items.map(item => <ContentItem item={item} key={item.id} />);
<ContentItem item={item} key={item.id} allowedMdComponents={this.props.allowedMdComponents} />
));
} }
} }
export interface ContentItemProps { export interface ContentItemProps {
item: ContentItemModel; item: ContentItemModel;
allowedMdComponents?: Dict<MDXComponentMeta>;
} }
@observer @observer
@ -68,44 +49,42 @@ export class ContentItem extends React.Component<ContentItemProps> {
throw new Error('Unknown item type'); throw new Error('Unknown item type');
} }
return [ return (
<div key="section" {...{ [SECTION_ATTR]: item.id }}> <>
{content} <Section id={item.id} underlined={item.type === 'operation'}>
</div>, {content}
(item as any).items && <ContentItems key="content" items={(item as any).items} />, </Section>
]; {item.items && <ContentItems items={item.items} />}
</>
);
} }
} }
const middlePanelWrap = component => <MiddlePanel>{component}</MiddlePanel>;
@observer @observer
export class SectionItem extends React.Component<ContentItemProps> { export class SectionItem extends React.Component<ContentItemProps> {
render() { render() {
const { name, description, externalDocs, level } = this.props.item as GroupModel; const { name, description, externalDocs, level } = this.props.item as GroupModel;
const components = this.props.allowedMdComponents;
const Header = level === 2 ? H2 : H1; const Header = level === 2 ? H2 : H1;
return ( return (
<Row> <>
<MiddlePanel> <Row>
<Header> <MiddlePanel>
<ShareLink href={'#' + this.props.item.id} /> <Header>
{name} <ShareLink to={this.props.item.id} />
</Header> {name}
{components ? ( </Header>
<StoreConsumer> </MiddlePanel>
{store => ( </Row>
<Markdown source={description || ''} allowedComponents={components} store={store} /> <AdvancedMarkdown source={description || ''} htmlWrap={middlePanelWrap} />
)} {externalDocs && (
</StoreConsumer> <p>
) : ( <ExternalDocumentation externalDocs={externalDocs} />
<Markdown source={description || ''} /> </p>
)} )}
{externalDocs && ( </>
<p>
<ExternalDocumentation externalDocs={externalDocs} />
</p>
)}
</MiddlePanel>
</Row>
); );
} }
} }

View File

@ -29,7 +29,14 @@ export class FieldDetails extends React.PureComponent<FieldProps> {
<div> <div>
<TypePrefix>{schema.typePrefix}</TypePrefix> <TypePrefix>{schema.typePrefix}</TypePrefix>
<TypeName>{schema.displayType}</TypeName> <TypeName>{schema.displayType}</TypeName>
{schema.displayFormat && <TypeFormat> &lt;{schema.displayFormat}&gt; </TypeFormat>} {schema.displayFormat && (
<TypeFormat>
{' '}
&lt;
{schema.displayFormat}
&gt;{' '}
</TypeFormat>
)}
{schema.title && <TypeTitle> ({schema.title}) </TypeTitle>} {schema.title && <TypeTitle> ({schema.title}) </TypeTitle>}
<ConstraintsView constraints={schema.constraints} /> <ConstraintsView constraints={schema.constraints} />
{schema.nullable && <NullableLabel> Nullable </NullableLabel>} {schema.nullable && <NullableLabel> Nullable </NullableLabel>}

View File

@ -0,0 +1,48 @@
import * as React from 'react';
import { AppStore, MarkdownRenderer, RedocNormalizedOptions } from '../../services';
import { BaseMarkdownProps } from './Markdown';
import { SanitizedMarkdownHTML } from './SanitizedMdBlock';
import { OptionsConsumer } from '../OptionsProvider';
import { StoreConsumer } from '../StoreBuilder';
export interface AdvancedMarkdownProps extends BaseMarkdownProps {
htmlWrap?: (part: JSX.Element) => JSX.Element;
}
export class AdvancedMarkdown extends React.Component<AdvancedMarkdownProps> {
render() {
return (
<OptionsConsumer>
{options => (
<StoreConsumer>{store => this.renderWithOptionsAndStore(options, store)}</StoreConsumer>
)}
</OptionsConsumer>
);
}
renderWithOptionsAndStore(options: RedocNormalizedOptions, store?: AppStore) {
const { source, htmlWrap = i => i } = this.props;
if (!store) {
throw new Error('When using componentes in markdown, store prop must be provided');
}
const renderer = new MarkdownRenderer(options);
const parts = renderer.renderMdWithComponents(source);
if (!parts.length) {
return null;
}
return parts.map((part, idx) => {
if (typeof part === 'string') {
return React.cloneElement(
htmlWrap(<SanitizedMarkdownHTML html={part} inline={false} dense={false} />),
{ key: idx },
);
}
return <part.component key={idx} {...{ ...part.attrs, ...part.propsSelector(store) }} />;
});
}
}

View File

@ -1,103 +1,35 @@
import * as React from 'react'; import * as React from 'react';
import * as DOMPurify from 'dompurify'; import { MarkdownRenderer } from '../../services';
import { AppStore, MarkdownRenderer, MDXComponentMeta } from '../../services'; import { SanitizedMarkdownHTML } from './SanitizedMdBlock';
import { OptionsContext } from '../OptionsProvider';
import { StyledMarkdownBlock } from './styled.elements';
const StyledMarkdownSpan = StyledMarkdownBlock.withComponent('span');
export interface StylingMarkdownProps { export interface StylingMarkdownProps {
dense?: boolean; dense?: boolean;
inline?: boolean; inline?: boolean;
} }
export interface BaseMarkdownProps extends StylingMarkdownProps { export interface BaseMarkdownProps {
sanitize?: boolean; sanitize?: boolean;
store?: AppStore;
}
const sanitize = (untrustedSpec, html) => (untrustedSpec ? DOMPurify.sanitize(html) : html);
function SanitizedMarkdownHTML(props: StylingMarkdownProps & { html: string }) {
const Wrap = props.inline ? StyledMarkdownSpan : StyledMarkdownBlock;
return (
<OptionsContext.Consumer>
{options => (
<Wrap
className={'redoc-markdown'}
dangerouslySetInnerHTML={{
__html: sanitize(options.untrustedSpec, props.html),
}}
{...props}
/>
)}
</OptionsContext.Consumer>
);
}
export interface MarkdownProps extends BaseMarkdownProps {
allowedComponents?: Dict<MDXComponentMeta>;
source: string; source: string;
} }
export type MarkdownProps = BaseMarkdownProps &
StylingMarkdownProps & {
source: string;
className?: string;
};
export class Markdown extends React.Component<MarkdownProps> { export class Markdown extends React.Component<MarkdownProps> {
constructor(props: MarkdownProps) {
super(props);
if (props.allowedComponents && props.inline) {
throw new Error('Markdown Component: "inline" mode doesn\'t support "components"');
}
}
render() { render() {
const { source, allowedComponents, store, inline, dense } = this.props; const { source, inline, dense, className } = this.props;
if (allowedComponents && !store) {
throw new Error('When using componentes in markdown, store prop must be provided');
}
const renderer = new MarkdownRenderer(); const renderer = new MarkdownRenderer();
if (allowedComponents) {
return (
<AdvancedMarkdown
parts={renderer.renderMdWithComponents(source, allowedComponents)}
{...this.props}
/>
);
} else {
return (
<SanitizedMarkdownHTML html={renderer.renderMd(source)} inline={inline} dense={dense} />
);
}
}
}
export interface AdvancedMarkdownProps extends BaseMarkdownProps {
parts: Array<string | MDXComponentMeta>;
}
export class AdvancedMarkdown extends React.Component<AdvancedMarkdownProps> {
render() {
const { inline, dense, store, parts } = this.props;
if (!parts.length) {
return null;
}
return ( return (
<> <SanitizedMarkdownHTML
{parts.map( html={renderer.renderMd(source)}
(part, idx) => inline={inline}
typeof part === 'string' ? ( dense={dense}
<SanitizedMarkdownHTML html={part} inline={inline} dense={dense} key={idx} /> className={className}
) : ( />
<part.component key={idx} {...{ ...part.attrs, ...part.propsSelector(store) }} />
),
)}
</>
); );
} }
} }

View File

@ -0,0 +1,30 @@
import * as DOMPurify from 'dompurify';
import * as React from 'react';
import { OptionsConsumer } from '../OptionsProvider';
import { StylingMarkdownProps } from './Markdown';
import { StyledMarkdownBlock } from './styled.elements';
const StyledMarkdownSpan = StyledMarkdownBlock.withComponent('span');
const sanitize = (untrustedSpec, html) => (untrustedSpec ? DOMPurify.sanitize(html) : html);
export function SanitizedMarkdownHTML(
props: StylingMarkdownProps & { html: string; className?: string },
) {
const Wrap = props.inline ? StyledMarkdownSpan : StyledMarkdownBlock;
return (
<OptionsConsumer>
{options => (
<Wrap
className={'redoc-markdown ' + (props.className || '')}
dangerouslySetInnerHTML={{
__html: sanitize(options.untrustedSpec, props.html),
}}
{...props}
/>
)}
</OptionsConsumer>
);
}

View File

@ -1,6 +1,6 @@
import { headerCommonMixin, linkifyMixin } from '../../common-elements'; import { headerCommonMixin, linkifyMixin } from '../../common-elements';
import { PrismDiv } from '../../common-elements/PrismDiv'; import { PrismDiv } from '../../common-elements/PrismDiv';
import { css, extensionsHook, withProps } from '../../styled-components'; import styled, { css, extensionsHook, withProps } from '../../styled-components';
export const linksCss = css` export const linksCss = css`
a { a {
@ -18,7 +18,7 @@ export const linksCss = css`
`; `;
export const StyledMarkdownBlock = withProps<{ dense?: boolean; inline?: boolean }>( export const StyledMarkdownBlock = withProps<{ dense?: boolean; inline?: boolean }>(
PrismDiv.extend, styled(PrismDiv),
)` )`
font-family: ${props => props.theme.typography.fontFamily}; font-family: ${props => props.theme.typography.fontFamily};

View File

@ -19,25 +19,15 @@ import { ResponseSamples } from '../ResponseSamples/ResponseSamples';
import { OperationModel as OperationType } from '../../services/models'; import { OperationModel as OperationType } from '../../services/models';
import styled from '../../styled-components'; import styled from '../../styled-components';
const OperationRow = Row.extend` const OperationRow = styled(Row)`
backface-visibility: hidden; backface-visibility: hidden;
contain: content; contain: content;
overflow: hidden; overflow: hidden;
position: relative;
&:after {
position: absolute;
bottom: 0;
width: 100%;
display: block;
content: '';
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
}
`; `;
const Description = styled(Markdown)` const Description = styled(Markdown)`
margin-bottom: ${({ theme }) => theme.spacing.unit * 8}; margin-bottom: ${({ theme }) => theme.spacing.unit * 6}px;
`; `;
export interface OperationProps { export interface OperationProps {
@ -56,7 +46,7 @@ export class Operation extends React.Component<OperationProps> {
<OperationRow> <OperationRow>
<MiddlePanel> <MiddlePanel>
<H2> <H2>
<ShareLink href={'#' + operation.id} /> <ShareLink to={operation.id} />
{summary} {deprecated && <Badge type="warning"> Deprecated </Badge>} {summary} {deprecated && <Badge type="warning"> Deprecated </Badge>}
</H2> </H2>
{options.pathInMiddlePanel && <Endpoint operation={operation} inverted={true} />} {options.pathInMiddlePanel && <Endpoint operation={operation} inverted={true} />}

View File

@ -34,7 +34,9 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps> {
return ( return (
<SmallTabs> <SmallTabs>
<TabList> <TabList>
{examplesNames.map(name => <Tab key={name}> {examples[name].summary || name} </Tab>)} {examplesNames.map(name => (
<Tab key={name}> {examples[name].summary || name} </Tab>
))}
</TabList> </TabList>
{examplesNames.map(name => ( {examplesNames.map(name => (
<TabPanel key={name}>{sampleView(examples[name].value)}</TabPanel> <TabPanel key={name}>{sampleView(examples[name].value)}</TabPanel>

View File

@ -9,7 +9,7 @@ export const MimeLabel = styled.div`
color: rgba(255, 255, 255, 0.8); color: rgba(255, 255, 255, 0.8);
`; `;
export const InvertedSimpleDropdown = StyledDropdown.extend` export const InvertedSimpleDropdown = styled(StyledDropdown)`
margin-left: 10px; margin-left: 10px;
text-transform: none; text-transform: none;
font-size: 0.929em; font-size: 0.929em;

View File

@ -2,12 +2,12 @@ import * as PropTypes from 'prop-types';
import * as React from 'react'; import * as React from 'react';
import { ThemeProvider } from '../../styled-components'; import { ThemeProvider } from '../../styled-components';
import { OptionsProvider } from '../OptionsProvider';
import { AppStore } from '../../services'; import { AppStore } from '../../services';
import { ApiDescription, ApiInfo } from '../ApiInfo/'; import { ApiInfo } from '../ApiInfo/';
import { ApiLogo } from '../ApiLogo/ApiLogo'; import { ApiLogo } from '../ApiLogo/ApiLogo';
import { ContentItems } from '../ContentItems/ContentItems'; import { ContentItems } from '../ContentItems/ContentItems';
import { OptionsProvider } from '../OptionsProvider';
import { SideMenu } from '../SideMenu/SideMenu'; import { SideMenu } from '../SideMenu/SideMenu';
import { StickyResponsiveSidebar } from '../StickySidebar/StickyResponsiveSidebar'; import { StickyResponsiveSidebar } from '../StickySidebar/StickyResponsiveSidebar';
import { ApiContentWrap, BackgroundStub, RedocWrap } from './styled.elements'; import { ApiContentWrap, BackgroundStub, RedocWrap } from './styled.elements';
@ -57,7 +57,6 @@ export class Redoc extends React.Component<RedocProps> {
</StickyResponsiveSidebar> </StickyResponsiveSidebar>
<ApiContentWrap className="api-content"> <ApiContentWrap className="api-content">
<ApiInfo store={store} /> <ApiInfo store={store} />
<ApiDescription description={store.spec.info.description} />
<ContentItems items={menu.items as any} /> <ContentItems items={menu.items as any} />
</ApiContentWrap> </ApiContentWrap>
<BackgroundStub /> <BackgroundStub />

View File

@ -33,7 +33,8 @@ export class RedocStandalone extends React.PureComponent<RedocStandaloneProps> {
} }
return null; return null;
}, },
options: PropTypes.object, options: PropTypes.any,
onLoaded: PropTypes.any,
}; };
render() { render() {

View File

@ -34,7 +34,7 @@ export const ResponseDetailsWrap = styled.div`
padding: 10px; padding: 10px;
`; `;
export const HeadersCaption = UnderlinedHeader.withComponent('caption').extend` export const HeadersCaption = styled(UnderlinedHeader.withComponent('caption'))`
text-align: left; text-align: left;
margin-top: 1em; margin-top: 1em;
caption-side: top; caption-side: top;

View File

@ -3,6 +3,11 @@ import * as React from 'react';
import { Schema, SchemaProps } from './Schema'; import { Schema, SchemaProps } from './Schema';
import { ArrayClosingLabel, ArrayOpenningLabel } from '../../common-elements'; import { ArrayClosingLabel, ArrayOpenningLabel } from '../../common-elements';
import styled from '../../styled-components';
const PaddedSchema = styled.div`
padding-left: ${({ theme }) => theme.spacing.unit * 2}px;
`;
export class ArraySchema extends React.PureComponent<SchemaProps> { export class ArraySchema extends React.PureComponent<SchemaProps> {
render() { render() {
@ -10,7 +15,9 @@ export class ArraySchema extends React.PureComponent<SchemaProps> {
return ( return (
<div> <div>
<ArrayOpenningLabel> Array </ArrayOpenningLabel> <ArrayOpenningLabel> Array </ArrayOpenningLabel>
<Schema {...this.props} schema={itemsSchema} /> <PaddedSchema>
<Schema {...this.props} schema={itemsSchema} />
</PaddedSchema>
<ArrayClosingLabel /> <ArrayClosingLabel />
</div> </div>
); );

View File

@ -7,6 +7,7 @@ import { MenuItem } from '../SideMenu/MenuItem';
import { MarkerService } from '../../services/MarkerService'; import { MarkerService } from '../../services/MarkerService';
import { SearchResult } from '../../services/SearchWorker.worker'; import { SearchResult } from '../../services/SearchWorker.worker';
import { PerfectScrollbarWrap } from '../../common-elements/perfect-scrollbar';
import { import {
ClearIcon, ClearIcon,
SearchIcon, SearchIcon,
@ -135,21 +136,27 @@ export class SearchBox extends React.PureComponent<SearchBoxProps, SearchBoxStat
onChange={this.search} onChange={this.search}
/> />
{results.length > 0 && ( {results.length > 0 && (
<SearchResultsBox data-role="search:results"> <PerfectScrollbarWrap
{results.map((res, idx) => ( options={{
<MenuItem wheelPropagation: false,
item={Object.create(res.item, { }}
active: { >
value: idx === activeItemIdx, <SearchResultsBox data-role="search:results">
}, {results.map((res, idx) => (
})} <MenuItem
onActivate={this.props.onActivate} item={Object.create(res.item, {
withoutChildren={true} active: {
key={res.item.id} value: idx === activeItemIdx,
data-role="search:result" },
/> })}
))} onActivate={this.props.onActivate}
</SearchResultsBox> withoutChildren={true}
key={res.item.id}
data-role="search:result"
/>
))}
</SearchResultsBox>
</PerfectScrollbarWrap>
)} )}
</SearchWrap> </SearchWrap>
); );

View File

@ -24,7 +24,7 @@ export const SearchInput = styled.input.attrs({
outline: none; outline: none;
`; `;
export const SearchIcon = styled((props: any) => ( export const SearchIcon = styled((props: { className?: string }) => (
<svg <svg
className={props.className} className={props.className}
version="1.1" version="1.1"
@ -58,7 +58,6 @@ export const SearchResultsBox = styled.div`
margin-top: 10px; margin-top: 10px;
line-height: 1.4; line-height: 1.4;
font-size: 0.9em; font-size: 0.9em;
overflow: auto;
${MenuItemLabel} { ${MenuItemLabel} {
padding-top: 6px; padding-top: 6px;

View File

@ -3,7 +3,7 @@ import * as React from 'react';
import styled from '../../styled-components'; import styled from '../../styled-components';
import { UnderlinedHeader } from '../../common-elements/headers'; import { Link, UnderlinedHeader } from '../../common-elements/';
import { SecurityRequirementModel } from '../../services/models/SecurityRequirement'; import { SecurityRequirementModel } from '../../services/models/SecurityRequirement';
import { linksCss } from '../Markdown/styled.elements'; import { linksCss } from '../Markdown/styled.elements';
@ -70,9 +70,11 @@ export class SecurityRequirement extends React.PureComponent<SecurityRequirement
{security.schemes.map(scheme => { {security.schemes.map(scheme => {
return ( return (
<SecurityRequirementAndWrap key={scheme.id}> <SecurityRequirementAndWrap key={scheme.id}>
<a href={'#' + scheme.sectionId}>{scheme.id}</a> <Link to={scheme.sectionId}>{scheme.id}</Link>
{scheme.scopes.length > 0 && ' ('} {scheme.scopes.length > 0 && ' ('}
{scheme.scopes.map(scope => <ScopeName key={scope}>{scope}</ScopeName>)} {scheme.scopes.map(scope => (
<ScopeName key={scope}>{scope}</ScopeName>
))}
{scheme.scopes.length > 0 && ') '} {scheme.scopes.length > 0 && ') '}
</SecurityRequirementAndWrap> </SecurityRequirementAndWrap>
); );
@ -82,18 +84,23 @@ export class SecurityRequirement extends React.PureComponent<SecurityRequirement
} }
} }
const AuthHeaderColumn = styled.td``; const AuthHeaderColumn = styled.div`
flex: 1;
`;
const SecuritiesColumn = styled.td` const SecuritiesColumn = styled.div`
width: ${props => props.theme.schema.defaultDetailsWidth}; width: ${props => props.theme.schema.defaultDetailsWidth};
`; `;
const AuthHeader = UnderlinedHeader.extend` const AuthHeader = styled(UnderlinedHeader)`
display: inline-block; display: inline-block;
margin: 0;
`; `;
const Table = styled.table` const Wrap = styled.div`
width: 100%; width: 100%;
display: flex;
margin: 1em 0;
`; `;
export interface SecurityRequirementsProps { export interface SecurityRequirementsProps {
@ -107,20 +114,16 @@ export class SecurityRequirements extends React.PureComponent<SecurityRequiremen
return null; return null;
} }
return ( return (
<Table> <Wrap>
<tbody> <AuthHeaderColumn>
<tr> <AuthHeader>Authorizations: </AuthHeader>
<AuthHeaderColumn> </AuthHeaderColumn>
<AuthHeader>Authorizations: </AuthHeader> <SecuritiesColumn>
</AuthHeaderColumn> {securities.map((security, idx) => (
<SecuritiesColumn> <SecurityRequirement key={idx} security={security} />
{securities.map((security, idx) => ( ))}
<SecurityRequirement key={idx} security={security} /> </SecuritiesColumn>
))} </Wrap>
</SecuritiesColumn>
</tr>
</tbody>
</Table>
); );
} }
} }

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import { SecuritySchemesModel } from '../../services/models'; import { SecuritySchemesModel } from '../../services/models';
import { H2, ShareLink } from '../../common-elements'; import { H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements';
import { OpenAPISecurityScheme } from '../../types'; import { OpenAPISecurityScheme } from '../../types';
import { Markdown } from '../Markdown/Markdown'; import { Markdown } from '../Markdown/Markdown';
import { StyledMarkdownBlock } from '../Markdown/styled.elements'; import { StyledMarkdownBlock } from '../Markdown/styled.elements';
@ -66,12 +66,12 @@ export interface SecurityDefsProps {
export class SecurityDefs extends React.PureComponent<SecurityDefsProps> { export class SecurityDefs extends React.PureComponent<SecurityDefsProps> {
render() { render() {
return ( return this.props.securitySchemes.schemes.map(scheme => (
<div> <Section id={scheme.sectionId} key={scheme.id}>
{this.props.securitySchemes.schemes.map(scheme => ( <Row>
<div data-section-id={scheme.sectionId} key={scheme.id}> <MiddlePanel>
<H2> <H2>
<ShareLink href={'#' + scheme.sectionId} /> <ShareLink to={scheme.sectionId} />
{scheme.id} {scheme.id}
</H2> </H2>
<Markdown source={scheme.description || ''} /> <Markdown source={scheme.description || ''} />
@ -118,9 +118,9 @@ export class SecurityDefs extends React.PureComponent<SecurityDefsProps> {
</tbody> </tbody>
</table> </table>
</StyledMarkdownBlock> </StyledMarkdownBlock>
</div> </MiddlePanel>
))} </Row>
</div> </Section>
); ));
} }
} }

View File

@ -3,6 +3,7 @@ import * as React from 'react';
import { ShelfIcon } from '../../common-elements/shelfs'; import { ShelfIcon } from '../../common-elements/shelfs';
import { IMenuItem, OperationModel } from '../../services'; import { IMenuItem, OperationModel } from '../../services';
import { shortenHTTPVerb } from '../../utils/openapi';
import { MenuItems } from './MenuItems'; import { MenuItems } from './MenuItems';
import { MenuItemLabel, MenuItemLi, MenuItemTitle, OperationBadge } from './styled.elements'; import { MenuItemLabel, MenuItemLi, MenuItemTitle, OperationBadge } from './styled.elements';
@ -51,7 +52,7 @@ export class MenuItem extends React.Component<MenuItemProps> {
{item.type === 'operation' ? ( {item.type === 'operation' ? (
<OperationMenuItemContent {...this.props} item={item as OperationModel} /> <OperationMenuItemContent {...this.props} item={item as OperationModel} />
) : ( ) : (
<MenuItemLabel depth={item.depth} active={item.active || item.expanded} type={item.type}> <MenuItemLabel depth={item.depth} active={item.active} type={item.type}>
<MenuItemTitle title={item.name}> <MenuItemTitle title={item.name}>
{item.name} {item.name}
{this.props.children} {this.props.children}
@ -93,7 +94,7 @@ class OperationMenuItemContent extends React.Component<OperationMenuItemContentP
active={item.active} active={item.active}
deprecated={item.deprecated} deprecated={item.deprecated}
> >
<OperationBadge type={item.httpVerb} /> <OperationBadge type={item.httpVerb}>{shortenHTTPVerb(item.httpVerb)}</OperationBadge>
<MenuItemTitle width="calc(100% - 32px)"> <MenuItemTitle width="calc(100% - 32px)">
{item.name} {item.name}
{this.props.children} {this.props.children}

View File

@ -1,11 +1,10 @@
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import * as React from 'react'; import * as React from 'react';
import { OptionsContext } from '../OptionsProvider';
import { IMenuItem, MenuStore } from '../../services/MenuStore'; import { IMenuItem, MenuStore } from '../../services/MenuStore';
import { MenuItems } from './MenuItems'; import { MenuItems } from './MenuItems';
import { PerfectScrollbar } from '../../common-elements/perfect-scrollbar'; import { PerfectScrollbarWrap } from '../../common-elements/perfect-scrollbar';
import { RedocAttribution } from './styled.elements'; import { RedocAttribution } from './styled.elements';
@observer @observer
@ -15,31 +14,20 @@ export class SideMenu extends React.Component<{ menu: MenuStore; className?: str
render() { render() {
const store = this.props.menu; const store = this.props.menu;
return ( return (
<OptionsContext.Consumer> <PerfectScrollbarWrap
{options => updateFn={this.saveScrollUpdate}
options.nativeScrollbars ? ( className={this.props.className}
<MenuItems options={{
className={this.props.className} wheelPropagation: false,
style={{ }}
overflow: 'auto', >
msOverflowStyle: '-ms-autohiding-scrollbar', <MenuItems items={store.items} onActivate={this.activate} root={true} />
}} <RedocAttribution>
items={store.items} <a target="_blank" href="https://github.com/Rebilly/ReDoc">
onActivate={this.activate} Documentation Powered by ReDoc
root={true} </a>
/> </RedocAttribution>
) : ( </PerfectScrollbarWrap>
<PerfectScrollbar updateFn={this.saveScrollUpdate} className={this.props.className}>
<MenuItems items={store.items} onActivate={this.activate} root={true} />
<RedocAttribution>
<a target="_blank" href="https://github.com/Rebilly/ReDoc">
Documentation Powered by ReDoc
</a>
</RedocAttribution>
</PerfectScrollbar>
)
}
</OptionsContext.Consumer>
); );
} }

View File

@ -8,59 +8,55 @@ export const OperationBadge = withProps<{ type: string }>(styled.span).attrs({
})` })`
width: 26px; width: 26px;
display: inline-block; display: inline-block;
height: ${props => props.theme.typography.code.fontSize};; height: ${props => props.theme.typography.code.fontSize};
line-height: ${props => props.theme.typography.code.fontSize};
background-color: #333; background-color: #333;
border-radius: 3px; border-radius: 3px;
vertical-align: top;
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAACgCAMAAADZ0KclAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAAZQTFRF////////VXz1bAAAAAJ0Uk5T/wDltzBKAAAA80lEQVR42uSWSwLCIAxEX+5/aa2QZBJw5UIt9QMdRqSPEAAw/TyvqzZf150NzdXL49qreXwXjeqz9bqN1tgJl/KLyaVrrL7K7gx+1vlNMqy+helOO4rfBGYZiEkq1ubQ3DeKvc97Et+d+e01vIZlLZZqb1WNJFd8ZKYsmv4Hh3H2fDgjMUI5WSExjiEZs7rEZ5T+/jQn9lhgsw53j/e9MQtxqPsbZY54M5fNl/MY/f1s7NbRSkYlYjc0KPsWMrmhIU9933ywxDiSE+upYNH8TdusUotllNvcAUzfnE/NC4OSYyklQhpdl9E4Tw0Cm4/G9xBgAO7VCkjWLOMfAAAAAElFTkSuQmCC");
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: 6px 4px; background-position: 6px 4px;
text-indent: -9000px; font-size: 7px;
font-family: Verdana; // web-safe
color: white;
text-transform: uppercase;
text-align: center;
font-weight: bold;
vertical-align: middle;
margin-right: 6px; margin-right: 6px;
margin-top: 2px; margin-top: 2px;
&.get { &.get {
background-position: 8px -12px;
background-color: ${props => props.theme.colors.http.get}; background-color: ${props => props.theme.colors.http.get};
} }
&.post { &.post {
background-position: 6px 4px;
background-color: ${props => props.theme.colors.http.post}; background-color: ${props => props.theme.colors.http.post};
} }
&.put { &.put {
background-position: 8px -28px;
background-color: ${props => props.theme.colors.http.put}; background-color: ${props => props.theme.colors.http.put};
} }
&.options { &.options {
background-position: 4px -148px;
background-color: ${props => props.theme.colors.http.options}; background-color: ${props => props.theme.colors.http.options};
} }
&.patch { &.patch {
background-position: 4px -114px;
background-color: ${props => props.theme.colors.http.patch}; background-color: ${props => props.theme.colors.http.patch};
} }
&.delete { &.delete {
background-position: 4px -44px;
background-color: ${props => props.theme.colors.http.delete}; background-color: ${props => props.theme.colors.http.delete};
} }
&.basic { &.basic {
background-position: 5px -79px;
background-color: ${props => props.theme.colors.http.basic}; background-color: ${props => props.theme.colors.http.basic};
} }
&.link { &.link {
background-position: 4px -131px;
background-color: ${props => props.theme.colors.http.link}; background-color: ${props => props.theme.colors.http.link};
} }
&.head { &.head {
background-position: 6px -102px;
background-color: ${props => props.theme.colors.http.head}; background-color: ${props => props.theme.colors.http.head};
} }
`; `;
@ -120,13 +116,15 @@ export const MenuItemLabel = withProps<{
active: boolean; active: boolean;
deprecated?: boolean; deprecated?: boolean;
type?: string; type?: string;
}>(styled.label).attrs({ }>(
role: 'menuitem', styled.label.attrs({
className: props => role: 'menuitem',
classnames('-depth' + props.depth, { className: props =>
active: props.active, classnames('-depth' + props.depth, {
}), active: props.active,
})` }),
}),
)`
cursor: pointer; cursor: pointer;
color: ${props => color: ${props =>
props.active ? props.theme.colors.primary.main : props.theme.colors.text.primary}; props.active ? props.theme.colors.primary.main : props.theme.colors.text.primary};
@ -164,9 +162,10 @@ export const MenuItemTitle = withProps<{ width?: string }>(styled.span)`
`; `;
export const RedocAttribution = styled.div` export const RedocAttribution = styled.div`
${({ theme }) => `
font-size: 0.8em; font-size: 0.8em;
margin-top: ${({ theme }) => `${theme.spacing.unit * 2}px`}; margin-top: ${theme.spacing.unit * 2}px;
padding: ${({ theme }) => `0 ${theme.spacing.unit * 4}px`}; padding: 0 ${theme.spacing.unit * 4}px;
text-align: left; text-align: left;
opacity: 0.7; opacity: 0.7;
@ -174,9 +173,10 @@ export const RedocAttribution = styled.div`
a, a,
a:visited, a:visited,
a:hover { a:hover {
color: ${({ theme }) => theme.colors.text.primary} !important; color: ${theme.colors.text.primary} !important;
border-top: 1px solid #e1e1e1; border-top: 1px solid #e1e1e1;
padding-top: 10px; padding: ${theme.spacing.unit}px 0;
display: block; display: block;
} }
`};
`; `;

View File

@ -4,8 +4,9 @@ import { highlight } from '../../utils';
import { SampleControls, SampleControlsWrap } from '../../common-elements'; import { SampleControls, SampleControlsWrap } from '../../common-elements';
import { CopyButtonWrapper } from '../../common-elements/CopyButtonWrapper'; import { CopyButtonWrapper } from '../../common-elements/CopyButtonWrapper';
import { PrismDiv } from '../../common-elements/PrismDiv'; import { PrismDiv } from '../../common-elements/PrismDiv';
import styled from '../../styled-components';
const StyledPre = PrismDiv.withComponent('pre').extend` const StyledPre = styled(PrismDiv.withComponent('pre'))`
font-family: ${props => props.theme.typography.code.fontFamily}; font-family: ${props => props.theme.typography.code.fontFamily};
font-size: ${props => props.theme.typography.code.fontSize}; font-size: ${props => props.theme.typography.code.fontSize};
overflow-x: auto; overflow-x: auto;

View File

@ -1,7 +1,6 @@
export * from './RedocStandalone'; export * from './RedocStandalone';
export * from './Redoc/Redoc'; export * from './Redoc/Redoc';
export * from './ApiInfo/ApiInfo'; export * from './ApiInfo/ApiInfo';
export * from './ApiInfo/ApiDescription';
export * from './ApiLogo/ApiLogo'; export * from './ApiLogo/ApiLogo';
export * from './ContentItems/ContentItems'; export * from './ContentItems/ContentItems';
export { ApiContentWrap, BackgroundStub, RedocWrap } from './Redoc/styled.elements'; export { ApiContentWrap, BackgroundStub, RedocWrap } from './Redoc/styled.elements';

View File

@ -1,5 +1,5 @@
export * from './components'; export * from './components';
export { MiddlePanel, Row, RightPanel } from './common-elements/'; export { MiddlePanel, Row, RightPanel, Section } from './common-elements/';
export * from './services'; export * from './services';
export * from './utils'; export * from './utils';

View File

@ -2,7 +2,7 @@ import { observe } from 'mobx';
import { OpenAPISpec } from '../types'; import { OpenAPISpec } from '../types';
import { loadAndBundleSpec } from '../utils/loadAndBundleSpec'; import { loadAndBundleSpec } from '../utils/loadAndBundleSpec';
import { HistoryService } from './HistoryService'; import { history } from './HistoryService';
import { MarkerService } from './MarkerService'; import { MarkerService } from './MarkerService';
import { MenuStore } from './MenuStore'; import { MenuStore } from './MenuStore';
import { SpecStore } from './models'; import { SpecStore } from './models';
@ -10,6 +10,9 @@ import { RedocNormalizedOptions, RedocRawOptions } from './RedocNormalizedOption
import { ScrollService } from './ScrollService'; import { ScrollService } from './ScrollService';
import { SearchStore } from './SearchStore'; import { SearchStore } from './SearchStore';
import { SecurityDefs } from '../components/SecuritySchemes/SecuritySchemes';
import { SECURITY_DEFINITIONS_COMPONENT_NAME } from '../utils/openapi';
export interface StoreState { export interface StoreState {
menu: { menu: {
activeItemIdx: number; activeItemIdx: number;
@ -64,14 +67,14 @@ export class AppStore {
createSearchIndex: boolean = true, createSearchIndex: boolean = true,
) { ) {
this.rawOptions = options; this.rawOptions = options;
this.options = new RedocNormalizedOptions(options); this.options = new RedocNormalizedOptions(options, DEFAULT_OPTIONS);
this.scroll = new ScrollService(this.options); this.scroll = new ScrollService(this.options);
// update position statically based on hash (in case of SSR) // update position statically based on hash (in case of SSR)
MenuStore.updateOnHash(HistoryService.hash, this.scroll); MenuStore.updateOnHistory(history.currentId, this.scroll);
this.spec = new SpecStore(spec, specUrl, this.options); this.spec = new SpecStore(spec, specUrl, this.options);
this.menu = new MenuStore(this.spec, this.scroll); this.menu = new MenuStore(this.spec, this.scroll, history);
if (!this.options.disableSearch) { if (!this.options.disableSearch) {
this.search = new SearchStore(); this.search = new SearchStore();
@ -86,7 +89,7 @@ export class AppStore {
} }
onDidMount() { onDidMount() {
this.menu.updateOnHash(); this.menu.updateOnHistory();
this.updateMarkOnMenu(this.menu.activeItemIdx); this.updateMarkOnMenu(this.menu.activeItemIdx);
} }
@ -137,3 +140,14 @@ export class AppStore {
this.marker.mark(); this.marker.mark();
} }
} }
const DEFAULT_OPTIONS: RedocRawOptions = {
allowedMdComponents: {
[SECURITY_DEFINITIONS_COMPONENT_NAME]: {
component: SecurityDefs,
propsSelector: (store: AppStore) => ({
securitySchemes: store.spec.securitySchemes,
}),
},
},
};

View File

@ -4,11 +4,7 @@ import { IS_BROWSER } from '../utils/';
const EVENT = 'hashchange'; const EVENT = 'hashchange';
function isSameHash(a: string, b: string): boolean { export class HistoryService {
return a === b || '#' + a === b || a === '#' + b;
}
export class IntHistoryService {
private _emiter; private _emiter;
constructor() { constructor() {
@ -16,8 +12,15 @@ export class IntHistoryService {
this.bind(); this.bind();
} }
get hash(): string { get currentId(): string {
return IS_BROWSER ? window.location.hash : ''; return IS_BROWSER ? window.location.hash.substring(1) : '';
}
linkForId(id: string) {
if (!id) {
return '';
}
return '#' + id;
} }
subscribe(cb): () => void { subscribe(cb): () => void {
@ -26,7 +29,7 @@ export class IntHistoryService {
} }
emit = () => { emit = () => {
this._emiter.emit(EVENT, this.hash); this._emiter.emit(EVENT, this.currentId);
}; };
bind() { bind() {
@ -43,26 +46,32 @@ export class IntHistoryService {
@bind @bind
@debounce @debounce
update(hash: string | null, rewriteHistory: boolean = false) { replace(id: string | null, rewriteHistory: boolean = false) {
if (hash == null || isSameHash(hash, this.hash)) { if (!IS_BROWSER) {
return;
}
if (id == null || id === this.currentId) {
return; return;
} }
if (rewriteHistory) { if (rewriteHistory) {
if (IS_BROWSER) { window.history.replaceState(
window.history.replaceState(null, '', window.location.href.split('#')[0] + '#' + hash); null,
} '',
window.location.href.split('#')[0] + this.linkForId(id),
);
return; return;
} }
if (IS_BROWSER) { window.history.pushState(null, '', window.location.href.split('#')[0] + this.linkForId(id));
window.history.pushState(null, '', window.location.href.split('#')[0] + '#' + hash); this.emit();
}
} }
} }
export const HistoryService = new IntHistoryService(); export const history = new HistoryService();
if (module.hot) { if (module.hot) {
module.hot.dispose(() => { module.hot.dispose(() => {
HistoryService.dispose(); history.dispose();
}); });
} }

View File

@ -2,6 +2,7 @@ import * as marked from 'marked';
import { highlight, safeSlugify } from '../utils'; import { highlight, safeSlugify } from '../utils';
import { AppStore } from './AppStore'; import { AppStore } from './AppStore';
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
const renderer = new marked.Renderer(); const renderer = new marked.Renderer();
@ -12,7 +13,7 @@ marked.setOptions({
}, },
}); });
export const LEGACY_REGEXP = '^\\s*<!-- ReDoc-Inject:\\s+?{component}\\s+?-->\\s*$'; export const LEGACY_REGEXP = '^\\s*<!-- ReDoc-Inject:\\s+?<{component}\\s*?/?>\\s+?-->\\s*$';
export const MDX_COMPONENT_REGEXP = '^\\s*<{component}\\s*?/>\\s*$'; export const MDX_COMPONENT_REGEXP = '^\\s*<{component}\\s*?/>\\s*$';
export const COMPONENT_REGEXP = '(?:' + LEGACY_REGEXP + '|' + MDX_COMPONENT_REGEXP + ')'; export const COMPONENT_REGEXP = '(?:' + LEGACY_REGEXP + '|' + MDX_COMPONENT_REGEXP + ')';
@ -35,13 +36,21 @@ export function buildComponentComment(name: string) {
} }
export class MarkdownRenderer { export class MarkdownRenderer {
static containsComponent(rawText: string, componentName: string) {
const anyCompRegexp = new RegExp(
COMPONENT_REGEXP.replace(/{component}/g, componentName),
'gmi',
);
return anyCompRegexp.test(rawText);
}
headings: MarkdownHeading[] = []; headings: MarkdownHeading[] = [];
currentTopHeading: MarkdownHeading; currentTopHeading: MarkdownHeading;
private headingEnhanceRenderer: marked.Renderer; private headingEnhanceRenderer: marked.Renderer;
private originalHeadingRule: typeof marked.Renderer.prototype.heading; private originalHeadingRule: typeof marked.Renderer.prototype.heading;
constructor() { constructor(public options?: RedocNormalizedOptions) {
this.headingEnhanceRenderer = new marked.Renderer(); this.headingEnhanceRenderer = new marked.Renderer();
this.originalHeadingRule = this.headingEnhanceRenderer.heading.bind( this.originalHeadingRule = this.headingEnhanceRenderer.heading.bind(
this.headingEnhanceRenderer, this.headingEnhanceRenderer,
@ -78,7 +87,9 @@ export class MarkdownRenderer {
} }
attachHeadingsDescriptions(rawText: string) { attachHeadingsDescriptions(rawText: string) {
const buildRegexp = heading => new RegExp(`##?\\s+${heading.name}`); const buildRegexp = heading => {
return new RegExp(`##?\\s+${heading.name.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}`);
};
const flatHeadings = this.flattenHeadings(this.headings); const flatHeadings = this.flattenHeadings(this.headings);
if (flatHeadings.length < 1) { if (flatHeadings.length < 1) {
@ -138,15 +149,17 @@ export class MarkdownRenderer {
// TODO: rewrite this completelly! Regexp-based 👎 // TODO: rewrite this completelly! Regexp-based 👎
// Use marked ecosystem // Use marked ecosystem
renderMdWithComponents( renderMdWithComponents(rawText: string): Array<string | MDXComponentMeta> {
rawText: string, const components = this.options && this.options.allowedMdComponents;
components: Dict<MDXComponentMeta>, if (!components || Object.keys(components).length === 0) {
): Array<string | MDXComponentMeta> { return [this.renderMd(rawText)];
}
const componentDefs: string[] = []; const componentDefs: string[] = [];
const names = '(?:' + Object.keys(components).join('|') + ')'; const names = '(?:' + Object.keys(components).join('|') + ')';
const anyCompRegexp = new RegExp( const anyCompRegexp = new RegExp(
COMPONENT_REGEXP.replace(/{component}/g, '(<?' + names + '.*?)'), COMPONENT_REGEXP.replace(/{component}/g, '(' + names + '.*?)'),
'gmi', 'gmi',
); );
let match = anyCompRegexp.exec(rawText); let match = anyCompRegexp.exec(rawText);
@ -187,10 +200,6 @@ function parseComponent(
componentName?: string; componentName?: string;
attrs: any; attrs: any;
} { } {
if (htmlTag.startsWith('<')) {
return legacyParse(htmlTag);
}
const match = /([\w_-]+)(\s+[\w_-]+\s*={[^}]*?})*/.exec(htmlTag); const match = /([\w_-]+)(\s+[\w_-]+\s*={[^}]*?})*/.exec(htmlTag);
if (match === null || match.length <= 1) { if (match === null || match.length <= 1) {
return { componentName: undefined, attrs: {} }; return { componentName: undefined, attrs: {} };
@ -214,20 +223,3 @@ function parseComponent(
attrs, attrs,
}; };
} }
function legacyParse(
htmlTag: string,
): {
componentName?: string;
attrs: any;
} {
const match = /<([\w_-]+).*?>/.exec(htmlTag);
if (match === null || match.length <= 1) {
return { componentName: undefined, attrs: {} };
}
const componentName = match[1];
return {
componentName,
attrs: {}, // TODO
};
}

View File

@ -1,5 +1,9 @@
import { OpenAPIOperation, OpenAPIParameter, OpenAPISpec, OpenAPITag, Referenced } from '../types'; import { OpenAPIOperation, OpenAPIParameter, OpenAPISpec, OpenAPITag, Referenced } from '../types';
import { isOperationName } from '../utils'; import {
isOperationName,
SECURITY_DEFINITIONS_COMPONENT_NAME,
setSecuritySchemePrefix,
} from '../utils';
import { MarkdownRenderer } from './MarkdownRenderer'; import { MarkdownRenderer } from './MarkdownRenderer';
import { GroupModel, OperationModel } from './models'; import { GroupModel, OperationModel } from './models';
import { OpenAPIParser } from './OpenAPIParser'; import { OpenAPIParser } from './OpenAPIParser';
@ -38,7 +42,7 @@ export class MenuBuilder {
const items: ContentItemModel[] = []; const items: ContentItemModel[] = [];
const tagsMap = MenuBuilder.getTagsWithOperations(spec); const tagsMap = MenuBuilder.getTagsWithOperations(spec);
items.push(...MenuBuilder.addMarkdownItems(spec.info.description || '')); items.push(...MenuBuilder.addMarkdownItems(spec.info.description || '', options));
if (spec['x-tagGroups']) { if (spec['x-tagGroups']) {
items.push( items.push(
...MenuBuilder.getTagGroupsItems(parser, undefined, spec['x-tagGroups'], tagsMap, options), ...MenuBuilder.getTagGroupsItems(parser, undefined, spec['x-tagGroups'], tagsMap, options),
@ -53,8 +57,11 @@ export class MenuBuilder {
* extracts items from markdown description * extracts items from markdown description
* @param description - markdown source * @param description - markdown source
*/ */
static addMarkdownItems(description: string): ContentItemModel[] { static addMarkdownItems(
const renderer = new MarkdownRenderer(); description: string,
options: RedocNormalizedOptions,
): ContentItemModel[] {
const renderer = new MarkdownRenderer(options);
const headings = renderer.extractHeadings(description || ''); const headings = renderer.extractHeadings(description || '');
const mapHeadingsDeep = (parent, items, depth = 1) => const mapHeadingsDeep = (parent, items, depth = 1) =>
@ -64,6 +71,14 @@ export class MenuBuilder {
if (heading.items) { if (heading.items) {
group.items = mapHeadingsDeep(group, heading.items, depth + 1); group.items = mapHeadingsDeep(group, heading.items, depth + 1);
} }
if (
MarkdownRenderer.containsComponent(
group.description || '',
SECURITY_DEFINITIONS_COMPONENT_NAME,
)
) {
setSecuritySchemePrefix(group.id + '/');
}
return group; return group;
}); });

View File

@ -2,10 +2,10 @@ import { action, observable } from 'mobx';
import { querySelector } from '../utils/dom'; import { querySelector } from '../utils/dom';
import { SpecStore } from './models'; import { SpecStore } from './models';
import { HistoryService } from './HistoryService'; import { history as historyInst, HistoryService } from './HistoryService';
import { ScrollService } from './ScrollService'; import { ScrollService } from './ScrollService';
import { flattenByProp, normalizeHash } from '../utils'; import { flattenByProp, SECURITY_SCHEMES_SECTION_PREFIX } from '../utils';
import { GROUP_DEPTH } from './MenuBuilder'; import { GROUP_DEPTH } from './MenuBuilder';
export type MenuItemGroupType = 'group' | 'tag' | 'section'; export type MenuItemGroupType = 'group' | 'tag' | 'section';
@ -42,22 +42,24 @@ export class MenuStore {
* Statically try update scroll position * Statically try update scroll position
* Used before hydrating from server-side rendered html to scroll page faster * Used before hydrating from server-side rendered html to scroll page faster
*/ */
static updateOnHash(hash: string = HistoryService.hash, scroll: ScrollService) { static updateOnHistory(id: string = historyInst.currentId, scroll: ScrollService) {
if (!hash) { if (!id) {
return; return;
} }
scroll.scrollIntoViewBySelector(`[${SECTION_ATTR}="${normalizeHash(hash)}"]`); scroll.scrollIntoViewBySelector(`[${SECTION_ATTR}="${id}"]`);
} }
/** /**
* active item absolute index (when flattened). -1 means nothing is selected * active item absolute index (when flattened). -1 means nothing is selected
*/ */
@observable activeItemIdx: number = -1; @observable
activeItemIdx: number = -1;
/** /**
* whether sidebar with menu is opened or not * whether sidebar with menu is opened or not
*/ */
@observable sideBarOpened: boolean = false; @observable
sideBarOpened: boolean = false;
items: IMenuItem[]; items: IMenuItem[];
flatItems: IMenuItem[]; flatItems: IMenuItem[];
@ -73,8 +75,8 @@ export class MenuStore {
* @param spec [SpecStore](#SpecStore) which contains page content structure * @param spec [SpecStore](#SpecStore) which contains page content structure
* @param scroll scroll service instance used by this menu * @param scroll scroll service instance used by this menu
*/ */
constructor(spec: SpecStore, public scroll: ScrollService) { constructor(spec: SpecStore, public scroll: ScrollService, public history: HistoryService) {
this.items = spec.operationGroups; this.items = spec.contentItems;
this.flatItems = flattenByProp(this.items || [], 'items'); this.flatItems = flattenByProp(this.items || [], 'items');
this.flatItems.forEach((item, idx) => (item.absoluteIdx = idx)); this.flatItems.forEach((item, idx) => (item.absoluteIdx = idx));
@ -84,7 +86,7 @@ export class MenuStore {
subscribe() { subscribe() {
this._unsubscribe = this.scroll.subscribe(this.updateOnScroll); this._unsubscribe = this.scroll.subscribe(this.updateOnScroll);
this._hashUnsubscribe = HistoryService.subscribe(this.updateOnHash); this._hashUnsubscribe = this.history.subscribe(this.updateOnHistory);
} }
@action @action
@ -132,22 +134,24 @@ export class MenuStore {
/** /**
* update active items on hash change * update active items on hash change
* @param hash current hash * @param id current hash
*/ */
updateOnHash = (hash: string = HistoryService.hash): boolean => { updateOnHistory = (id: string = this.history.currentId) => {
if (!hash) { if (!id) {
return false; return;
} }
let item: IMenuItem | undefined; let item: IMenuItem | undefined;
hash = normalizeHash(hash);
item = this.flatItems.find(i => i.id === hash); item = this.flatItems.find(i => i.id === id);
if (item) { if (item) {
this.activateAndScroll(item, false); this.activateAndScroll(item, false);
} else { } else {
this.scroll.scrollIntoViewBySelector(`[${SECTION_ATTR}="${hash}"]`); if (id.startsWith(SECURITY_SCHEMES_SECTION_PREFIX)) {
item = this.flatItems.find(i => SECURITY_SCHEMES_SECTION_PREFIX.startsWith(i.id));
this.activate(item);
}
this.scroll.scrollIntoViewBySelector(`[${SECTION_ATTR}="${id}"]`);
} }
return item !== undefined;
}; };
/** /**
@ -173,13 +177,13 @@ export class MenuStore {
/** /**
* activate menu item * activate menu item
* @param item item to activate * @param item item to activate
* @param updateHash [true] whether to update location hash * @param updateLocation [true] whether to update location
* @param rewriteHistory [false] whether to rewrite browser history (do not create new enrty) * @param rewriteHistory [false] whether to rewrite browser history (do not create new enrty)
*/ */
@action @action
activate( activate(
item: IMenuItem | undefined, item: IMenuItem | undefined,
updateHash: boolean = true, updateLocation: boolean = true,
rewriteHistory: boolean = false, rewriteHistory: boolean = false,
) { ) {
if ((this.activeItem && this.activeItem.id) === (item && item.id)) { if ((this.activeItem && this.activeItem.id) === (item && item.id)) {
@ -187,7 +191,7 @@ export class MenuStore {
} }
this.deactivate(this.activeItem); this.deactivate(this.activeItem);
if (!item) { if (!item) {
HistoryService.update('', rewriteHistory); this.history.replace('', rewriteHistory);
return; return;
} }
@ -198,8 +202,8 @@ export class MenuStore {
} }
this.activeItemIdx = item.absoluteIdx!; this.activeItemIdx = item.absoluteIdx!;
if (updateHash) { if (updateLocation) {
HistoryService.update(item.id, rewriteHistory); this.history.replace(item.id, rewriteHistory);
} }
item.activate(); item.activate();
@ -226,10 +230,14 @@ export class MenuStore {
* @see MenuStore.activate * @see MenuStore.activate
*/ */
@action.bound @action.bound
activateAndScroll(item: IMenuItem | undefined, updateHash?: boolean, rewriteHistory?: boolean) { activateAndScroll(
item: IMenuItem | undefined,
updateLocation?: boolean,
rewriteHistory?: boolean,
) {
// item here can be a copy from search results so find corresponding item from menu // item here can be a copy from search results so find corresponding item from menu
const menuItem = (item && this.getItemById(item.id)) || item; const menuItem = (item && this.getItemById(item.id)) || item;
this.activate(menuItem, updateHash, rewriteHistory); this.activate(menuItem, updateLocation, rewriteHistory);
this.scrollToActive(); this.scrollToActive();
if (!menuItem || !menuItem.items.length) { if (!menuItem || !menuItem.items.length) {
this.closeSidebar(); this.closeSidebar();

View File

@ -4,8 +4,8 @@ import { OpenAPIRef, OpenAPISchema, OpenAPISpec, Referenced } from '../types';
import { appendToMdHeading, IS_BROWSER } from '../utils/'; import { appendToMdHeading, IS_BROWSER } from '../utils/';
import { JsonPointer } from '../utils/JsonPointer'; import { JsonPointer } from '../utils/JsonPointer';
import { isNamedDefinition } from '../utils/openapi'; import { isNamedDefinition, SECURITY_DEFINITIONS_COMPONENT_NAME } from '../utils/openapi';
import { buildComponentComment, COMPONENT_REGEXP, MDX_COMPONENT_REGEXP } from './MarkdownRenderer'; import { buildComponentComment, MarkdownRenderer } from './MarkdownRenderer';
import { RedocNormalizedOptions } from './RedocNormalizedOptions'; import { RedocNormalizedOptions } from './RedocNormalizedOptions';
export type MergedOpenAPISchema = OpenAPISchema & { parentRefs?: string[] }; export type MergedOpenAPISchema = OpenAPISchema & { parentRefs?: string[] };
@ -74,16 +74,8 @@ export class OpenAPIParser {
) { ) {
// Automatically inject Authentication section with SecurityDefinitions component // Automatically inject Authentication section with SecurityDefinitions component
const description = spec.info.description || ''; const description = spec.info.description || '';
const legacySecurityRegexp = new RegExp( if (!MarkdownRenderer.containsComponent(description, SECURITY_DEFINITIONS_COMPONENT_NAME)) {
COMPONENT_REGEXP.replace('{component}', '<security-definitions>'), const comment = buildComponentComment(SECURITY_DEFINITIONS_COMPONENT_NAME);
'mi',
);
const securityRegexp = new RegExp(
MDX_COMPONENT_REGEXP.replace('{component}', 'security-definitions'),
'mi',
);
if (!legacySecurityRegexp.test(description) && !securityRegexp.test(description)) {
const comment = buildComponentComment('security-definitions');
spec.info.description = appendToMdHeading(description, 'Authentication', comment); spec.info.description = appendToMdHeading(description, 'Authentication', comment);
} }
} }
@ -264,7 +256,9 @@ export class OpenAPIParser {
if (subSchemaRef) { if (subSchemaRef) {
receiver.parentRefs!.push(subSchemaRef); receiver.parentRefs!.push(subSchemaRef);
if (receiver.title === undefined && isNamedDefinition(subSchemaRef)) { if (receiver.title === undefined && isNamedDefinition(subSchemaRef)) {
receiver.title = JsonPointer.baseName(subSchemaRef); // this is not so correct behaviour. comented out for now
// ref: https://github.com/Rebilly/ReDoc/issues/601
// receiver.title = JsonPointer.baseName(subSchemaRef);
} }
} }
} }

View File

@ -2,6 +2,8 @@ import defaultTheme, { ResolvedThemeInterface, resolveTheme, ThemeInterface } fr
import { querySelector } from '../utils/dom'; import { querySelector } from '../utils/dom';
import { isNumeric, mergeObjects } from '../utils/helpers'; import { isNumeric, mergeObjects } from '../utils/helpers';
import { MDXComponentMeta } from './MarkdownRenderer';
export interface RedocRawOptions { export interface RedocRawOptions {
theme?: ThemeInterface; theme?: ThemeInterface;
scrollYOffset?: number | string | (() => number); scrollYOffset?: number | string | (() => number);
@ -17,6 +19,8 @@ export interface RedocRawOptions {
disableSearch?: boolean | string; disableSearch?: boolean | string;
unstable_ignoreMimeParameters?: boolean; unstable_ignoreMimeParameters?: boolean;
allowedMdComponents?: Dict<MDXComponentMeta>;
} }
function argValueToBoolean(val?: string | boolean): boolean { function argValueToBoolean(val?: string | boolean): boolean {
@ -98,9 +102,11 @@ export class RedocNormalizedOptions {
/* tslint:disable-next-line */ /* tslint:disable-next-line */
unstable_ignoreMimeParameters: boolean; unstable_ignoreMimeParameters: boolean;
allowedMdComponents: Dict<MDXComponentMeta>;
constructor(raw: RedocRawOptions) { constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) {
let hook; let hook;
raw = { ...defaults, ...raw };
if (raw.theme && raw.theme.extensionsHook) { if (raw.theme && raw.theme.extensionsHook) {
hook = raw.theme.extensionsHook; hook = raw.theme.extensionsHook;
raw.theme.extensionsHook = undefined; raw.theme.extensionsHook = undefined;
@ -120,5 +126,7 @@ export class RedocNormalizedOptions {
this.disableSearch = argValueToBoolean(raw.disableSearch); this.disableSearch = argValueToBoolean(raw.disableSearch);
this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters); this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters);
this.allowedMdComponents = raw.allowedMdComponents || {};
} }
} }

View File

@ -13,7 +13,7 @@ export class SpecStore {
info: ApiInfoModel; info: ApiInfoModel;
externalDocs?: OpenAPIExternalDocumentation; externalDocs?: OpenAPIExternalDocumentation;
operationGroups: ContentItemModel[]; contentItems: ContentItemModel[];
securitySchemes: SecuritySchemesModel; securitySchemes: SecuritySchemesModel;
constructor( constructor(
@ -24,8 +24,7 @@ export class SpecStore {
this.parser = new OpenAPIParser(spec, specUrl, options); this.parser = new OpenAPIParser(spec, specUrl, options);
this.info = new ApiInfoModel(this.parser); this.info = new ApiInfoModel(this.parser);
this.externalDocs = this.parser.spec.externalDocs; this.externalDocs = this.parser.spec.externalDocs;
this.operationGroups = MenuBuilder.buildStructure(this.parser, this.options); this.contentItems = MenuBuilder.buildStructure(this.parser, this.options);
this.securitySchemes = new SecuritySchemesModel(this.parser); this.securitySchemes = new SecuritySchemesModel(this.parser);
} }
} }

View File

@ -1,9 +1,21 @@
import { MarkdownRenderer } from '../MarkdownRenderer'; import { MarkdownRenderer, MDXComponentMeta } from '../MarkdownRenderer';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
const TestComponent = () => null;
describe('Markdown renderer', () => { describe('Markdown renderer', () => {
let renderer: MarkdownRenderer; let renderer: MarkdownRenderer;
beforeEach(() => { beforeEach(() => {
renderer = new MarkdownRenderer(); renderer = new MarkdownRenderer(
new RedocNormalizedOptions({
allowedMdComponents: {
'security-definitions': {
component: TestComponent,
propsSelector: () => ({}),
},
},
}),
);
}); });
test('should return a level-1 heading even though only level-2 is present', () => { test('should return a level-1 heading even though only level-2 is present', () => {
@ -19,4 +31,33 @@ describe('Markdown renderer', () => {
expect(headings[0].items).toBeDefined(); expect(headings[0].items).toBeDefined();
expect(headings[0].items).toHaveLength(1); expect(headings[0].items).toHaveLength(1);
}); });
test('renderMdWithComponents should work with legacy syntax', () => {
const source = 'Hello!\n<!-- ReDoc-Inject: <security-definitions> -->\nBye';
const parts = renderer.renderMdWithComponents(source);
expect(parts).toHaveLength(3);
expect(parts[0]).toEqual('<p>Hello!</p>\n');
expect(typeof parts[1]).toEqual('object');
expect((parts[1] as MDXComponentMeta).component).toEqual(TestComponent);
expect(parts[2]).toEqual('<p>Bye</p>\n');
});
test('renderMdWithComponents should work with mdx-like syntax', () => {
const source = 'Hello!\n<security-definitions/>\nBye';
const parts = renderer.renderMdWithComponents(source);
expect(parts).toHaveLength(3);
expect(parts[0]).toEqual('<p>Hello!</p>\n');
expect(typeof parts[1]).toEqual('object');
expect((parts[1] as MDXComponentMeta).component).toBe(TestComponent);
expect(parts[2]).toEqual('<p>Bye</p>\n');
});
test('renderMdWithComponents should parse attribute names', () => {
const source = '<security-definitions pointer={"test"}/>';
const parts = renderer.renderMdWithComponents(source);
expect(parts).toHaveLength(1);
const part = parts[0] as MDXComponentMeta;
expect(part.component).toBe(TestComponent);
expect(part.attrs).toEqual({ pointer: 'test' });
});
}); });

View File

@ -0,0 +1,79 @@
{
"openapi": "3.0.0",
"info": {
"version": "1.0",
"title": "Foo"
},
"components": {
"schemas": {
"Test": {
"type": "object",
"properties": {
"any": {
"anyOf": [
{
"$ref": "#/components/schemas/Foo"
},
{
"$ref": "#/components/schemas/Bar"
}
]
},
"one": {
"oneOf": [
{
"$ref": "#/components/schemas/Foo"
},
{
"$ref": "#/components/schemas/Bar"
}
]
},
"all": {
"allOf": [
{
"$ref": "#/components/schemas/Foo"
},
{
"$ref": "#/components/schemas/Bar"
}
]
}
}
},
"Foo": {
"type": "object",
"properties": {
"foo": {
"type": "string"
}
}
},
"Bar": {
"type": "object",
"properties": {
"bar": {
"type": "string"
}
}
},
"WithArray": {
"oneOf": [{
"type" : "array",
"items": {
"oneOf": [
{
"type": "string"
},
{
"type": "number"
}
]
}
}, {
"type": "string"
}]
}
}
}
}

View File

@ -1,25 +1,38 @@
import { HistoryService } from '../HistoryService'; import { history } from '../HistoryService';
describe('History service', () => { describe('History service', () => {
test('should be an instance', () => { test('should be an instance', () => {
expect(typeof HistoryService).not.toBe('function'); expect(typeof history).not.toBe('function');
expect(HistoryService.subscribe).toBeDefined(); expect(history.subscribe).toBeDefined();
}); });
test('History subscribe', () => { test('History subscribe', () => {
const fn = jest.fn(); const fn = jest.fn();
HistoryService.subscribe(fn); history.subscribe(fn);
HistoryService.emit(); history.emit();
expect(fn).toHaveBeenCalled(); expect(fn).toHaveBeenCalled();
}); });
test('History subscribe should return unsubsribe function', () => { test('History subscribe should return unsubsribe function', () => {
const fn = jest.fn(); const fn = jest.fn();
const unsubscribe = HistoryService.subscribe(fn); const unsubscribe = history.subscribe(fn);
HistoryService.emit(); history.emit();
expect(fn).toHaveBeenCalled(); expect(fn).toHaveBeenCalled();
unsubscribe(); unsubscribe();
HistoryService.emit(); history.emit();
expect(fn).toHaveBeenCalledTimes(1); expect(fn).toHaveBeenCalledTimes(1);
}); });
test('currentId should return correct id', () => {
window.location.hash = '#testid';
expect(history.currentId).toEqual('testid');
});
test('should return correct link for id', () => {
expect(history.linkForId('testid')).toEqual('#testid');
});
test('should return empty link for empty id', () => {
expect(history.linkForId('')).toEqual('');
});
}); });

View File

@ -15,5 +15,29 @@ describe('Models', () => {
expect(schema.oneOf).toHaveLength(1); expect(schema.oneOf).toHaveLength(1);
expect(schema.discriminatorProp).toEqual('type'); expect(schema.discriminatorProp).toEqual('type');
}); });
test('oneOf/allOf titles', () => {
const spec = require('../fixtures/oneOfTitles.json');
parser = new OpenAPIParser(spec, undefined, opts);
const schema = new SchemaModel(parser, spec.components.schemas.Test, '', opts);
expect(schema.fields).toHaveLength(3);
const oneOfField = schema.fields[0];
expect(oneOfField.schema.displayType).toBe('Foo (object) or Bar (object)');
expect(oneOfField.schema.oneOf[0].title).toBe('Foo');
expect(oneOfField.schema.oneOf[1].title).toBe('Bar');
const anyOfField = schema.fields[1];
expect(anyOfField.schema.displayType).toBe('Foo (object) or Bar (object)');
expect(anyOfField.schema.oneOf[0].title).toBe('Foo');
expect(anyOfField.schema.oneOf[1].title).toBe('Bar');
});
test('oneOf/allOf schema complex displayType', () => {
const spec = require('../fixtures/oneOfTitles.json');
parser = new OpenAPIParser(spec, undefined, opts);
const schema = new SchemaModel(parser, spec.components.schemas.WithArray, '', opts);
expect(schema.oneOf).toHaveLength(2);
expect(schema.displayType).toBe('(Array of string or number) or string');
});
}); });
}); });

View File

@ -16,4 +16,8 @@ describe('prism.js helpers', () => {
test('highlight raw text should just return text', () => { test('highlight raw text should just return text', () => {
expect(highlight('Hello world', 'clike')).toBe('Hello world'); expect(highlight('Hello world', 'clike')).toBe('Hello world');
}); });
test('highlight should not throw with lang undefined', () => {
expect(highlight('Hello world', undefined)).toBe('Hello world');
});
}); });

View File

@ -10,7 +10,8 @@ import { SchemaModel } from './Schema';
* Field or Parameter model ready to be used by components * Field or Parameter model ready to be used by components
*/ */
export class FieldModel { export class FieldModel {
@observable expanded: boolean = false; @observable
expanded: boolean = false;
schema: SchemaModel; schema: SchemaModel;
name: string; name: string;

View File

@ -21,8 +21,10 @@ export class GroupModel implements IMenuItem {
parent?: GroupModel; parent?: GroupModel;
externalDocs?: OpenAPIExternalDocumentation; externalDocs?: OpenAPIExternalDocumentation;
@observable active: boolean = false; @observable
@observable expanded: boolean = false; active: boolean = false;
@observable
expanded: boolean = false;
depth: number; depth: number;
level: number; level: number;

View File

@ -14,7 +14,8 @@ import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
export class MediaContentModel { export class MediaContentModel {
mediaTypes: MediaTypeModel[]; mediaTypes: MediaTypeModel[];
@observable activeMimeIdx = 0; @observable
activeMimeIdx = 0;
/** /**
* @param isRequestType needed to know if skipe RO/RW fields in objects * @param isRequestType needed to know if skipe RO/RW fields in objects

View File

@ -56,7 +56,7 @@ export class MediaTypeModel {
value: sample, value: sample,
}; };
} }
} else { } else if (this.schema) {
this.examples = { this.examples = {
default: new ExampleModel(parser, { default: new ExampleModel(parser, {
value: Sampler.sample( value: Sampler.sample(

View File

@ -40,9 +40,12 @@ export class OperationModel implements IMenuItem {
depth: number; depth: number;
@observable ready?: boolean = true; @observable
@observable active: boolean = false; ready?: boolean = true;
@observable expanded: boolean = false; @observable
active: boolean = false;
@observable
expanded: boolean = false;
//#endregion //#endregion
pointer: string; pointer: string;

View File

@ -9,7 +9,8 @@ import { FieldModel } from './Field';
import { MediaContentModel } from './MediaContent'; import { MediaContentModel } from './MediaContent';
export class ResponseModel { export class ResponseModel {
@observable expanded: boolean; @observable
expanded: boolean;
content?: MediaContentModel; content?: MediaContentModel;
code: string; code: string;

View File

@ -49,7 +49,8 @@ export class SchemaModel {
oneOf?: SchemaModel[]; oneOf?: SchemaModel[];
oneOfType: string; oneOfType: string;
discriminatorProp: string; discriminatorProp: string;
@observable activeOneOf: number = 0; @observable
activeOneOf: number = 0;
rawSchema: OpenAPISchema; rawSchema: OpenAPISchema;
schema: MergedOpenAPISchema; schema: MergedOpenAPISchema;
@ -162,13 +163,24 @@ export class SchemaModel {
parser, parser,
{ {
// merge base schema into each of oneOf's subschemas // merge base schema into each of oneOf's subschemas
allOf: [variant, { ...this.schema, oneOf: undefined, anyOf: undefined }], ...variant,
allOf: [{ ...this.schema, oneOf: undefined, anyOf: undefined }],
} as OpenAPISchema, } as OpenAPISchema,
this.pointer + '/oneOf/' + idx, this.pointer + '/oneOf/' + idx,
this.options, this.options,
), ),
); );
this.displayType = this.oneOf.map(schema => schema.displayType).join(' or '); this.displayType = this.oneOf
.map(schema => {
let name =
schema.typePrefix +
(schema.title ? `${schema.title} (${schema.displayType})` : schema.displayType);
if (name.indexOf(' or ') > -1) {
name = `(${name})`;
}
return name;
})
.join(' or ');
} }
private initDiscriminator( private initDiscriminator(

View File

@ -1,5 +1,5 @@
import { OpenAPISecurityRequirement, OpenAPISecurityScheme } from '../../types'; import { OpenAPISecurityRequirement, OpenAPISecurityScheme } from '../../types';
import { SECURITY_SCHEMES_SECTION } from '../../utils/openapi'; import { SECURITY_SCHEMES_SECTION_PREFIX } from '../../utils/openapi';
import { OpenAPIParser } from '../OpenAPIParser'; import { OpenAPIParser } from '../OpenAPIParser';
export interface SecurityScheme extends OpenAPISecurityScheme { export interface SecurityScheme extends OpenAPISecurityScheme {
@ -27,7 +27,7 @@ export class SecurityRequirementModel {
return { return {
...scheme, ...scheme,
id, id,
sectionId: SECURITY_SCHEMES_SECTION + id, sectionId: SECURITY_SCHEMES_SECTION_PREFIX + id,
scopes, scopes,
}; };
}) })

View File

@ -1,5 +1,5 @@
import { OpenAPISecurityScheme, Referenced } from '../../types'; import { OpenAPISecurityScheme, Referenced } from '../../types';
import { SECURITY_SCHEMES_SECTION } from '../../utils/openapi'; import { SECURITY_SCHEMES_SECTION_PREFIX } from '../../utils/openapi';
import { OpenAPIParser } from '../OpenAPIParser'; import { OpenAPIParser } from '../OpenAPIParser';
export class SecuritySchemeModel { export class SecuritySchemeModel {
@ -25,7 +25,7 @@ export class SecuritySchemeModel {
constructor(parser: OpenAPIParser, id: string, scheme: Referenced<OpenAPISecurityScheme>) { constructor(parser: OpenAPIParser, id: string, scheme: Referenced<OpenAPISecurityScheme>) {
const info = parser.deref(scheme); const info = parser.deref(scheme);
this.id = id; this.id = id;
this.sectionId = SECURITY_SCHEMES_SECTION + id; this.sectionId = SECURITY_SCHEMES_SECTION_PREFIX + id;
this.type = info.type; this.type = info.type;
this.description = info.description || ''; this.description = info.description || '';
if (info.type === 'apiKey') { if (info.type === 'apiKey') {

View File

@ -24,10 +24,6 @@ export function html2Str(html: string): string {
.join(' '); .join(' ');
} }
export function normalizeHash(hash: string): string {
return hash.startsWith('#') ? hash.substr(1) : hash;
}
// scrollIntoViewIfNeeded polyfill // scrollIntoViewIfNeeded polyfill
if (typeof Element !== 'undefined' && !(Element as any).prototype.scrollIntoViewIfNeeded) { if (typeof Element !== 'undefined' && !(Element as any).prototype.scrollIntoViewIfNeeded) {

View File

@ -16,6 +16,7 @@ import 'prismjs/components/prism-php.js';
import 'prismjs/components/prism-python.js'; import 'prismjs/components/prism-python.js';
import 'prismjs/components/prism-ruby.js'; import 'prismjs/components/prism-ruby.js';
import 'prismjs/components/prism-scala.js'; import 'prismjs/components/prism-scala.js';
import 'prismjs/components/prism-sql.js';
import 'prismjs/components/prism-swift.js'; import 'prismjs/components/prism-swift.js';
const DEFAULT_LANG = 'clike'; const DEFAULT_LANG = 'clike';
@ -42,7 +43,7 @@ export function mapLang(lang: string): string {
* @param lang highlight language * @param lang highlight language
* @return highlighted souce code as **html string** * @return highlighted souce code as **html string**
*/ */
export function highlight(source: string, lang: string): string { export function highlight(source: string, lang: string = DEFAULT_LANG): string {
lang = lang.toLowerCase(); lang = lang.toLowerCase();
let grammar = Prism.languages[lang]; let grammar = Prism.languages[lang];
if (!grammar) { if (!grammar) {

View File

@ -275,4 +275,14 @@ export function normalizeServers(
}); });
} }
export const SECURITY_SCHEMES_SECTION = 'section/Authentication/'; export const SECURITY_DEFINITIONS_COMPONENT_NAME = 'security-definitions';
export let SECURITY_SCHEMES_SECTION_PREFIX = 'section/Authentication/';
export function setSecuritySchemePrefix(prefix: string) {
SECURITY_SCHEMES_SECTION_PREFIX = prefix;
}
export const shortenHTTPVerb = verb =>
({
delete: 'del',
options: 'opts',
}[verb] || verb);

View File

@ -88,8 +88,11 @@ export default (env: { standalone?: boolean } = {}, { mode }) => ({
{ {
loader: 'babel-loader', loader: 'babel-loader',
options: { options: {
generatorOpts: {
decoratorsBeforeExport: true,
},
plugins: [ plugins: [
'@babel/plugin-syntax-typescript', ['@babel/plugin-syntax-typescript', { isTSX: true }],
['@babel/plugin-syntax-decorators', { legacy: true }], ['@babel/plugin-syntax-decorators', { legacy: true }],
'@babel/plugin-syntax-jsx', '@babel/plugin-syntax-jsx',
[ [
@ -114,6 +117,7 @@ export default (env: { standalone?: boolean } = {}, { mode }) => ({
transpileOnly: true, transpileOnly: true,
compilerOptions: { compilerOptions: {
allowJs: true, allowJs: true,
declaration: false,
}, },
}, },
}, },

1303
yarn.lock

File diff suppressed because it is too large Load Diff