/** * Helpers */ function escape(html, encode) { if (encode) { if (escape.escapeTest.test(html)) { return html.replace(escape.escapeReplace, escape.getReplacement); } } else { if (escape.escapeTestNoEncode.test(html)) { return html.replace(escape.escapeReplaceNoEncode, escape.getReplacement); } } return html; } escape.escapeTest = /[&<>"']/; escape.escapeReplace = /[&<>"']/g; escape.escapeTestNoEncode = /[<>"']|&(?!#?\w+;)/; escape.escapeReplaceNoEncode = /[<>"']|&(?!#?\w+;)/g; escape.replacements = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; escape.getReplacement = (ch) => escape.replacements[ch]; function unescape(html) { // explicitly match decimal, hex, and named HTML entities return html.replace(unescape.unescapeTest, (_, n) => { n = n.toLowerCase(); if (n === 'colon') return ':'; if (n.charAt(0) === '#') { return n.charAt(1) === 'x' ? String.fromCharCode(parseInt(n.substring(2), 16)) : String.fromCharCode(+n.substring(1)); } return ''; }); } unescape.unescapeTest = /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig; function edit(regex, opt) { regex = regex.source || regex; opt = opt || ''; const obj = { replace: (name, val) => { val = val.source || val; val = val.replace(edit.caret, '$1'); regex = regex.replace(name, val); return obj; }, getRegex: () => { return new RegExp(regex, opt); } }; return obj; } edit.caret = /(^|[^\[])\^/g; function cleanUrl(sanitize, base, href) { if (sanitize) { let prot; try { prot = decodeURIComponent(unescape(href)) .replace(cleanUrl.protocol, '') .toLowerCase(); } catch (e) { return null; } if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) { return null; } } if (base && !cleanUrl.originIndependentUrl.test(href)) { href = resolveUrl(base, href); } try { href = encodeURI(href).replace(/%25/g, '%'); } catch (e) { return null; } return href; } cleanUrl.protocol = /[^\w:]/g; cleanUrl.originIndependentUrl = /^$|^[a-z][a-z0-9+.-]*:|^[?#]/i; function resolveUrl(base, href) { if (!resolveUrl.baseUrls[' ' + base]) { // we can ignore everything in base after the last slash of its path component, // but we might need to add _that_ // https://tools.ietf.org/html/rfc3986#section-3 if (resolveUrl.justDomain.test(base)) { resolveUrl.baseUrls[' ' + base] = base + '/'; } else { resolveUrl.baseUrls[' ' + base] = rtrim(base, '/', true); } } base = resolveUrl.baseUrls[' ' + base]; const relativeBase = base.indexOf(':') === -1; if (href.substring(0, 2) === '//') { if (relativeBase) { return href; } return base.replace(resolveUrl.protocol, '$1') + href; } else if (href.charAt(0) === '/') { if (relativeBase) { return href; } return base.replace(resolveUrl.domain, '$1') + href; } else { return base + href; } } resolveUrl.baseUrls = {}; resolveUrl.justDomain = /^[^:]+:\/*[^/]*$/; resolveUrl.protocol = /^([^:]+:)[\s\S]*$/; resolveUrl.domain = /^([^:]+:\/*[^/]*)[\s\S]*$/; function noop() {} noop.exec = noop; function merge(obj) { let i = 1, target, key; for (; i < arguments.length; i++) { target = arguments[i]; for (key in target) { if (Object.prototype.hasOwnProperty.call(target, key)) { obj[key] = target[key]; } } } return obj; } function splitCells(tableRow, count) { // ensure that every cell-delimiting pipe has a space // before it to distinguish it from an escaped pipe const row = tableRow.replace(/\|/g, (match, offset, str) => { let escaped = false, curr = offset; while (--curr >= 0 && str[curr] === '\\') escaped = !escaped; if (escaped) { // odd number of slashes means | is escaped // so we leave it alone return '|'; } else { // add space before unescaped | return ' |'; } }), cells = row.split(/ \|/); let i = 0; if (cells.length > count) { cells.splice(count); } else { while (cells.length < count) cells.push(''); } for (; i < cells.length; i++) { // leading or trailing whitespace is ignored per the gfm spec cells[i] = cells[i].trim().replace(/\\\|/g, '|'); } return cells; } // Remove trailing 'c's. Equivalent to str.replace(/c*$/, ''). // /c*$/ is vulnerable to REDOS. // invert: Remove suffix of non-c chars instead. Default falsey. function rtrim(str, c, invert) { const l = str.length; if (l === 0) { return ''; } // Length of suffix matching the invert condition. let suffLen = 0; // Step left until we fail to match the invert condition. while (suffLen < l) { const currChar = str.charAt(l - suffLen - 1); if (currChar === c && !invert) { suffLen++; } else if (currChar !== c && invert) { suffLen++; } else { break; } } return str.substr(0, l - suffLen); } function findClosingBracket(str, b) { if (str.indexOf(b[1]) === -1) { return -1; } const l = str.length; let level = 0, i = 0; for (; i < l; i++) { if (str[i] === '\\') { i++; } else if (str[i] === b[0]) { level++; } else if (str[i] === b[1]) { level--; if (level < 0) { return i; } } } return -1; } function checkSanitizeDeprecation(opt) { if (opt && opt.sanitize && !opt.silent) { console.warn('marked(): sanitize and sanitizer parameters are deprecated since version 0.7.0, should not be used and will be removed in the future. Read more here: https://marked.js.org/#/USING_ADVANCED.md#options'); } } module.exports = { escape, unescape, edit, cleanUrl, resolveUrl, noop, merge, splitCells, rtrim, findClosingBracket, checkSanitizeDeprecation };