marked/src/helpers.ts

146 lines
3.4 KiB
TypeScript
Raw Normal View History

2024-11-08 19:56:12 -07:00
import { other } from './rules.ts';
/**
* Helpers
*/
const escapeReplacements: { [index: string]: string } = {
2019-12-04 21:14:51 +01:00
'&': '&',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
2019-12-04 21:14:51 +01:00
};
const getEscapeReplacement = (ch: string) => escapeReplacements[ch];
export function escape(html: string, encode?: boolean) {
if (encode) {
2024-11-08 19:56:12 -07:00
if (other.escapeTest.test(html)) {
return html.replace(other.escapeReplace, getEscapeReplacement);
}
} else {
2024-11-08 19:56:12 -07:00
if (other.escapeTestNoEncode.test(html)) {
return html.replace(other.escapeReplaceNoEncode, getEscapeReplacement);
}
}
return html;
}
2019-12-04 21:14:51 +01:00
export function unescape(html: string) {
// explicitly match decimal, hex, and named HTML entities
2024-11-08 19:56:12 -07:00
return html.replace(other.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 '';
});
}
export function cleanUrl(href: string) {
try {
2024-11-08 19:56:12 -07:00
href = encodeURI(href).replace(other.percentDecode, '%');
} catch {
return null;
}
return href;
}
2019-12-04 21:14:51 +01:00
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
2024-11-08 19:56:12 -07:00
const row = tableRow.replace(other.findPipe, (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 ' |';
}
}),
2024-11-08 19:56:12 -07:00
cells = row.split(other.splitPipe);
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
2024-11-08 19:56:12 -07:00
cells[i] = cells[i].trim().replace(other.slashPipe, '|');
}
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;
}