chore: refactor, unify markdown headings

This commit is contained in:
Roman Hotsiy 2018-07-30 13:04:27 +03:00
parent d731518c8c
commit 35ba10f759
No known key found for this signature in database
GPG Key ID: 5CB7B3ACABA57CB0
7 changed files with 72 additions and 67 deletions

View File

@ -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<ApiDescriptionProps> {
render() {
const { store } = this.props;
const description = store.spec.info.description;
const { description } = this.props;
return (
<Row>
<MiddlePanel>
<Markdown
source={description || ''}
raw={false}
allowedComponents={ALLOWED_COMPONENTS}
store={store}
/>
<Markdown source={description} />
</MiddlePanel>
</Row>
);

View File

@ -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<MDXComponentMeta>;
}> {
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 => <ContentItem item={item} key={item.id} />);
return items.map(item => (
<ContentItem item={item} key={item.id} allowedMdComponents={this.props.allowedMdComponents} />
));
}
}
export interface ContentItemProps {
item: ContentItemModel;
allowedMdComponents?: Dict<MDXComponentMeta>;
}
@observer
@ -37,10 +55,11 @@ export class ContentItem extends React.Component<ContentItemProps> {
content = null;
break;
case 'tag':
content = <TagItem item={item} />;
content = <SectionItem {...this.props} />;
break;
case 'section':
return null;
content = <SectionItem {...this.props} />;
break;
case 'operation':
content = <OperationItem item={item as any} />;
break;
@ -58,9 +77,10 @@ export class ContentItem extends React.Component<ContentItemProps> {
}
@observer
export class TagItem extends React.Component<ContentItemProps> {
export class SectionItem extends React.Component<ContentItemProps> {
render() {
const { name, description } = this.props.item;
const components = this.props.allowedMdComponents;
return (
<Row>
<MiddlePanel>
@ -68,7 +88,15 @@ export class TagItem extends React.Component<ContentItemProps> {
<ShareLink href={'#' + this.props.item.id} />
{name}
</H1>
{description !== undefined && <Markdown source={description} />}
{components ? (
<Markdown source={description || ''} />
) : (
<StoreConsumer>
{store => (
<Markdown source={description || ''} allowedComponents={components} store={store} />
)}
</StoreConsumer>
)}
</MiddlePanel>
</Row>
);

View File

@ -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<MarkdownProps> {
}
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<MarkdownProps> {
if (allowedComponents) {
return (
<AdvancedMarkdown
parts={renderer.renderMdWithComponents(source, allowedComponents, raw)}
parts={renderer.renderMdWithComponents(source, allowedComponents)}
{...this.props}
/>
);
} else {
return (
<SanitizedMarkdownHTML
html={renderer.renderMd(source, raw)}
inline={inline}
dense={dense}
/>
<SanitizedMarkdownHTML html={renderer.renderMd(source)} inline={inline} dense={dense} />
);
}
}

View File

@ -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(`<h\\d ${SECTION_ATTR}="${heading.id}" id="${heading.id}">`);
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 (
`<a name="${id}"></a>` +
`<h${level} ${SECTION_ATTR}="${id}" id="${id}">` +
`<a class="share-link" href="#${id}"></a>${text}</h${level}>`
);
} else if (level === 2) {
const { id } = this.saveHeading(
this.saveHeading(
text,
this.currentTopHeading && this.currentTopHeading.items,
this.currentTopHeading && this.currentTopHeading.id,
);
return (
`<a name="${id}"></a>` +
`<h${level} ${SECTION_ATTR}="${id}" id="${id}">` +
`<a class="share-link" href="#${id}"></a>${text}</h${level}>`
);
} 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<MDXComponentMeta>,
raw: boolean = true,
): Array<string | MDXComponentMeta> {
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]);

View File

@ -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}', '<security-definitions>'),
'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);
}

View File

@ -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');
});

View File

@ -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 {