helpers.js (5845B)
1 /** 2 * Helpers 3 */ 4 const escapeTest = /[&<>"']/; 5 const escapeReplace = /[&<>"']/g; 6 const escapeTestNoEncode = /[<>"']|&(?!#?\w+;)/; 7 const escapeReplaceNoEncode = /[<>"']|&(?!#?\w+;)/g; 8 const escapeReplacements = { 9 '&': '&', 10 '<': '<', 11 '>': '>', 12 '"': '"', 13 "'": ''' 14 }; 15 const getEscapeReplacement = (ch) => escapeReplacements[ch]; 16 function escape(html, encode) { 17 if (encode) { 18 if (escapeTest.test(html)) { 19 return html.replace(escapeReplace, getEscapeReplacement); 20 } 21 } else { 22 if (escapeTestNoEncode.test(html)) { 23 return html.replace(escapeReplaceNoEncode, getEscapeReplacement); 24 } 25 } 26 27 return html; 28 } 29 30 const unescapeTest = /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig; 31 32 function unescape(html) { 33 // explicitly match decimal, hex, and named HTML entities 34 return html.replace(unescapeTest, (_, n) => { 35 n = n.toLowerCase(); 36 if (n === 'colon') return ':'; 37 if (n.charAt(0) === '#') { 38 return n.charAt(1) === 'x' 39 ? String.fromCharCode(parseInt(n.substring(2), 16)) 40 : String.fromCharCode(+n.substring(1)); 41 } 42 return ''; 43 }); 44 } 45 46 const caret = /(^|[^\[])\^/g; 47 function edit(regex, opt) { 48 regex = regex.source || regex; 49 opt = opt || ''; 50 const obj = { 51 replace: (name, val) => { 52 val = val.source || val; 53 val = val.replace(caret, '$1'); 54 regex = regex.replace(name, val); 55 return obj; 56 }, 57 getRegex: () => { 58 return new RegExp(regex, opt); 59 } 60 }; 61 return obj; 62 } 63 64 const nonWordAndColonTest = /[^\w:]/g; 65 const originIndependentUrl = /^$|^[a-z][a-z0-9+.-]*:|^[?#]/i; 66 function cleanUrl(sanitize, base, href) { 67 if (sanitize) { 68 let prot; 69 try { 70 prot = decodeURIComponent(unescape(href)) 71 .replace(nonWordAndColonTest, '') 72 .toLowerCase(); 73 } catch (e) { 74 return null; 75 } 76 if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) { 77 return null; 78 } 79 } 80 if (base && !originIndependentUrl.test(href)) { 81 href = resolveUrl(base, href); 82 } 83 try { 84 href = encodeURI(href).replace(/%25/g, '%'); 85 } catch (e) { 86 return null; 87 } 88 return href; 89 } 90 91 const baseUrls = {}; 92 const justDomain = /^[^:]+:\/*[^/]*$/; 93 const protocol = /^([^:]+:)[\s\S]*$/; 94 const domain = /^([^:]+:\/*[^/]*)[\s\S]*$/; 95 96 function resolveUrl(base, href) { 97 if (!baseUrls[' ' + base]) { 98 // we can ignore everything in base after the last slash of its path component, 99 // but we might need to add _that_ 100 // https://tools.ietf.org/html/rfc3986#section-3 101 if (justDomain.test(base)) { 102 baseUrls[' ' + base] = base + '/'; 103 } else { 104 baseUrls[' ' + base] = rtrim(base, '/', true); 105 } 106 } 107 base = baseUrls[' ' + base]; 108 const relativeBase = base.indexOf(':') === -1; 109 110 if (href.substring(0, 2) === '//') { 111 if (relativeBase) { 112 return href; 113 } 114 return base.replace(protocol, '$1') + href; 115 } else if (href.charAt(0) === '/') { 116 if (relativeBase) { 117 return href; 118 } 119 return base.replace(domain, '$1') + href; 120 } else { 121 return base + href; 122 } 123 } 124 125 const noopTest = { exec: function noopTest() {} }; 126 127 function merge(obj) { 128 let i = 1, 129 target, 130 key; 131 132 for (; i < arguments.length; i++) { 133 target = arguments[i]; 134 for (key in target) { 135 if (Object.prototype.hasOwnProperty.call(target, key)) { 136 obj[key] = target[key]; 137 } 138 } 139 } 140 141 return obj; 142 } 143 144 function splitCells(tableRow, count) { 145 // ensure that every cell-delimiting pipe has a space 146 // before it to distinguish it from an escaped pipe 147 const row = tableRow.replace(/\|/g, (match, offset, str) => { 148 let escaped = false, 149 curr = offset; 150 while (--curr >= 0 && str[curr] === '\\') escaped = !escaped; 151 if (escaped) { 152 // odd number of slashes means | is escaped 153 // so we leave it alone 154 return '|'; 155 } else { 156 // add space before unescaped | 157 return ' |'; 158 } 159 }), 160 cells = row.split(/ \|/); 161 let i = 0; 162 163 if (cells.length > count) { 164 cells.splice(count); 165 } else { 166 while (cells.length < count) cells.push(''); 167 } 168 169 for (; i < cells.length; i++) { 170 // leading or trailing whitespace is ignored per the gfm spec 171 cells[i] = cells[i].trim().replace(/\\\|/g, '|'); 172 } 173 return cells; 174 } 175 176 // Remove trailing 'c's. Equivalent to str.replace(/c*$/, ''). 177 // /c*$/ is vulnerable to REDOS. 178 // invert: Remove suffix of non-c chars instead. Default falsey. 179 function rtrim(str, c, invert) { 180 const l = str.length; 181 if (l === 0) { 182 return ''; 183 } 184 185 // Length of suffix matching the invert condition. 186 let suffLen = 0; 187 188 // Step left until we fail to match the invert condition. 189 while (suffLen < l) { 190 const currChar = str.charAt(l - suffLen - 1); 191 if (currChar === c && !invert) { 192 suffLen++; 193 } else if (currChar !== c && invert) { 194 suffLen++; 195 } else { 196 break; 197 } 198 } 199 200 return str.substr(0, l - suffLen); 201 } 202 203 function findClosingBracket(str, b) { 204 if (str.indexOf(b[1]) === -1) { 205 return -1; 206 } 207 const l = str.length; 208 let level = 0, 209 i = 0; 210 for (; i < l; i++) { 211 if (str[i] === '\\') { 212 i++; 213 } else if (str[i] === b[0]) { 214 level++; 215 } else if (str[i] === b[1]) { 216 level--; 217 if (level < 0) { 218 return i; 219 } 220 } 221 } 222 return -1; 223 } 224 225 function checkSanitizeDeprecation(opt) { 226 if (opt && opt.sanitize && !opt.silent) { 227 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'); 228 } 229 } 230 231 module.exports = { 232 escape, 233 unescape, 234 edit, 235 cleanUrl, 236 resolveUrl, 237 noopTest, 238 merge, 239 splitCells, 240 rtrim, 241 findClosingBracket, 242 checkSanitizeDeprecation 243 };