mirror of
				https://github.com/Redocly/redoc.git
				synced 2025-10-30 23:37:28 +03:00 
			
		
		
		
	chore: sections/markdown refactor
This commit is contained in:
		
							parent
							
								
									e0d82f4aa8
								
							
						
					
					
						commit
						eae11682b8
					
				|  | @ -1,19 +1,44 @@ | ||||||
| import styled, { media } from '../styled-components'; | import { SECTION_ATTR } from '../services/MenuStore'; | ||||||
|  | import styled, { media, withProps } from '../styled-components'; | ||||||
| 
 | 
 | ||||||
| export const MiddlePanel = styled.div` | export const MiddlePanel = styled.div` | ||||||
|   width: calc(100% - ${props => props.theme.rightPanel.width}); |   width: calc(100% - ${props => props.theme.rightPanel.width}); | ||||||
|   padding: ${props => props.theme.spacing.unit * 8}px; |   padding: 0 ${props => props.theme.spacing.unit * 8}px; | ||||||
| 
 | 
 | ||||||
|   ${media.lessThan('medium')` |   ${media.lessThan('medium')` | ||||||
|     width: 100%; |     width: 100%; | ||||||
|   `};
 |   `};
 | ||||||
| `;
 | `;
 | ||||||
| 
 | 
 | ||||||
|  | export const Section = withProps<{ underlined?: boolean }>( | ||||||
|  |   styled.div.attrs({ | ||||||
|  |     [SECTION_ATTR]: props => props.id, | ||||||
|  |   } as any), | ||||||
|  | )` | ||||||
|  |   padding: ${props => props.theme.spacing.unit * 8}px 0; | ||||||
|  | 
 | ||||||
|  |   ${props => | ||||||
|  |     (props.underlined && | ||||||
|  |       ` | ||||||
|  |     position: relative; | ||||||
|  | 
 | ||||||
|  |     &:not(:last-of-type):after { | ||||||
|  |       position: absolute; | ||||||
|  |       bottom: 0; | ||||||
|  |       width: 100%; | ||||||
|  |       display: block; | ||||||
|  |       content: ''; | ||||||
|  |       border-bottom: 1px solid rgba(0, 0, 0, 0.2); | ||||||
|  |     } | ||||||
|  |   `) ||
 | ||||||
|  |     ''} | ||||||
|  | `;
 | ||||||
|  | 
 | ||||||
| export const RightPanel = styled.div` | export const RightPanel = styled.div` | ||||||
|   width: ${props => props.theme.rightPanel.width}; |   width: ${props => props.theme.rightPanel.width}; | ||||||
|   color: #fafbfc; |   color: #fafbfc; | ||||||
|   background-color: ${props => props.theme.rightPanel.backgroundColor}; |   background-color: ${props => props.theme.rightPanel.backgroundColor}; | ||||||
|   padding: ${props => props.theme.spacing.unit * 8}px; |   padding: 0 ${props => props.theme.spacing.unit * 8}px; | ||||||
| 
 | 
 | ||||||
