marked/src/helpers.ts

171 lines
4.2 KiB
TypeScript
Raw Normal View History

/**
* Helpers
*/
2019-12-04 21:14:51 +01:00
const escapeTest = /[&<>"']/;
2022-11-20 10:05:26 -06:00
const escapeReplace = new RegExp(escapeTest.source, 'g');
const escapeTestNoEncode = /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/;
const escapeReplaceNoEncode = new RegExp(escapeTestNoEncode.source, 'g');
2023-08-09 23:33:46 -06:00
const escapeReplacements: {[index: string]: string} = {
2019-12-04 21:14:51 +01:00
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
};
const getEscapeReplacement = (ch: string) => escapeReplacements[ch];
export function escape(html: string, encode?: boolean) {
if (encode) {
2019-12-04 21:14:51 +01:00
if (escapeTest.test(html)) {
return html.replace(escapeReplace, getEscapeReplacement);
}
} else {
2019-12-04 21:14:51 +01:00
if (escapeTestNoEncode.test(html)) {
return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
}
}
return html;
}
2019-12-04 21:14:51 +01:00
const unescapeTest = /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig;
2019-11-06 11:11:06 -06:00
export function unescape(html: string) {
// explicitly match decimal, hex, and named HTML entities
2019-12-04 21:14:51 +01:00
return html.replace(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 '';
});
}
2019-12-04 21:14:51 +01:00
const caret = /(^|[^\[])\^/g;
export function edit(regex: string | RegExp, opt?: string) {
let source = typeof regex === 'string' ? regex : regex.source;
opt = opt || '';
2019-11-07 12:46:32 -06:00
const obj = {
replace: (name: string | RegExp, val: string | RegExp) => {
let valSource = typeof val === 'string' ? val : val.source;
valSource = valSource.replace(caret, '$1');
source = source.replace(name, valSource);
2019-11-07 12:46:32 -06:00
return obj;
},
2019-11-07 12:46:32 -06:00
getRegex: () => {
return new RegExp(source, opt);
}
};
2019-11-07 12:46:32 -06:00
return obj;
}
export function cleanUrl(href: string) {
try {
href = encodeURI(href).replace(/%25/g, '%');
} catch (e) {
return null;
}
return href;
}
2019-12-04 21:14:51 +01:00
export const noopTest = { exec: () => null } as unknown as RegExp;
2023-08-09 23:33:46 -06:00
export function splitCells(tableRow: string, count?: number) {
// ensure that every cell-delimiting pipe has a space
// before it to distinguish it from an escaped pipe
2019-11-07 12:46:32 -06:00
const row = tableRow.replace(/\|/g, (match, offset, str) => {
let escaped = false;
let curr = offset;
2019-11-06 11:11:06 -06:00
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;
// First/last cell in a row cannot be empty if it has no leading/trailing pipe
if (!cells[0].trim()) {
cells.shift();
}
if (cells.length > 0 && !cells[cells.length - 1].trim()) {
cells.pop();
}
2023-08-09 23:33:46 -06:00
if (count) {
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.
*
* @param str
* @param c
* @param invert Remove suffix of non-c chars instead. Default falsey.
*/
export function rtrim(str: string, c: string, invert?: boolean) {
2019-11-06 11:11:06 -06:00
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.
2019-11-06 11:11:06 -06:00
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.slice(0, l - suffLen);
}
export function findClosingBracket(str: string, b: string) {
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;
}