2016-10-30 18:56:24 +03:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
import { Injectable } from '@angular/core';
|
|
|
|
import * as slugify from 'slugify';
|
|
|
|
import * as Remarkable from 'remarkable';
|
2017-01-30 18:21:12 +03:00
|
|
|
import { StringMap } from './';
|
2016-10-30 18:56:24 +03:00
|
|
|
|
2017-11-15 10:40:55 +03:00
|
|
|
function HTMLescape(html: string): string {
|
2018-01-21 21:48:49 +03:00
|
|
|
return (document.createElement('div')
|
2017-11-15 10:40:55 +03:00
|
|
|
.appendChild(document.createTextNode(html))
|
2018-01-21 21:48:49 +03:00
|
|
|
.parentNode as Element)
|
2017-11-15 10:40:55 +03:00
|
|
|
.innerHTML;
|
|
|
|
}
|
|
|
|
|
2016-10-30 18:56:24 +03:00
|
|
|
declare var Prism: any;
|
|
|
|
const md = new Remarkable({
|
|
|
|
html: true,
|
|
|
|
linkify: true,
|
|
|
|
breaks: false,
|
|
|
|
typographer: false,
|
|
|
|
highlight: (str, lang) => {
|
|
|
|
if (lang === 'json') lang = 'js';
|
|
|
|
let grammar = Prism.languages[lang];
|
2017-01-31 03:11:01 +03:00
|
|
|
// fallback to click
|
2017-11-15 10:40:55 +03:00
|
|
|
if (!grammar) return HTMLescape(str);
|
2016-10-30 18:56:24 +03:00
|
|
|
return Prism.highlight(str, grammar);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2017-01-30 18:21:12 +03:00
|
|
|
export interface MarkdownHeading {
|
|
|
|
title?: string;
|
|
|
|
id: string;
|
2017-04-23 15:23:20 +03:00
|
|
|
slug?: string;
|
2017-01-30 18:21:12 +03:00
|
|
|
content?: string;
|
|
|
|
children?: StringMap<MarkdownHeading>;
|
|
|
|
}
|
|
|
|
|
2016-10-30 18:56:24 +03:00
|
|
|
export class MdRenderer {
|
2017-01-30 18:21:12 +03:00
|
|
|
public headings: StringMap<MarkdownHeading> = {};
|
|
|
|
currentTopHeading: MarkdownHeading;
|
2016-10-31 11:15:04 +03:00
|
|
|
|
2016-10-30 18:56:24 +03:00
|
|
|
private _origRules:any = {};
|
|
|
|
private _preProcessors:Function[] = [];
|
|
|
|
|
|
|
|
constructor(private raw: boolean = false) {
|
|
|
|
}
|
|
|
|
|
|
|
|
addPreprocessor(p: Function) {
|
|
|
|
this._preProcessors.push(p);
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2017-01-30 18:21:12 +03:00
|
|
|
saveHeading(title: string, parent:MarkdownHeading = {id:null, children: this.headings}) :MarkdownHeading {
|
2017-05-03 11:01:53 +03:00
|
|
|
// if title contains some non-ASCII characters (e.g. chinese) slugify returns empty string
|
|
|
|
let slug = slugify(title) || title;
|
2017-04-23 15:23:20 +03:00
|
|
|
let id = slug;
|
2017-01-30 18:21:12 +03:00
|
|
|
if (parent && parent.id) id = `${parent.id}/${id}`;
|
|
|
|
parent.children = parent.children || {};
|
|
|
|
parent.children[id] = {
|
|
|
|
title,
|
2017-04-23 15:23:20 +03:00
|
|
|
id,
|
|
|
|
slug
|
2017-01-30 18:21:12 +03:00
|
|
|
};
|
|
|
|
return parent.children[id];
|
|
|
|
}
|
|
|
|
|
2017-01-30 19:59:57 +03:00
|
|
|
flattenHeadings(container: StringMap<MarkdownHeading>): MarkdownHeading[] {
|
|
|
|
if (!container) return [];
|
|
|
|
let res = [];
|
|
|
|
Object.keys(container).forEach(k => {
|
|
|
|
let heading = container[k];
|
|
|
|
res.push(heading);
|
|
|
|
res.push(...this.flattenHeadings(heading.children));
|
|
|
|
});
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
attachHeadingsContent(rawText:string) {
|
|
|
|
const buildRegexp = heading => new RegExp(
|
|
|
|
`<h\\d section="section/${heading.id}">`
|
|
|
|
);
|
|
|
|
|
|
|
|
const tmpEl = document.createElement('DIV');
|
|
|
|
|
|
|
|
const html2Str = html => {
|
|
|
|
tmpEl.innerHTML = html;
|
|
|
|
return tmpEl.innerText;
|
|
|
|
};
|
|
|
|
|
|
|
|
let flatHeadings = this.flattenHeadings(this.headings);
|
|
|
|
if (flatHeadings.length < 1) return;
|
|
|
|
let prevHeading = flatHeadings[0];
|
|
|
|
|
|
|
|
let prevPos = rawText.search(buildRegexp(prevHeading));
|
|
|
|
for (let i=1; i < flatHeadings.length; i++) {
|
|
|
|
let heading = flatHeadings[i];
|
|
|
|
let currentPos = rawText.substr(prevPos + 1).search(buildRegexp(heading)) + prevPos + 1;
|
|
|
|
prevHeading.content = html2Str(rawText.substring(prevPos, currentPos));
|
|
|
|
|
|
|
|
prevHeading = heading;
|
|
|
|
prevPos = currentPos;
|
|
|
|
}
|
|
|
|
prevHeading.content = html2Str(rawText.substring(prevPos));
|
|
|
|
}
|
|
|
|
|
2016-10-30 18:56:24 +03:00
|
|
|
headingOpenRule(tokens, idx) {
|
2016-12-30 00:36:39 +03:00
|
|
|
if (tokens[idx].hLevel > 2 ) {
|
2016-10-30 18:56:24 +03:00
|
|
|
return this._origRules.open(tokens, idx);
|
|
|
|
} else {
|
|
|
|
let content = tokens[idx + 1].content;
|
2016-12-30 00:36:39 +03:00
|
|
|
if (tokens[idx].hLevel === 1 ) {
|
2017-05-05 12:45:03 +03:00
|
|
|
this.currentTopHeading = this.saveHeading(content);
|
2017-01-30 18:21:12 +03:00
|
|
|
let id = this.currentTopHeading.id;
|
|
|
|
return `<h${tokens[idx].hLevel} section="section/${id}">` +
|
2017-04-23 15:23:20 +03:00
|
|
|
`<a class="share-link" href="#section/${id}"></a>` +
|
|
|
|
`<a name="${id.toLowerCase()}"></a>`;
|
2016-12-30 00:36:39 +03:00
|
|
|
} else if (tokens[idx].hLevel === 2 ) {
|
2017-01-30 18:21:12 +03:00
|
|
|
let heading = this.saveHeading(content, this.currentTopHeading);
|
|
|
|
let contentSlug = `${heading.id}`;
|
|
|
|
return `<h${tokens[idx].hLevel} section="section/${heading.id}">` +
|
2017-04-23 15:23:20 +03:00
|
|
|
`<a class="share-link" href="#section/${contentSlug}"></a>` +
|
|
|
|
`<a name="${heading.slug.toLowerCase()}"></a>`;
|
2016-12-30 00:36:39 +03:00
|
|
|
}
|
2016-10-30 18:56:24 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
headingCloseRule(tokens, idx) {
|
2016-12-30 00:36:39 +03:00
|
|
|
if (tokens[idx].hLevel > 2 ) {
|
2016-10-30 18:56:24 +03:00
|
|
|
return this._origRules.close(tokens, idx);
|
|
|
|
} else {
|
|
|
|
return `</h${tokens[idx].hLevel}>\n`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
renderMd(rawText:string) {
|
|
|
|
if (!this.raw) {
|
|
|
|
this.saveOrigRules();
|
|
|
|
md.renderer.rules.heading_open = this.headingOpenRule.bind(this);
|
|
|
|
md.renderer.rules.heading_close = this.headingCloseRule.bind(this);
|
|
|
|
}
|
|
|
|
let text = rawText;
|
|
|
|
|
|
|
|
for (let i=0; i<this._preProcessors.length; i++) {
|
|
|
|
text = this._preProcessors[i](text);
|
|
|
|
}
|
|
|
|
|
|
|
|
let res = md.render(text);
|
|
|
|
|
2017-01-30 19:59:57 +03:00
|
|
|
this.attachHeadingsContent(res);
|
|
|
|
|
2016-10-30 18:56:24 +03:00
|
|
|
if (!this.raw) {
|
|
|
|
this.restoreOrigRules();
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
}
|