2024-11-08 19:56:12 -07:00
|
|
|
import { other } from './rules.ts';
|
|
|
|
|
2019-11-05 15:29:42 -06:00
|
|
|
/**
|
|
|
|
* Helpers
|
|
|
|
*/
|
2024-07-14 18:54:46 -06:00
|
|
|
const escapeReplacements: { [index: string]: string } = {
|
2019-12-04 21:14:51 +01:00
|
|
|
'&': '&',
|
|
|
|
'<': '<',
|
|
|
|
'>': '>',
|
|
|
|
'"': '"',
|
2024-07-14 18:54:46 -06:00
|
|
|
"'": ''',
|
2019-12-04 21:14:51 +01:00
|
|
|
};
|
2023-07-29 08:31:34 +02:00
|
|
|
const getEscapeReplacement = (ch: string) => escapeReplacements[ch];
|
|
|
|
|
|
|
|
export function escape(html: string, encode?: boolean) {
|
2019-11-05 15:29:42 -06:00
|
|
|
if (encode) {
|
2024-11-08 19:56:12 -07:00
|
|
|
if (other.escapeTest.test(html)) {
|
|
|
|
return html.replace(other.escapeReplace, getEscapeReplacement);
|
2019-11-05 15:29:42 -06:00
|
|
|
}
|
|
|
|
} else {
|
2024-11-08 19:56:12 -07:00
|
|
|
if (other.escapeTestNoEncode.test(html)) {
|
|
|
|
return html.replace(other.escapeReplaceNoEncode, getEscapeReplacement);
|
2019-11-05 15:29:42 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return html;
|
|
|
|
}
|
2019-12-04 21:14:51 +01:00
|
|
|
|
2023-07-29 08:31:34 +02:00
|
|
|
export function unescape(html: string) {
|
2019-11-05 15:29:42 -06:00
|
|
|
// explicitly match decimal, hex, and named HTML entities
|
2024-11-08 19:56:12 -07:00
|
|
|
return html.replace(other.unescapeTest, (_, n) => {
|
2019-11-05 15:29:42 -06:00
|
|
|
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 '';
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-09-02 22:02:24 -06:00
|
|
|
export function cleanUrl(href: string) {
|
2019-11-05 15:29:42 -06:00
|
|
|
try {
|
2024-11-08 19:56:12 -07:00
|
|
|
href = encodeURI(href).replace(other.percentDecode, '%');
|
2024-07-14 00:38:17 -06:00
|
|
|
} catch {
|
2019-11-05 15:29:42 -06:00
|
|
|
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) {
|
2019-11-05 15:29:42 -06:00
|
|
|
// 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) => {
|
2023-08-19 16:55:56 -06:00
|
|
|
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);
|
2019-11-05 15:29:42 -06:00
|
|
|
let i = 0;
|
|
|
|
|
2021-08-02 15:12:43 -04:00
|
|
|
// First/last cell in a row cannot be empty if it has no leading/trailing pipe
|
2023-07-29 08:31:34 +02:00
|
|
|
if (!cells[0].trim()) {
|
|
|
|
cells.shift();
|
|
|
|
}
|
2024-11-17 23:53:28 -05:00
|
|
|
if (cells.length > 0 && !cells.at(-1)?.trim()) {
|
2023-07-29 08:31:34 +02:00
|
|
|
cells.pop();
|
|
|
|
}
|
2021-08-02 15:12:43 -04:00
|
|
|
|
2023-08-09 23:33:46 -06:00
|
|
|
if (count) {
|
|
|
|
if (cells.length > count) {
|
|
|
|
cells.splice(count);
|
|
|
|
} else {
|
|
|
|
while (cells.length < count) cells.push('');
|
|
|
|
}
|
2019-11-05 15:29:42 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
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, '|');
|
2019-11-05 15:29:42 -06:00
|
|
|
}
|
|
|
|
return cells;
|
|
|
|
}
|
|
|
|
|
2022-03-30 17:42:54 +02:00
|
|
|
/**
|
|
|
|
* Remove trailing 'c's. Equivalent to str.replace(/c*$/, '').
|
|
|
|
* /c*$/ is vulnerable to REDOS.
|
|
|
|
*
|
2023-07-29 08:31:34 +02:00
|
|
|
* @param str
|
|
|
|
* @param c
|
|
|
|
* @param invert Remove suffix of non-c chars instead. Default falsey.
|
2022-03-30 17:42:54 +02:00
|
|
|
*/
|
2023-07-29 08:31:34 +02:00
|
|
|
export function rtrim(str: string, c: string, invert?: boolean) {
|
2019-11-06 11:11:06 -06:00
|
|
|
const l = str.length;
|
|
|
|
if (l === 0) {
|
2019-11-05 15:29:42 -06:00
|
|
|
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);
|
2019-11-05 15:29:42 -06:00
|
|
|
if (currChar === c && !invert) {
|
|
|
|
suffLen++;
|
|
|
|
} else if (currChar !== c && invert) {
|
|
|
|
suffLen++;
|
|
|
|
} else {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-19 06:11:14 +01:00
|
|
|
return str.slice(0, l - suffLen);
|
2019-11-05 15:29:42 -06:00
|
|
|
}
|
|
|
|
|
2023-07-29 08:31:34 +02:00
|
|
|
export function findClosingBracket(str: string, b: string) {
|
2019-11-05 15:29:42 -06:00
|
|
|
if (str.indexOf(b[1]) === -1) {
|
|
|
|
return -1;
|
|
|
|
}
|
2023-08-19 16:55:56 -06:00
|
|
|
|
|
|
|
let level = 0;
|
|
|
|
for (let i = 0; i < str.length; i++) {
|
2019-11-05 15:29:42 -06:00
|
|
|
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;
|
|
|
|
}
|