mirror of
				https://github.com/Redocly/redoc.git
				synced 2025-10-31 15:57:30 +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