const Renderer = require('./Renderer.js'); const { defaults } = require('./defaults.js'); const { inline } = require('./rules.js'); const { findClosingBracket, escape } = require('./helpers.js'); /** * Inline Lexer & Compiler */ module.exports = class InlineLexer { constructor(links, options) { this.options = options || defaults; this.links = links; this.rules = inline.normal; this.options.renderer = this.options.renderer || new Renderer(); this.renderer = this.options.renderer; this.renderer.options = this.options; if (!this.links) { throw new Error('Tokens array requires a `links` property.'); } if (this.options.pedantic) { this.rules = inline.pedantic; } else if (this.options.gfm) { if (this.options.breaks) { this.rules = inline.breaks; } else { this.rules = inline.gfm; } } } /** * Expose Inline Rules */ static get rules() { return inline; } /** * Static Lexing/Compiling Method */ static output(src, links, options) { const inline = new InlineLexer(links, options); return inline.output(src); } /** * Lexing/Compiling */ output(src) { let out = '', link, text, href, title, cap, prevCapZero; while (src) { // escape if (cap = this.rules.escape.exec(src)) { src = src.substring(cap[0].length); out += escape(cap[1]); continue; } // tag if (cap = this.rules.tag.exec(src)) { if (!this.inLink && /^/i.test(cap[0])) { this.inLink = false; } if (!this.inRawBlock && /^<(pre|code|kbd|script)(\s|>)/i.test(cap[0])) { this.inRawBlock = true; } else if (this.inRawBlock && /^<\/(pre|code|kbd|script)(\s|>)/i.test(cap[0])) { this.inRawBlock = false; } src = src.substring(cap[0].length); out += this.options.sanitize ? this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0]) : cap[0]; continue; } // link if (cap = this.rules.link.exec(src)) { const lastParenIndex = findClosingBracket(cap[2], '()'); if (lastParenIndex > -1) { const start = cap[0].indexOf('!') === 0 ? 5 : 4; const linkLen = start + cap[1].length + lastParenIndex; cap[2] = cap[2].substring(0, lastParenIndex); cap[0] = cap[0].substring(0, linkLen).trim(); cap[3] = ''; } src = src.substring(cap[0].length); this.inLink = true; href = cap[2]; if (this.options.pedantic) { link = /^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(href); if (link) { href = link[1]; title = link[3]; } else { title = ''; } } else { title = cap[3] ? cap[3].slice(1, -1) : ''; } href = href.trim().replace(/^<([\s\S]*)>$/, '$1'); out += this.outputLink(cap, { href: InlineLexer.escapes(href), title: InlineLexer.escapes(title) }); this.inLink = false; continue; } // reflink, nolink if ((cap = this.rules.reflink.exec(src)) || (cap = this.rules.nolink.exec(src))) { src = src.substring(cap[0].length); link = (cap[2] || cap[1]).replace(/\s+/g, ' '); link = this.links[link.toLowerCase()]; if (!link || !link.href) { out += cap[0].charAt(0); src = cap[0].substring(1) + src; continue; } this.inLink = true; out += this.outputLink(cap, link); this.inLink = false; continue; } // strong if (cap = this.rules.strong.exec(src)) { src = src.substring(cap[0].length); out += this.renderer.strong(this.output(cap[4] || cap[3] || cap[2] || cap[1])); continue; } // em if (cap = this.rules.em.exec(src)) { src = src.substring(cap[0].length); out += this.renderer.em(this.output(cap[6] || cap[5] || cap[4] || cap[3] || cap[2] || cap[1])); continue; } // code if (cap = this.rules.code.exec(src)) { src = src.substring(cap[0].length); out += this.renderer.codespan(escape(cap[2].trim(), true)); continue; } // br if (cap = this.rules.br.exec(src)) { src = src.substring(cap[0].length); out += this.renderer.br(); continue; } // del (gfm) if (cap = this.rules.del.exec(src)) { src = src.substring(cap[0].length); out += this.renderer.del(this.output(cap[1])); continue; } // autolink if (cap = this.rules.autolink.exec(src)) { src = src.substring(cap[0].length); if (cap[2] === '@') { text = escape(this.mangle(cap[1])); href = 'mailto:' + text; } else { text = escape(cap[1]); href = text; } out += this.renderer.link(href, null, text); continue; } // url (gfm) if (!this.inLink && (cap = this.rules.url.exec(src))) { if (cap[2] === '@') { text = escape(cap[0]); href = 'mailto:' + text; } else { // do extended autolink path validation do { prevCapZero = cap[0]; cap[0] = this.rules._backpedal.exec(cap[0])[0]; } while (prevCapZero !== cap[0]); text = escape(cap[0]); if (cap[1] === 'www.') { href = 'http://' + text; } else { href = text; } } src = src.substring(cap[0].length); out += this.renderer.link(href, null, text); continue; } // text if (cap = this.rules.text.exec(src)) { src = src.substring(cap[0].length); if (this.inRawBlock) { out += this.renderer.text(this.options.sanitize ? (this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0])) : cap[0]); } else { out += this.renderer.text(escape(this.smartypants(cap[0]))); } continue; } if (src) { throw new Error('Infinite loop on byte: ' + src.charCodeAt(0)); } } return out; } static escapes(text) { return text ? text.replace(InlineLexer.rules._escapes, '$1') : text; } /** * Compile Link */ outputLink(cap, link) { const href = link.href, title = link.title ? escape(link.title) : null; return cap[0].charAt(0) !== '!' ? this.renderer.link(href, title, this.output(cap[1])) : this.renderer.image(href, title, escape(cap[1])); } /** * Smartypants Transformations */ smartypants(text) { 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 */ mangle(text) { if (!this.options.mangle) return text; const l = text.length; let out = '', i = 0, ch; for (; i < l; i++) { ch = text.charCodeAt(i); if (Math.random() > 0.5) { ch = 'x' + ch.toString(16); } out += '&#' + ch + ';'; } return out; } };