marked/src/helpers.js

227 lines
5.6 KiB
JavaScript
Raw Normal View History

/**
* marked - a markdown parser
* Copyright (c) 2011-2018, Christopher Jeffrey. (MIT Licensed)
* https://github.com/markedjs/marked
*/
/**
* Helpers
*/
export function escape(html, encode) {
const escapeTest = /[&<>"']/;
const escapeReplace = /[&<>"']/g;
const replacements = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
};
const escapeTestNoEncode = /[<>"']|&(?!#?\w+;)/;
const escapeReplaceNoEncode = /[<>"']|&(?!#?\w+;)/g;
if (encode) {
if (escapeTest.test(html)) {
return html.replace(escapeReplace, function(ch) { return replacements[ch]; });
}
} else {
if (escapeTestNoEncode.test(html)) {
return html.replace(escapeReplaceNoEncode, function(ch) { return replacements[ch]; });
}
}
return html;
}
export function unescape(html) {
// explicitly match decimal, hex, and named HTML entities
return html.replace(/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig, function(_, 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 '';
});
}
export function edit(regex, opt) {
regex = regex.source || regex;
opt = opt || '';
return {
replace: function(name, val) {
val = val.source || val;
val = val.replace(/(^|[^\[])\^/g, '$1');
regex = regex.replace(name, val);
return this;
},
getRegex: function() {
return new RegExp(regex, opt);
}
};
}
export function cleanUrl(sanitize, base, href) {
if (sanitize) {
let prot;
try {
prot = decodeURIComponent(unescape(href))
.replace(/[^\w:]/g, '')
.toLowerCase();
} catch (e) {
return null;
}
if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) {
return null;
}
}
const originIndependentUrl = /^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;
if (base && !originIndependentUrl.test(href)) {
href = resolveUrl(base, href);
}
try {
href = encodeURI(href).replace(/%25/g, '%');
} catch (e) {
return null;
}
return href;
}
export 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 (/^[^:]+:\/*[^/]*$/.test(base)) {
resolveUrl.baseUrls[' ' + base] = base + '/';
} else {
resolveUrl.baseUrls[' ' + base] = rtrim(base, '/', true);
}
}
base = resolveUrl.baseUrls[' ' + base];
const relativeBase = base.indexOf(':') === -1;
if (href.slice(0, 2) === '//') {
if (relativeBase) {
return href;
}
return base.replace(/^([^:]+:)[\s\S]*$/, '$1') + href;
} else if (href.charAt(0) === '/') {
if (relativeBase) {
return href;
}
return base.replace(/^([^:]+:\/*[^/]*)[\s\S]*$/, '$1') + href;
} else {
return base + href;
}
}
resolveUrl.baseUrls = {};
export function noop() {}
noop.exec = noop;
export function merge(obj) {
let i = 1;
let target;
let 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;
}
export 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, function(match, offset, str) {
let escaped = false;
let 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 ' |';
}
});
const 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.
export function rtrim(str, c, invert) {
if (str.length === 0) {
return '';
}
// Length of suffix matching the invert condition.
let suffLen = 0;
// Step left until we fail to match the invert condition.
while (suffLen < str.length) {
const currChar = str.charAt(str.length - suffLen - 1);
if (currChar === c && !invert) {
suffLen++;
} else if (currChar !== c && invert) {
suffLen++;
} else {
break;
}
}
return str.substr(0, str.length - suffLen);
}
export function findClosingBracket(str, b) {
if (str.indexOf(b[1]) === -1) {
return -1;
}
let level = 0;
for (let i = 0; i < str.length; 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;
}
export 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');
}
}