mirror of
https://github.com/Redocly/redoc.git
synced 2024-11-11 11:26:37 +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 { 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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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++) {
|
||||
|
|
|
@ -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 || {};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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' });
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue
Block a user