redoc/src/services/MarkdownRenderer.ts

238 lines
6.4 KiB
TypeScript
Raw Normal View History

import * as marked from 'marked';
2018-02-08 00:01:56 +03:00
import slugify from 'slugify';
2018-01-22 21:30:53 +03:00
import { highlight, html2Str } from '../utils';
import { AppStore } from './AppStore';
import { SECTION_ATTR } from './MenuStore';
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 = '^\\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 {
id: string;
2017-10-12 00:01:37 +03:00
name: string;
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 {
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() {
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,
container: MarkdownHeading[] = this.headings,
parentId?: string,
): MarkdownHeading {
const item = {
id: parentId ? `${parentId}/${slugify(name)}` : `section/${slugify(name)}`,
2017-10-12 00:01:37 +03:00
name,
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 =>
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;
prevHeading.description = html2Str(rawText.substring(prevPos, currentPos));
2017-10-12 00:01:37 +03:00
prevHeading = heading;
prevPos = currentPos;
}
prevHeading.description = html2Str(rawText.substring(prevPos));
2017-10-12 00:01:37 +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) {
const { id } = 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}>`
);
2017-10-12 00:01:37 +03:00
} else {
return this.originalHeadingRule(text, level, raw);
2017-10-12 00:01:37 +03:00
}
};
renderMd(rawText: string, raw: boolean = true): string {
const opts = raw ? undefined : { renderer: this.headingEnhanceRenderer };
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[] {
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;
}
// TODO: rewrite this completelly! Regexp-based 👎
// Use marked ecosystem
2017-10-12 00:01:37 +03:00
renderMdWithComponents(
rawText: string,
components: Dict<MDXComponentMeta>,
2017-10-12 00:01:37 +03:00
raw: boolean = true,
): Array<string | MDXComponentMeta> {
2018-01-22 21:30:53 +03:00
const componentDefs: string[] = [];
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) {
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
}
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;
} {
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
};
}