chore: refactor, move allowedMdComponents to options

This commit is contained in:
Roman Hotsiy 2018-08-17 14:41:22 +03:00
parent f903406c14
commit d3d35189f5
No known key found for this signature in database
GPG Key ID: 5CB7B3ACABA57CB0
7 changed files with 93 additions and 52 deletions

View File

@ -4,48 +4,25 @@ import * as React from 'react';
import { AdvancedMarkdown } from '../Markdown/AdvancedMarkdown';
import { H1, H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements';
import { MDXComponentMeta } from '../../services/MarkdownRenderer';
import { ContentItemModel } from '../../services/MenuBuilder';
import { GroupModel, OperationModel } from '../../services/models';
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
export class ContentItems extends React.Component<{
items: ContentItemModel[];
allowedMdComponents?: Dict<MDXComponentMeta>;
}> {
static defaultProps = {
allowedMdComponents: DEFAULT_ALLOWED_COMPONENTS,
};
render() {
const items = this.props.items;
if (items.length === 0) {
return null;
}
return items.map(item => (
<ContentItem
item={item}
key={item.id}
allowedMdComponents={this.props.allowedMdComponents!}
/>
));
return items.map(item => <ContentItem item={item} key={item.id} />);
}
}
export interface ContentItemProps {
item: ContentItemModel;
allowedMdComponents: Dict<MDXComponentMeta>;
}
@observer
@ -76,9 +53,7 @@ export class ContentItem extends React.Component<ContentItemProps> {
<Section id={item.id} underlined={item.type === 'operation'}>
{content}
</Section>
{item.items && (
<ContentItems items={item.items} allowedMdComponents={this.props.allowedMdComponents} />
)}
{item.items && <ContentItems items={item.items} />}
</>
);
}
@ -90,7 +65,6 @@ const middlePanelWrap = component => <MiddlePanel>{component}</MiddlePanel>;
export class SectionItem extends React.Component<ContentItemProps> {
render() {
const { name, description, level } = this.props.item as GroupModel;
const { allowedMdComponents } = this.props;
const Header = level === 2 ? H2 : H1;
return (
@ -103,11 +77,7 @@ export class SectionItem extends React.Component<ContentItemProps> {
</Header>
</MiddlePanel>
</Row>
<AdvancedMarkdown
allowedComponents={allowedMdComponents}
source={description || ''}
htmlWrap={middlePanelWrap}
/>
<AdvancedMarkdown source={description || ''} htmlWrap={middlePanelWrap} />
</>
);
}

View File

@ -1,29 +1,35 @@
import * as React from 'react';
import { AppStore, MarkdownRenderer, MDXComponentMeta } from '../../services';
import { AppStore, MarkdownRenderer, RedocNormalizedOptions } from '../../services';
import { BaseMarkdownProps } from './Markdown';
import { SanitizedMarkdownHTML } from './SanitizedMdBlock';
import { OptionsConsumer } from '../OptionsProvider';
import { StoreConsumer } from '../StoreBuilder';
export interface AdvancedMarkdownProps extends BaseMarkdownProps {
allowedComponents: Dict<MDXComponentMeta>;
htmlWrap?: (part: JSX.Element) => JSX.Element;
}
export class AdvancedMarkdown extends React.Component<AdvancedMarkdownProps> {
render() {
return <StoreConsumer>{store => this.renderWithStore(store)}</StoreConsumer>;
return (
<OptionsConsumer>
{options => (
<StoreConsumer>{store => this.renderWithOptionsAndStore(options, store)}</StoreConsumer>
)}
</OptionsConsumer>
);
}
renderWithStore(store?: AppStore) {
const { allowedComponents, source, htmlWrap = i => i } = this.props;
renderWithOptionsAndStore(options: RedocNormalizedOptions, store?: AppStore) {
const { source, htmlWrap = i => i } = this.props;
if (!store) {
throw new Error('When using componentes in markdown, store prop must be provided');
}
const renderer = new MarkdownRenderer();
const parts = renderer.renderMdWithComponents(source, allowedComponents);
const renderer = new MarkdownRenderer(options);
const parts = renderer.renderMdWithComponents(source);
if (!parts.length) {
return null;

View File

@ -2,12 +2,12 @@ import * as PropTypes from 'prop-types';
import * as React from 'react';
import { ThemeProvider } from '../../styled-components';
import { OptionsProvider } from '../OptionsProvider';
import { AppStore } from '../../services';
import { ApiInfo } from '../ApiInfo/';
import { ApiLogo } from '../ApiLogo/ApiLogo';
import { ContentItems } from '../ContentItems/ContentItems';
import { OptionsProvider } from '../OptionsProvider';
import { SideMenu } from '../SideMenu/SideMenu';
import { StickyResponsiveSidebar } from '../StickySidebar/StickyResponsiveSidebar';
import { ApiContentWrap, BackgroundStub, RedocWrap } from './styled.elements';

View File

@ -10,6 +10,9 @@ import { RedocNormalizedOptions, RedocRawOptions } from './RedocNormalizedOption
import { ScrollService } from './ScrollService';
import { SearchStore } from './SearchStore';
import { SecurityDefs } from '../components/SecuritySchemes/SecuritySchemes';
import { SECURITY_DEFINITIONS_COMPONENT_NAME } from '../utils/openapi';
export interface StoreState {
menu: {
activeItemIdx: number;
@ -64,7 +67,7 @@ export class AppStore {
createSearchIndex: boolean = true,
) {
this.rawOptions = options;
this.options = new RedocNormalizedOptions(options);
this.options = new RedocNormalizedOptions(options, DEFAULT_OPTIONS);
this.scroll = new ScrollService(this.options);
// update position statically based on hash (in case of SSR)
@ -137,3 +140,14 @@ export class AppStore {
this.marker.mark();
}
}
const DEFAULT_OPTIONS: RedocRawOptions = {
allowedMdComponents: {
[SECURITY_DEFINITIONS_COMPONENT_NAME]: {
component: SecurityDefs,
propsSelector: (store: AppStore) => ({
securitySchemes: store.spec.securitySchemes,
}),
},
},
};

View File

@ -2,6 +2,7 @@ import * as marked from 'marked';
import { highlight, safeSlugify } from '../utils';
import { AppStore } from './AppStore';
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
const renderer = new marked.Renderer();
@ -49,7 +50,7 @@ export class MarkdownRenderer {
private headingEnhanceRenderer: marked.Renderer;
private originalHeadingRule: typeof marked.Renderer.prototype.heading;
constructor() {
constructor(public options?: RedocNormalizedOptions) {
this.headingEnhanceRenderer = new marked.Renderer();
this.originalHeadingRule = this.headingEnhanceRenderer.heading.bind(
this.headingEnhanceRenderer,
@ -148,10 +149,8 @@ export class MarkdownRenderer {
// TODO: rewrite this completelly! Regexp-based 👎
// Use marked ecosystem
renderMdWithComponents(
rawText: string,
components?: Dict<MDXComponentMeta>,
): Array<string | MDXComponentMeta> {
renderMdWithComponents(rawText: string): Array<string | MDXComponentMeta> {
const components = this.options && this.options.allowedMdComponents;
if (!components || Object.keys(components).length === 0) {
return [this.renderMd(rawText)];
}
@ -160,7 +159,7 @@ export class MarkdownRenderer {
const names = '(?:' + Object.keys(components).join('|') + ')';
const anyCompRegexp = new RegExp(
COMPONENT_REGEXP.replace(/{component}/g, '(' + names + ')'),
COMPONENT_REGEXP.replace(/{component}/g, '(' + names + '.*?)'),
'gmi',
);
let match = anyCompRegexp.exec(rawText);
@ -169,7 +168,10 @@ export class MarkdownRenderer {
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 res: any[] = [];
for (let i = 0; i < htmlParts.length; i++) {

View File

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

View File

@ -1,9 +1,21 @@
import { MarkdownRenderer } from '../MarkdownRenderer';
import { MarkdownRenderer, MDXComponentMeta } from '../MarkdownRenderer';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
const TestComponent = () => null;
describe('Markdown renderer', () => {
let renderer: MarkdownRenderer;
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', () => {
@ -19,4 +31,33 @@ describe('Markdown renderer', () => {
expect(headings[0].items).toBeDefined();
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' });
});
});