const _ = require('lodash'); const Markdown = require('markedLegacy'); 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)} `; } // if(_.startsWith(_.trim(html), '')){ // const openTag = html.substring(0, html.indexOf('>')+1); // html = html.substring(html.indexOf('>')+1); // html = html.substring(0, html.lastIndexOf('')); // html = html.replaceAll(/\s(\.[^{]*)/gm, '.legacy $1'); // return `${openTag} ${html} `; // } return html; }; 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 { 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 { 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 tagTypes = ['div', 'span', 'a']; const tagRegex = new RegExp(`(${ _.map(tagTypes, (type)=>{ return `\\<${type}\\b|\\`; }).join('|')})`, 'g'); // Special "void" tags that can be self-closed but don't need to be. const voidTags = new Set([ 'area', 'base', 'br', 'col', 'command', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source' ]); module.exports = { marked : Markdown, render : (rawBrewText)=>{ return Markdown( rawBrewText, { renderer: renderer } ); }, validate : (rawBrewText)=>{ const errors = []; const leftovers = _.reduce(rawBrewText.split('\n'), (acc, line, _lineNumber)=>{ const lineNumber = _lineNumber + 1; const matches = line.match(tagRegex); if(!matches || !matches.length) return acc; _.each(matches, (match)=>{ _.each(tagTypes, (type)=>{ if(match == `<${type}`){ acc.push({ type : type, line : lineNumber }); } if(match === ``){ // Closing tag: Check we expect it to be closed. // The accumulator may contain a sequence of voidable opening tags, // over which we skip before checking validity of the close. while (acc.length && voidTags.has(_.last(acc).type) && _.last(acc).type != type) { acc.pop(); } // Now check that what remains in the accumulator is valid. if(!acc.length){ errors.push({ line : lineNumber, type : type, text : 'Unmatched closing tag', id : 'CLOSE' }); } else if(_.last(acc).type == type){ acc.pop(); } else { errors.push({ line : `${_.last(acc).line} to ${lineNumber}`, type : type, text : 'Type mismatch on closing tag', id : 'MISMATCH' }); acc.pop(); } } }); }); return acc; }, []); _.each(leftovers, (unmatched)=>{ errors.push({ line : unmatched.line, type : unmatched.type, text : 'Unmatched opening tag', id : 'OPEN' }); }); return errors; }, };