redoc/src/services/MarkdownRenderer.ts

200 lines
5.4 KiB
TypeScript
Raw Normal View History

2017-10-12 00:01:37 +03:00
import * as Remarkable from 'remarkable';
2017-11-23 00:38:38 +03:00
import { MDComponent } from '../components/Markdown/Markdown';
2018-01-22 21:30:53 +03:00
import { highlight, html2Str } from '../utils';
2017-10-12 00:01:37 +03:00
import { IMenuItem, SECTION_ATTR } from './MenuStore';
import { GroupModel } from './models';
const md = new Remarkable('default', {
html: true,
linkify: true,
breaks: false,
typographer: false,
highlight: (str, lang) => {
return highlight(str, lang);
},
});
2017-11-21 17:33:22 +03:00
export const COMPONENT_REGEXP = '^\\s*<!-- ReDoc-Inject:\\s+?{component}\\s+?-->\\s*$';
export function buildComponentComment(name: string) {
return `<!-- ReDoc-Inject: <${name}> -->`;
}
2017-10-12 00:01:37 +03:00
2018-01-22 21:30:53 +03:00
interface MarkdownHeading {
2017-10-12 00:01:37 +03:00
name: string;
children?: MarkdownHeading[];
content?: string;
2018-01-22 21:30:53 +03:00
}
2017-10-12 00:01:37 +03:00
export class MarkdownRenderer {
2018-01-22 21:30:53 +03:00
headings: GroupModel[] = [];
2017-10-12 00:01:37 +03:00
currentTopHeading: GroupModel;
private _origRules: any = {};
saveOrigRules() {
this._origRules.open = md.renderer.rules.heading_open;
this._origRules.close = md.renderer.rules.heading_close;
}
restoreOrigRules() {
md.renderer.rules.heading_open = this._origRules.open;
md.renderer.rules.heading_close = this._origRules.close;
}
saveHeading(name: string, container: IMenuItem[] = this.headings): GroupModel {
const item = new GroupModel('section', {
name,
});
item.depth = 1;
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.children));
}
return res;
}
attachHeadingsContent(rawText: string) {
const buildRegexp = heading => new RegExp(`<h\\d ${SECTION_ATTR}="section/${heading.id}">`);
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;
2017-10-12 00:01:37 +03:00
prevHeading.content = html2Str(rawText.substring(prevPos, currentPos));
prevHeading = heading;
prevPos = currentPos;
}
prevHeading.content = html2Str(rawText.substring(prevPos));
}
headingOpenRule = (tokens, idx) => {
if (tokens[idx].hLevel > 2) {
return this._origRules.open(tokens, idx);
} else {
2018-01-22 21:30:53 +03:00
const content = tokens[idx + 1].content;
2017-10-12 00:01:37 +03:00
if (tokens[idx].hLevel === 1) {
this.currentTopHeading = this.saveHeading(content);
2018-01-22 21:30:53 +03:00
const id = this.currentTopHeading.id;
2017-10-12 00:01:37 +03:00
return (
`<a name="${id}"></a>` +
`<h${tokens[idx].hLevel} ${SECTION_ATTR}="${id}" id="${id}">` +
2017-10-12 00:01:37 +03:00
`<a class="share-link" href="#${id}"></a>`
);
} else if (tokens[idx].hLevel === 2) {
const { id } = this.saveHeading(
content,
this.currentTopHeading && this.currentTopHeading.items,
);
2017-10-12 00:01:37 +03:00
return (
`<a name="${id}"></a>` +
`<h${tokens[idx].hLevel} ${SECTION_ATTR}="${id}" id="${id}">` +
2017-10-12 00:01:37 +03:00
`<a class="share-link" href="#${id}"></a>`
);
}
}
};
headingCloseRule = (tokens, idx) => {
if (tokens[idx].hLevel > 2) {
return this._origRules.close(tokens, idx);
} else {
return `</h${tokens[idx].hLevel}>\n`;
}
};
renderMd(rawText: string, raw: boolean = true): string {
if (!raw) {
this.saveOrigRules();
md.renderer.rules.heading_open = this.headingOpenRule;
md.renderer.rules.heading_close = this.headingCloseRule;
}
2018-01-22 21:30:53 +03:00
const text = rawText;
2017-10-12 00:01:37 +03:00
2018-01-22 21:30:53 +03:00
const res = md.render(text);
2017-10-12 00:01:37 +03:00
this.attachHeadingsContent(res);
if (!raw) {
this.restoreOrigRules();
}
return res;
}
extractHeadings(rawText: string): GroupModel[] {
this.renderMd(rawText, false);
const res = this.headings;
this.headings = [];
return res;
}
renderMdWithComponents(
rawText: string,
2017-11-23 00:38:38 +03:00
components: Dict<MDComponent>,
2017-10-12 00:01:37 +03:00
raw: boolean = true,
2018-01-22 21:30:53 +03:00
): Array<string | MDComponent> {
const componentDefs: string[] = [];
const anyCompRegexp = new RegExp(COMPONENT_REGEXP.replace('{component}', '(.*?)'), 'gmi');
let match = anyCompRegexp.exec(rawText);
while (match) {
2017-10-12 00:01:37 +03:00
componentDefs.push(match[1]);
2018-01-22 21:30:53 +03:00
match = anyCompRegexp.exec(rawText);
2017-10-12 00:01:37 +03:00
}
2018-01-22 21:30:53 +03:00
const splitCompRegexp = new RegExp(COMPONENT_REGEXP.replace('{component}', '.*?'), 'mi');
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;
} {
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
};
}