mirror of
https://github.com/Redocly/redoc.git
synced 2025-08-07 13:44:54 +03:00
feat: Add bundles
This commit is contained in:
parent
49f04d321f
commit
422ec09771
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -24,7 +24,7 @@ node_modules
|
|||
lib/
|
||||
stats.json
|
||||
cypress/
|
||||
bundles/
|
||||
# bundles/
|
||||
typings/*
|
||||
!typings/styled-patch.d.ts
|
||||
cli/index.js
|
||||
|
|
1
bundles/6ab91c58bf337b7164c3.worker.js.map
Normal file
1
bundles/6ab91c58bf337b7164c3.worker.js.map
Normal file
File diff suppressed because one or more lines are too long
1
bundles/95952f773ec942db46dc.worker.js.map
Normal file
1
bundles/95952f773ec942db46dc.worker.js.map
Normal file
File diff suppressed because one or more lines are too long
14845
bundles/redoc.lib.js
Normal file
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
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
110
bundles/redoc.standalone.js
Normal file
File diff suppressed because one or more lines are too long
1
bundles/redoc.standalone.js.map
Normal file
1
bundles/redoc.standalone.js.map
Normal file
File diff suppressed because one or more lines are too long
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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>
|
||||
|
15
demo/playground/markdown/index.ts
Normal file
15
demo/playground/markdown/index.ts
Normal 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)
|
3
demo/playground/markdown/manifest.json
Normal file
3
demo/playground/markdown/manifest.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"names": ["welcome", "auth"]
|
||||
}
|
|
@ -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/'
|
||||
|
|
|
@ -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
22
package-lock.json
generated
|
@ -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": {
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -9,3 +9,4 @@ export * from './mixins';
|
|||
export * from './tabs';
|
||||
export * from './samples';
|
||||
export * from './perfect-scrollbar';
|
||||
export * as Flex from './flex';
|
||||
|
|
|
@ -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>
|
||||
) : (
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
89
src/markdown/components.tsx
Normal file
89
src/markdown/components.tsx
Normal 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,
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 ');
|
||||
}
|
||||
|
|
|
@ -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/],
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue
Block a user