redoc/src/services/MarkdownRenderer.ts

227 lines
6.4 KiB
TypeScript
Raw Normal View History

import * as marked from 'marked';
import { highlight, safeSlugify } from '../utils';
import { AppStore } from './AppStore';
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
2017-10-12 00:01:37 +03:00
const renderer = new marked.Renderer();
marked.setOptions({
renderer,
2017-10-12 00:01:37 +03:00
highlight: (str, lang) => {
return highlight(str, lang);
},
});
export const LEGACY_REGEXP = '^ {0,3}<!-- ReDoc-Inject:\\s+?<({component}).*?/?>\\s+?-->\\s*$';
// prettier-ignore
export const MDX_COMPONENT_REGEXP = '(?:^ {0,3}<({component})([\\s\\S]*?)>([\\s\\S]*?)</\\2>' // with children
+ '|^ {0,3}<({component})([\\s\\S]*?)(?:/>|\\n{2,}))'; // self-closing
export const COMPONENT_REGEXP = '(?:' + LEGACY_REGEXP + '|' + MDX_COMPONENT_REGEXP + ')';
export interface MDXComponentMeta {
component: React.ComponentType;
propsSelector: (store?: AppStore) => any;
props?: object;
2017-11-21 17:33:22 +03:00
}
2017-10-12 00:01:37 +03:00
2018-05-16 12:44:36 +03:00
export interface MarkdownHeading {
id: string;
2017-10-12 00:01:37 +03:00
name: string;
level: number;
items?: MarkdownHeading[];
description?: string;
2018-01-22 21:30:53 +03:00
}
2017-10-12 00:01:37 +03:00
export function buildComponentComment(name: string) {
return `<!-- ReDoc-Inject: <${name}> -->`;
}
2017-10-12 00:01:37 +03:00
export class MarkdownRenderer {
2018-08-16 12:37:39 +03:00
static containsComponent(rawText: string, componentName: string) {
const compRegexp = new RegExp(COMPONENT_REGEXP.replace(/{component}/g, componentName), 'gmi');
return compRegexp.test(rawText);
2018-08-16 12:37:39 +03:00
}
headings: MarkdownHeading[] = [];
currentTopHeading: MarkdownHeading;
2017-10-12 00:01:37 +03:00
private headingEnhanceRenderer: marked.Renderer;
private originalHeadingRule: typeof marked.Renderer.prototype.heading;
2017-10-12 00:01:37 +03:00
constructor(public options?: RedocNormalizedOptions) {
this.headingEnhanceRenderer = new marked.Renderer();
this.originalHeadingRule = this.headingEnhanceRenderer.heading.bind(
this.headingEnhanceRenderer,
);
this.headingEnhanceRenderer.heading = this.headingRule;
2017-10-12 00:01:37 +03:00
}
saveHeading(
name: string,
level: number,
container: MarkdownHeading[] = this.headings,
parentId?: string,
): MarkdownHeading {
const item = {
id: parentId ? `${parentId}/${safeSlugify(name)}` : `section/${safeSlugify(name)}`,
2017-10-12 00:01:37 +03:00
name,
level,
2018-02-08 00:01:56 +03:00
items: [],
};
2017-10-12 00:01:37 +03:00
container.push(item);
return item;
}
flattenHeadings(container?: MarkdownHeading[]): MarkdownHeading[] {
2018-01-22 21:30:53 +03:00
if (container === undefined) {
return [];
}
const res: MarkdownHeading[] = [];
for (const heading of container) {
2017-10-12 00:01:37 +03:00
res.push(heading);
res.push(...this.flattenHeadings(heading.items));
2017-10-12 00:01:37 +03:00
}
return res;
}
attachHeadingsDescriptions(rawText: string) {
const buildRegexp = heading => {
return new RegExp(`##?\\s+${heading.name.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}`);
};
2017-10-12 00:01:37 +03:00
2018-01-22 21:30:53 +03:00
const flatHeadings = this.flattenHeadings(this.headings);
if (flatHeadings.length < 1) {
return;
}
2017-10-12 00:01:37 +03:00
let prevHeading = flatHeadings[0];
let prevRegexp = buildRegexp(prevHeading);
let prevPos = rawText.search(prevRegexp);
2017-10-12 00:01:37 +03:00
for (let i = 1; i < flatHeadings.length; i++) {
2018-01-22 21:30:53 +03:00
const heading = flatHeadings[i];
const regexp = buildRegexp(heading);
const currentPos = rawText.substr(prevPos + 1).search(regexp) + prevPos + 1;
prevHeading.description = rawText
.substring(prevPos, currentPos)
.replace(prevRegexp, '')
.trim();
2017-10-12 00:01:37 +03:00
prevHeading = heading;
prevRegexp = regexp;
2017-10-12 00:01:37 +03:00
prevPos = currentPos;
}
prevHeading.description = rawText
.substring(prevPos)
.replace(prevRegexp, '')
.trim();
2017-10-12 00:01:37 +03:00
}
headingRule = (text: string, level: number, raw: string) => {
if (level === 1) {
this.currentTopHeading = this.saveHeading(text, level);
} else if (level === 2) {
this.saveHeading(
text,
level,
this.currentTopHeading && this.currentTopHeading.items,
this.currentTopHeading && this.currentTopHeading.id,
);
2017-10-12 00:01:37 +03:00
}
return this.originalHeadingRule(text, level, raw);
2017-10-12 00:01:37 +03:00
};
renderMd(rawText: string, extractHeadings: boolean = false): string {
const opts = extractHeadings ? { renderer: this.headingEnhanceRenderer } : undefined;
2017-10-12 00:01:37 +03:00
const res = marked(rawText.toString(), opts);
2017-10-12 00:01:37 +03:00
return res;
}
extractHeadings(rawText: string): MarkdownHeading[] {
this.renderMd(rawText, true);
this.attachHeadingsDescriptions(rawText);
2017-10-12 00:01:37 +03:00
const res = this.headings;
this.headings = [];
return res;
}
// regexp-based 👎: remark is slow and too big so for now using marked + regexps soup
renderMdWithComponents(rawText: string): Array<string | MDXComponentMeta> {
const components = this.options && this.options.allowedMdComponents;
2018-08-16 12:37:39 +03:00
if (!components || Object.keys(components).length === 0) {
return [this.renderMd(rawText)];
}
const names = Object.keys(components).join('|');
const componentsRegexp = new RegExp(COMPONENT_REGEXP.replace(/{component}/g, names), 'mig');
const htmlParts: string[] = [];
const componentDefs: MDXComponentMeta[] = [];
let match = componentsRegexp.exec(rawText);
let lasxtIdx = 0;
2018-01-22 21:30:53 +03:00
while (match) {
htmlParts.push(rawText.substring(lasxtIdx, match.index));
lasxtIdx = componentsRegexp.lastIndex;
const compName = match[1] || match[2] || match[5];
const componentMeta = components[compName];
const props = match[3] || match[6];
const children = match[4];
if (componentMeta) {
componentDefs.push({
component: componentMeta.component,
propsSelector: componentMeta.propsSelector,
props: { ...parseProps(props), ...componentMeta.props, children },
});
}
match = componentsRegexp.exec(rawText);
2017-10-12 00:01:37 +03:00
}
htmlParts.push(rawText.substring(lasxtIdx));
2017-10-12 00:01:37 +03:00
2018-01-22 21:30:53 +03:00
const res: any[] = [];
2017-10-12 00:01:37 +03:00
for (let i = 0; i < htmlParts.length; i++) {
const htmlPart = htmlParts[i];
if (htmlPart) {
res.push(this.renderMd(htmlPart));
2017-10-12 00:01:37 +03:00
}
if (componentDefs[i]) {
res.push(componentDefs[i]);
2017-10-12 00:01:37 +03:00
}
}
return res;
}
}
function parseProps(props: string): object {
if (!props) {
return {};
}
const regex = /([\w-]+)\s*=\s*(?:{([^}]+?)}|"([^"]+?)")/gim;
const parsed = {};
let match;
// tslint:disable-next-line
while ((match = regex.exec(props)) !== null) {
if (match[3]) {
// string prop match (in double quotes)
parsed[match[1]] = match[3];
} else if (match[2]) {
// jsx prop match (in curly braces)
let val;
try {
val = JSON.parse(match[2]);
} catch (e) {
/* noop */
}
parsed[match[1]] = val;
}
}
return parsed;
}