266 lines
8.0 KiB
TypeScript
266 lines
8.0 KiB
TypeScript
import type { MarkedOptions } from './MarkedOptions.ts';
|
|
import type { ResultCallback } from './Instance.ts';
|
|
import type { Rule } from './rules.ts';
|
|
|
|
/**
|
|
* Helpers
|
|
*/
|
|
const escapeTest = /[&<>"']/;
|
|
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');
|
|
const escapeReplacements: {[index: string]: string} = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
};
|
|
const getEscapeReplacement = (ch: string) => escapeReplacements[ch];
|
|
|
|
export function escape(html: string, encode?: boolean) {
|
|
if (encode) {
|
|
if (escapeTest.test(html)) {
|
|
return html.replace(escapeReplace, getEscapeReplacement);
|
|
}
|
|
} else {
|
|
if (escapeTestNoEncode.test(html)) {
|
|
return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
|
|
}
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
const unescapeTest = /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig;
|
|
|
|
export function unescape(html: string) {
|
|
// explicitly match decimal, hex, and named HTML entities
|
|
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 '';
|
|
});
|
|
}
|
|
|
|
const caret = /(^|[^\[])\^/g;
|
|
|
|
export function edit(regex: Rule, opt?: string) {
|
|
regex = typeof regex === 'string' ? regex : regex.source;
|
|
opt = opt || '';
|
|
const obj = {
|
|
replace: (name: string | RegExp, val: string | RegExp) => {
|
|
val = typeof val === 'object' && 'source' in val ? val.source : val;
|
|
val = val.replace(caret, '$1');
|
|
regex = (regex as string).replace(name, val);
|
|
return obj;
|
|
},
|
|
getRegex: () => {
|
|
return new RegExp(regex, opt);
|
|
}
|
|
};
|
|
return obj;
|
|
}
|
|
|
|
const nonWordAndColonTest = /[^\w:]/g;
|
|
const originIndependentUrl = /^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;
|
|
|
|
export function cleanUrl(sanitize: boolean | undefined, base: string | undefined | null, href: string) {
|
|
if (sanitize) {
|
|
let prot;
|
|
try {
|
|
prot = decodeURIComponent(unescape(href))
|
|
.replace(nonWordAndColonTest, '')
|
|
.toLowerCase();
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) {
|
|
return null;
|
|
}
|
|
}
|
|
if (base && !originIndependentUrl.test(href)) {
|
|
href = resolveUrl(base, href);
|
|
}
|
|
try {
|
|
href = encodeURI(href).replace(/%25/g, '%');
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
return href;
|
|
}
|
|
|
|
const baseUrls: Record<string, string> = {};
|
|
const justDomain = /^[^:]+:\/*[^/]*$/;
|
|
const protocol = /^([^:]+:)[\s\S]*$/;
|
|
const domain = /^([^:]+:\/*[^/]*)[\s\S]*$/;
|
|
|
|
export function resolveUrl(base: string, href: string) {
|
|
if (!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 (justDomain.test(base)) {
|
|
baseUrls[' ' + base] = base + '/';
|
|
} else {
|
|
baseUrls[' ' + base] = rtrim(base, '/', true);
|
|
}
|
|
}
|
|
base = baseUrls[' ' + base];
|
|
const relativeBase = base.indexOf(':') === -1;
|
|
|
|
if (href.substring(0, 2) === '//') {
|
|
if (relativeBase) {
|
|
return href;
|
|
}
|
|
return base.replace(protocol, '$1') + href;
|
|
} else if (href.charAt(0) === '/') {
|
|
if (relativeBase) {
|
|
return href;
|
|
}
|
|
return base.replace(domain, '$1') + href;
|
|
} else {
|
|
return base + href;
|
|
}
|
|
}
|
|
|
|
export const noopTest = { exec: () => null };
|
|
|
|
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(/\|/g, (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(/ \|/);
|
|
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();
|
|
}
|
|
|
|
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) {
|
|
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;
|
|
}
|
|
|
|
export function checkDeprecations(opt: MarkedOptions, callback?: ResultCallback) {
|
|
if (!opt || opt.silent) {
|
|
return;
|
|
}
|
|
|
|
if (callback) {
|
|
console.warn('marked(): callback is deprecated since version 5.0.0, should not be used and will be removed in the future. Read more here: https://marked.js.org/using_pro#async');
|
|
}
|
|
|
|
if (opt.sanitize || opt.sanitizer) {
|
|
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');
|
|
}
|
|
|
|
if (opt.highlight || opt.langPrefix !== 'language-') {
|
|
console.warn('marked(): highlight and langPrefix parameters are deprecated since version 5.0.0, should not be used and will be removed in the future. Instead use https://www.npmjs.com/package/marked-highlight.');
|
|
}
|
|
|
|
if (opt.mangle) {
|
|
console.warn('marked(): mangle parameter is enabled by default, but is deprecated since version 5.0.0, and will be removed in the future. To clear this warning, install https://www.npmjs.com/package/marked-mangle, or disable by setting `{mangle: false}`.');
|
|
}
|
|
|
|
if (opt.baseUrl) {
|
|
console.warn('marked(): baseUrl parameter is deprecated since version 5.0.0, should not be used and will be removed in the future. Instead use https://www.npmjs.com/package/marked-base-url.');
|
|
}
|
|
|
|
if (opt.smartypants) {
|
|
console.warn('marked(): smartypants parameter is deprecated since version 5.0.0, should not be used and will be removed in the future. Instead use https://www.npmjs.com/package/marked-smartypants.');
|
|
}
|
|
|
|
if (opt.xhtml) {
|
|
console.warn('marked(): xhtml parameter is deprecated since version 5.0.0, should not be used and will be removed in the future. Instead use https://www.npmjs.com/package/marked-xhtml.');
|
|
}
|
|
|
|
if (opt.headerIds || opt.headerPrefix) {
|
|
console.warn('marked(): headerIds and headerPrefix parameters enabled by default, but are deprecated since version 5.0.0, and will be removed in the future. To clear this warning, install https://www.npmjs.com/package/marked-gfm-heading-id, or disable by setting `{headerIds: false}`.');
|
|
}
|
|
}
|