diff --git a/src/components/ContentItems/ContentItems.tsx b/src/components/ContentItems/ContentItems.tsx index 5d299ff9..aa8c4376 100644 --- a/src/components/ContentItems/ContentItems.tsx +++ b/src/components/ContentItems/ContentItems.tsx @@ -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; }> { - static defaultProps = { - allowedMdComponents: DEFAULT_ALLOWED_COMPONENTS, - }; - render() { const items = this.props.items; if (items.length === 0) { return null; } - return items.map(item => ( - - )); + return items.map(item => ); } } export interface ContentItemProps { item: ContentItemModel; - allowedMdComponents: Dict; } @observer @@ -76,9 +53,7 @@ export class ContentItem extends React.Component {
{content}
- {item.items && ( - - )} + {item.items && } ); } @@ -90,7 +65,6 @@ const middlePanelWrap = component => {component}; export class SectionItem extends React.Component { 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 { - + ); } diff --git a/src/components/Markdown/AdvancedMarkdown.tsx b/src/components/Markdown/AdvancedMarkdown.tsx index cba6232f..b75631d3 100644 --- a/src/components/Markdown/AdvancedMarkdown.tsx +++ b/src/components/Markdown/AdvancedMarkdown.tsx @@ -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; htmlWrap?: (part: JSX.Element) => JSX.Element; } export class AdvancedMarkdown extends React.Component { render() { - return {store => this.renderWithStore(store)}; + return ( + + {options => ( + {store => this.renderWithOptionsAndStore(options, store)} + )} + + ); } - 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; diff --git a/src/components/Redoc/Redoc.tsx b/src/components/Redoc/Redoc.tsx index 323cdbd4..2b43e77d 100644 --- a/src/components/Redoc/Redoc.tsx +++ b/src/components/Redoc/Redoc.tsx @@ -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'; diff --git a/src/services/AppStore.ts b/src/services/AppStore.ts index 1d3614f0..81419b15 100644 --- a/src/services/AppStore.ts +++ b/src/services/AppStore.ts @@ -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, + }), + }, + }, +}; diff --git a/src/services/MarkdownRenderer.ts b/src/services/MarkdownRenderer.ts index bb83efb7..3494415b 100644 --- a/src/services/MarkdownRenderer.ts +++ b/src/services/MarkdownRenderer.ts @@ -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, - ): Array { + renderMdWithComponents(rawText: string): Array { + 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++) { diff --git a/src/services/RedocNormalizedOptions.ts b/src/services/RedocNormalizedOptions.ts index 8c3e0462..851d4b1b 100644 --- a/src/services/RedocNormalizedOptions.ts +++ b/src/services/RedocNormalizedOptions.ts @@ -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; } function argValueToBoolean(val?: string | boolean): boolean { @@ -98,9 +102,11 @@ export class RedocNormalizedOptions { /* tslint:disable-next-line */ unstable_ignoreMimeParameters: boolean; + allowedMdComponents: Dict; - 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 || {}; } } diff --git a/src/services/__tests__/MarkdownRenderer.test.ts b/src/services/__tests__/MarkdownRenderer.test.ts index 4ed71494..89f308c6 100644 --- a/src/services/__tests__/MarkdownRenderer.test.ts +++ b/src/services/__tests__/MarkdownRenderer.test.ts @@ -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\nBye'; + const parts = renderer.renderMdWithComponents(source); + expect(parts).toHaveLength(3); + expect(parts[0]).toEqual('

Hello!

\n'); + expect(typeof parts[1]).toEqual('object'); + expect((parts[1] as MDXComponentMeta).component).toEqual(TestComponent); + expect(parts[2]).toEqual('

Bye

\n'); + }); + + test('renderMdWithComponents should work with mdx-like syntax', () => { + const source = 'Hello!\n\nBye'; + const parts = renderer.renderMdWithComponents(source); + expect(parts).toHaveLength(3); + expect(parts[0]).toEqual('

Hello!

\n'); + expect(typeof parts[1]).toEqual('object'); + expect((parts[1] as MDXComponentMeta).component).toBe(TestComponent); + expect(parts[2]).toEqual('

Bye

\n'); + }); + + test('renderMdWithComponents should parse attribute names', () => { + const source = ''; + 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' }); + }); });