mirror of
https://github.com/Redocly/redoc.git
synced 2024-11-25 10:03:45 +03:00
chore: sections/markdown refactor
This commit is contained in:
parent
e0d82f4aa8
commit
eae11682b8
|
@ -1,19 +1,44 @@
|
||||||
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%;
|
||||||
|
@ -27,6 +52,7 @@ export const DarkRightPanel = RightPanel.extend`
|
||||||
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;
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,8 +3,8 @@ import * as React from 'react';
|
||||||
|
|
||||||
import { AppStore } from '../../services/AppStore';
|
import { AppStore } from '../../services/AppStore';
|
||||||
|
|
||||||
import { MiddlePanel, Row } from '../../common-elements/';
|
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,
|
||||||
|
@ -70,43 +70,46 @@ 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:
|
|
||||||
<DownloadButton
|
|
||||||
download={downloadFilename}
|
|
||||||
target="_blank"
|
|
||||||
href={downloadLink}
|
|
||||||
onClick={this.handleDownloadClick}
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</DownloadButton>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<StyledMarkdownBlock>
|
|
||||||
{((info.license || info.contact || info.termsOfService) && (
|
|
||||||
<InfoSpanBoxWrap>
|
|
||||||
<InfoSpanBox>
|
|
||||||
{email} {website} {license} {terms}
|
|
||||||
</InfoSpanBox>
|
|
||||||
</InfoSpanBoxWrap>
|
|
||||||
)) ||
|
|
||||||
null}
|
|
||||||
|
|
||||||
{(externalDocs && (
|
|
||||||
<p>
|
<p>
|
||||||
<a href={externalDocs.url}>{externalDocs.description || externalDocs.url}</a>
|
Download OpenAPI specification:
|
||||||
|
<DownloadButton
|
||||||
|
download={downloadFilename}
|
||||||
|
target="_blank"
|
||||||
|
href={downloadLink}
|
||||||
|
onClick={this.handleDownloadClick}
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</DownloadButton>
|
||||||
</p>
|
</p>
|
||||||
)) ||
|
)}
|
||||||
null}
|
<StyledMarkdownBlock>
|
||||||
</StyledMarkdownBlock>
|
{((info.license || info.contact || info.termsOfService) && (
|
||||||
</MiddlePanel>
|
<InfoSpanBoxWrap>
|
||||||
</Row>
|
<InfoSpanBox>
|
||||||
|
{email} {website} {license} {terms}
|
||||||
|
</InfoSpanBox>
|
||||||
|
</InfoSpanBoxWrap>
|
||||||
|
)) ||
|
||||||
|
null}
|
||||||
|
|
||||||
|
{(externalDocs && (
|
||||||
|
<p>
|
||||||
|
<a href={externalDocs.url}>{externalDocs.description || externalDocs.url}</a>
|
||||||
|
</p>
|
||||||
|
)) ||
|
||||||
|
null}
|
||||||
|
</StyledMarkdownBlock>
|
||||||
|
<Markdown source={store.spec.info.description} />
|
||||||
|
</MiddlePanel>
|
||||||
|
</Row>
|
||||||
|
</Section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
export { ApiDescription } from './ApiDescription';
|
|
||||||
export { ApiInfo } from './ApiInfo';
|
export { ApiInfo } from './ApiInfo';
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
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 { 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 { 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';
|
||||||
|
@ -12,20 +11,22 @@ import { Operation } from '../Operation/Operation';
|
||||||
import { SecurityDefs } from '../SecuritySchemes/SecuritySchemes';
|
import { SecurityDefs } from '../SecuritySchemes/SecuritySchemes';
|
||||||
import { StoreConsumer } from '../StoreBuilder';
|
import { StoreConsumer } from '../StoreBuilder';
|
||||||
|
|
||||||
|
const DEFAULT_ALLOWED_COMPONENTS = {
|
||||||
|
'security-definitions': {
|
||||||
|
component: SecurityDefs,
|
||||||
|
propsSelector: _store => ({
|
||||||
|
securitySchemes: _store!.spec.securitySchemes,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ContentItems extends React.Component<{
|
export class ContentItems extends React.Component<{
|
||||||
items: ContentItemModel[];
|
items: ContentItemModel[];
|
||||||
allowedMdComponents?: Dict<MDXComponentMeta>;
|
allowedMdComponents?: Dict<MDXComponentMeta>;
|
||||||
}> {
|
}> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
allowedMdComponents: {
|
allowedMdComponents: DEFAULT_ALLOWED_COMPONENTS,
|
||||||
'security-definitions': {
|
|
||||||
component: SecurityDefs,
|
|
||||||
propsSelector: _store => ({
|
|
||||||
securitySchemes: _store!.spec.securitySchemes,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -34,14 +35,18 @@ export class ContentItems extends React.Component<{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return items.map(item => (
|
return items.map(item => (
|
||||||
<ContentItem item={item} key={item.id} allowedMdComponents={this.props.allowedMdComponents} />
|
<ContentItem
|
||||||
|
item={item}
|
||||||
|
key={item.id}
|
||||||
|
allowedMdComponents={this.props.allowedMdComponents!}
|
||||||
|
/>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContentItemProps {
|
export interface ContentItemProps {
|
||||||
item: ContentItemModel;
|
item: ContentItemModel;
|
||||||
allowedMdComponents?: Dict<MDXComponentMeta>;
|
allowedMdComponents: Dict<MDXComponentMeta>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
|
@ -67,15 +72,21 @@ 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 === 'section'}>
|
||||||
</div>,
|
{content}
|
||||||
(item as any).items && <ContentItems key="content" items={(item as any).items} />,
|
</Section>
|
||||||
];
|
{item.items && (
|
||||||
|
<ContentItems items={item.items} allowedMdComponents={this.props.allowedMdComponents} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const middlePanelWrap = component => <MiddlePanel>{component}</MiddlePanel>;
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class SectionItem extends React.Component<ContentItemProps> {
|
export class SectionItem extends React.Component<ContentItemProps> {
|
||||||
render() {
|
render() {
|
||||||
|
@ -83,23 +94,26 @@ export class SectionItem extends React.Component<ContentItemProps> {
|
||||||
const components = this.props.allowedMdComponents;
|
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 href={'#' + this.props.item.id} />
|
||||||
</Header>
|
{name}
|
||||||
{components ? (
|
</Header>
|
||||||
<StoreConsumer>
|
</MiddlePanel>
|
||||||
{store => (
|
</Row>
|
||||||
<Markdown source={description || ''} allowedComponents={components} store={store} />
|
<StoreConsumer>
|
||||||
)}
|
{store => (
|
||||||
</StoreConsumer>
|
<AdvancedMarkdown
|
||||||
) : (
|
source={description || ''}
|
||||||
<Markdown source={description || ''} />
|
allowedComponents={components}
|
||||||
|
store={store}
|
||||||
|
htmlWrap={middlePanelWrap}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</MiddlePanel>
|
</StoreConsumer>
|
||||||
</Row>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
38
src/components/Markdown/AdvancedMarkdown.tsx
Normal file
38
src/components/Markdown/AdvancedMarkdown.tsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { AppStore, MarkdownRenderer, MDXComponentMeta } from '../../services';
|
||||||
|
import { BaseMarkdownProps } from './Markdown';
|
||||||
|
import { SanitizedMarkdownHTML } from './SanitizedMdBlock';
|
||||||
|
|
||||||
|
export interface AdvancedMarkdownProps extends BaseMarkdownProps {
|
||||||
|
store?: AppStore;
|
||||||
|
allowedComponents: Dict<MDXComponentMeta>;
|
||||||
|
htmlWrap?: (part: JSX.Element) => JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AdvancedMarkdown extends React.Component<AdvancedMarkdownProps> {
|
||||||
|
render() {
|
||||||
|
const { store, source, allowedComponents, htmlWrap = i => i } = this.props;
|
||||||
|
|
||||||
|
if (!store) {
|
||||||
|
throw new Error('When using componentes in markdown, store prop must be provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderer = new MarkdownRenderer();
|
||||||
|
const parts = renderer.renderMdWithComponents(source, allowedComponents);
|
||||||
|
|
||||||
|
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) }} />;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) }} />
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
30
src/components/Markdown/SanitizedMdBlock.tsx
Normal file
30
src/components/Markdown/SanitizedMdBlock.tsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import * as DOMPurify from 'dompurify';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { OptionsContext } 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 (
|
||||||
|
<OptionsContext.Consumer>
|
||||||
|
{options => (
|
||||||
|
<Wrap
|
||||||
|
className={'redoc-markdown ' + (props.className || '')}
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: sanitize(options.untrustedSpec, props.html),
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</OptionsContext.Consumer>
|
||||||
|
);
|
||||||
|
}
|
|
@ -23,20 +23,10 @@ const OperationRow = Row.extend`
|
||||||
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 {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import * as React from 'react';
|
||||||
import { ThemeProvider } from '../../styled-components';
|
import { ThemeProvider } from '../../styled-components';
|
||||||
|
|
||||||
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 { OptionsProvider } from '../OptionsProvider';
|
||||||
|
@ -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 />
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -82,20 +82,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 = UnderlinedHeader.extend`
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
margin: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Table = styled.table`
|
const Wrap = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
display: flex;
|
||||||
font-size: inherit;
|
margin: 1em 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export interface SecurityRequirementsProps {
|
export interface SecurityRequirementsProps {
|
||||||
|
@ -109,20 +112,14 @@ 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) => <SecurityRequirement key={idx} security={security} />)}
|
||||||
<SecuritiesColumn>
|
</SecuritiesColumn>
|
||||||
{securities.map((security, idx) => (
|
</Wrap>
|
||||||
<SecurityRequirement key={idx} security={security} />
|
|
||||||
))}
|
|
||||||
</SecuritiesColumn>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</Table>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,10 +66,10 @@ 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 href={'#' + scheme.sectionId} />
|
||||||
{scheme.id}
|
{scheme.id}
|
||||||
|
@ -118,9 +118,9 @@ export class SecurityDefs extends React.PureComponent<SecurityDefsProps> {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</StyledMarkdownBlock>
|
</StyledMarkdownBlock>
|
||||||
</div>
|
</MiddlePanel>
|
||||||
))}
|
</Row>
|
||||||
</div>
|
</Section>
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -164,9 +164,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 +175,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;
|
||||||
}
|
}
|
||||||
|
`};
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user