mirror of
https://github.com/Redocly/redoc.git
synced 2025-02-12 07:50:34 +03:00
chore: refactor, move allowedMdComponents to options
This commit is contained in:
parent
f903406c14
commit
d3d35189f5
|
@ -4,48 +4,25 @@ import * as React from 'react';
|
||||||
import { AdvancedMarkdown } from '../Markdown/AdvancedMarkdown';
|
import { AdvancedMarkdown } from '../Markdown/AdvancedMarkdown';
|
||||||
|
|
||||||
import { H1, H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements';
|
import { H1, H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements';
|
||||||
import { MDXComponentMeta } from '../../services/MarkdownRenderer';
|
|
||||||
import { ContentItemModel } from '../../services/MenuBuilder';
|
import { ContentItemModel } from '../../services/MenuBuilder';
|
||||||
import { GroupModel, OperationModel } from '../../services/models';
|
import { GroupModel, OperationModel } from '../../services/models';
|
||||||
import { Operation } from '../Operation/Operation';
|
import { Operation } from '../Operation/Operation';
|
||||||
import { SecurityDefs } from '../SecuritySchemes/SecuritySchemes';
|
|
||||||
|
|
||||||
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>;
|
|
||||||
}> {
|
}> {
|
||||||
static defaultProps = {
|
|
||||||
allowedMdComponents: DEFAULT_ALLOWED_COMPONENTS,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const items = this.props.items;
|
const items = this.props.items;
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return items.map(item => (
|
return items.map(item => <ContentItem item={item} key={item.id} />);
|
||||||
<ContentItem
|
|
||||||
item={item}
|
|
||||||
key={item.id}
|
|
||||||
allowedMdComponents={this.props.allowedMdComponents!}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContentItemProps {
|
export interface ContentItemProps {
|
||||||
item: ContentItemModel;
|
item: ContentItemModel;
|
||||||
allowedMdComponents: Dict<MDXComponentMeta>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
|
@ -76,9 +53,7 @@ export class ContentItem extends React.Component<ContentItemProps> {
|
||||||
<Section id={item.id} underlined={item.type === 'operation'}>
|
<Section id={item.id} underlined={item.type === 'operation'}>
|
||||||
{content}
|
{content}
|
||||||
</Section>
|
</Section>
|
||||||
{item.items && (
|
{item.items && <ContentItems items={item.items} />}
|
||||||
<ContentItems items={item.items} allowedMdComponents={this.props.allowedMdComponents} />
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -90,7 +65,6 @@ const middlePanelWrap = component => <MiddlePanel>{component}</MiddlePanel>;
|
||||||
export class SectionItem extends React.Component<ContentItemProps> {
|
export class SectionItem extends React.Component<ContentItemProps> {
|
||||||
render() {
|
render() {
|
||||||
const { name, description, level } = this.props.item as GroupModel;
|
const { name, description, level } = this.props.item as GroupModel;
|
||||||
const { allowedMdComponents } = this.props;
|
|
||||||
|
|
||||||
const Header = level === 2 ? H2 : H1;
|
const Header = level === 2 ? H2 : H1;
|
||||||
return (
|
return (
|
||||||
|
@ -103,11 +77,7 @@ export class SectionItem extends React.Component<ContentItemProps> {
|
||||||
</Header>
|
</Header>
|
||||||
</MiddlePanel>
|
</MiddlePanel>
|
||||||
</Row>
|
</Row>
|
||||||
<AdvancedMarkdown
|
<AdvancedMarkdown source={description || ''} htmlWrap={middlePanelWrap} />
|
||||||
allowedComponents={allowedMdComponents}
|
|
||||||
source={description || ''}
|
|
||||||
htmlWrap={middlePanelWrap}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,35 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { AppStore, MarkdownRenderer, MDXComponentMeta } from '../../services';
|
import { AppStore, MarkdownRenderer, RedocNormalizedOptions } from '../../services';
|
||||||
import { BaseMarkdownProps } from './Markdown';
|
import { BaseMarkdownProps } from './Markdown';
|
||||||
import { SanitizedMarkdownHTML } from './SanitizedMdBlock';
|
import { SanitizedMarkdownHTML } from './SanitizedMdBlock';
|
||||||
|
|
||||||
|
import { OptionsConsumer } from '../OptionsProvider';
|
||||||
import { StoreConsumer } from '../StoreBuilder';
|
import { StoreConsumer } from '../StoreBuilder';
|
||||||
|
|
||||||
export interface AdvancedMarkdownProps extends BaseMarkdownProps {
|
export interface AdvancedMarkdownProps extends BaseMarkdownProps {
|
||||||
allowedComponents: Dict<MDXComponentMeta>;
|
|
||||||
htmlWrap?: (part: JSX.Element) => JSX.Element;
|
htmlWrap?: (part: JSX.Element) => JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AdvancedMarkdown extends React.Component<AdvancedMarkdownProps> {
|
export class AdvancedMarkdown extends React.Component<AdvancedMarkdownProps> {
|
||||||
render() {
|
render() {
|
||||||
return <StoreConsumer>{store => this.renderWithStore(store)}</StoreConsumer>;
|
return (
|
||||||
|
<OptionsConsumer>
|
||||||
|
{options => (
|
||||||
|
<StoreConsumer>{store => this.renderWithOptionsAndStore(options, store)}</StoreConsumer>
|
||||||
|
)}
|
||||||
|
</OptionsConsumer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderWithStore(store?: AppStore) {
|
renderWithOptionsAndStore(options: RedocNormalizedOptions, store?: AppStore) {
|
||||||
const { allowedComponents, source, htmlWrap = i => i } = this.props;
|
const { source, htmlWrap = i => i } = this.props;
|
||||||
if (!store) {
|
if (!store) {
|
||||||
throw new Error('When using componentes in markdown, store prop must be provided');
|
throw new Error('When using componentes in markdown, store prop must be provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderer = new MarkdownRenderer();
|
const renderer = new MarkdownRenderer(options);
|
||||||
const parts = renderer.renderMdWithComponents(source, allowedComponents);
|
const parts = renderer.renderMdWithComponents(source);
|
||||||
|
|
||||||
if (!parts.length) {
|
if (!parts.length) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -2,12 +2,12 @@ import * as PropTypes from 'prop-types';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { ThemeProvider } from '../../styled-components';
|
import { ThemeProvider } from '../../styled-components';
|
||||||
|
import { OptionsProvider } from '../OptionsProvider';
|
||||||
|
|
||||||
import { AppStore } from '../../services';
|
import { AppStore } from '../../services';
|
||||||
import { ApiInfo } from '../ApiInfo/';
|
import { ApiInfo } from '../ApiInfo/';
|
||||||
import { ApiLogo } from '../ApiLogo/ApiLogo';
|
import { ApiLogo } from '../ApiLogo/ApiLogo';
|
||||||
import { ContentItems } from '../ContentItems/ContentItems';
|
import { ContentItems } from '../ContentItems/ContentItems';
|
||||||
import { OptionsProvider } from '../OptionsProvider';
|
|
||||||
import { SideMenu } from '../SideMenu/SideMenu';
|
import { SideMenu } from '../SideMenu/SideMenu';
|
||||||
import { StickyResponsiveSidebar } from '../StickySidebar/StickyResponsiveSidebar';
|
import { StickyResponsiveSidebar } from '../StickySidebar/StickyResponsiveSidebar';
|
||||||
import { ApiContentWrap, BackgroundStub, RedocWrap } from './styled.elements';
|
import { ApiContentWrap, BackgroundStub, RedocWrap } from './styled.elements';
|
||||||
|
|
|
@ -10,6 +10,9 @@ import { RedocNormalizedOptions, RedocRawOptions } from './RedocNormalizedOption
|
||||||
import { ScrollService } from './ScrollService';
|
import { ScrollService } from './ScrollService';
|
||||||
import { SearchStore } from './SearchStore';
|
import { SearchStore } from './SearchStore';
|
||||||
|
|
||||||
|
import { SecurityDefs } from '../components/SecuritySchemes/SecuritySchemes';
|
||||||
|
import { SECURITY_DEFINITIONS_COMPONENT_NAME } from '../utils/openapi';
|
||||||
|
|
||||||
export interface StoreState {
|
export interface StoreState {
|
||||||
menu: {
|
menu: {
|
||||||
activeItemIdx: number;
|
activeItemIdx: number;
|
||||||
|
@ -64,7 +67,7 @@ export class AppStore {
|
||||||
createSearchIndex: boolean = true,
|
createSearchIndex: boolean = true,
|
||||||
) {
|
) {
|
||||||
this.rawOptions = options;
|
this.rawOptions = options;
|
||||||
this.options = new RedocNormalizedOptions(options);
|
this.options = new RedocNormalizedOptions(options, DEFAULT_OPTIONS);
|
||||||
this.scroll = new ScrollService(this.options);
|
this.scroll = new ScrollService(this.options);
|
||||||
|
|
||||||
// update position statically based on hash (in case of SSR)
|
// update position statically based on hash (in case of SSR)
|
||||||
|
@ -137,3 +140,14 @@ export class AppStore {
|
||||||
this.marker.mark();
|
this.marker.mark();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_OPTIONS: RedocRawOptions = {
|
||||||
|
allowedMdComponents: {
|
||||||
|
[SECURITY_DEFINITIONS_COMPONENT_NAME]: {
|
||||||
|
component: SecurityDefs,
|
||||||
|
propsSelector: (store: AppStore) => ({
|
||||||
|
securitySchemes: store.spec.securitySchemes,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
@ -2,6 +2,7 @@ import * as marked from 'marked';
|
||||||
|
|
||||||
import { highlight, safeSlugify } from '../utils';
|
import { highlight, safeSlugify } from '../utils';
|
||||||
import { AppStore } from './AppStore';
|
import { AppStore } from './AppStore';
|
||||||
|
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
|
||||||
|
|
||||||
const renderer = new marked.Renderer();
|
const renderer = new marked.Renderer();
|
||||||
|
|
||||||
|
@ -49,7 +50,7 @@ export class MarkdownRenderer {
|
||||||
private headingEnhanceRenderer: marked.Renderer;
|
private headingEnhanceRenderer: marked.Renderer;
|
||||||
private originalHeadingRule: typeof marked.Renderer.prototype.heading;
|
private originalHeadingRule: typeof marked.Renderer.prototype.heading;
|
||||||
|
|
||||||
constructor() {
|
constructor(public options?: RedocNormalizedOptions) {
|
||||||
this.headingEnhanceRenderer = new marked.Renderer();
|
this.headingEnhanceRenderer = new marked.Renderer();
|
||||||
this.originalHeadingRule = this.headingEnhanceRenderer.heading.bind(
|
this.originalHeadingRule = this.headingEnhanceRenderer.heading.bind(
|
||||||
this.headingEnhanceRenderer,
|
this.headingEnhanceRenderer,
|
||||||
|
@ -148,10 +149,8 @@ export class MarkdownRenderer {
|
||||||
|
|
||||||
// TODO: rewrite this completelly! Regexp-based 👎
|
// TODO: rewrite this completelly! Regexp-based 👎
|
||||||
// Use marked ecosystem
|
// Use marked ecosystem
|
||||||
renderMdWithComponents(
|
renderMdWithComponents(rawText: string): Array<string | MDXComponentMeta> {
|
||||||
rawText: string,
|
const components = this.options && this.options.allowedMdComponents;
|
||||||
components?: Dict<MDXComponentMeta>,
|
|
||||||
): Array<string | MDXComponentMeta> {
|
|
||||||
if (!components || Object.keys(components).length === 0) {
|
if (!components || Object.keys(components).length === 0) {
|
||||||
return [this.renderMd(rawText)];
|
return [this.renderMd(rawText)];
|
||||||
}
|
}
|
||||||
|
@ -160,7 +159,7 @@ export class MarkdownRenderer {
|
||||||
const names = '(?:' + Object.keys(components).join('|') + ')';
|
const names = '(?:' + Object.keys(components).join('|') + ')';
|
||||||
|
|
||||||
const anyCompRegexp = new RegExp(
|
const anyCompRegexp = new RegExp(
|
||||||
COMPONENT_REGEXP.replace(/{component}/g, '(' + names + ')'),
|
COMPONENT_REGEXP.replace(/{component}/g, '(' + names + '.*?)'),
|
||||||
'gmi',
|
'gmi',
|
||||||
);
|
);
|
||||||
let match = anyCompRegexp.exec(rawText);
|
let match = anyCompRegexp.exec(rawText);
|
||||||
|
@ -169,7 +168,10 @@ export class MarkdownRenderer {
|
||||||
match = anyCompRegexp.exec(rawText);
|
match = anyCompRegexp.exec(rawText);
|
||||||
}
|
}
|
||||||
|
|
||||||
const splitCompRegexp = new RegExp(COMPONENT_REGEXP.replace(/{component}/g, names), 'mi');
|
const splitCompRegexp = new RegExp(
|
||||||
|
COMPONENT_REGEXP.replace(/{component}/g, names + '.*?'),
|
||||||
|
'mi',
|
||||||
|
);
|
||||||
const htmlParts = rawText.split(splitCompRegexp);
|
const htmlParts = rawText.split(splitCompRegexp);
|
||||||
const res: any[] = [];
|
const res: any[] = [];
|
||||||
for (let i = 0; i < htmlParts.length; i++) {
|
for (let i = 0; i < htmlParts.length; i++) {
|
||||||
|
|
|
@ -2,6 +2,8 @@ import defaultTheme, { ResolvedThemeInterface, resolveTheme, ThemeInterface } fr
|
||||||
import { querySelector } from '../utils/dom';
|
import { querySelector } from '../utils/dom';
|
||||||
import { isNumeric, mergeObjects } from '../utils/helpers';
|
import { isNumeric, mergeObjects } from '../utils/helpers';
|
||||||
|
|
||||||
|
import { MDXComponentMeta } from './MarkdownRenderer';
|
||||||
|
|
||||||
export interface RedocRawOptions {
|
export interface RedocRawOptions {
|
||||||
theme?: ThemeInterface;
|
theme?: ThemeInterface;
|
||||||
scrollYOffset?: number | string | (() => number);
|
scrollYOffset?: number | string | (() => number);
|
||||||
|
@ -17,6 +19,8 @@ export interface RedocRawOptions {
|
||||||
disableSearch?: boolean | string;
|
disableSearch?: boolean | string;
|
||||||
|
|
||||||
unstable_ignoreMimeParameters?: boolean;
|
unstable_ignoreMimeParameters?: boolean;
|
||||||
|
|
||||||
|
allowedMdComponents?: Dict<MDXComponentMeta>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function argValueToBoolean(val?: string | boolean): boolean {
|
function argValueToBoolean(val?: string | boolean): boolean {
|
||||||
|
@ -98,9 +102,11 @@ export class RedocNormalizedOptions {
|
||||||
|
|
||||||
/* tslint:disable-next-line */
|
/* tslint:disable-next-line */
|
||||||
unstable_ignoreMimeParameters: boolean;
|
unstable_ignoreMimeParameters: boolean;
|
||||||
|
allowedMdComponents: Dict<MDXComponentMeta>;
|
||||||
|
|
||||||
constructor(raw: RedocRawOptions) {
|
constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) {
|
||||||
let hook;
|
let hook;
|
||||||
|
raw = { ...defaults, ...raw };
|
||||||
if (raw.theme && raw.theme.extensionsHook) {
|
if (raw.theme && raw.theme.extensionsHook) {
|
||||||
hook = raw.theme.extensionsHook;
|
hook = raw.theme.extensionsHook;
|
||||||
raw.theme.extensionsHook = undefined;
|
raw.theme.extensionsHook = undefined;
|
||||||
|
@ -120,5 +126,7 @@ export class RedocNormalizedOptions {
|
||||||
this.disableSearch = argValueToBoolean(raw.disableSearch);
|
this.disableSearch = argValueToBoolean(raw.disableSearch);
|
||||||
|
|
||||||
this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters);
|
this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters);
|
||||||
|
|
||||||
|
this.allowedMdComponents = raw.allowedMdComponents || {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,21 @@
|
||||||
import { MarkdownRenderer } from '../MarkdownRenderer';
|
import { MarkdownRenderer, MDXComponentMeta } from '../MarkdownRenderer';
|
||||||
|
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
|
||||||
|
|
||||||
|
const TestComponent = () => null;
|
||||||
|
|
||||||
describe('Markdown renderer', () => {
|
describe('Markdown renderer', () => {
|
||||||
let renderer: MarkdownRenderer;
|
let renderer: MarkdownRenderer;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
renderer = new MarkdownRenderer();
|
renderer = new MarkdownRenderer(
|
||||||
|
new RedocNormalizedOptions({
|
||||||
|
allowedMdComponents: {
|
||||||
|
'security-definitions': {
|
||||||
|
component: TestComponent,
|
||||||
|
propsSelector: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return a level-1 heading even though only level-2 is present', () => {
|
test('should return a level-1 heading even though only level-2 is present', () => {
|
||||||
|
@ -19,4 +31,33 @@ describe('Markdown renderer', () => {
|
||||||
expect(headings[0].items).toBeDefined();
|
expect(headings[0].items).toBeDefined();
|
||||||
expect(headings[0].items).toHaveLength(1);
|
expect(headings[0].items).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('renderMdWithComponents should work with legacy syntax', () => {
|
||||||
|
const source = 'Hello!\n<!-- ReDoc-Inject: <security-definitions> -->\nBye';
|
||||||
|
const parts = renderer.renderMdWithComponents(source);
|
||||||
|
expect(parts).toHaveLength(3);
|
||||||
|
expect(parts[0]).toEqual('<p>Hello!</p>\n');
|
||||||
|
expect(typeof parts[1]).toEqual('object');
|
||||||
|
expect((parts[1] as MDXComponentMeta).component).toEqual(TestComponent);
|
||||||
|
expect(parts[2]).toEqual('<p>Bye</p>\n');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renderMdWithComponents should work with mdx-like syntax', () => {
|
||||||
|
const source = 'Hello!\n<security-definitions/>\nBye';
|
||||||
|
const parts = renderer.renderMdWithComponents(source);
|
||||||
|
expect(parts).toHaveLength(3);
|
||||||
|
expect(parts[0]).toEqual('<p>Hello!</p>\n');
|
||||||
|
expect(typeof parts[1]).toEqual('object');
|
||||||
|
expect((parts[1] as MDXComponentMeta).component).toBe(TestComponent);
|
||||||
|
expect(parts[2]).toEqual('<p>Bye</p>\n');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renderMdWithComponents should parse attribute names', () => {
|
||||||
|
const source = '<security-definitions pointer={"test"}/>';
|
||||||
|
const parts = renderer.renderMdWithComponents(source);
|
||||||
|
expect(parts).toHaveLength(1);
|
||||||
|
const part = parts[0] as MDXComponentMeta;
|
||||||
|
expect(part.component).toBe(TestComponent);
|
||||||
|
expect(part.attrs).toEqual({ pointer: 'test' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue
Block a user