mirror of
https://github.com/Redocly/redoc.git
synced 2025-02-12 07:50:34 +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 { MiddlePanel, Row } from '../../common-elements/';
|
||||||
|
|
||||||
import { AppStore } from '../../services/AppStore';
|
|
||||||
import { Markdown } from '../Markdown/Markdown';
|
import { Markdown } from '../Markdown/Markdown';
|
||||||
import { SecurityDefs } from '../SecuritySchemes/SecuritySchemes';
|
|
||||||
|
|
||||||
export interface ApiDescriptionProps {
|
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> {
|
export class ApiDescription extends React.PureComponent<ApiDescriptionProps> {
|
||||||
render() {
|
render() {
|
||||||
const { store } = this.props;
|
const { description } = this.props;
|
||||||
const description = store.spec.info.description;
|
|
||||||
return (
|
return (
|
||||||
<Row>
|
<Row>
|
||||||
<MiddlePanel>
|
<MiddlePanel>
|
||||||
<Markdown
|
<Markdown source={description} />
|
||||||
source={description || ''}
|
|
||||||
raw={false}
|
|
||||||
allowedComponents={ALLOWED_COMPONENTS}
|
|
||||||
store={store}
|
|
||||||
/>
|
|
||||||
</MiddlePanel>
|
</MiddlePanel>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,25 +5,43 @@ import { SECTION_ATTR } from '../../services/MenuStore';
|
||||||
import { Markdown } from '../Markdown/Markdown';
|
import { Markdown } from '../Markdown/Markdown';
|
||||||
|
|
||||||
import { H1, MiddlePanel, Row, ShareLink } from '../../common-elements';
|
import { H1, MiddlePanel, Row, ShareLink } from '../../common-elements';
|
||||||
|
import { MDXComponentMeta } from '../../services/MarkdownRenderer';
|
||||||
import { ContentItemModel } from '../../services/MenuBuilder';
|
import { ContentItemModel } from '../../services/MenuBuilder';
|
||||||
import { OperationModel } from '../../services/models';
|
import { OperationModel } from '../../services/models';
|
||||||
import { Operation } from '../Operation/Operation';
|
import { Operation } from '../Operation/Operation';
|
||||||
|
import { SecurityDefs } from '../SecuritySchemes/SecuritySchemes';
|
||||||
|
import { StoreConsumer } from '../StoreBuilder';
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ContentItems extends React.Component<{
|
export class ContentItems extends React.Component<{
|
||||||
items: ContentItemModel[];
|
items: ContentItemModel[];
|
||||||
|
allowedMdComponents?: Dict<MDXComponentMeta>;
|
||||||
}> {
|
}> {
|
||||||
|
static defaultProps = {
|
||||||
|
allowedMdComponents: {
|
||||||
|
'security-definitions': {
|
||||||
|
component: SecurityDefs,
|
||||||
|
propsSelector: _store => ({
|
||||||
|
securitySchemes: _store!.spec.securitySchemes,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const items = this.props.items;
|
const items = this.props.items;
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return null;
|
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 {
|
export interface ContentItemProps {
|
||||||
item: ContentItemModel;
|
item: ContentItemModel;
|
||||||
|
allowedMdComponents?: Dict<MDXComponentMeta>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
|
@ -37,10 +55,11 @@ export class ContentItem extends React.Component<ContentItemProps> {
|
||||||
content = null;
|
content = null;
|
||||||
break;
|
break;
|
||||||
case 'tag':
|
case 'tag':
|
||||||
content = <TagItem item={item} />;
|
content = <SectionItem {...this.props} />;
|
||||||
break;
|
break;
|
||||||
case 'section':
|
case 'section':
|
||||||
return null;
|
content = <SectionItem {...this.props} />;
|
||||||
|
break;
|
||||||
case 'operation':
|
case 'operation':
|
||||||
content = <OperationItem item={item as any} />;
|
content = <OperationItem item={item as any} />;
|
||||||
break;
|
break;
|
||||||
|
@ -58,9 +77,10 @@ export class ContentItem extends React.Component<ContentItemProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class TagItem extends React.Component<ContentItemProps> {
|
export class SectionItem extends React.Component<ContentItemProps> {
|
||||||
render() {
|
render() {
|
||||||
const { name, description } = this.props.item;
|
const { name, description } = this.props.item;
|
||||||
|
const components = this.props.allowedMdComponents;
|
||||||
return (
|
return (
|
||||||
<Row>
|
<Row>
|
||||||
<MiddlePanel>
|
<MiddlePanel>
|
||||||
|
@ -68,7 +88,15 @@ export class TagItem extends React.Component<ContentItemProps> {
|
||||||
<ShareLink href={'#' + this.props.item.id} />
|
<ShareLink href={'#' + this.props.item.id} />
|
||||||
{name}
|
{name}
|
||||||
</H1>
|
</H1>
|
||||||
{description !== undefined && <Markdown source={description} />}
|
{components ? (
|
||||||
|
<Markdown source={description || ''} />
|
||||||
|
) : (
|
||||||
|
<StoreConsumer>
|
||||||
|
{store => (
|
||||||
|
<Markdown source={description || ''} allowedComponents={components} store={store} />
|
||||||
|
)}
|
||||||
|
</StoreConsumer>
|
||||||
|
)}
|
||||||
</MiddlePanel>
|
</MiddlePanel>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,7 +14,6 @@ export interface StylingMarkdownProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BaseMarkdownProps extends StylingMarkdownProps {
|
export interface BaseMarkdownProps extends StylingMarkdownProps {
|
||||||
raw?: boolean;
|
|
||||||
sanitize?: boolean;
|
sanitize?: boolean;
|
||||||
store?: AppStore;
|
store?: AppStore;
|
||||||
}
|
}
|
||||||
|
@ -54,7 +53,7 @@ export class Markdown extends React.Component<MarkdownProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { source, raw, allowedComponents, store, inline, dense } = this.props;
|
const { source, allowedComponents, store, inline, dense } = this.props;
|
||||||
|
|
||||||
if (allowedComponents && !store) {
|
if (allowedComponents && !store) {
|
||||||
throw new Error('When using componentes in markdown, store prop must be provided');
|
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) {
|
if (allowedComponents) {
|
||||||
return (
|
return (
|
||||||
<AdvancedMarkdown
|
<AdvancedMarkdown
|
||||||
parts={renderer.renderMdWithComponents(source, allowedComponents, raw)}
|
parts={renderer.renderMdWithComponents(source, allowedComponents)}
|
||||||
{...this.props}
|
{...this.props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<SanitizedMarkdownHTML
|
<SanitizedMarkdownHTML html={renderer.renderMd(source)} inline={inline} dense={dense} />
|
||||||
html={renderer.renderMd(source, raw)}
|
|
||||||
inline={inline}
|
|
||||||
dense={dense}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import * as marked from 'marked';
|
import * as marked from 'marked';
|
||||||
|
|
||||||
import { highlight, html2Str, safeSlugify } from '../utils';
|
import { highlight, safeSlugify } from '../utils';
|
||||||
import { AppStore } from './AppStore';
|
import { AppStore } from './AppStore';
|
||||||
import { SECTION_ATTR } from './MenuStore';
|
|
||||||
|
|
||||||
const renderer = new marked.Renderer();
|
const renderer = new marked.Renderer();
|
||||||
|
|
||||||
|
@ -76,54 +75,49 @@ export class MarkdownRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
attachHeadingsDescriptions(rawText: string) {
|
attachHeadingsDescriptions(rawText: string) {
|
||||||
const buildRegexp = heading =>
|
const buildRegexp = heading => new RegExp(`##?\\s+${heading.name}`);
|
||||||
new RegExp(`<h\\d ${SECTION_ATTR}="${heading.id}" id="${heading.id}">`);
|
|
||||||
|
|
||||||
const flatHeadings = this.flattenHeadings(this.headings);
|
const flatHeadings = this.flattenHeadings(this.headings);
|
||||||
if (flatHeadings.length < 1) {
|
if (flatHeadings.length < 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let prevHeading = flatHeadings[0];
|
let prevHeading = flatHeadings[0];
|
||||||
|
let prevRegexp = buildRegexp(prevHeading);
|
||||||
let prevPos = rawText.search(buildRegexp(prevHeading));
|
let prevPos = rawText.search(prevRegexp);
|
||||||
for (let i = 1; i < flatHeadings.length; i++) {
|
for (let i = 1; i < flatHeadings.length; i++) {
|
||||||
const heading = flatHeadings[i];
|
const heading = flatHeadings[i];
|
||||||
const currentPos = rawText.substr(prevPos + 1).search(buildRegexp(heading)) + prevPos + 1;
|
const regexp = buildRegexp(heading);
|
||||||
prevHeading.description = html2Str(rawText.substring(prevPos, currentPos));
|
const currentPos = rawText.substr(prevPos + 1).search(regexp) + prevPos + 1;
|
||||||
|
prevHeading.description = rawText
|
||||||
|
.substring(prevPos, currentPos)
|
||||||
|
.replace(prevRegexp, '')
|
||||||
|
.trim();
|
||||||
|
|
||||||
prevHeading = heading;
|
prevHeading = heading;
|
||||||
|
prevRegexp = regexp;
|
||||||
prevPos = currentPos;
|
prevPos = currentPos;
|
||||||
}
|
}
|
||||||
prevHeading.description = html2Str(rawText.substring(prevPos));
|
prevHeading.description = rawText
|
||||||
|
.substring(prevPos)
|
||||||
|
.replace(prevRegexp, '')
|
||||||
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
headingRule = (text: string, level: number, raw: string) => {
|
headingRule = (text: string, level: number, raw: string) => {
|
||||||
if (level === 1) {
|
if (level === 1) {
|
||||||
this.currentTopHeading = this.saveHeading(text);
|
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) {
|
} else if (level === 2) {
|
||||||
const { id } = this.saveHeading(
|
this.saveHeading(
|
||||||
text,
|
text,
|
||||||
this.currentTopHeading && this.currentTopHeading.items,
|
this.currentTopHeading && this.currentTopHeading.items,
|
||||||
this.currentTopHeading && this.currentTopHeading.id,
|
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 {
|
renderMd(rawText: string, extractHeadings: boolean = false): string {
|
||||||
const opts = raw ? undefined : { renderer: this.headingEnhanceRenderer };
|
const opts = extractHeadings ? { renderer: this.headingEnhanceRenderer } : undefined;
|
||||||
|
|
||||||
const res = marked(rawText.toString(), opts);
|
const res = marked(rawText.toString(), opts);
|
||||||
|
|
||||||
|
@ -131,8 +125,8 @@ export class MarkdownRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
extractHeadings(rawText: string): MarkdownHeading[] {
|
extractHeadings(rawText: string): MarkdownHeading[] {
|
||||||
const text = this.renderMd(rawText, false);
|
this.renderMd(rawText, true);
|
||||||
this.attachHeadingsDescriptions(text);
|
this.attachHeadingsDescriptions(rawText);
|
||||||
const res = this.headings;
|
const res = this.headings;
|
||||||
this.headings = [];
|
this.headings = [];
|
||||||
return res;
|
return res;
|
||||||
|
@ -143,7 +137,6 @@ export class MarkdownRenderer {
|
||||||
renderMdWithComponents(
|
renderMdWithComponents(
|
||||||
rawText: string,
|
rawText: string,
|
||||||
components: Dict<MDXComponentMeta>,
|
components: Dict<MDXComponentMeta>,
|
||||||
raw: boolean = true,
|
|
||||||
): Array<string | MDXComponentMeta> {
|
): Array<string | MDXComponentMeta> {
|
||||||
const componentDefs: string[] = [];
|
const componentDefs: string[] = [];
|
||||||
const names = '(?:' + Object.keys(components).join('|') + ')';
|
const names = '(?:' + Object.keys(components).join('|') + ')';
|
||||||
|
@ -167,7 +160,7 @@ export class MarkdownRenderer {
|
||||||
for (let i = 0; i < htmlParts.length; i++) {
|
for (let i = 0; i < htmlParts.length; i++) {
|
||||||
const htmlPart = htmlParts[i];
|
const htmlPart = htmlParts[i];
|
||||||
if (htmlPart) {
|
if (htmlPart) {
|
||||||
res.push(this.renderMd(htmlPart, raw));
|
res.push(this.renderMd(htmlPart));
|
||||||
}
|
}
|
||||||
if (componentDefs[i]) {
|
if (componentDefs[i]) {
|
||||||
const { componentName, attrs } = parseComponent(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 { appendToMdHeading, IS_BROWSER } from '../utils/';
|
||||||
import { JsonPointer } from '../utils/JsonPointer';
|
import { JsonPointer } from '../utils/JsonPointer';
|
||||||
import { isNamedDefinition } from '../utils/openapi';
|
import { isNamedDefinition } from '../utils/openapi';
|
||||||
import { buildComponentComment, COMPONENT_REGEXP } from './MarkdownRenderer';
|
import { buildComponentComment, COMPONENT_REGEXP, MDX_COMPONENT_REGEXP } from './MarkdownRenderer';
|
||||||
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
|
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
|
||||||
|
|
||||||
export type MergedOpenAPISchema = OpenAPISchema & { parentRefs?: string[] };
|
export type MergedOpenAPISchema = OpenAPISchema & { parentRefs?: string[] };
|
||||||
|
@ -74,11 +74,15 @@ export class OpenAPIParser {
|
||||||
) {
|
) {
|
||||||
// Automatically inject Authentication section with SecurityDefinitions component
|
// Automatically inject Authentication section with SecurityDefinitions component
|
||||||
const description = spec.info.description || '';
|
const description = spec.info.description || '';
|
||||||
const securityRegexp = new RegExp(
|
const legacySecurityRegexp = new RegExp(
|
||||||
COMPONENT_REGEXP.replace('{component}', '<security-definitions>'),
|
COMPONENT_REGEXP.replace('{component}', '<security-definitions>'),
|
||||||
'gmi',
|
'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');
|
const comment = buildComponentComment('security-definitions');
|
||||||
spec.info.description = appendToMdHeading(description, 'Authentication', comment);
|
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', () => {
|
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(Object.keys(renderer.headings)).toHaveLength(1);
|
||||||
expect(renderer.headings[0].name).toEqual('Sub Intro');
|
expect(renderer.headings[0].name).toEqual('Sub Intro');
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,13 +6,15 @@ export class ApiInfoModel implements OpenAPIInfo {
|
||||||
title: string;
|
title: string;
|
||||||
version: string;
|
version: string;
|
||||||
|
|
||||||
description?: string;
|
description: string;
|
||||||
termsOfService?: string;
|
termsOfService?: string;
|
||||||
contact?: OpenAPIContact;
|
contact?: OpenAPIContact;
|
||||||
license?: OpenAPILicense;
|
license?: OpenAPILicense;
|
||||||
|
|
||||||
constructor(private parser: OpenAPIParser) {
|
constructor(private parser: OpenAPIParser) {
|
||||||
Object.assign(this, parser.spec.info);
|
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 {
|
get downloadLink(): string | undefined {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user