l0bsterssg

node.js static responsive blog post generator
Log | Files | Refs | README

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   '&': '&amp;',
     10   '<': '&lt;',
     11   '>': '&gt;',
     12   '"': '&quot;',
     13   "'": '&#39;'
     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 };