marked/src/helpers.ts
Spencer Whitehead 1f579f7628
fix: Remove unused plus typescript tightening (#3527)
* chore: remove unused build file

* chore: remove unused package

* chore: remove unused function

* chore: remove unnecessary | undefineds

* core: replace unnecessary &&s with optional chaining

* chore: use .at(-x) instead of .length - x property access

gives stricter TS typing, is more concise

* chore: tighten TS types

* chore: sort tokens alphabetically

* fix: typeof plus !== null check

* chore: type test for .parse, .use

* fix: if check
2024-11-17 21:53:28 -07:00

146 lines
3.4 KiB
TypeScript

import { other } from './rules.ts';
/**
* Helpers
*/
const escapeReplacements: { [index: string]: string } = {
'&': '&',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
};
const getEscapeReplacement = (ch: string) => escapeReplacements[ch];
export function escape(html: string, encode?: boolean) {
if (encode) {
if (other.escapeTest.test(html)) {
return html.replace(other.escapeReplace, getEscapeReplacement);
}
} else {
if (other.escapeTestNoEncode.test(html)) {
return html.replace(other.escapeReplaceNoEncode, getEscapeReplacement);
}
}
return html;
}
export function unescape(html: string) {
// explicitly match decimal, hex, and named HTML entities
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 {
href = encodeURI(href).replace(other.percentDecode, '%');
} catch {
return null;
}
return href;
}
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
const row = tableRow.replace(other.findPipe, (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 ' |';
}
}),
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.at(-1)?.trim()) {
cells.pop();
}
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(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) {
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.
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;
}