/* eslint-disable max-lines */ const _ = require('lodash'); const Markdown = require('marked'); const renderer = new Markdown.Renderer(); //Processes the markdown within an HTML block if it's just a class-wrapper renderer.html = function (html) { if(_.startsWith(_.trim(html), '')){ const openTag = html.substring(0, html.indexOf('>')+1); html = html.substring(html.indexOf('>')+1); html = html.substring(0, html.lastIndexOf('')); return `${openTag} ${Markdown(html)} `; } return html; }; // Don't wrap {{ Divs or {{ empty Spans in

tags renderer.paragraph = function(text){ let match; if(text.startsWith('${text}

\n`; }; const mustacheSpans = { name : 'mustacheSpans', level : 'inline', // Is this a block-level or inline-level tokenizer? start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token const inlineRegex = /{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*\s*|}}/g; const match = completeSpan.exec(src); if(match) { //Find closing delimiter let blockCount = 0; let tags = ''; let endTags = 0; let endToken = 0; let delim; while (delim = inlineRegex.exec(match[0])) { if(!tags) { tags = ` ${processStyleTags(delim[0].substring(2))}`; endTags = delim[0].length; } if(delim[0].startsWith('{{')) { blockCount++; } else if(delim[0] == '}}' && blockCount !== 0) { blockCount--; if(blockCount == 0) { endToken = inlineRegex.lastIndex; break; } } } if(endToken) { const raw = src.slice(0, endToken); const text = raw.slice(endTags || -2, -2); return { // Token to generate type : 'mustacheSpans', // Should match "name" above raw : raw, // Text to consume from the source text : text, // Additional custom properties tags : tags, tokens : this.lexer.inlineTokens(text) // inlineTokens to process **bold**, *italics*, etc. }; } } }, renderer(token) { return `]+)([^\n<>]*>.*)/s.exec(text); if(openingTag) { return `${openingTag[1]} class="${token.tags}${openingTag[2]}`; } return text; } }; const mustacheInjectBlock = { extensions : [{ name : 'mustacheInjectBlock', level : 'block', start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { const inlineRegex = /^ *{((?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*)}/ym; const match = inlineRegex.exec(src); if(match) { const lastToken = tokens[tokens.length - 1]; if(!lastToken) return false; lastToken.originalType = 'mustacheInjectBlock'; lastToken.tags = ` ${processStyleTags(match[1])}`; return { type : 'mustacheInjectBlock', // Should match "name" above raw : match[0], // Text to consume from the source text : '' }; } }, renderer(token) { if(!token.originalType){ return; } token.type = token.originalType; const text = this.parser.parse([token]); const openingTag = /(<[^\s<>]+)([^\n<>]*>.*)/s.exec(text); if(openingTag) { return `${openingTag[1]} class="${token.tags}${openingTag[2]}`; } return text; } }], walkTokens(token) { // After token tree is finished, tag tokens to apply styles to so Renderer can find them // Does not work with tables since Marked.js tables generate invalid "tokens", and changing "type" ruins Marked handling that edge-case if(token.originalType == 'mustacheInjectBlock' && token.type !== 'table') { token.originalType = token.type; token.type = 'mustacheInjectBlock'; } } }; const definitionLists = { name : 'definitionLists', level : 'block', start(src) { return src.match(/^.*?::.*/m)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { const regex = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym; let match; let endIndex = 0; const definitions = []; while (match = regex.exec(src)) { definitions.push({ dt : this.lexer.inlineTokens(match[1].trim()), dd : this.lexer.inlineTokens(match[2].trim()) }); endIndex = regex.lastIndex; } if(definitions.length) { return { type : 'definitionLists', raw : src.slice(0, endIndex), definitions }; } }, renderer(token) { return `
${token.definitions.reduce((html, def)=>{ return `${html}
${this.parser.parseInline(def.dt)}
` + `
${this.parser.parseInline(def.dd)}
\n`; }, '')}
`; } }; const spanTable = { name : 'spanTable', level : 'block', // Is this a block-level or inline-level tokenizer? start(src) { return src.match(/^\n *([^\n ].*\|.*)\n/)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { //const regex = this.tokenizer.rules.block.table; const regex = new RegExp('^ *([^\\n ].*\\|.*\\n(?: *[^\\s].*\\n)*?)' // Header + ' {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)\\|?' // Align + '(?:\\n *((?:(?!\\n| {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})' // Cells + '(?:\\n+|$)| {0,3}#{1,6} | {0,3}>| {4}[^\\n]| {0,3}(?:`{3,}' + '(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n| {0,3}(?:[*+-]|1[.)]) |' + '<\\/?(?:address|article|aside|base|basefont|blockquote|body|' + 'caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)(?: +|\\n|\\/?>)|<(?:script|pre|style|textarea|!--)).*(?:\\n|$))*)\\n*|$)'); // Cells const cap = regex.exec(src); if(cap) { const item = { type : 'spanTable', header : cap[1].replace(/\n$/, '').split('\n'), align : cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), rows : cap[3] ? cap[3].replace(/\n$/, '').split('\n') : [] }; // Get first header row to determine how many columns item.header[0] = splitCells(item.header[0]); const colCount = item.header[0].reduce((length, header)=>{ return length + header.colspan; }, 0); if(colCount === item.align.length) { item.raw = cap[0]; let i, j, k, row; // Get alignment row (:---:) let l = item.align.length; for (i = 0; i < l; i++) { if(/^ *-+: *$/.test(item.align[i])) { item.align[i] = 'right'; } else if(/^ *:-+: *$/.test(item.align[i])) { item.align[i] = 'center'; } else if(/^ *:-+ *$/.test(item.align[i])) { item.align[i] = 'left'; } else { item.align[i] = null; } } // Get any remaining header rows l = item.header.length; for (i = 1; i < l; i++) { item.header[i] = splitCells(item.header[i], colCount, item.header[i-1]); } // Get main table cells l = item.rows.length; for (i = 0; i < l; i++) { item.rows[i] = splitCells(item.rows[i], colCount, item.rows[i-1]); } // header child tokens l = item.header.length; for (j = 0; j < l; j++) { row = item.header[j]; for (k = 0; k < row.length; k++) { row[k].tokens = []; this.lexer.inlineTokens(row[k].text, row[k].tokens); } } // cell child tokens l = item.rows.length; for (j = 0; j < l; j++) { row = item.rows[j]; for (k = 0; k < row.length; k++) { row[k].tokens = []; this.lexer.inlineTokens(row[k].text, row[k].tokens); } } return item; } } }, renderer(token) { let i, j, row, cell, col, text; let output = ``; output += ``; for (i = 0; i < token.header.length; i++) { row = token.header[i]; let col = 0; output += ``; for (j = 0; j < row.length; j++) { cell = row[j]; text = this.parser.parseInline(cell.tokens); output += getTableCell(text, cell, 'th', token.align[col]); col += cell.colspan; } output += ``; } output += ``; if(token.rows.length) { output += ``; for (i = 0; i < token.rows.length; i++) { row = token.rows[i]; col = 0; output += ``; for (j = 0; j < row.length; j++) { cell = row[j]; text = this.parser.parseInline(cell.tokens); output += getTableCell(text, cell, 'td', token.align[col]); col += cell.colspan; } output += ``; } output += ``; } output += `
`; return output; } }; const getTableCell = (text, cell, type, align)=>{ if(!cell.rowspan) { return ''; } const tag = `<${type}` + `${cell.colspan > 1 ? ` colspan=${cell.colspan}` : ''}` + `${cell.rowspan > 1 ? ` rowspan=${cell.rowspan}` : ''}` + `${align ? ` align=${align}` : ''}>`; return `${tag + text}\n`; }; const splitCells = (tableRow, count, prevRow = [])=>{ const cells = [...tableRow.matchAll(/(?:[^|\\]|\\.?)+(?:\|+|$)/g)].map((x)=>x[0]); // Remove first/last cell in a row if whitespace only and no leading/trailing pipe if(!cells[0]?.trim()) { cells.shift(); } if(!cells[cells.length - 1]?.trim()) { cells.pop(); } let numCols = 0; let i, j, trimmedCell, prevCell, prevCols; for (i = 0; i < cells.length; i++) { trimmedCell = cells[i].split(/\|+$/)[0]; cells[i] = { rowspan : 1, colspan : Math.max(cells[i].length - trimmedCell.length, 1), text : trimmedCell.trim().replace(/\\\|/g, '|') // display escaped pipes as normal character }; // Handle Rowspan if(trimmedCell.slice(-1) == '^' && prevRow.length) { // Find matching cell in previous row prevCols = 0; for (j = 0; j < prevRow.length; j++) { prevCell = prevRow[j]; if((prevCols == numCols) && (prevCell.colspan == cells[i].colspan)) { // merge into matching cell in previous row (the "target") cells[i].rowSpanTarget = prevCell.rowSpanTarget ?? prevCell; cells[i].rowSpanTarget.text += ` ${cells[i].text.slice(0, -1)}`; cells[i].rowSpanTarget.rowspan += 1; cells[i].rowspan = 0; break; } prevCols += prevCell.colspan; if(prevCols > numCols) break; } } numCols += cells[i].colspan; } // Force main cell rows to match header column count if(numCols > count) { cells.splice(count); } else { while (numCols < count) { cells.push({ colspan : 1, text : '' }); numCols += 1; } } return cells; }; Markdown.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists, spanTable] }); Markdown.use(mustacheInjectBlock); Markdown.use({ smartypants: true }); //Fix local links in the Preview iFrame to link inside the frame renderer.link = function (href, title, text) { let self = false; if(href[0] == '#') { self = true; } href = cleanUrl(this.options.sanitize, this.options.baseUrl, href); if(href === null) { return text; } let out = `${text}`; return out; }; const nonWordAndColonTest = /[^\w:]/g; const cleanUrl = function (sanitize, base, href) { if(sanitize) { let prot; try { prot = decodeURIComponent(unescape(href)) .replace(nonWordAndColonTest, '') .toLowerCase(); } catch (e) { return null; } if(prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) { return null; } } try { href = encodeURI(href).replace(/%25/g, '%'); } catch (e) { return null; } return href; }; const escapeTest = /[&<>"']/; const escapeReplace = /[&<>"']/g; const escapeTestNoEncode = /[<>"']|&(?!#?\w+;)/; const escapeReplaceNoEncode = /[<>"']|&(?!#?\w+;)/g; const escapeReplacements = { '&' : '&', '<' : '<', '>' : '>', '"' : '"', '\'' : ''' }; const getEscapeReplacement = (ch)=>escapeReplacements[ch]; const escape = function (html, encode) { if(encode) { if(escapeTest.test(html)) { return html.replace(escapeReplace, getEscapeReplacement); } } else { if(escapeTestNoEncode.test(html)) { return html.replace(escapeReplaceNoEncode, getEscapeReplacement); } } return html; }; const sanatizeScriptTags = (content)=>{ return content .replace(/