feat: Add bundles

This commit is contained in:
Till Kolter 2020-08-25 23:36:10 +02:00
parent 49f04d321f
commit 422ec09771
34 changed files with 15457 additions and 426 deletions

2
.gitignore vendored
View File

@ -24,7 +24,7 @@ node_modules
lib/
stats.json
cypress/
bundles/
# bundles/
typings/*
!typings/styled-patch.d.ts
cli/index.js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

14845
bundles/redoc.lib.js Normal file

File diff suppressed because one or more lines are too long

1
bundles/redoc.lib.js.map Normal file

File diff suppressed because one or more lines are too long

110
bundles/redoc.standalone.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
import * as React from 'react';
import { render } from 'react-dom';
import styled from 'styled-components';
import styled from '../src/styled-components';
import { resolve as urlResolve } from 'url';
import { RedocStandalone } from '../src';
import ComboBox from './ComboBox';

View File

@ -30,14 +30,6 @@ info:
And that allows cross-domain communication from the browser.
All responses have a wildcard same-origin which makes them completely public and accessible to everyone, including any code on any site.
# Authentication
Petstore offers two forms of authentication:
- API Key
- OAuth2
OAuth2 - an open protocol to allow secure authorization in a simple
and standard method from web, mobile and desktop applications.
<SecurityDefinitions />
version: 1.0.0
@ -1193,7 +1185,7 @@ x-webhooks:
summary: New pet
description: Information about a new pet in the systems
operationId: newPet
tags:
tags:
- pet
requestBody:
content:
@ -1202,4 +1194,4 @@ x-webhooks:
$ref: "#/components/schemas/Pet"
responses:
"200":
description: Return a 200 status to indicate that the data was received successfully
description: Return a 200 status to indicate that the data was received successfully

View File

@ -8,6 +8,7 @@ import { Redoc, RedocProps } from '../../src/components/Redoc/Redoc';
import { AppStore } from '../../src/services/AppStore';
import { RedocRawOptions } from '../../src/services/RedocNormalizedOptions';
import { loadAndBundleSpec } from '../../src/utils/loadAndBundleSpec';
import markdown from './markdown';
const renderRoot = (props: RedocProps) =>
render(
@ -32,11 +33,12 @@ const options: RedocRawOptions = {
hideShelfIcon: true,
maxDisplayedEnumValues: 3,
nativeScrollbars: false,
noAutoAuth: true,
};
async function init() {
const spec = await loadAndBundleSpec(specUrl);
store = new AppStore(spec, specUrl, options);
store = new AppStore(spec, specUrl, options, true, markdown);
renderRoot({ store });
}

View File

@ -1,6 +1,6 @@
import * as React from 'react';
import { HFlex } from '../common-elements/flex';
import styled from 'styled-components';
import { HFlex } from '../../../src/common-elements/flex';
import styled from '../../../src/styled-components';
const Button = styled.button`
margin-left: auto;

View File

@ -4,17 +4,17 @@
```bash
#!/bin/bash
BSDEX_API_KEY=put-your-api-key-here
BSDEX_API_SECRET=put-your-api-secret-here
YOUR_API_KEY=put-your-api-key-here
YOUR_SECRET=put-your-api-secret-here
DATE="`date -u '+%a, %d %b %Y %T %Z'`"
# The signature is in fact HMAC signature of a few fields, currently only "date", using your API secret.
AUTHORIZATION="hmac username=\"$BSDEX_API_KEY\", algorithm=\"hmac-sha1\", headers=\"date\", signature=\"`/bin/echo -n "date: ${DATE}" | openssl sha1 -binary -hmac "${BSDEX_API_SECRET}" | base64 `\""
AUTHORIZATION="hmac username=\"$YOUR_API_KEY\", algorithm=\"hmac-sha1\", headers=\"date\", signature=\"`/bin/echo -n "date: ${DATE}" | openssl sha1 -binary -hmac "${YOUR_API_SECRET}" | base64 `\""
curl -v \
-H "Date: $DATE" \
-H "ApiKey: $BSDEX_API_KEY" \
-H "ApiKey: $YOUR_API_KEY" \
-H "Authorization: $AUTHORIZATION" \
-X GET "https://api-public.prelive.cex.tribe.sh/api/v1/balance"
-X GET "https://example.com/api/v1/pets"
```
> And you'll receive a response such as:
@ -22,14 +22,10 @@ curl -v \
```json
[
{
"asset_id": "btc",
"available": "0",
"locked": "0"
"foo": "bar"
},
{
"asset_id": "eur",
"available": "34163",
"locked": "123"
"bar": "foo"
}
]
```
@ -49,7 +45,7 @@ where `signed_date` is the base64-encoded HMAC-SHA1 signature for the expression
Have a look at the code example to understand the HMAC signing process better.
<aside class="notice">
<aside className="notice">
It is important to note that the value of the <em>Date</em> header must match the signed date value.
</aside>

View File

@ -0,0 +1,15 @@
import { loadMarkdownIndexFromComponents } from '../../../src/markdown';
export const loadMarkdownIndex = (names) => {
const mdxComponents = names.map((include) => require(`./${include}.mdx`));
return loadMarkdownIndexFromComponents(mdxComponents)
};
const NAMES = [
'welcome',
'auth',
]
export default loadMarkdownIndex(NAMES)

View File

@ -0,0 +1,3 @@
{
"names": ["welcome", "auth"]
}

View File

@ -26,14 +26,7 @@ info:
This API features Cross-Origin Resource Sharing (CORS) implemented in compliance with [W3C spec](https://www.w3.org/TR/cors/).
And that allows cross-domain communication from the browser.
All responses have a wildcard same-origin which makes them completely public and accessible to everyone, including any code on any site.
# Authentication
Petstore offers two forms of authentication:
- API Key
- OAuth2
OAuth2 - an open protocol to allow secure authorization in a simple
and standard method from web, mobile and desktop applications.
<!-- ReDoc-Inject: <security-definitions> -->
version: 1.0.0
title: Swagger Petstore
termsOfService: 'http://swagger.io/terms/'

View File

@ -28,7 +28,7 @@ const tsLoader = (env) => ({
},
});
const babelLoader = (mode) => ({
const babelLoader = (mode, transformJsx = false) => ({
loader: 'babel-loader',
options: {
generatorOpts: {
@ -38,7 +38,7 @@ const babelLoader = (mode) => ({
['@babel/plugin-syntax-typescript', { isTSX: true }],
['@babel/plugin-syntax-decorators', { legacy: true }],
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-react-jsx',
...(transformJsx ? ['@babel/plugin-transform-react-jsx'] : []),
'@babel/plugin-syntax-jsx',
[
'babel-plugin-styled-components',
@ -114,7 +114,7 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
{
test: /\.mdx?$/,
use: [
babelLoader(mode),
babelLoader(mode, true),
{
loader: '@mdx-js/loader',
options: {

22
package-lock.json generated
View File

@ -2366,9 +2366,9 @@
}
},
"@types/react-native": {
"version": "0.63.2",
"resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.63.2.tgz",
"integrity": "sha512-oxbp084lUsZvwfdWmWxKjJAuqEraQDRf+cE/JgwmrHQMguSrmgIHZ3xkeoQ5FYnW5NHIPpHudB3BbjL1Zn3vnA==",
"version": "0.63.9",
"resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.63.9.tgz",
"integrity": "sha512-6ec/z9zjAkFH3rD1RYqbrA/Lj+jux6bumWCte4yRy3leyelTdqtmOd2Ph+86IXQQzsIArEMBwmraAbNQ0J3UAA==",
"dev": true,
"requires": {
"@types/react": "*"
@ -2408,15 +2408,23 @@
"dev": true
},
"@types/styled-components": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.1.tgz",
"integrity": "sha512-fIjKvDU1LJExBZWEQilHqzfpOK4KUwBsj5zC79lxa94ekz8oDQSBNcayMACBImxIuevF+NbBGL9O/2CQ67Zhig==",
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.2.tgz",
"integrity": "sha512-HNocYLfrsnNNm8NTS/W53OERSjRA8dx5Bn6wBd2rXXwt4Z3s+oqvY6/PbVt3e6sgtzI63GX//WiWiRhWur08qQ==",
"dev": true,
"requires": {
"@types/hoist-non-react-statics": "*",
"@types/react": "*",
"@types/react-native": "*",
"csstype": "^2.2.0"
"csstype": "^3.0.2"
},
"dependencies": {
"csstype": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.2.tgz",
"integrity": "sha512-ofovWglpqoqbfLNOTBNZLSbMuGrblAf1efvvArGKOZMBrIoJeu5UsAipQolkijtyQx5MtAzT/J9IHj/CEY1mJw==",
"dev": true
}
}
},
"@types/tapable": {

View File

@ -1,6 +1,12 @@
import styled from 'styled-components'
import styled from '../styled-components';
export const Flex = styled.div`
type FlexProps = {
justifyContent?: string;
};
export const Flex = styled.div.attrs((props: FlexProps) => ({
justifyContent: props.justifyContent,
}))<FlexProps>`
display: flex;
justify-content: ${(props) => props.justifyContent};
width: 100%;

View File

@ -9,3 +9,4 @@ export * from './mixins';
export * from './tabs';
export * from './samples';
export * from './perfect-scrollbar';
export * as Flex from './flex';

View File

@ -1,10 +1,9 @@
import * as React from 'react';
import PerfectScrollbarType, * as PerfectScrollbarNamespace from 'perfect-scrollbar';
import psStyles from 'perfect-scrollbar/css/perfect-scrollbar.css';
import { OptionsContext } from '../components/OptionsProvider';
import styled, { createGlobalStyle } from '../styled-components';
import styled from '../styled-components';
/*
* perfect scrollbar umd bundle uses exports assignment while module uses default export
@ -14,7 +13,7 @@ import styled, { createGlobalStyle } from '../styled-components';
const PerfectScrollbarConstructor =
PerfectScrollbarNamespace.default || ((PerfectScrollbarNamespace as any) as PerfectScrollbarType);
const PSStyling = createGlobalStyle`${psStyles && psStyles.toString()}`;
// const PSStyling = createGlobalStyle`${require('perfect-scrollbar/css/perfect-scrollbar.css').toString()}`;
const StyledScrollWrapper = styled.div`
position: relative;
@ -46,7 +45,7 @@ export class PerfectScrollbar extends React.Component<PerfectScrollbarProps> {
this.inst.destroy();
}
handleRef = ref => {
handleRef = (ref) => {
this._container = ref;
};
@ -59,7 +58,7 @@ export class PerfectScrollbar extends React.Component<PerfectScrollbarProps> {
return (
<>
<PSStyling />
{/* <PSStyling /> */}
<StyledScrollWrapper className={`scrollbar-container ${className}`} ref={this.handleRef}>
{children}
</StyledScrollWrapper>
@ -73,7 +72,7 @@ export function PerfectScrollbarWrap(
) {
return (
<OptionsContext.Consumer>
{options =>
{(options) =>
!options.nativeScrollbars ? (
<PerfectScrollbar {...props}>{props.children}</PerfectScrollbar>
) : (

View File

@ -15,8 +15,7 @@ import { ApiContentWrap, BackgroundStub, RedocWrap } from './styled.elements';
import { SearchBox } from '../SearchBox/SearchBox';
import { StoreProvider } from '../StoreBuilder';
import { sections, components } from '../../markdown';
import components from '../../markdown/components';
export interface RedocProps {
store: AppStore;
@ -37,9 +36,12 @@ export class Redoc extends React.Component<RedocProps> {
render() {
const {
store: { spec, menu, options, search, marker },
store: { spec, menu, options, search, marker, markdownIndex },
} = this.props;
const store = this.props.store;
console.log('components', components);
return (
<ThemeProvider theme={options.theme}>
<StoreProvider value={this.props.store}>
@ -61,9 +63,10 @@ export class Redoc extends React.Component<RedocProps> {
</StickyResponsiveSidebar>
<ApiContentWrap className="api-content">
<ApiInfo store={store} />
{sections.map((MDXComponent, idx) => {
return <MDXComponent key={`section-${idx}`} />;
})}
{markdownIndex &&
markdownIndex.components.map((MDXComponent, idx) => {
return <MDXComponent key={`mdxsection-${idx}`} />;
})}
<ContentItems items={menu.items as any} />
</ApiContentWrap>
<BackgroundStub />

View File

@ -6,11 +6,13 @@ import { ErrorBoundary } from './ErrorBoundary';
import { Loading } from './Loading/Loading';
import { Redoc } from './Redoc/Redoc';
import { StoreBuilder } from './StoreBuilder';
import { MarkdownIndex } from '../markdown';
export interface RedocStandaloneProps {
spec?: object;
specUrl?: string;
options?: RedocRawOptions;
markdownIndex?: MarkdownIndex;
onLoaded?: (e?: Error) => any;
}

View File

@ -12,7 +12,7 @@ export class SelectOnClick extends React.PureComponent {
const { children } = this.props;
return (
<div
ref={el => (this.child = el)}
ref={el => (this.child = (el as HTMLDivElement))}
onClick={this.selectElement}
onFocus={this.selectElement}
tabIndex={0}

View File

@ -1,8 +1,9 @@
export * from './components';
export { MiddlePanel, Row, RightPanel, Section } from './common-elements/';
export { MiddlePanel, Row, RightPanel, Section, Flex } from './common-elements/';
export { OpenAPIEncoding } from './types';
export * from './services';
export * from './utils';
export * from './styled-components';
export { default as styled } from './styled-components';
export * from './markdown';

View File

@ -1,17 +1,20 @@
import * as React from 'react';
import Highlight, { defaultProps, Language } from 'prism-react-renderer';
import styled from 'styled-components';
import styled from '../../../styled-components';
type Props = {
className: string;
style: any;
componentId?: string;
};
const OverflowHighlighter = styled.pre`
overflow-x: scroll;
`;
const Highligher: React.FC<Props> = ({ children, className }) => {
const language = className.replace(/language-/, '');
const Highlighter: React.FC<Props> = ({ children, className }) => {
console.log('className', className);
const language = className ? className.replace(/language-/, '').trim() : 'bash';
if (!children) {
return null;
@ -34,4 +37,4 @@ const Highligher: React.FC<Props> = ({ children, className }) => {
);
};
export default Highligher;
export default Highlighter;

View File

@ -0,0 +1,89 @@
import * as React from 'react';
import {
DarkRightPanel,
H1,
H2,
MiddlePanel,
PropertiesTable,
Row,
Section,
ShareLink,
} from '../common-elements';
import Highlighter from './code/Highlighter';
const H1Comp = ({ children, ...props }) => {
return (
<H1 id={props.id}>
<ShareLink to={props.id as string} />
{children}
</H1>
);
};
const H2Comp = ({ children, ...props }) => (
<H2 id={props.id}>
<ShareLink to={props.id as string} />
{children}
</H2>
);
const Wrapper = ({ children }) => {
const getSections = (children) => {
let currentSection: any[] = [];
let currentId: number | null = null;
let currentCodeBlocks: any[] = [];
const result: any[] = [];
children.forEach((child) => {
const type = child.props.mdxType;
if (['h1', 'h2', 'h3', 'h4', 'h5'].includes(type)) {
if (currentSection.length > 0) {
result.push({
id: currentId,
middle: currentSection,
right: currentCodeBlocks,
});
}
currentId = child.props.id;
currentSection = [];
currentCodeBlocks = [];
}
if (['code', 'pre', 'blockquote'].includes(type)) {
currentCodeBlocks.push(child);
} else {
currentSection.push(child);
}
});
if (currentSection.length > 0) {
result.push({ id: currentId, middle: currentSection, right: currentCodeBlocks });
}
return result;
};
const sections = getSections(children);
console.log('sections', sections);
return (
<>
{sections.map((section) => (
<Section key={`resection-${section.id}`} id={section.id}>
<Row id={section.id}>
{section.middle && <MiddlePanel>{section.middle}</MiddlePanel>}
{section.right && <DarkRightPanel>{section.right}</DarkRightPanel>}
</Row>
</Section>
))}
</>
);
};
export default {
code: Highlighter,
h1: H1Comp,
h2: H2Comp,
table: PropertiesTable,
wrapper: Wrapper,
};

View File

@ -1,100 +1,30 @@
import * as React from 'react';
import {
DarkRightPanel,
H1,
H2,
MiddlePanel,
PropertiesTable,
Row,
Section,
ShareLink,
} from '../common-elements';
import Highlighter from './code/Highlighter';
const TocPlugin = require('./plugins/export-toc');
const INCLUDES = ['auth', 'welcome'];
const H1Comp = ({ children, ...props }) => {
return (
<H1 id={props.id}>
<ShareLink to={props.id as string} />
{children}
</H1>
);
type Header = {
depth: number;
id: number;
text: string;
};
export type MarkdownDocument = {
component: (props: any) => JSX.Element;
headings: Header[];
};
const H2Comp = ({ children, ...props }) => (
<H2 id={props.id}>
<ShareLink to={props.id as string} />
{children}
</H2>
);
export type MarkdownIndex = {
components: ((props: any) => JSX.Element)[];
headings: Header[];
};
const Wrapper = ({ children }) => {
const getSections = (children) => {
let currentSection: any[] = [];
let currentId: number | null = null;
let currentCodeBlocks: any[] = [];
const result: any[] = [];
export const loadMarkdownIndexFromComponents = (mdxComponents) => {
const components = mdxComponents.map((sect) => sect.default);
const headings = mdxComponents.reduce((acc, section) => [...acc, ...section.headings], []);
children.forEach((child) => {
const type = child.props.mdxType;
if (['h1', 'h2', 'h3', 'h4', 'h5'].includes(type)) {
if (currentSection.length > 0) {
result.push({
id: currentId,
middle: currentSection,
right: currentCodeBlocks,
});
}
currentId = child.props.id;
currentSection = [];
currentCodeBlocks = [];
}
if (['code', 'pre', 'blockquote'].includes(type)) {
currentCodeBlocks.push(child);
} else {
currentSection.push(child);
}
});
if (currentSection.length > 0) {
result.push({ id: currentId, middle: currentSection, right: currentCodeBlocks });
}
return result;
return {
components,
headings,
};
const sections = getSections(children);
return (
<>
{sections.map((section) => (
<Section key={`section-${section.id}`} id={section.id}>
<Row id={section.id}>
{section.middle && <MiddlePanel>{section.middle}</MiddlePanel>}
{section.right && <DarkRightPanel>{section.right}</DarkRightPanel>}
</Row>
</Section>
))}
</>
);
};
const includedSections = INCLUDES.map((include) => require(`./${include}.mdx`));
export const sections = includedSections.map((sect) => sect.default);
export const headings = includedSections.reduce(
(acc, section) => [...acc, ...section.headings],
[],
);
export const components = {
code: Highlighter,
h1: H1Comp,
h2: H2Comp,
table: PropertiesTable,
wrapper: Wrapper,
export default {
remarkPlugins: [TocPlugin],
};
export default sections;

View File

@ -19,26 +19,29 @@ import {
} from '../utils/openapi';
import { IS_BROWSER } from '../utils';
import { MarkdownIndex } from '../markdown';
export interface StoreState {
menu: {
activeItemIdx: number;
};
spec: {
url?: string;
url?: string | null;
data: any;
};
searchIndex: any;
options: RedocRawOptions;
markdownIndex?: MarkdownIndex;
}
export async function createStore(
spec: object,
specUrl: string | undefined,
specUrl: string | undefined | null,
options: RedocRawOptions = {},
markdownIndex?: MarkdownIndex,
) {
const resolvedSpec = await loadAndBundleSpec(spec || specUrl);
return new AppStore(resolvedSpec, specUrl, options);
return new AppStore(resolvedSpec, specUrl, options, true, markdownIndex);
}
export class AppStore {
@ -47,8 +50,8 @@ export class AppStore {
* **SUPER HACKY AND NOT OPTIMAL IMPLEMENTATION**
*/
// TODO:
static fromJS(state: StoreState): AppStore {
const inst = new AppStore(state.spec.data, state.spec.url, state.options, false);
static fromJS(state: StoreState, markdownIndex?: MarkdownIndex): AppStore {
const inst = new AppStore(state.spec.data, state.spec.url, state.options, false, markdownIndex);
inst.menu.activeItemIdx = state.menu.activeItemIdx || 0;
inst.menu.activate(inst.menu.flatItems[inst.menu.activeItemIdx]);
if (!inst.options.disableSearch) {
@ -63,15 +66,17 @@ export class AppStore {
options: RedocNormalizedOptions;
search?: SearchStore<string>;
marker = new MarkerService();
markdownIndex?: MarkdownIndex;
private scroll: ScrollService;
private disposer: Lambda | null = null;
constructor(
spec: OpenAPISpec,
specUrl?: string,
specUrl?: string | null,
options: RedocRawOptions = {},
createSearchIndex: boolean = true,
markdownIndex?: MarkdownIndex,
) {
this.rawOptions = options;
this.options = new RedocNormalizedOptions(options, DEFAULT_OPTIONS);
@ -81,7 +86,9 @@ export class AppStore {
MenuStore.updateOnHistory(history.currentId, this.scroll);
this.spec = new SpecStore(spec, specUrl, this.options);
this.menu = new MenuStore(this.spec, this.scroll, history);
this.menu = new MenuStore(this.spec, this.scroll, history, markdownIndex);
this.markdownIndex = markdownIndex;
if (!this.options.disableSearch) {
this.search = new SearchStore();
@ -122,11 +129,12 @@ export class AppStore {
activeItemIdx: this.menu.activeItemIdx,
},
spec: {
url: this.spec.parser.specUrl,
url: this.spec.parser.specUrl || null,
data: this.spec.parser.spec,
},
searchIndex: this.search ? await this.search.toJS() : undefined,
options: this.rawOptions,
markdownIndex: this.markdownIndex,
};
}

View File

@ -6,7 +6,7 @@ import { GROUP_DEPTH } from './MenuBuilder';
import { SpecStore } from './models';
import { ScrollService } from './ScrollService';
import { ExtraContent } from './models/ExtraContent';
import { headings } from '../markdown';
import { MarkdownIndex } from '../markdown';
export type MenuItemGroupType = 'group' | 'tag' | 'section' | 'extra';
export type MenuItemType = MenuItemGroupType | 'operation';
@ -38,246 +38,252 @@ export const SECTION_ATTR = 'data-section-id';
* Stores all side-menu related information
*/
export class MenuStore {
/**
* Statically try update scroll position
* Used before hydrating from server-side rendered html to scroll page faster
*/
static updateOnHistory(id: string = historyInst.currentId, scroll: ScrollService) {
if (!id) {
return;
}
scroll.scrollIntoViewBySelector(`[${SECTION_ATTR}="${id}"]`);
}
/**
* Statically try update scroll position
* Used before hydrating from server-side rendered html to scroll page faster
*/
static updateOnHistory(id: string = historyInst.currentId, scroll: ScrollService) {
if (!id) {
return;
}
scroll.scrollIntoViewBySelector(`[${SECTION_ATTR}="${id}"]`);
}
/**
* active item absolute index (when flattened). -1 means nothing is selected
*/
@observable
activeItemIdx: number = -1;
/**
* active item absolute index (when flattened). -1 means nothing is selected
*/
@observable
activeItemIdx: number = -1;
/**
* whether sidebar with menu is opened or not
*/
@observable
sideBarOpened: boolean = false;
/**
* whether sidebar with menu is opened or not
*/
@observable
sideBarOpened: boolean = false;
items: IMenuItem[];
flatItems: IMenuItem[];
items: IMenuItem[];
flatItems: IMenuItem[];
/**
* cached flattened menu items to support absolute indexing
*/
private _unsubscribe: () => void;
private _hashUnsubscribe: () => void;
/**
* cached flattened menu items to support absolute indexing
*/
private _unsubscribe: () => void;
private _hashUnsubscribe: () => void;
/**
*
* @param spec [SpecStore](#SpecStore) which contains page content structure
* @param scroll scroll service instance used by this menu
*/
constructor(spec: SpecStore, public scroll: ScrollService, public history: HistoryService) {
const extraItems = headings.map(
({ id, text, depth }) =>
new ExtraContent({
id,
name: text,
depth,
}),
);
this.items = [...extraItems, ...spec.contentItems];
/**
*
* @param spec [SpecStore](#SpecStore) which contains page content structure
* @param scroll scroll service instance used by this menu
*/
constructor(
spec: SpecStore,
public scroll: ScrollService,
public history: HistoryService,
markdownIndex?: MarkdownIndex,
) {
const { headings: extraHeadings } = markdownIndex || {};
const extraItems = extraHeadings ? extraHeadings.map(
({ id, text, depth }) =>
new ExtraContent({
id,
name: text,
depth,
})
) : [];
this.items = [...extraItems, ...spec.contentItems];
this.flatItems = flattenByProp(this.items || [], 'items');
this.flatItems.forEach((item, idx) => (item.absoluteIdx = idx));
this.flatItems = flattenByProp(this.items || [], 'items');
this.flatItems.forEach((item, idx) => (item.absoluteIdx = idx));
this.subscribe();
}
this.subscribe();
}
subscribe() {
this._unsubscribe = this.scroll.subscribe(this.updateOnScroll);
this._hashUnsubscribe = this.history.subscribe(this.updateOnHistory);
}
subscribe() {
this._unsubscribe = this.scroll.subscribe(this.updateOnScroll);
this._hashUnsubscribe = this.history.subscribe(this.updateOnHistory);
}
@action
toggleSidebar() {
this.sideBarOpened = this.sideBarOpened ? false : true;
}
@action
toggleSidebar() {
this.sideBarOpened = this.sideBarOpened ? false : true;
}
@action
closeSidebar() {
this.sideBarOpened = false;
}
@action
closeSidebar() {
this.sideBarOpened = false;
}
/**
* update active items on scroll
* @param isScrolledDown whether last scroll was downside
*/
updateOnScroll = (isScrolledDown: boolean): void => {
const step = isScrolledDown ? 1 : -1;
let itemIdx = this.activeItemIdx;
while (true) {
if (itemIdx === -1 && !isScrolledDown) {
break;
}
/**
* update active items on scroll
* @param isScrolledDown whether last scroll was downside
*/
updateOnScroll = (isScrolledDown: boolean): void => {
const step = isScrolledDown ? 1 : -1;
let itemIdx = this.activeItemIdx;
while (true) {
if (itemIdx === -1 && !isScrolledDown) {
break;
}
if (itemIdx >= this.flatItems.length - 1 && isScrolledDown) {
break;
}
if (itemIdx >= this.flatItems.length - 1 && isScrolledDown) {
break;
}
if (isScrolledDown) {
const el = this.getElementAtOrFirstChild(itemIdx + 1);
if (this.scroll.isElementBellow(el)) {
break;
}
} else {
const el = this.getElementAt(itemIdx);
if (this.scroll.isElementAbove(el)) {
break;
}
}
itemIdx += step;
}
if (isScrolledDown) {
const el = this.getElementAtOrFirstChild(itemIdx + 1);
if (this.scroll.isElementBellow(el)) {
break;
}
} else {
const el = this.getElementAt(itemIdx);
if (this.scroll.isElementAbove(el)) {
break;
}
}
itemIdx += step;
}
this.activate(this.flatItems[itemIdx], true, true);
};
this.activate(this.flatItems[itemIdx], true, true);
};
/**
* update active items on hash change
* @param id current hash
*/
updateOnHistory = (id: string = this.history.currentId) => {
if (!id) {
return;
}
let item: IMenuItem | undefined;
/**
* update active items on hash change
* @param id current hash
*/
updateOnHistory = (id: string = this.history.currentId) => {
if (!id) {
return;
}
let item: IMenuItem | undefined;
item = this.flatItems.find((i) => i.id === id);
if (item) {
this.activateAndScroll(item, false);
} else {
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}"]`);
}
};
item = this.flatItems.find((i) => i.id === id);
if (item) {
this.activateAndScroll(item, false);
} else {
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}"]`);
}
};
/**
* get section/operation DOM Node related to the item or null if it doesn't exist
* @param idx item absolute index
*/
getElementAt(idx: number): Element | null {
const item = this.flatItems[idx];
return (item && querySelector(`[${SECTION_ATTR}="${item.id}"]`)) || null;
}
/**
* get section/operation DOM Node related to the item or null if it doesn't exist
* @param idx item absolute index
*/
getElementAt(idx: number): Element | null {
const item = this.flatItems[idx];
return (item && querySelector(`[${SECTION_ATTR}="${item.id}"]`)) || null;
}
/**
* get section/operation DOM Node related to the item or if it is group item, returns first item of the group
* @param idx item absolute index
*/
getElementAtOrFirstChild(idx: number): Element | null {
let item = this.flatItems[idx];
if (item && item.type === 'group') {
item = item.items[0];
}
return (item && querySelector(`[${SECTION_ATTR}="${item.id}"]`)) || null;
}
/**
* get section/operation DOM Node related to the item or if it is group item, returns first item of the group
* @param idx item absolute index
*/
getElementAtOrFirstChild(idx: number): Element | null {
let item = this.flatItems[idx];
if (item && item.type === 'group') {
item = item.items[0];
}
return (item && querySelector(`[${SECTION_ATTR}="${item.id}"]`)) || null;
}
/**
* current active item
*/
get activeItem(): IMenuItem {
return this.flatItems[this.activeItemIdx] || undefined;
}
/**
* current active item
*/
get activeItem(): IMenuItem {
return this.flatItems[this.activeItemIdx] || undefined;
}
getItemById = (id: string) => {
return this.flatItems.find((item) => item.id === id);
};
getItemById = (id: string) => {
return this.flatItems.find((item) => item.id === id);
};
/**
* activate menu item
* @param item item to activate
* @param updateLocation [true] whether to update location
* @param rewriteHistory [false] whether to rewrite browser history (do not create new entry)
*/
@action
activate(
item: IMenuItem | undefined,
updateLocation: boolean = true,
rewriteHistory: boolean = false,
) {
if ((this.activeItem && this.activeItem.id) === (item && item.id)) {
return;
}
/**
* activate menu item
* @param item item to activate
* @param updateLocation [true] whether to update location
* @param rewriteHistory [false] whether to rewrite browser history (do not create new entry)
*/
@action
activate(
item: IMenuItem | undefined,
updateLocation: boolean = true,
rewriteHistory: boolean = false,
) {
if ((this.activeItem && this.activeItem.id) === (item && item.id)) {
return;
}
if (item && item.type === 'group') {
return;
}
if (item && item.type === 'group') {
return;
}
this.deactivate(this.activeItem);
if (!item) {
this.history.replace('', rewriteHistory);
return;
}
this.deactivate(this.activeItem);
if (!item) {
this.history.replace('', rewriteHistory);
return;
}
// do not allow activating group items
// TODO: control over options
if (item.depth <= GROUP_DEPTH) {
return;
}
// do not allow activating group items
// TODO: control over options
if (item.depth <= GROUP_DEPTH) {
return;
}
this.activeItemIdx = item.absoluteIdx!;
if (updateLocation) {
this.history.replace(item.id, rewriteHistory);
}
this.activeItemIdx = item.absoluteIdx!;
if (updateLocation) {
this.history.replace(item.id, rewriteHistory);
}
item.activate();
item.expand();
}
item.activate();
item.expand();
}
/**
* makes item and all the parents not active
* @param item item to deactivate
*/
deactivate(item: IMenuItem | undefined) {
if (item === undefined) {
return;
}
item.deactivate();
while (item !== undefined) {
item.collapse();
item = item.parent;
}
}
/**
* makes item and all the parents not active
* @param item item to deactivate
*/
deactivate(item: IMenuItem | undefined) {
if (item === undefined) {
return;
}
item.deactivate();
while (item !== undefined) {
item.collapse();
item = item.parent;
}
}
/**
* activate menu item and scroll to it
* @see MenuStore.activate
*/
@action.bound
activateAndScroll(
item: IMenuItem | undefined,
updateLocation?: boolean,
rewriteHistory?: boolean,
) {
// item here can be a copy from search results so find corresponding item from menu
const menuItem = (item && this.getItemById(item.id)) || item;
this.activate(menuItem, updateLocation, rewriteHistory);
this.scrollToActive();
if (!menuItem || !menuItem.items.length) {
this.closeSidebar();
}
}
/**
* activate menu item and scroll to it
* @see MenuStore.activate
*/
@action.bound
activateAndScroll(
item: IMenuItem | undefined,
updateLocation?: boolean,
rewriteHistory?: boolean,
) {
// item here can be a copy from search results so find corresponding item from menu
const menuItem = (item && this.getItemById(item.id)) || item;
this.activate(menuItem, updateLocation, rewriteHistory);
this.scrollToActive();
if (!menuItem || !menuItem.items.length) {
this.closeSidebar();
}
}
/**
* scrolls to active section
*/
scrollToActive(): void {
this.scroll.scrollIntoView(this.getElementAt(this.activeItemIdx));
}
/**
* scrolls to active section
*/
scrollToActive(): void {
this.scroll.scrollIntoView(this.getElementAt(this.activeItemIdx));
}
dispose() {
this._unsubscribe();
this._hashUnsubscribe();
}
}
dispose() {
this._unsubscribe();
this._hashUnsubscribe();
}
}

View File

@ -2,7 +2,7 @@ import { resolve as urlResolve } from 'url';
import { OpenAPIRef, OpenAPISchema, OpenAPISpec, Referenced } from '../types';
import { appendToMdHeading, IS_BROWSER } from '../utils/';
import { appendToMdHeading } from '../utils/';
import { JsonPointer } from '../utils/JsonPointer';
import {
isNamedDefinition,
@ -42,7 +42,7 @@ class RefCounter {
* Loads and keeps spec. Provides raw spec operations
*/
export class OpenAPIParser {
specUrl?: string;
specUrl?: string | null;
spec: OpenAPISpec;
mergeRefs: Set<string>;
@ -50,7 +50,7 @@ export class OpenAPIParser {
constructor(
spec: OpenAPISpec,
specUrl?: string,
specUrl?: string | null,
private options: RedocNormalizedOptions = new RedocNormalizedOptions({}),
) {
this.validate(spec);
@ -60,7 +60,7 @@ export class OpenAPIParser {
this.mergeRefs = new Set();
const href = IS_BROWSER ? window.location.href : '';
const href = '';
if (typeof specUrl === 'string') {
this.specUrl = urlResolve(href, specUrl);
}
@ -214,7 +214,7 @@ export class OpenAPIParser {
}
const allOfSchemas = schema.allOf
.map(subSchema => {
.map((subSchema) => {
if (subSchema && subSchema.$ref && used$Refs.has(subSchema.$ref)) {
return undefined;
}
@ -228,7 +228,7 @@ export class OpenAPIParser {
schema: subMerged,
};
})
.filter(child => child !== undefined) as Array<{
.filter((child) => child !== undefined) as Array<{
$ref: string | undefined;
schema: MergedOpenAPISchema;
}>;
@ -305,7 +305,7 @@ export class OpenAPIParser {
const def = this.deref(schemas[defName]);
if (
def.allOf !== undefined &&
def.allOf.find(obj => obj.$ref !== undefined && $refs.indexOf(obj.$ref) > -1)
def.allOf.find((obj) => obj.$ref !== undefined && $refs.indexOf(obj.$ref) > -1)
) {
res['#/components/schemas/' + defName] = [def['x-discriminator-value'] || defName];
}
@ -331,7 +331,7 @@ export class OpenAPIParser {
const beforeAllOf = allOf.slice(0, i);
const afterAllOf = allOf.slice(i + 1);
return {
oneOf: sub.oneOf.map(part => {
oneOf: sub.oneOf.map((part) => {
const merged = this.mergeAllOf({
allOf: [...beforeAllOf, part, ...afterAllOf],
});

View File

@ -20,7 +20,7 @@ export class SpecStore {
constructor(
spec: OpenAPISpec,
specUrl: string | undefined,
specUrl: string | undefined | null,
private options: RedocNormalizedOptions,
) {
this.parser = new OpenAPIParser(spec, specUrl, options);

View File

@ -13,7 +13,6 @@ import {
OpenAPIServer,
Referenced,
} from '../types';
import { IS_BROWSER } from './dom';
import { isNumeric, removeQueryString, resolveUrl } from './helpers';
function isWildcardStatusCode(statusCode: string | number): statusCode is string {
@ -140,10 +139,10 @@ export function isFormUrlEncoded(contentType: string): boolean {
function delimitedEncodeField(fieldVal: any, fieldName: string, delimiter: string): string {
if (Array.isArray(fieldVal)) {
return fieldVal.map(v => v.toString()).join(delimiter);
return fieldVal.map((v) => v.toString()).join(delimiter);
} else if (typeof fieldVal === 'object') {
return Object.keys(fieldVal)
.map(k => `${k}${delimiter}${fieldVal[k]}`)
.map((k) => `${k}${delimiter}${fieldVal[k]}`)
.join(delimiter);
} else {
return fieldName + '=' + fieldVal.toString();
@ -156,7 +155,7 @@ function deepObjectEncodeField(fieldVal: any, fieldName: string): string {
return '';
} else if (typeof fieldVal === 'object') {
return Object.keys(fieldVal)
.map(k => `${fieldName}[${k}]=${fieldVal[k]}`)
.map((k) => `${fieldName}[${k}]=${fieldVal[k]}`)
.join('&');
} else {
console.warn('deepObject style cannot be used with non-object value:' + fieldVal.toString());
@ -188,7 +187,7 @@ export function urlFormEncodePayload(
throw new Error('Payload must have fields: ' + payload.toString());
} else {
return Object.keys(payload)
.map(fieldName => {
.map((fieldName) => {
const fieldVal = payload[fieldName];
const { style = 'form', explode = true } = encoding[fieldName] || {};
switch (style) {
@ -450,7 +449,7 @@ export function sortByRequired(fields: FieldModel[], order: string[] = []) {
const orderedFields: FieldModel[] = [];
const unorderedFields: FieldModel[] = [];
fields.forEach(field => {
fields.forEach((field) => {
if (field.required) {
order.includes(field.name) ? orderedFields.push(field) : unorderedFields.push(field);
} else {
@ -478,13 +477,13 @@ export function mergeParams(
operationParams: Array<Referenced<OpenAPIParameter>> = [],
): Array<Referenced<OpenAPIParameter>> {
const operationParamNames = {};
operationParams.forEach(param => {
operationParams.forEach((param) => {
param = parser.shalowDeref(param);
operationParamNames[param.name + '_' + param.in] = true;
});
// filter out path params overridden by operation ones with the same name
pathParams = pathParams.filter(param => {
pathParams = pathParams.filter((param) => {
param = parser.shalowDeref(param);
return !operationParamNames[param.name + '_' + param.in];
});
@ -496,7 +495,7 @@ export function mergeSimilarMediaTypes(
types: Record<string, OpenAPIMediaType>,
): Record<string, OpenAPIMediaType> {
const mergedTypes = {};
Object.keys(types).forEach(name => {
Object.keys(types).forEach((name) => {
const mime = types[name];
// ignore content type parameters (e.g. charset) and merge
const normalizedMimeName = name.split(';')[0].trim();
@ -518,18 +517,14 @@ export function expandDefaultServerVariables(url: string, variables: object = {}
}
export function normalizeServers(
specUrl: string | undefined,
specUrl: string | undefined | null,
servers: OpenAPIServer[],
): OpenAPIServer[] {
const getHref = () => {
if (!IS_BROWSER) {
return '';
}
const href = window.location.href;
return href.endsWith('.html') ? dirname(href) : href;
return '';
};
const baseUrl = specUrl === undefined ? removeQueryString(getHref()) : dirname(specUrl);
const baseUrl = !specUrl ? removeQueryString(getHref()) : dirname(specUrl);
if (servers.length === 0) {
// Behaviour defined in OpenAPI spec: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#openapi-object
@ -544,7 +539,7 @@ export function normalizeServers(
return resolveUrl(baseUrl, url);
}
return servers.map(server => {
return servers.map((server) => {
return {
...server,
url: normalizeUrl(server.url),
@ -562,7 +557,7 @@ export function setSecuritySchemePrefix(prefix: string) {
SECURITY_SCHEMES_SECTION_PREFIX = prefix;
}
export const shortenHTTPVerb = verb =>
export const shortenHTTPVerb = (verb) =>
({
delete: 'del',
options: 'opts',
@ -593,7 +588,7 @@ export function extractExtensions(
showExtensions: string[] | true,
): Record<string, any> {
return Object.keys(obj)
.filter(key => {
.filter((key) => {
if (showExtensions === true) {
return key.startsWith('x-') && !isRedocExtension(key);
}
@ -608,6 +603,6 @@ export function extractExtensions(
export function pluralizeType(displayType: string): string {
return displayType
.split(' or ')
.map(type => type.replace(/^(string|object|number|integer|array|boolean)s?( ?.*)/, '$1s$2'))
.map((type) => type.replace(/^(string|object|number|integer|array|boolean)s?( ?.*)/, '$1s$2'))
.join(' or ');
}

View File

@ -4,6 +4,10 @@ import * as webpack from 'webpack';
import * as path from 'path';
const remarkSlugs = require('remark-slug');
const rehypeHtml = require('rehype-stringify');
const exportToc = require('./src/markdown/plugins/export-toc');
const nodeExternals = require('webpack-node-externals')({
// bundle in modules that need transpiling + non-js (e.g. css)
allowlist: [
@ -27,6 +31,28 @@ try {
console.error('Skipping REDOC_REVISION');
}
const babelLoader = (mode, transformJsx = false) => ({
loader: 'babel-loader',
options: {
generatorOpts: {
decoratorsBeforeExport: true,
},
plugins: [
['@babel/plugin-syntax-typescript', { isTSX: true }],
['@babel/plugin-syntax-decorators', { legacy: true }],
'@babel/plugin-syntax-jsx',
...(transformJsx ? ['@babel/plugin-transform-react-jsx'] : []),
[
'babel-plugin-styled-components',
{
minify: true,
displayName: mode !== 'production',
},
],
],
},
});
const BANNER = `ReDoc - OpenAPI/Swagger-generated API Reference Documentation
-------------------------------------------------------------
Version: ${VERSION}
@ -76,6 +102,19 @@ export default (env: { standalone?: boolean } = {}, { mode }) => ({
module: {
rules: [
{
test: /\.mdx?$/,
use: [
babelLoader(mode, true),
{
loader: '@mdx-js/loader',
options: {
remarkPlugins: [remarkSlugs, exportToc],
rehypePlugins: [rehypeHtml],
},
},
],
},
{
test: /\.tsx?$/,
use: [
@ -88,26 +127,7 @@ export default (env: { standalone?: boolean } = {}, { mode }) => ({
},
},
},
{
loader: 'babel-loader',
options: {
generatorOpts: {
decoratorsBeforeExport: true,
},
plugins: [
['@babel/plugin-syntax-typescript', { isTSX: true }],
['@babel/plugin-syntax-decorators', { legacy: true }],
'@babel/plugin-syntax-jsx',
[
'babel-plugin-styled-components',
{
minify: true,
displayName: mode !== 'production',
},
],
],
},
},
babelLoader(mode),
],
exclude: [/node_modules/],
},