|   ${media.lessThan('medium')` |   ${media.lessThan('medium')` | ||||||
|     width: 100%; |     width: 100%; | ||||||
|  | @ -27,6 +52,7 @@ export const DarkRightPanel = RightPanel.extend` | ||||||
| export const Row = styled.div` | export const Row = styled.div` | ||||||
|   display: flex; |   display: flex; | ||||||
|   width: 100%; |   width: 100%; | ||||||
|  |   padding: 0; | ||||||
| 
 | 
 | ||||||
|   ${media.lessThan('medium')` |   ${media.lessThan('medium')` | ||||||
|     flex-direction: column; |     flex-direction: column; | ||||||
|  |  | ||||||
|  | @ -2,6 +2,8 @@ import * as React from 'react'; | ||||||
| 
 | 
 | ||||||
| import PerfectScrollbarType, * as PerfectScrollbarNamespace from 'perfect-scrollbar'; | import PerfectScrollbarType, * as PerfectScrollbarNamespace from 'perfect-scrollbar'; | ||||||
| import psStyles from 'perfect-scrollbar/css/perfect-scrollbar.css'; | import psStyles from 'perfect-scrollbar/css/perfect-scrollbar.css'; | ||||||
|  | 
 | ||||||
|  | import { OptionsContext } from '../components/OptionsProvider'; | ||||||
| import styled, { injectGlobal } from '../styled-components'; | import styled, { injectGlobal } from '../styled-components'; | ||||||
| 
 | 
 | ||||||
| /* | /* | ||||||
|  | @ -18,11 +20,13 @@ const StyledScrollWrapper = styled.div` | ||||||
|   position: relative; |   position: relative; | ||||||
| `;
 | `;
 | ||||||
| 
 | 
 | ||||||
| export class PerfectScrollbar extends React.Component<{ | export interface PerfectScrollbarProps { | ||||||
|   options?: PerfectScrollbarType.Options; |   options?: PerfectScrollbarType.Options; | ||||||
|   className?: string; |   className?: string; | ||||||
|   updateFn: (fn) => void; |   updateFn?: (fn) => void; | ||||||
| }> { | } | ||||||
|  | 
 | ||||||
|  | export class PerfectScrollbar extends React.Component<PerfectScrollbarProps> { | ||||||
|   private _container: HTMLElement; |   private _container: HTMLElement; | ||||||
|   private inst: PerfectScrollbarType; |   private inst: PerfectScrollbarType; | ||||||
| 
 | 
 | ||||||
|  | @ -49,7 +53,9 @@ export class PerfectScrollbar extends React.Component<{ | ||||||
|   render() { |   render() { | ||||||
|     const { children, className, updateFn } = this.props; |     const { children, className, updateFn } = this.props; | ||||||
| 
 | 
 | ||||||
|     updateFn(this.componentDidUpdate.bind(this)); |     if (updateFn) { | ||||||
|  |       updateFn(this.componentDidUpdate.bind(this)); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|       <StyledScrollWrapper className={`scrollbar-container ${className}`} innerRef={this.handleRef}> |       <StyledScrollWrapper className={`scrollbar-container ${className}`} innerRef={this.handleRef}> | ||||||
|  | @ -58,3 +64,26 @@ export class PerfectScrollbar extends React.Component<{ | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export function PerfectScrollbarWrap( | ||||||
|  |   props: PerfectScrollbarProps & { children: JSX.Element[] | JSX.Element }, | ||||||
|  | ) { | ||||||
|  |   return ( | ||||||
|  |     <OptionsContext.Consumer> | ||||||
|  |       {options => | ||||||
|  |         !options.nativeScrollbars ? ( | ||||||
|  |           <PerfectScrollbar {...props}>{props.children}</PerfectScrollbar> | ||||||
|  |         ) : ( | ||||||
|  |           <div | ||||||
|  |             style={{ | ||||||
|  |               overflow: 'auto', | ||||||
|  |               msOverflowStyle: '-ms-autohiding-scrollbar', | ||||||
|  |             }} | ||||||
|  |           > | ||||||
|  |             {props.children} | ||||||
|  |           </div> | ||||||
|  |         ) | ||||||
|  |       } | ||||||
|  |     </OptionsContext.Consumer> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -1,22 +0,0 @@ | ||||||
| import * as React from 'react'; |  | ||||||
| 
 |  | ||||||
| import { MiddlePanel, Row } from '../../common-elements/'; |  | ||||||
| 
 |  | ||||||
| import { Markdown } from '../Markdown/Markdown'; |  | ||||||
| 
 |  | ||||||
| export interface ApiDescriptionProps { |  | ||||||
|   description: string; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export class ApiDescription extends React.PureComponent<ApiDescriptionProps> { |  | ||||||
|   render() { |  | ||||||
|     const { description } = this.props; |  | ||||||
|     return ( |  | ||||||
|       <Row> |  | ||||||
|         <MiddlePanel> |  | ||||||
|           <Markdown source={description} /> |  | ||||||
|         </MiddlePanel> |  | ||||||
|       </Row> |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  | @ -3,8 +3,8 @@ import * as React from 'react'; | ||||||
| 
 | 
 | ||||||
| import { AppStore } from '../../services/AppStore'; | import { AppStore } from '../../services/AppStore'; | ||||||
| 
 | 
 | ||||||
| import { MiddlePanel, Row } from '../../common-elements/'; | import { MiddlePanel, Row, Section } from '../../common-elements/'; | ||||||
| 
 | import { Markdown } from '../Markdown/Markdown'; | ||||||
| import { StyledMarkdownBlock } from '../Markdown/styled.elements'; | import { StyledMarkdownBlock } from '../Markdown/styled.elements'; | ||||||
| import { | import { | ||||||
|   ApiHeader, |   ApiHeader, | ||||||
|  | @ -70,43 +70,46 @@ export class ApiInfo extends React.Component<ApiInfoProps> { | ||||||
|       null; |       null; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|       <Row> |       <Section> | ||||||
|         <MiddlePanel className="api-info"> |         <Row> | ||||||
|           <ApiHeader> |           <MiddlePanel className="api-info"> | ||||||
|             {info.title} <span>({info.version})</span> |             <ApiHeader> | ||||||
|           </ApiHeader> |               {info.title} <span>({info.version})</span> | ||||||
|           {!hideDownloadButton && ( |             </ApiHeader> | ||||||
|             <p> |             {!hideDownloadButton && ( | ||||||
|               Download OpenAPI specification: |  | ||||||
|               <DownloadButton |  | ||||||
|                 download={downloadFilename} |  | ||||||
|                 target="_blank" |  | ||||||
|                 href={downloadLink} |  | ||||||
|                 onClick={this.handleDownloadClick} |  | ||||||
|               > |  | ||||||
|                 Download |  | ||||||
|               </DownloadButton> |  | ||||||
|             </p> |  | ||||||
|           )} |  | ||||||
|           <StyledMarkdownBlock> |  | ||||||
|             {((info.license || info.contact || info.termsOfService) && ( |  | ||||||
|               <InfoSpanBoxWrap> |  | ||||||
|                 <InfoSpanBox> |  | ||||||
|                   {email} {website} {license} {terms} |  | ||||||
|                 </InfoSpanBox> |  | ||||||
|               </InfoSpanBoxWrap> |  | ||||||
|             )) || |  | ||||||
|               null} |  | ||||||
| 
 |  | ||||||
|             {(externalDocs && ( |  | ||||||
|               <p> |               <p> | ||||||
|                 <a href={externalDocs.url}>{externalDocs.description || externalDocs.url}</a> |                 Download OpenAPI specification: | ||||||
|  |                 <DownloadButton | ||||||
|  |                   download={downloadFilename} | ||||||
|  |                   target="_blank" | ||||||
|  |                   href={downloadLink} | ||||||
|  |                   onClick={this.handleDownloadClick} | ||||||
|  |                 > | ||||||
|  |                   Download | ||||||
|  |                 </DownloadButton> | ||||||
|               </p> |               </p> | ||||||
|             )) || |             )} | ||||||
|               null} |             <StyledMarkdownBlock> | ||||||
|           </StyledMarkdownBlock> |               {((info.license || info.contact || info.termsOfService) && ( | ||||||
|         </MiddlePanel> |                 <InfoSpanBoxWrap> | ||||||
|       </Row> |                   <InfoSpanBox> | ||||||
|  |                     {email} {website} {license} {terms} | ||||||
|  |                   </InfoSpanBox> | ||||||
|  |                 </InfoSpanBoxWrap> | ||||||
|  |               )) || | ||||||
|  |                 null} | ||||||
|  | 
 | ||||||
|  |               {(externalDocs && ( | ||||||
|  |                 <p> | ||||||
|  |                   <a href={externalDocs.url}>{externalDocs.description || externalDocs.url}</a> | ||||||
|  |                 </p> | ||||||
|  |               )) || | ||||||
|  |                 null} | ||||||
|  |             </StyledMarkdownBlock> | ||||||
|  |             <Markdown source={store.spec.info.description} /> | ||||||
|  |           </MiddlePanel> | ||||||
|  |         </Row> | ||||||
|  |       </Section> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,2 +1 @@ | ||||||
| export { ApiDescription } from './ApiDescription'; |  | ||||||
| export { ApiInfo } from './ApiInfo'; | export { ApiInfo } from './ApiInfo'; | ||||||
|  |  | ||||||
|  | @ -1,10 +1,9 @@ | ||||||
| import { observer } from 'mobx-react'; | import { observer } from 'mobx-react'; | ||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| 
 | 
 | ||||||
| import { SECTION_ATTR } from '../../services/MenuStore'; | import { AdvancedMarkdown } from '../Markdown/AdvancedMarkdown'; | ||||||
| import { Markdown } from '../Markdown/Markdown'; |  | ||||||
| 
 | 
 | ||||||
| import { H1, H2, MiddlePanel, Row, ShareLink } from '../../common-elements'; | import { H1, H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements'; | ||||||
| import { MDXComponentMeta } from '../../services/MarkdownRenderer'; | 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'; | ||||||
|  | @ -12,20 +11,22 @@ import { Operation } from '../Operation/Operation'; | ||||||
| import { SecurityDefs } from '../SecuritySchemes/SecuritySchemes'; | import { SecurityDefs } from '../SecuritySchemes/SecuritySchemes'; | ||||||
| import { StoreConsumer } from '../StoreBuilder'; | import { StoreConsumer } from '../StoreBuilder'; | ||||||
| 
 | 
 | ||||||
|  | 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>; |   allowedMdComponents?: Dict<MDXComponentMeta>; | ||||||
| }> { | }> { | ||||||
|   static defaultProps = { |   static defaultProps = { | ||||||
|     allowedMdComponents: { |     allowedMdComponents: DEFAULT_ALLOWED_COMPONENTS, | ||||||
|       'security-definitions': { |  | ||||||
|         component: SecurityDefs, |  | ||||||
|         propsSelector: _store => ({ |  | ||||||
|           securitySchemes: _store!.spec.securitySchemes, |  | ||||||
|         }), |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   render() { |   render() { | ||||||
|  | @ -34,14 +35,18 @@ export class ContentItems extends React.Component<{ | ||||||
|       return null; |       return null; | ||||||
|     } |     } | ||||||
|     return items.map(item => ( |     return items.map(item => ( | ||||||
|       <ContentItem item={item} key={item.id} allowedMdComponents={this.props.allowedMdComponents} /> |       <ContentItem | ||||||
|  |         item={item} | ||||||
|  |         key={item.id} | ||||||
|  |         allowedMdComponents={this.props.allowedMdComponents!} | ||||||
|  |       /> | ||||||
|     )); |     )); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface ContentItemProps { | export interface ContentItemProps { | ||||||
|   item: ContentItemModel; |   item: ContentItemModel; | ||||||
|   allowedMdComponents?: Dict<MDXComponentMeta>; |   allowedMdComponents: Dict<MDXComponentMeta>; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @observer | @observer | ||||||
|  | @ -67,15 +72,21 @@ export class ContentItem extends React.Component<ContentItemProps> { | ||||||
|         throw new Error('Unknown item type'); |         throw new Error('Unknown item type'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return [ |     return ( | ||||||
|       <div key="section" {...{ [SECTION_ATTR]: item.id }}> |       <> | ||||||
|         {content} |         <Section id={item.id} underlined={item.type === 'section'}> | ||||||
|       </div>, |           {content} | ||||||
|       (item as any).items && <ContentItems key="content" items={(item as any).items} />, |         </Section> | ||||||
|     ]; |         {item.items && ( | ||||||
|  |           <ContentItems items={item.items} allowedMdComponents={this.props.allowedMdComponents} /> | ||||||
|  |         )} | ||||||
|  |       </> | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | const middlePanelWrap = component => <MiddlePanel>{component}</MiddlePanel>; | ||||||
|  | 
 | ||||||
| @observer | @observer | ||||||
| export class SectionItem extends React.Component<ContentItemProps> { | export class SectionItem extends React.Component<ContentItemProps> { | ||||||
|   render() { |   render() { | ||||||
|  | @ -83,23 +94,26 @@ export class SectionItem extends React.Component<ContentItemProps> { | ||||||
|     const components = this.props.allowedMdComponents; |     const components = this.props.allowedMdComponents; | ||||||
|     const Header = level === 2 ? H2 : H1; |     const Header = level === 2 ? H2 : H1; | ||||||
|     return ( |     return ( | ||||||
|       <Row> |       <> | ||||||
|         <MiddlePanel> |         <Row> | ||||||
|           <Header> |           <MiddlePanel> | ||||||
|             <ShareLink href={'#' + this.props.item.id} /> |             <Header> | ||||||
|             {name} |               <ShareLink href={'#' + this.props.item.id} /> | ||||||
|           </Header> |               {name} | ||||||
|           {components ? ( |             </Header> | ||||||
|             <StoreConsumer> |           </MiddlePanel> | ||||||
|               {store => ( |         </Row> | ||||||
|                 <Markdown source={description || ''} allowedComponents={components} store={store} /> |         <StoreConsumer> | ||||||
|               )} |           {store => ( | ||||||
|             </StoreConsumer> |             <AdvancedMarkdown | ||||||
|           ) : ( |               source={description || ''} | ||||||
|             <Markdown source={description || ''} /> |               allowedComponents={components} | ||||||
|  |               store={store} | ||||||
|  |               htmlWrap={middlePanelWrap} | ||||||
|  |             /> | ||||||
|           )} |           )} | ||||||
|         </MiddlePanel> |         </StoreConsumer> | ||||||
|       </Row> |       </> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										38
									
								
								src/components/Markdown/AdvancedMarkdown.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/components/Markdown/AdvancedMarkdown.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,38 @@ | ||||||
|  | import * as React from 'react'; | ||||||
|  | 
 | ||||||
|  | import { AppStore, MarkdownRenderer, MDXComponentMeta } from '../../services'; | ||||||
|  | import { BaseMarkdownProps } from './Markdown'; | ||||||
|  | import { SanitizedMarkdownHTML } from './SanitizedMdBlock'; | ||||||
|  | 
 | ||||||
|  | export interface AdvancedMarkdownProps extends BaseMarkdownProps { | ||||||
|  |   store?: AppStore; | ||||||
|  |   allowedComponents: Dict<MDXComponentMeta>; | ||||||
|  |   htmlWrap?: (part: JSX.Element) => JSX.Element; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export class AdvancedMarkdown extends React.Component<AdvancedMarkdownProps> { | ||||||
|  |   render() { | ||||||
|  |     const { store, source, allowedComponents, 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); | ||||||
|  | 
 | ||||||
|  |     if (!parts.length) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return parts.map((part, idx) => { | ||||||
|  |       if (typeof part === 'string') { | ||||||
|  |         return React.cloneElement( | ||||||
|  |           htmlWrap(<SanitizedMarkdownHTML html={part} inline={false} dense={false} />), | ||||||
|  |           { key: idx }, | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |       return <part.component key={idx} {...{ ...part.attrs, ...part.propsSelector(store) }} />; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -1,103 +1,35 @@ | ||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| 
 | 
 | ||||||
| import * as DOMPurify from 'dompurify'; | import { MarkdownRenderer } from '../../services'; | ||||||
| import { AppStore, MarkdownRenderer, MDXComponentMeta } from '../../services'; | import { SanitizedMarkdownHTML } from './SanitizedMdBlock'; | ||||||
| import { OptionsContext } from '../OptionsProvider'; |  | ||||||
| 
 |  | ||||||
| import { StyledMarkdownBlock } from './styled.elements'; |  | ||||||
| 
 |  | ||||||
| const StyledMarkdownSpan = StyledMarkdownBlock.withComponent('span'); |  | ||||||
| 
 | 
 | ||||||
| export interface StylingMarkdownProps { | export interface StylingMarkdownProps { | ||||||
|   dense?: boolean; |   dense?: boolean; | ||||||
|   inline?: boolean; |   inline?: boolean; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface BaseMarkdownProps extends StylingMarkdownProps { | export interface BaseMarkdownProps { | ||||||
|   sanitize?: boolean; |   sanitize?: boolean; | ||||||
|   store?: AppStore; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const sanitize = (untrustedSpec, html) => (untrustedSpec ? DOMPurify.sanitize(html) : html); |  | ||||||
| 
 |  | ||||||
| function SanitizedMarkdownHTML(props: StylingMarkdownProps & { html: string }) { |  | ||||||
|   const Wrap = props.inline ? StyledMarkdownSpan : StyledMarkdownBlock; |  | ||||||
| 
 |  | ||||||
|   return ( |  | ||||||
|     <OptionsContext.Consumer> |  | ||||||
|       {options => ( |  | ||||||
|         <Wrap |  | ||||||
|           className={'redoc-markdown'} |  | ||||||
|           dangerouslySetInnerHTML={{ |  | ||||||
|             __html: sanitize(options.untrustedSpec, props.html), |  | ||||||
|           }} |  | ||||||
|           {...props} |  | ||||||
|         /> |  | ||||||
|       )} |  | ||||||
|     </OptionsContext.Consumer> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export interface MarkdownProps extends BaseMarkdownProps { |  | ||||||
|   allowedComponents?: Dict<MDXComponentMeta>; |  | ||||||
|   source: string; |   source: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export type MarkdownProps = BaseMarkdownProps & | ||||||
|  |   StylingMarkdownProps & { | ||||||
|  |     source: string; | ||||||
|  |     className?: string; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
| export class Markdown extends React.Component<MarkdownProps> { | export class Markdown extends React.Component<MarkdownProps> { | ||||||
|   constructor(props: MarkdownProps) { |  | ||||||
|     super(props); |  | ||||||
| 
 |  | ||||||
|     if (props.allowedComponents && props.inline) { |  | ||||||
|       throw new Error('Markdown Component: "inline" mode doesn\'t support "components"'); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   render() { |   render() { | ||||||
|     const { source, allowedComponents, store, inline, dense } = this.props; |     const { source, inline, dense, className } = this.props; | ||||||
| 
 |  | ||||||
|     if (allowedComponents && !store) { |  | ||||||
|       throw new Error('When using componentes in markdown, store prop must be provided'); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const renderer = new MarkdownRenderer(); |     const renderer = new MarkdownRenderer(); | ||||||
|     if (allowedComponents) { |  | ||||||
|       return ( |  | ||||||
|         <AdvancedMarkdown |  | ||||||
|           parts={renderer.renderMdWithComponents(source, allowedComponents)} |  | ||||||
|           {...this.props} |  | ||||||
|         /> |  | ||||||
|       ); |  | ||||||
|     } else { |  | ||||||
|       return ( |  | ||||||
|         <SanitizedMarkdownHTML html={renderer.renderMd(source)} inline={inline} dense={dense} /> |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export interface AdvancedMarkdownProps extends BaseMarkdownProps { |  | ||||||
|   parts: Array<string | MDXComponentMeta>; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export class AdvancedMarkdown extends React.Component<AdvancedMarkdownProps> { |  | ||||||
|   render() { |  | ||||||
|     const { inline, dense, store, parts } = this.props; |  | ||||||
| 
 |  | ||||||
|     if (!parts.length) { |  | ||||||
|       return null; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return ( |     return ( | ||||||
|       <> |       <SanitizedMarkdownHTML | ||||||
|         {parts.map( |         html={renderer.renderMd(source)} | ||||||
|           (part, idx) => |         inline={inline} | ||||||
|             typeof part === 'string' ? ( |         dense={dense} | ||||||
|               <SanitizedMarkdownHTML html={part} inline={inline} dense={dense} key={idx} /> |         className={className} | ||||||
|             ) : ( |       /> | ||||||
|               <part.component key={idx} {...{ ...part.attrs, ...part.propsSelector(store) }} /> |  | ||||||
|             ), |  | ||||||
|         )} |  | ||||||
|       </> |  | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										30
									
								
								src/components/Markdown/SanitizedMdBlock.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/components/Markdown/SanitizedMdBlock.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | ||||||
|  | import * as DOMPurify from 'dompurify'; | ||||||
|  | import * as React from 'react'; | ||||||
|  | 
 | ||||||
|  | import { OptionsContext } from '../OptionsProvider'; | ||||||
|  | import { StylingMarkdownProps } from './Markdown'; | ||||||
|  | import { StyledMarkdownBlock } from './styled.elements'; | ||||||
|  | 
 | ||||||
|  | const StyledMarkdownSpan = StyledMarkdownBlock.withComponent('span'); | ||||||
|  | 
 | ||||||
|  | const sanitize = (untrustedSpec, html) => (untrustedSpec ? DOMPurify.sanitize(html) : html); | ||||||
|  | 
 | ||||||
|  | export function SanitizedMarkdownHTML( | ||||||
|  |   props: StylingMarkdownProps & { html: string; className?: string }, | ||||||
|  | ) { | ||||||
|  |   const Wrap = props.inline ? StyledMarkdownSpan : StyledMarkdownBlock; | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <OptionsContext.Consumer> | ||||||
|  |       {options => ( | ||||||
|  |         <Wrap | ||||||
|  |           className={'redoc-markdown ' + (props.className || '')} | ||||||
|  |           dangerouslySetInnerHTML={{ | ||||||
|  |             __html: sanitize(options.untrustedSpec, props.html), | ||||||
|  |           }} | ||||||
|  |           {...props} | ||||||
|  |         /> | ||||||
|  |       )} | ||||||
|  |     </OptionsContext.Consumer> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  | @ -23,20 +23,10 @@ const OperationRow = Row.extend` | ||||||
|   contain: content; |   contain: content; | ||||||
| 
 | 
 | ||||||
|   overflow: hidden; |   overflow: hidden; | ||||||
|   position: relative; |  | ||||||
| 
 |  | ||||||
|   &:after { |  | ||||||
|     position: absolute; |  | ||||||
|     bottom: 0; |  | ||||||
|     width: 100%; |  | ||||||
|     display: block; |  | ||||||
|     content: ''; |  | ||||||
|     border-bottom: 1px solid rgba(0, 0, 0, 0.2); |  | ||||||
|   } |  | ||||||
| `;
 | `;
 | ||||||
| 
 | 
 | ||||||
| const Description = styled(Markdown)` | const Description = styled(Markdown)` | ||||||
|   margin-bottom: ${({ theme }) => theme.spacing.unit * 8}; |   margin-bottom: ${({ theme }) => theme.spacing.unit * 6}px; | ||||||
| `;
 | `;
 | ||||||
| 
 | 
 | ||||||
| export interface OperationProps { | export interface OperationProps { | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ import * as React from 'react'; | ||||||
| import { ThemeProvider } from '../../styled-components'; | import { ThemeProvider } from '../../styled-components'; | ||||||
| 
 | 
 | ||||||
| import { AppStore } from '../../services'; | import { AppStore } from '../../services'; | ||||||
| import { ApiDescription, 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 { OptionsProvider } from '../OptionsProvider'; | ||||||
|  | @ -57,7 +57,6 @@ export class Redoc extends React.Component<RedocProps> { | ||||||
|               </StickyResponsiveSidebar> |               </StickyResponsiveSidebar> | ||||||
|               <ApiContentWrap className="api-content"> |               <ApiContentWrap className="api-content"> | ||||||
|                 <ApiInfo store={store} /> |                 <ApiInfo store={store} /> | ||||||
|                 <ApiDescription description={store.spec.info.description} /> |  | ||||||
|                 <ContentItems items={menu.items as any} /> |                 <ContentItems items={menu.items as any} /> | ||||||
|               </ApiContentWrap> |               </ApiContentWrap> | ||||||
|               <BackgroundStub /> |               <BackgroundStub /> | ||||||
|  |  | ||||||
|  | @ -7,6 +7,7 @@ import { MenuItem } from '../SideMenu/MenuItem'; | ||||||
| import { MarkerService } from '../../services/MarkerService'; | import { MarkerService } from '../../services/MarkerService'; | ||||||
| import { SearchResult } from '../../services/SearchWorker.worker'; | import { SearchResult } from '../../services/SearchWorker.worker'; | ||||||
| 
 | 
 | ||||||
|  | import { PerfectScrollbarWrap } from '../../common-elements/perfect-scrollbar'; | ||||||
| import { | import { | ||||||
|   ClearIcon, |   ClearIcon, | ||||||
|   SearchIcon, |   SearchIcon, | ||||||
|  | @ -135,21 +136,27 @@ export class SearchBox extends React.PureComponent<SearchBoxProps, SearchBoxStat | ||||||
|           onChange={this.search} |           onChange={this.search} | ||||||
|         /> |         /> | ||||||
|         {results.length > 0 && ( |         {results.length > 0 && ( | ||||||
|           <SearchResultsBox data-role="search:results"> |           <PerfectScrollbarWrap | ||||||
|             {results.map((res, idx) => ( |             options={{ | ||||||
|               <MenuItem |               wheelPropagation: false, | ||||||
|                 item={Object.create(res.item, { |             }} | ||||||
|                   active: { |           > | ||||||
|                     value: idx === activeItemIdx, |             <SearchResultsBox data-role="search:results"> | ||||||
|                   }, |               {results.map((res, idx) => ( | ||||||
|                 })} |                 <MenuItem | ||||||
|                 onActivate={this.props.onActivate} |                   item={Object.create(res.item, { | ||||||
|                 withoutChildren={true} |                     active: { | ||||||
|                 key={res.item.id} |                       value: idx === activeItemIdx, | ||||||
|                 data-role="search:result" |                     }, | ||||||
|               /> |                   })} | ||||||
|             ))} |                   onActivate={this.props.onActivate} | ||||||
|           </SearchResultsBox> |                   withoutChildren={true} | ||||||
|  |                   key={res.item.id} | ||||||
|  |                   data-role="search:result" | ||||||
|  |                 /> | ||||||
|  |               ))} | ||||||
|  |             </SearchResultsBox> | ||||||
|  |           </PerfectScrollbarWrap> | ||||||
|         )} |         )} | ||||||
|       </SearchWrap> |       </SearchWrap> | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|  | @ -58,7 +58,6 @@ export const SearchResultsBox = styled.div` | ||||||
|   margin-top: 10px; |   margin-top: 10px; | ||||||
|   line-height: 1.4; |   line-height: 1.4; | ||||||
|   font-size: 0.9em; |   font-size: 0.9em; | ||||||
|   overflow: auto; |  | ||||||
| 
 | 
 | ||||||
|   ${MenuItemLabel} { |   ${MenuItemLabel} { | ||||||
|     padding-top: 6px; |     padding-top: 6px; | ||||||
|  |  | ||||||
|  | @ -82,20 +82,23 @@ export class SecurityRequirement extends React.PureComponent<SecurityRequirement | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const AuthHeaderColumn = styled.td``; | const AuthHeaderColumn = styled.div` | ||||||
|  |   flex: 1; | ||||||
|  | `;
 | ||||||
| 
 | 
 | ||||||
| const SecuritiesColumn = styled.td` | const SecuritiesColumn = styled.div` | ||||||
|   width: ${props => props.theme.schema.defaultDetailsWidth}; |   width: ${props => props.theme.schema.defaultDetailsWidth}; | ||||||
| `;
 | `;
 | ||||||
| 
 | 
 | ||||||
| const AuthHeader = UnderlinedHeader.extend` | const AuthHeader = UnderlinedHeader.extend` | ||||||
|   display: inline-block; |   display: inline-block; | ||||||
|  |   margin: 0; | ||||||
| `;
 | `;
 | ||||||
| 
 | 
 | ||||||
| const Table = styled.table` | const Wrap = styled.div` | ||||||
|   width: 100%; |   width: 100%; | ||||||
|   border-collapse: collapse; |   display: flex; | ||||||
|   font-size: inherit; |   margin: 1em 0; | ||||||
| `;
 | `;
 | ||||||
| 
 | 
 | ||||||
| export interface SecurityRequirementsProps { | export interface SecurityRequirementsProps { | ||||||
|  | @ -109,20 +112,14 @@ export class SecurityRequirements extends React.PureComponent<SecurityRequiremen | ||||||
|       return null; |       return null; | ||||||
|     } |     } | ||||||
|     return ( |     return ( | ||||||
|       <Table> |       <Wrap> | ||||||
|         <tbody> |         <AuthHeaderColumn> | ||||||
|           <tr> |           <AuthHeader>Authorizations: </AuthHeader> | ||||||
|             <AuthHeaderColumn> |         </AuthHeaderColumn> | ||||||
|               <AuthHeader>Authorizations: </AuthHeader> |         <SecuritiesColumn> | ||||||
|             </AuthHeaderColumn> |           {securities.map((security, idx) => <SecurityRequirement key={idx} security={security} />)} | ||||||
|             <SecuritiesColumn> |         </SecuritiesColumn> | ||||||
|               {securities.map((security, idx) => ( |       </Wrap> | ||||||
|                 <SecurityRequirement key={idx} security={security} /> |  | ||||||
|               ))} |  | ||||||
|             </SecuritiesColumn> |  | ||||||
|           </tr> |  | ||||||
|         </tbody> |  | ||||||
|       </Table> |  | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ import * as React from 'react'; | ||||||
| 
 | 
 | ||||||
| import { SecuritySchemesModel } from '../../services/models'; | import { SecuritySchemesModel } from '../../services/models'; | ||||||
| 
 | 
 | ||||||
| import { H2, ShareLink } from '../../common-elements'; | import { H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements'; | ||||||
| import { OpenAPISecurityScheme } from '../../types'; | import { OpenAPISecurityScheme } from '../../types'; | ||||||
| import { Markdown } from '../Markdown/Markdown'; | import { Markdown } from '../Markdown/Markdown'; | ||||||
| import { StyledMarkdownBlock } from '../Markdown/styled.elements'; | import { StyledMarkdownBlock } from '../Markdown/styled.elements'; | ||||||
|  | @ -66,10 +66,10 @@ export interface SecurityDefsProps { | ||||||
| 
 | 
 | ||||||
| export class SecurityDefs extends React.PureComponent<SecurityDefsProps> { | export class SecurityDefs extends React.PureComponent<SecurityDefsProps> { | ||||||
|   render() { |   render() { | ||||||
|     return ( |     return this.props.securitySchemes.schemes.map(scheme => ( | ||||||
|       <div> |       <Section id={scheme.sectionId} key={scheme.id}> | ||||||
|         {this.props.securitySchemes.schemes.map(scheme => ( |         <Row> | ||||||
|           <div data-section-id={scheme.sectionId} key={scheme.id}> |           <MiddlePanel> | ||||||
|             <H2> |             <H2> | ||||||
|               <ShareLink href={'#' + scheme.sectionId} /> |               <ShareLink href={'#' + scheme.sectionId} /> | ||||||
|               {scheme.id} |               {scheme.id} | ||||||
|  | @ -118,9 +118,9 @@ export class SecurityDefs extends React.PureComponent<SecurityDefsProps> { | ||||||
|                 </tbody> |                 </tbody> | ||||||
|               </table> |               </table> | ||||||
|             </StyledMarkdownBlock> |             </StyledMarkdownBlock> | ||||||
|           </div> |           </MiddlePanel> | ||||||
|         ))} |         </Row> | ||||||
|       </div> |       </Section> | ||||||
|     ); |     )); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,11 +1,10 @@ | ||||||
| import { observer } from 'mobx-react'; | import { observer } from 'mobx-react'; | ||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { OptionsContext } from '../OptionsProvider'; |  | ||||||
| 
 | 
 | ||||||
| import { IMenuItem, MenuStore } from '../../services/MenuStore'; | import { IMenuItem, MenuStore } from '../../services/MenuStore'; | ||||||
| import { MenuItems } from './MenuItems'; | import { MenuItems } from './MenuItems'; | ||||||
| 
 | 
 | ||||||
| import { PerfectScrollbar } from '../../common-elements/perfect-scrollbar'; | import { PerfectScrollbarWrap } from '../../common-elements/perfect-scrollbar'; | ||||||
| import { RedocAttribution } from './styled.elements'; | import { RedocAttribution } from './styled.elements'; | ||||||
| 
 | 
 | ||||||
| @observer | @observer | ||||||
|  | @ -15,31 +14,20 @@ export class SideMenu extends React.Component<{ menu: MenuStore; className?: str | ||||||
|   render() { |   render() { | ||||||
|     const store = this.props.menu; |     const store = this.props.menu; | ||||||
|     return ( |     return ( | ||||||
|       <OptionsContext.Consumer> |       <PerfectScrollbarWrap | ||||||
|         {options => |         updateFn={this.saveScrollUpdate} | ||||||
|           options.nativeScrollbars ? ( |         className={this.props.className} | ||||||
|             <MenuItems |         options={{ | ||||||
|               className={this.props.className} |           wheelPropagation: false, | ||||||
|               style={{ |         }} | ||||||
|                 overflow: 'auto', |       > | ||||||
|                 msOverflowStyle: '-ms-autohiding-scrollbar', |         <MenuItems items={store.items} onActivate={this.activate} root={true} /> | ||||||
|               }} |         <RedocAttribution> | ||||||
|               items={store.items} |           <a target="_blank" href="https://github.com/Rebilly/ReDoc"> | ||||||
|               onActivate={this.activate} |             Documentation Powered by ReDoc | ||||||
|               root={true} |           </a> | ||||||
|             /> |         </RedocAttribution> | ||||||
|           ) : ( |       </PerfectScrollbarWrap> | ||||||
|             <PerfectScrollbar updateFn={this.saveScrollUpdate} className={this.props.className}> |  | ||||||
|               <MenuItems items={store.items} onActivate={this.activate} root={true} /> |  | ||||||
|               <RedocAttribution> |  | ||||||
|                 <a target="_blank" href="https://github.com/Rebilly/ReDoc"> |  | ||||||
|                   Documentation Powered by ReDoc |  | ||||||
|                 </a> |  | ||||||
|               </RedocAttribution> |  | ||||||
|             </PerfectScrollbar> |  | ||||||
|           ) |  | ||||||
|         } |  | ||||||
|       </OptionsContext.Consumer> |  | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -164,9 +164,10 @@ export const MenuItemTitle = withProps<{ width?: string }>(styled.span)` | ||||||
| `;
 | `;
 | ||||||
| 
 | 
 | ||||||
| export const RedocAttribution = styled.div` | export const RedocAttribution = styled.div` | ||||||
|  |   ${({ theme }) => ` | ||||||
|   font-size: 0.8em; |   font-size: 0.8em; | ||||||
|   margin-top: ${({ theme }) => `${theme.spacing.unit * 2}px`}; |   margin-top: ${theme.spacing.unit * 2}px; | ||||||
|   padding: ${({ theme }) => `0 ${theme.spacing.unit * 4}px`}; |   padding: 0 ${theme.spacing.unit * 4}px; | ||||||
|   text-align: left; |   text-align: left; | ||||||
| 
 | 
 | ||||||
|   opacity: 0.7; |   opacity: 0.7; | ||||||
|  | @ -174,9 +175,10 @@ export const RedocAttribution = styled.div` | ||||||
|   a, |   a, | ||||||
|   a:visited, |   a:visited, | ||||||
|   a:hover { |   a:hover { | ||||||
|     color: ${({ theme }) => theme.colors.text.primary} !important; |     color: ${theme.colors.text.primary} !important; | ||||||
|     border-top: 1px solid #e1e1e1; |     border-top: 1px solid #e1e1e1; | ||||||
|     padding-top: 10px; |     padding: ${theme.spacing.unit}px 0; | ||||||
|     display: block; |     display: block; | ||||||
|   } |   } | ||||||
|  | `};
 | ||||||
| `;
 | `;
 | ||||||
|  |  | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| export * from './RedocStandalone'; | export * from './RedocStandalone'; | ||||||
| export * from './Redoc/Redoc'; | export * from './Redoc/Redoc'; | ||||||
| export * from './ApiInfo/ApiInfo'; | export * from './ApiInfo/ApiInfo'; | ||||||
| export * from './ApiInfo/ApiDescription'; |  | ||||||
| export * from './ApiLogo/ApiLogo'; | export * from './ApiLogo/ApiLogo'; | ||||||
| export * from './ContentItems/ContentItems'; | export * from './ContentItems/ContentItems'; | ||||||
| export { ApiContentWrap, BackgroundStub, RedocWrap } from './Redoc/styled.elements'; | export { ApiContentWrap, BackgroundStub, RedocWrap } from './Redoc/styled.elements'; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| export * from './components'; | export * from './components'; | ||||||
| export { MiddlePanel, Row, RightPanel } from './common-elements/'; | export { MiddlePanel, Row, RightPanel, Section } from './common-elements/'; | ||||||
| export * from './services'; | export * from './services'; | ||||||
| export * from './utils'; | export * from './utils'; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user