mirror of
https://github.com/Redocly/redoc.git
synced 2024-11-11 03:16:48 +03:00
chore: refactor, unify markdown headings
This commit is contained in:
parent
d731518c8c
commit
35ba10f759
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue
Block a user