diff --git a/src/components/ApiInfo/ApiDescription.tsx b/src/components/ApiInfo/ApiDescription.tsx index 7f5d2dc2..0696578e 100644 --- a/src/components/ApiInfo/ApiDescription.tsx +++ b/src/components/ApiInfo/ApiDescription.tsx @@ -2,36 +2,19 @@ import * as React from 'react'; import { MiddlePanel, Row } from '../../common-elements/'; -import { AppStore } from '../../services/AppStore'; import { Markdown } from '../Markdown/Markdown'; -import { SecurityDefs } from '../SecuritySchemes/SecuritySchemes'; export interface ApiDescriptionProps { - store: AppStore; + description: string; } -const ALLOWED_COMPONENTS = { - 'security-definitions': { - component: SecurityDefs, - propsSelector: _store => ({ - securitySchemes: _store!.spec.securitySchemes, - }), - }, -}; - export class ApiDescription extends React.PureComponent { render() { - const { store } = this.props; - const description = store.spec.info.description; + const { description } = this.props; return ( - + ); diff --git a/src/components/ContentItems/ContentItems.tsx b/src/components/ContentItems/ContentItems.tsx index 3a86251e..f3ecc1e1 100644 --- a/src/components/ContentItems/ContentItems.tsx +++ b/src/components/ContentItems/ContentItems.tsx @@ -5,25 +5,43 @@ import { SECTION_ATTR } from '../../services/MenuStore'; import { Markdown } from '../Markdown/Markdown'; import { H1, MiddlePanel, Row, ShareLink } from '../../common-elements'; +import { MDXComponentMeta } from '../../services/MarkdownRenderer'; import { ContentItemModel } from '../../services/MenuBuilder'; import { OperationModel } from '../../services/models'; import { Operation } from '../Operation/Operation'; +import { SecurityDefs } from '../SecuritySchemes/SecuritySchemes'; +import { StoreConsumer } from '../StoreBuilder'; @observer export class ContentItems extends React.Component<{ items: ContentItemModel[]; + allowedMdComponents?: Dict; }> { + static defaultProps = { + allowedMdComponents: { + 'security-definitions': { + component: SecurityDefs, + propsSelector: _store => ({ + securitySchemes: _store!.spec.securitySchemes, + }), + }, + }, + }; + 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 @@ -37,10 +55,11 @@ export class ContentItem extends React.Component { content = null; break; case 'tag': - content = ; + content = ; break; case 'section': - return null; + content = ; + break; case 'operation': content = ; break; @@ -58,9 +77,10 @@ export class ContentItem extends React.Component { } @observer -export class TagItem extends React.Component { +export class SectionItem extends React.Component { render() { const { name, description } = this.props.item; + const components = this.props.allowedMdComponents; return ( @@ -68,7 +88,15 @@ export class TagItem extends React.Component { {name} - {description !== undefined && } + {components ? ( + + ) : ( + + {store => ( + + )} + + )} ); diff --git a/src/components/Markdown/Markdown.tsx b/src/components/Markdown/Markdown.tsx index 06332ae6..c5233227 100644 --- a/src/components/Markdown/Markdown.tsx +++ b/src/components/Markdown/Markdown.tsx @@ -14,7 +14,6 @@ export interface StylingMarkdownProps { } export interface BaseMarkdownProps extends StylingMarkdownProps { - raw?: boolean; sanitize?: boolean; store?: AppStore; } @@ -54,7 +53,7 @@ export class Markdown extends React.Component { } render() { - const { source, raw, allowedComponents, store, inline, dense } = this.props; + const { source, allowedComponents, store, inline, dense } = this.props; if (allowedComponents && !store) { throw new Error('When using componentes in markdown, store prop must be provided'); @@ -64,17 +63,13 @@ export class Markdown extends React.Component { if (allowedComponents) { return ( ); } else { return ( - + ); } } diff --git a/src/services/MarkdownRenderer.ts b/src/services/MarkdownRenderer.ts index 56a864a6..ab2835c6 100644 --- a/src/services/MarkdownRenderer.ts +++ b/src/services/MarkdownRenderer.ts @@ -1,8 +1,7 @@ import * as marked from 'marked'; -import { highlight, html2Str, safeSlugify } from '../utils'; +import { highlight, safeSlugify } from '../utils'; import { AppStore } from './AppStore'; -import { SECTION_ATTR } from './MenuStore'; const renderer = new marked.Renderer(); @@ -76,54 +75,49 @@ export class MarkdownRenderer { } attachHeadingsDescriptions(rawText: string) { - const buildRegexp = heading => - new RegExp(``); + const buildRegexp = heading => new RegExp(`##?\\s+${heading.name}`); const flatHeadings = this.flattenHeadings(this.headings); if (flatHeadings.length < 1) { return; } let prevHeading = flatHeadings[0]; - - let prevPos = rawText.search(buildRegexp(prevHeading)); + let prevRegexp = buildRegexp(prevHeading); + let prevPos = rawText.search(prevRegexp); for (let i = 1; i < flatHeadings.length; i++) { const heading = flatHeadings[i]; - const currentPos = rawText.substr(prevPos + 1).search(buildRegexp(heading)) + prevPos + 1; - prevHeading.description = html2Str(rawText.substring(prevPos, currentPos)); + const regexp = buildRegexp(heading); + const currentPos = rawText.substr(prevPos + 1).search(regexp) + prevPos + 1; + prevHeading.description = rawText + .substring(prevPos, currentPos) + .replace(prevRegexp, '') + .trim(); prevHeading = heading; + prevRegexp = regexp; prevPos = currentPos; } - prevHeading.description = html2Str(rawText.substring(prevPos)); + prevHeading.description = rawText + .substring(prevPos) + .replace(prevRegexp, '') + .trim(); } headingRule = (text: string, level: number, raw: string) => { if (level === 1) { this.currentTopHeading = this.saveHeading(text); - const id = this.currentTopHeading.id; - return ( - `` + - `` + - `${text}` - ); } else if (level === 2) { - const { id } = this.saveHeading( + this.saveHeading( text, this.currentTopHeading && this.currentTopHeading.items, this.currentTopHeading && this.currentTopHeading.id, ); - return ( - `` + - `` + - `${text}` - ); - } else { - return this.originalHeadingRule(text, level, raw); } + return this.originalHeadingRule(text, level, raw); }; - renderMd(rawText: string, raw: boolean = true): string { - const opts = raw ? undefined : { renderer: this.headingEnhanceRenderer }; + renderMd(rawText: string, extractHeadings: boolean = false): string { + const opts = extractHeadings ? { renderer: this.headingEnhanceRenderer } : undefined; const res = marked(rawText.toString(), opts); @@ -131,8 +125,8 @@ export class MarkdownRenderer { } extractHeadings(rawText: string): MarkdownHeading[] { - const text = this.renderMd(rawText, false); - this.attachHeadingsDescriptions(text); + this.renderMd(rawText, true); + this.attachHeadingsDescriptions(rawText); const res = this.headings; this.headings = []; return res; @@ -143,7 +137,6 @@ export class MarkdownRenderer { renderMdWithComponents( rawText: string, components: Dict, - raw: boolean = true, ): Array { const componentDefs: string[] = []; const names = '(?:' + Object.keys(components).join('|') + ')'; @@ -167,7 +160,7 @@ export class MarkdownRenderer { for (let i = 0; i < htmlParts.length; i++) { const htmlPart = htmlParts[i]; if (htmlPart) { - res.push(this.renderMd(htmlPart, raw)); + res.push(this.renderMd(htmlPart)); } if (componentDefs[i]) { const { componentName, attrs } = parseComponent(componentDefs[i]); diff --git a/src/services/OpenAPIParser.ts b/src/services/OpenAPIParser.ts index ab807efc..8037705a 100644 --- a/src/services/OpenAPIParser.ts +++ b/src/services/OpenAPIParser.ts @@ -5,7 +5,7 @@ import { OpenAPIRef, OpenAPISchema, OpenAPISpec, Referenced } from '../types'; import { appendToMdHeading, IS_BROWSER } from '../utils/'; import { JsonPointer } from '../utils/JsonPointer'; import { isNamedDefinition } from '../utils/openapi'; -import { buildComponentComment, COMPONENT_REGEXP } from './MarkdownRenderer'; +import { buildComponentComment, COMPONENT_REGEXP, MDX_COMPONENT_REGEXP } from './MarkdownRenderer'; import { RedocNormalizedOptions } from './RedocNormalizedOptions'; export type MergedOpenAPISchema = OpenAPISchema & { parentRefs?: string[] }; @@ -74,11 +74,15 @@ export class OpenAPIParser { ) { // Automatically inject Authentication section with SecurityDefinitions component const description = spec.info.description || ''; - const securityRegexp = new RegExp( + const legacySecurityRegexp = new RegExp( COMPONENT_REGEXP.replace('{component}', ''), 'gmi', ); - if (!securityRegexp.test(description)) { + const securityRegexp = new RegExp( + MDX_COMPONENT_REGEXP.replace('{component}', 'security-definitions'), + 'gmi', + ); + if (!legacySecurityRegexp.test(description) && !securityRegexp.test(description)) { const comment = buildComponentComment('security-definitions'); spec.info.description = appendToMdHeading(description, 'Authentication', comment); } diff --git a/src/services/__tests__/MarkdownRenderer.test.ts b/src/services/__tests__/MarkdownRenderer.test.ts index 35d05fc1..4ed71494 100644 --- a/src/services/__tests__/MarkdownRenderer.test.ts +++ b/src/services/__tests__/MarkdownRenderer.test.ts @@ -7,7 +7,7 @@ describe('Markdown renderer', () => { }); test('should return a level-1 heading even though only level-2 is present', () => { - renderer.renderMd('## Sub Intro', false); + renderer.renderMd('## Sub Intro', true); expect(Object.keys(renderer.headings)).toHaveLength(1); expect(renderer.headings[0].name).toEqual('Sub Intro'); }); diff --git a/src/services/models/ApiInfo.ts b/src/services/models/ApiInfo.ts index 6e78b5e1..29455012 100644 --- a/src/services/models/ApiInfo.ts +++ b/src/services/models/ApiInfo.ts @@ -6,13 +6,15 @@ export class ApiInfoModel implements OpenAPIInfo { title: string; version: string; - description?: string; + description: string; termsOfService?: string; contact?: OpenAPIContact; license?: OpenAPILicense; constructor(private parser: OpenAPIParser) { Object.assign(this, parser.spec.info); + this.description = parser.spec.info.description || ''; + this.description = this.description.substring(0, this.description.search(/^##?\s+/m)); } get downloadLink(): string | undefined {