2018-02-09 14:29:18 +03:00
|
|
|
import * as marked from 'marked';
|
2018-06-28 20:12:39 +03:00
|
|
|
|
2019-07-07 21:26:27 +03:00
|
|
|
import { highlight, safeSlugify, unescapeHTMLChars } from '../utils';
|
2018-06-28 20:12:39 +03:00
|
|
|
import { AppStore } from './AppStore';
|
2018-08-17 14:41:22 +03:00
|
|
|
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
|
2017-10-12 00:01:37 +03:00
|
|
|
|
2018-02-09 14:29:18 +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);
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2018-10-04 10:49:43 +03:00
|
|
|
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
|
|
|
|
|
2018-06-28 20:12:39 +03:00
|
|
|
export const COMPONENT_REGEXP = '(?:' + LEGACY_REGEXP + '|' + MDX_COMPONENT_REGEXP + ')';
|
|
|
|
|
|
|
|
export interface MDXComponentMeta {
|
|
|
|
component: React.ComponentType;
|
|
|
|
propsSelector: (store?: AppStore) => any;
|
2018-10-04 10:49:43 +03:00
|
|
|
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 {
|
2018-02-07 23:54:55 +03:00
|
|
|
id: string;
|
2017-10-12 00:01:37 +03:00
|
|
|
name: string;
|
2018-08-08 11:37:48 +03:00
|
|
|
level: number;
|
2018-02-07 23:54:55 +03:00
|
|
|
items?: MarkdownHeading[];
|
|
|
|
description?: string;
|
2018-01-22 21:30:53 +03:00
|
|
|
}
|
2017-10-12 00:01:37 +03:00
|
|
|
|
2018-06-28 20:12:39 +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) {
|
2018-10-04 10:49:43 +03:00
|
|
|
const compRegexp = new RegExp(COMPONENT_REGEXP.replace(/{component}/g, componentName), 'gmi');
|
|
|
|
return compRegexp.test(rawText);
|
2018-08-16 12:37:39 +03:00
|
|
|
}
|
|
|
|
|
2019-12-12 17:39:56 +03:00
|
|
|
static getTextBeforeHading(md: string, heading: string): string {
|
2020-01-10 16:18:32 +03:00
|
|
|
const headingLinePos = md.search(new RegExp(`^##?\\s+${heading}`, 'm'));
|
2019-12-12 17:39:56 +03:00
|
|
|
if (headingLinePos > -1) {
|
|
|
|
return md.substring(0, headingLinePos);
|
|
|
|
}
|
|
|
|
return md;
|
|
|
|
}
|
|
|
|
|
2018-02-07 23:54:55 +03:00
|
|
|
headings: MarkdownHeading[] = [];
|
|
|
|
currentTopHeading: MarkdownHeading;
|
2017-10-12 00:01:37 +03:00
|
|
|
|
2018-02-09 14:29:18 +03:00
|
|
|
private headingEnhanceRenderer: marked.Renderer;
|
|
|
|
private originalHeadingRule: typeof marked.Renderer.prototype.heading;
|
2017-10-12 00:01:37 +03:00
|
|
|
|
2018-08-17 14:41:22 +03:00
|
|
|
constructor(public options?: RedocNormalizedOptions) {
|
2018-02-09 14:29:18 +03:00
|
|
|
this.headingEnhanceRenderer = new marked.Renderer();
|
2018-03-07 14:00:03 +03:00
|
|
|
this.originalHeadingRule = this.headingEnhanceRenderer.heading.bind(
|
|
|
|
this.headingEnhanceRenderer,
|
|
|
|
);
|
2018-02-09 14:29:18 +03:00
|
|
|
this.headingEnhanceRenderer.heading = this.headingRule;
|
2017-10-12 00:01:37 +03:00
|
|
|
}
|
|
|
|
|
2018-06-20 19:29:46 +03:00
|
|
|
saveHeading(
|
|
|
|
name: string,
|
2018-08-08 11:37:48 +03:00
|
|
|
level: number,
|
2018-06-20 19:29:46 +03:00
|
|
|
container: MarkdownHeading[] = this.headings,
|
|
|
|
parentId?: string,
|
|
|
|
): MarkdownHeading {
|
2019-07-07 21:26:27 +03:00
|
|
|
name = unescapeHTMLChars(name);
|
2018-02-07 23:54:55 +03:00
|
|
|
const item = {
|
2018-07-17 15:21:03 +03:00
|
|
|
id: parentId ? `${parentId}/${safeSlugify(name)}` : `section/${safeSlugify(name)}`,
|
2017-10-12 00:01:37 +03:00
|
|
|
name,
|
2018-08-08 11:37:48 +03:00
|
|
|
level,
|
2018-02-08 00:01:56 +03:00
|
|
|
items: [],
|
2018-02-07 23:54:55 +03:00
|
|
|
};
|
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);
|
2018-02-07 23:54:55 +03:00
|
|
|
res.push(...this.flattenHeadings(heading.items));
|
2017-10-12 00:01:37 +03:00
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
2018-02-07 23:54:55 +03:00
|
|
|
attachHeadingsDescriptions(rawText: string) {
|
2019-07-07 21:26:27 +03:00
|
|
|
const buildRegexp = (heading: MarkdownHeading) => {
|
2018-08-11 23:14:09 +03:00
|
|
|
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];
|
2018-07-30 13:04:27 +03:00
|
|
|
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];
|
2018-07-30 13:04:27 +03:00
|
|
|
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;
|
2018-07-30 13:04:27 +03:00
|
|
|
prevRegexp = regexp;
|
2017-10-12 00:01:37 +03:00
|
|
|
prevPos = currentPos;
|
|
|
|
}
|
2018-07-30 13:04:27 +03:00
|
|
|
prevHeading.description = rawText
|
|
|
|
.substring(prevPos)
|
|
|
|
.replace(prevRegexp, '')
|
|
|
|
.trim();
|
2017-10-12 00:01:37 +03:00
|
|
|
}
|
|
|
|
|
2019-12-12 13:46:17 +03:00
|
|
|
headingRule = (
|
|
|
|
text: string,
|
|
|
|
level: 1 | 2 | 3 | 4 | 5 | 6,
|
|
|
|
raw: string,
|
|
|
|
slugger: marked.Slugger,
|
|
|
|
) => {
|
2018-02-09 14:29:18 +03:00
|
|
|
if (level === 1) {
|
2018-08-08 11:37:48 +03:00
|
|
|
this.currentTopHeading = this.saveHeading(text, level);
|
2018-02-09 14:29:18 +03:00
|
|
|
} else if (level === 2) {
|
2018-07-30 13:04:27 +03:00
|
|
|
this.saveHeading(
|
2018-06-20 19:29:46 +03:00
|
|
|
text,
|
2018-08-08 11:37:48 +03:00
|
|
|
level,
|
2018-06-20 19:29:46 +03:00
|
|
|
this.currentTopHeading && this.currentTopHeading.items,
|
|
|
|
this.currentTopHeading && this.currentTopHeading.id,
|
|
|
|
);
|
2017-10-12 00:01:37 +03:00
|
|
|
}
|
2019-01-17 13:06:35 +03:00
|
|
|
return this.originalHeadingRule(text, level, raw, slugger);
|
2017-10-12 00:01:37 +03:00
|
|
|
};
|
|
|
|
|
2018-07-30 13:04:27 +03:00
|
|
|
renderMd(rawText: string, extractHeadings: boolean = false): string {
|
|
|
|
const opts = extractHeadings ? { renderer: this.headingEnhanceRenderer } : undefined;
|
2017-10-12 00:01:37 +03:00
|
|
|
|
2018-02-09 14:29:18 +03:00
|
|
|
const res = marked(rawText.toString(), opts);
|
2017-10-12 00:01:37 +03:00
|
|
|
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
2018-02-07 23:54:55 +03:00
|
|
|
extractHeadings(rawText: string): MarkdownHeading[] {
|
2018-07-30 13:04:27 +03:00
|
|
|
this.renderMd(rawText, true);
|
|
|
|
this.attachHeadingsDescriptions(rawText);
|
2017-10-12 00:01:37 +03:00
|
|
|
const res = this.headings;
|
|
|
|
this.headings = [];
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
2018-10-04 10:49:43 +03:00
|
|
|
// regexp-based 👎: remark is slow and too big so for now using marked + regexps soup
|
2018-08-17 14:41:22 +03:00
|
|
|
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)];
|
|
|
|
}
|
|
|
|
|
2018-10-04 10:49:43 +03:00
|
|
|
const names = Object.keys(components).join('|');
|
|
|
|
const componentsRegexp = new RegExp(COMPONENT_REGEXP.replace(/{component}/g, names), 'mig');
|
2018-06-28 20:12:39 +03:00
|
|
|
|
2018-10-04 10:49:43 +03:00
|
|
|
const htmlParts: string[] = [];
|
|
|
|
const componentDefs: MDXComponentMeta[] = [];
|
|
|
|
|
|
|
|
let match = componentsRegexp.exec(rawText);
|
|
|
|
let lasxtIdx = 0;
|
2018-01-22 21:30:53 +03:00
|
|
|
while (match) {
|
2018-10-04 10:49:43 +03:00
|
|
|
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
|
|
|
}
|
2018-10-04 10:49:43 +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) {
|
2018-07-30 13:04:27 +03:00
|
|
|
res.push(this.renderMd(htmlPart));
|
2017-10-12 00:01:37 +03:00
|
|
|
}
|
|
|
|
if (componentDefs[i]) {
|
2018-10-04 10:49:43 +03:00
|
|
|
res.push(componentDefs[i]);
|
2017-10-12 00:01:37 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-04 10:49:43 +03:00
|
|
|
function parseProps(props: string): object {
|
|
|
|
if (!props) {
|
|
|
|
return {};
|
2018-06-28 20:12:39 +03:00
|
|
|
}
|
|
|
|
|
2018-10-04 10:49:43 +03:00
|
|
|
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;
|
|
|
|
}
|
2018-06-28 20:12:39 +03:00
|
|
|
}
|
2018-10-04 10:49:43 +03:00
|
|
|
|
|
|
|
return parsed;
|
2018-06-28 20:12:39 +03:00
|
|
|
}
|