420 lines
11 KiB
TypeScript
420 lines
11 KiB
TypeScript
/**
|
|
* @license
|
|
*
|
|
* Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed)
|
|
* https://github.com/chjj/marked
|
|
*
|
|
* Copyright (c) 2018, Костя Третяк. (MIT Licensed)
|
|
* https://github.com/ts-stack/markdown
|
|
*/
|
|
|
|
import { ExtendRegexp } from "./extend-regexp.ts";
|
|
import type {
|
|
Link,
|
|
Links,
|
|
MarkedOptions,
|
|
RulesInlineBase,
|
|
RulesInlineBreaks,
|
|
RulesInlineCallback,
|
|
RulesInlineGfm,
|
|
RulesInlinePedantic,
|
|
} from "./interfaces.ts";
|
|
import { Marked } from "./marked.ts";
|
|
import { Renderer } from "./renderer.ts";
|
|
|
|
/**
|
|
* Inline Lexer & Compiler.
|
|
*/
|
|
export class InlineLexer {
|
|
protected static rulesBase: RulesInlineBase;
|
|
/**
|
|
* Pedantic Inline Grammar.
|
|
*/
|
|
protected static rulesPedantic: RulesInlinePedantic;
|
|
/**
|
|
* GFM Inline Grammar
|
|
*/
|
|
protected static rulesGfm: RulesInlineGfm;
|
|
/**
|
|
* GFM + Line Breaks Inline Grammar.
|
|
*/
|
|
protected static rulesBreaks: RulesInlineBreaks;
|
|
protected rules!:
|
|
| RulesInlineBase
|
|
| RulesInlinePedantic
|
|
| RulesInlineGfm
|
|
| RulesInlineBreaks;
|
|
protected renderer: Renderer;
|
|
protected inLink!: boolean;
|
|
protected hasRulesGfm!: boolean;
|
|
protected ruleCallbacks!: RulesInlineCallback[];
|
|
|
|
constructor(
|
|
protected staticThis: typeof InlineLexer,
|
|
protected links: Links,
|
|
protected options: MarkedOptions = Marked.options,
|
|
renderer?: Renderer
|
|
) {
|
|
this.renderer =
|
|
renderer || this.options.renderer || new Renderer(this.options);
|
|
|
|
if (!this.links) {
|
|
throw new Error(`InlineLexer requires 'links' parameter.`);
|
|
}
|
|
|
|
this.setRules();
|
|
}
|
|
|
|
/**
|
|
* Static Lexing/Compiling Method.
|
|
*/
|
|
static output(src: string, links: Links, options: MarkedOptions): string {
|
|
const inlineLexer = new this(this, links, options);
|
|
return inlineLexer.output(src);
|
|
}
|
|
|
|
protected static getRulesBase(): RulesInlineBase {
|
|
if (this.rulesBase) {
|
|
return this.rulesBase;
|
|
}
|
|
|
|
/**
|
|
* Inline-Level Grammar.
|
|
*/
|
|
const base: RulesInlineBase = {
|
|
escape: /^\\([\\`*{}\[\]()#+\-.!_>])/,
|
|
autolink: /^<([^ <>]+(@|:\/)[^ <>]+)>/,
|
|
tag: /^<!--[\s\S]*?-->|^<\/?\w+(?:"[^"]*"|'[^']*'|[^<'">])*?>/,
|
|
link: /^!?\[(inside)\]\(href\)/,
|
|
reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/,
|
|
nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,
|
|
strong: /^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,
|
|
em: /^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,
|
|
code: /^(`+)([\s\S]*?[^`])\1(?!`)/,
|
|
br: /^ {2,}\n(?!\s*$)/,
|
|
text: /^[\s\S]+?(?=[\\<!\[_*`]| {2,}\n|$)/,
|
|
_inside: /(?:\[[^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*/,
|
|
_href: /\s*<?([\s\S]*?)>?(?:\s+['"]([\s\S]*?)['"])?\s*/,
|
|
};
|
|
|
|
base.link = new ExtendRegexp(base.link)
|
|
.setGroup("inside", base._inside)
|
|
.setGroup("href", base._href)
|
|
.getRegexp();
|
|
|
|
base.reflink = new ExtendRegexp(base.reflink)
|
|
.setGroup("inside", base._inside)
|
|
.getRegexp();
|
|
|
|
return (this.rulesBase = base);
|
|
}
|
|
|
|
protected static getRulesPedantic(): RulesInlinePedantic {
|
|
if (this.rulesPedantic) {
|
|
return this.rulesPedantic;
|
|
}
|
|
|
|
return (this.rulesPedantic = {
|
|
...this.getRulesBase(),
|
|
...{
|
|
strong: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,
|
|
em: /^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/,
|
|
},
|
|
});
|
|
}
|
|
|
|
protected static getRulesGfm(): RulesInlineGfm {
|
|
if (this.rulesGfm) {
|
|
return this.rulesGfm;
|
|
}
|
|
|
|
const base = this.getRulesBase();
|
|
|
|
const escape = new ExtendRegexp(base.escape)
|
|
.setGroup("])", "~|])")
|
|
.getRegexp();
|
|
|
|
const text = new ExtendRegexp(base.text)
|
|
.setGroup("]|", "~]|")
|
|
.setGroup("|", "|https?://|")
|
|
.getRegexp();
|
|
|
|
return (this.rulesGfm = {
|
|
...base,
|
|
...{
|
|
escape,
|
|
url: /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/,
|
|
del: /^~~(?=\S)([\s\S]*?\S)~~/,
|
|
text,
|
|
},
|
|
});
|
|
}
|
|
|
|
protected static getRulesBreaks(): RulesInlineBreaks {
|
|
if (this.rulesBreaks) {
|
|
return this.rulesBreaks;
|
|
}
|
|
|
|
const inline = this.getRulesGfm();
|
|
const gfm = this.getRulesGfm();
|
|
|
|
return (this.rulesBreaks = {
|
|
...gfm,
|
|
...{
|
|
br: new ExtendRegexp(inline.br).setGroup("{2,}", "*").getRegexp(),
|
|
text: new ExtendRegexp(gfm.text).setGroup("{2,}", "*").getRegexp(),
|
|
},
|
|
});
|
|
}
|
|
|
|
protected setRules() {
|
|
if (this.options.gfm) {
|
|
if (this.options.breaks) {
|
|
this.rules = this.staticThis.getRulesBreaks();
|
|
} else {
|
|
this.rules = this.staticThis.getRulesGfm();
|
|
}
|
|
} else if (this.options.pedantic) {
|
|
this.rules = this.staticThis.getRulesPedantic();
|
|
} else {
|
|
this.rules = this.staticThis.getRulesBase();
|
|
}
|
|
|
|
this.hasRulesGfm = (this.rules as RulesInlineGfm).url !== undefined;
|
|
}
|
|
|
|
/**
|
|
* Lexing/Compiling.
|
|
*/
|
|
output(nextPart: string): string {
|
|
nextPart = nextPart;
|
|
let execArr: RegExpExecArray | null;
|
|
let out = "";
|
|
|
|
while (nextPart) {
|
|
// escape
|
|
if ((execArr = this.rules.escape.exec(nextPart))) {
|
|
nextPart = nextPart.substring(execArr[0].length);
|
|
out += execArr[1];
|
|
continue;
|
|
}
|
|
|
|
// autolink
|
|
if ((execArr = this.rules.autolink.exec(nextPart))) {
|
|
let text: string;
|
|
let href: string;
|
|
nextPart = nextPart.substring(execArr[0].length);
|
|
|
|
if (!this.options.escape) throw ReferenceError;
|
|
|
|
if (execArr[2] === "@") {
|
|
text = this.options.escape(
|
|
execArr[1].charAt(6) === ":"
|
|
? this.mangle(execArr[1].substring(7))
|
|
: this.mangle(execArr[1])
|
|
);
|
|
href = this.mangle("mailto:") + text;
|
|
} else {
|
|
text = this.options.escape(execArr[1]);
|
|
href = text;
|
|
}
|
|
|
|
out += this.renderer.link(href, "", text);
|
|
continue;
|
|
}
|
|
|
|
// url (gfm)
|
|
if (
|
|
!this.inLink &&
|
|
this.hasRulesGfm &&
|
|
(execArr = (this.rules as RulesInlineGfm).url.exec(nextPart))
|
|
) {
|
|
if (!this.options.escape) throw ReferenceError;
|
|
let text: string;
|
|
let href: string;
|
|
nextPart = nextPart.substring(execArr[0].length);
|
|
text = this.options.escape(execArr[1]);
|
|
href = text;
|
|
out += this.renderer.link(href, "", text);
|
|
continue;
|
|
}
|
|
|
|
// tag
|
|
if ((execArr = this.rules.tag.exec(nextPart))) {
|
|
if (!this.inLink && /^<a /i.test(execArr[0])) {
|
|
this.inLink = true;
|
|
} else if (this.inLink && /^<\/a>/i.test(execArr[0])) {
|
|
this.inLink = false;
|
|
}
|
|
|
|
nextPart = nextPart.substring(execArr[0].length);
|
|
|
|
if (!this.options.escape) throw ReferenceError;
|
|
|
|
out += this.options.sanitize
|
|
? this.options.sanitizer
|
|
? this.options.sanitizer(execArr[0])
|
|
: this.options.escape(execArr[0])
|
|
: execArr[0];
|
|
continue;
|
|
}
|
|
|
|
// link
|
|
if ((execArr = this.rules.link.exec(nextPart))) {
|
|
nextPart = nextPart.substring(execArr[0].length);
|
|
this.inLink = true;
|
|
|
|
out += this.outputLink(execArr, {
|
|
href: execArr[2],
|
|
title: execArr[3],
|
|
});
|
|
|
|
this.inLink = false;
|
|
continue;
|
|
}
|
|
|
|
// reflink, nolink
|
|
if (
|
|
(execArr = this.rules.reflink.exec(nextPart)) ||
|
|
(execArr = this.rules.nolink.exec(nextPart))
|
|
) {
|
|
nextPart = nextPart.substring(execArr[0].length);
|
|
const keyLink = (execArr[2] || execArr[1]).replace(/\s+/g, " ");
|
|
const link = this.links[keyLink.toLowerCase()];
|
|
|
|
if (!link || !link.href) {
|
|
out += execArr[0].charAt(0);
|
|
nextPart = execArr[0].substring(1) + nextPart;
|
|
continue;
|
|
}
|
|
|
|
this.inLink = true;
|
|
out += this.outputLink(execArr, link);
|
|
this.inLink = false;
|
|
continue;
|
|
}
|
|
|
|
// strong
|
|
if ((execArr = this.rules.strong.exec(nextPart))) {
|
|
nextPart = nextPart.substring(execArr[0].length);
|
|
out += this.renderer.strong(this.output(execArr[2] || execArr[1]));
|
|
continue;
|
|
}
|
|
|
|
// em
|
|
if ((execArr = this.rules.em.exec(nextPart))) {
|
|
nextPart = nextPart.substring(execArr[0].length);
|
|
out += this.renderer.em(this.output(execArr[2] || execArr[1]));
|
|
continue;
|
|
}
|
|
|
|
// code
|
|
if ((execArr = this.rules.code.exec(nextPart))) {
|
|
if (!this.options.escape) throw ReferenceError;
|
|
nextPart = nextPart.substring(execArr[0].length);
|
|
out += this.renderer.codespan(
|
|
this.options.escape(execArr[2].trim(), true)
|
|
);
|
|
continue;
|
|
}
|
|
|
|
// br
|
|
if ((execArr = this.rules.br.exec(nextPart))) {
|
|
nextPart = nextPart.substring(execArr[0].length);
|
|
out += this.renderer.br();
|
|
continue;
|
|
}
|
|
|
|
// del (gfm)
|
|
if (
|
|
this.hasRulesGfm &&
|
|
(execArr = (this.rules as RulesInlineGfm).del.exec(nextPart))
|
|
) {
|
|
nextPart = nextPart.substring(execArr[0].length);
|
|
out += this.renderer.del(this.output(execArr[1]));
|
|
continue;
|
|
}
|
|
|
|
// text
|
|
if ((execArr = this.rules.text.exec(nextPart))) {
|
|
if (!this.options.escape) throw ReferenceError;
|
|
nextPart = nextPart.substring(execArr[0].length);
|
|
out += this.renderer.text(
|
|
this.options.escape(this.smartypants(execArr[0]))
|
|
);
|
|
continue;
|
|
}
|
|
|
|
if (nextPart) {
|
|
throw new Error("Infinite loop on byte: " + nextPart.charCodeAt(0));
|
|
}
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Compile Link.
|
|
*/
|
|
protected outputLink(execArr: RegExpExecArray, link: Link) {
|
|
if (!this.options.escape) throw ReferenceError;
|
|
const href = this.options.escape(link.href);
|
|
const title = link.title ? this.options.escape(link.title) : null;
|
|
|
|
return execArr[0].charAt(0) !== "!"
|
|
? this.renderer.link(href, title || "", this.output(execArr[1]))
|
|
: this.renderer.image(href, title || "", this.options.escape(execArr[1]));
|
|
}
|
|
|
|
/**
|
|
* Smartypants Transformations.
|
|
*/
|
|
protected smartypants(text: string) {
|
|
if (!this.options.smartypants) {
|
|
return text;
|
|
}
|
|
|
|
return (
|
|
text
|
|
// em-dashes
|
|
.replace(/---/g, "\u2014")
|
|
// en-dashes
|
|
.replace(/--/g, "\u2013")
|
|
// opening singles
|
|
.replace(/(^|[-\u2014/(\[{"\s])'/g, "$1\u2018")
|
|
// closing singles & apostrophes
|
|
.replace(/'/g, "\u2019")
|
|
// opening doubles
|
|
.replace(/(^|[-\u2014/(\[{\u2018\s])"/g, "$1\u201c")
|
|
// closing doubles
|
|
.replace(/"/g, "\u201d")
|
|
// ellipses
|
|
.replace(/\.{3}/g, "\u2026")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Mangle Links.
|
|
*/
|
|
protected mangle(text: string) {
|
|
if (!this.options.mangle) {
|
|
return text;
|
|
}
|
|
|
|
let out = "";
|
|
const length = text.length;
|
|
|
|
for (let i = 0; i < length; i++) {
|
|
let str: string = "";
|
|
|
|
if (Math.random() > 0.5) {
|
|
str = "x" + text.charCodeAt(i).toString(16);
|
|
}
|
|
|
|
out += "&#" + str + ";";
|
|
}
|
|
|
|
return out;
|
|
}
|
|
}
|