2018-02-09 14:29:18 +03:00
|
|
|
import * as marked from 'marked';
|
2018-06-28 20:12:39 +03:00
|
|
|
|
2018-07-17 15:21:03 +03:00
|
|
|
import { highlight, html2Str, safeSlugify } from '../utils';
|
2018-06-28 20:12:39 +03:00
|
|
|
import { AppStore } from './AppStore';
|
2018-02-07 23:54:55 +03:00
|
|
|
import { SECTION_ATTR } from './MenuStore';
|
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-06-28 20:12:39 +03:00
|
|
|
export const LEGACY_REGEXP = '^\\s*<!-- ReDoc-Inject:\\s+?{component}\\s+?-->\\s*$';
|
|
|
|
export const MDX_COMPONENT_REGEXP = '^\\s*<{component}\\s*?/>\\s*$';
|
|
|
|
export const COMPONENT_REGEXP = '(?:' + LEGACY_REGEXP + '|' + MDX_COMPONENT_REGEXP + ')';
|
|
|
|
|
|
|
|
export interface MDXComponentMeta {
|
|
|
|
component: React.ComponentType;
|
|
|
|
propsSelector: (store?: AppStore) => any;
|
|
|
|
attrs?: 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-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-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-02-09 14:29:18 +03:00
|
|
|
constructor() {
|
|
|
|
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,
|
|
|
|
container: MarkdownHeading[] = this.headings,
|
|
|
|
parentId?: string,
|
|
|
|
): MarkdownHeading {
|
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-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) {
|
|
|
|
const buildRegexp = heading =>
|
|
|
|
new RegExp(`<h\\d ${SECTION_ATTR}="${heading.id}" id="${heading.id}">`);
|
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 prevPos = rawText.search(buildRegexp(prevHeading));
|
|
|
|
for (let i = 1; i < flatHeadings.length; i++) {
|
2018-01-22 21:30:53 +03:00
|
|
|
const heading = flatHeadings[i];
|
|
|
|
const currentPos = rawText.substr(prevPos + 1).search(buildRegexp(heading)) + prevPos + 1;
|
2018-02-07 23:54:55 +03:00
|
|
|
prevHeading.description = html2Str(rawText.substring(prevPos, currentPos));
|
2017-10-12 00:01:37 +03:00
|
|
|
|
|
|
|
prevHeading = heading;
|
|
|
|
prevPos = currentPos;
|
|
|
|
}
|
2018-02-07 23:54:55 +03:00
|
|
|
prevHeading.description = html2Str(rawText.substring(prevPos));
|
2017-10-12 00:01:37 +03:00
|
|
|
}
|
|
|
|
|
2018-02-09 14:29:18 +03:00
|
|
|
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) {
|
2018-06-20 19:29:46 +03:00
|
|
|
const { id } = this.saveHeading(
|
|
|
|
text,
|
|
|
|
this.currentTopHeading && this.currentTopHeading.items,
|
|
|
|
this.currentTopHeading && this.currentTopHeading.id,
|
|
|
|
);
|
2018-02-09 14:29:18 +03:00
|
|
|
return (
|
|
|
|
`<a name="${id}"></a>` +
|
|
|
|
`<h${level} ${SECTION_ATTR}="${id}" id="${id}">` +
|
|
|
|
`<a class="share-link" href="#${id}"></a>${text}</h${level}>`
|
|
|
|
);
|
2017-10-12 00:01:37 +03:00
|
|
|
} else {
|
2018-02-09 14:29:18 +03:00
|
|
|
return this.originalHeadingRule(text, level, raw);
|
2017-10-12 00:01:37 +03:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
renderMd(rawText: string, raw: boolean = true): string {
|
2018-02-09 14:29:18 +03:00
|
|
|
const opts = raw ? undefined : { renderer: this.headingEnhanceRenderer };
|
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-02-08 00:01:56 +03:00
|
|
|
const text = this.renderMd(rawText, false);
|
|
|
|
this.attachHeadingsDescriptions(text);
|
2017-10-12 00:01:37 +03:00
|
|
|
const res = this.headings;
|
|
|
|
this.headings = [];
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
2018-06-28 20:12:39 +03:00
|
|
|
// TODO: rewrite this completelly! Regexp-based 👎
|
|
|
|
// Use marked ecosystem
|
2017-10-12 00:01:37 +03:00
|
|
|
renderMdWithComponents(
|
|
|
|
rawText: string,
|
2018-06-28 20:12:39 +03:00
|
|
|
components: Dict<MDXComponentMeta>,
|
2017-10-12 00:01:37 +03:00
|
|
|
raw: boolean = true,
|
2018-06-28 20:12:39 +03:00
|
|
|
): Array<string | MDXComponentMeta> {
|
2018-01-22 21:30:53 +03:00
|
|
|
const componentDefs: string[] = [];
|
2018-06-28 20:12:39 +03:00
|
|
|
const names = '(?:' + Object.keys(components).join('|') + ')';
|
|
|
|
|
|
|
|
const anyCompRegexp = new RegExp(
|
|
|
|
COMPONENT_REGEXP.replace(/{component}/g, '(<?' + names + '.*?)'),
|
|
|
|
'gmi',
|
|
|
|
);
|
2018-01-22 21:30:53 +03:00
|
|
|
let match = anyCompRegexp.exec(rawText);
|
|
|
|
while (match) {
|
2018-06-28 20:12:39 +03:00
|
|
|
componentDefs.push(match[1] || match[2]);
|
2018-01-22 21:30:53 +03:00
|
|
|
match = anyCompRegexp.exec(rawText);
|
2017-10-12 00:01:37 +03:00
|
|
|
}
|
|
|
|
|
2018-06-28 20:12:39 +03:00
|
|
|
const splitCompRegexp = new RegExp(
|
|
|
|
COMPONENT_REGEXP.replace(/{component}/g, names + '.*?'),
|
|
|
|
'mi',
|
|
|
|
);
|
2018-01-22 21:30:53 +03:00
|
|
|
const htmlParts = rawText.split(splitCompRegexp);
|
|
|
|
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, raw));
|
|
|
|
}
|
|
|
|
if (componentDefs[i]) {
|
|
|
|
const { componentName, attrs } = parseComponent(componentDefs[i]);
|
2018-01-22 21:30:53 +03:00
|
|
|
if (!componentName) {
|
|
|
|
continue;
|
|
|
|
}
|
2017-10-12 00:01:37 +03:00
|
|
|
res.push({
|
2017-11-23 00:38:38 +03:00
|
|
|
...components[componentName],
|
2018-01-22 21:30:53 +03:00
|
|
|
attrs,
|
2017-10-12 00:01:37 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseComponent(
|
|
|
|
htmlTag: string,
|
|
|
|
): {
|
|
|
|
componentName?: string;
|
|
|
|
attrs: any;
|
2018-06-28 20:12:39 +03:00
|
|
|
} {
|
|
|
|
if (htmlTag.startsWith('<')) {
|
|
|
|
return legacyParse(htmlTag);
|
|
|
|
}
|
|
|
|
|
|
|
|
const match = /([\w_-]+)(\s+[\w_-]+\s*={[^}]*?})*/.exec(htmlTag);
|
|
|
|
if (match === null || match.length <= 1) {
|
|
|
|
return { componentName: undefined, attrs: {} };
|
|
|
|
}
|
|
|
|
const componentName = match[1];
|
|
|
|
const attrs = {};
|
|
|
|
for (let i = 2; i < match.length; i++) {
|
|
|
|
if (!match[i]) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
const [name, value] = match[i]
|
|
|
|
.trim()
|
|
|
|
.split('=')
|
|
|
|
.map(p => p.trim());
|
|
|
|
|
|
|
|
// tslint:disable-next-line
|
|
|
|
attrs[name] = value.startsWith('{') ? eval(value.substr(1, value.length - 2)) : eval(value);
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
componentName,
|
|
|
|
attrs,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function legacyParse(
|
|
|
|
htmlTag: string,
|
|
|
|
): {
|
|
|
|
componentName?: string;
|
|
|
|
attrs: any;
|
2017-10-12 00:01:37 +03:00
|
|
|
} {
|
|
|
|
const match = /<([\w_-]+).*?>/.exec(htmlTag);
|
2018-01-22 21:30:53 +03:00
|
|
|
if (match === null || match.length <= 1) {
|
|
|
|
return { componentName: undefined, attrs: {} };
|
|
|
|
}
|
2017-10-12 00:01:37 +03:00
|
|
|
const componentName = match[1];
|
|
|
|
return {
|
|
|
|
componentName,
|
|
|
|
attrs: {}, // TODO
|
|
|
|
};
|
|
|
|
}
|