helpers.js 6.1 KB

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