mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-10 00:23:30 +01:00
* fix(formatters): don't escape * in links * add test for * -> * in url * `\S+` -> `\S*` (tested locally) * @Qjuh: handle italics both inside+outside <url>s * more accurate <link> matcher --------- Co-authored-by: Qjuh <76154676+Qjuh@users.noreply.github.com>
360 lines
7.3 KiB
TypeScript
360 lines
7.3 KiB
TypeScript
/* eslint-disable prefer-named-capture-group */
|
|
|
|
/**
|
|
* The options that affect what will be escaped.
|
|
*/
|
|
export interface EscapeMarkdownOptions {
|
|
/**
|
|
* Whether to escape block quotes.
|
|
*
|
|
* @defaultValue `true`
|
|
*/
|
|
blockQuote?: boolean;
|
|
|
|
/**
|
|
* Whether to escape bold text.
|
|
*
|
|
* @defaultValue `true`
|
|
*/
|
|
bold?: boolean;
|
|
|
|
/**
|
|
* Whether to escape bulleted lists.
|
|
*
|
|
* @defaultValue `true`
|
|
*/
|
|
bulletedList?: boolean;
|
|
|
|
/**
|
|
* Whether to escape code blocks.
|
|
*
|
|
* @defaultValue `true`
|
|
*/
|
|
codeBlock?: boolean;
|
|
|
|
/**
|
|
* Whether to escape text inside code blocks.
|
|
*
|
|
* @defaultValue `true`
|
|
*/
|
|
codeBlockContent?: boolean;
|
|
|
|
/**
|
|
* Whether to escape `\`.
|
|
*
|
|
* @defaultValue `true`
|
|
*/
|
|
escape?: boolean;
|
|
|
|
/**
|
|
* Whether to escape headings.
|
|
*
|
|
* @defaultValue `true`
|
|
*/
|
|
heading?: boolean;
|
|
|
|
/**
|
|
* Whether to escape inline code.
|
|
*
|
|
* @defaultValue `true`
|
|
*/
|
|
inlineCode?: boolean;
|
|
|
|
/**
|
|
* Whether to escape text inside inline code.
|
|
*
|
|
* @defaultValue `true`
|
|
*/
|
|
inlineCodeContent?: boolean;
|
|
/**
|
|
* Whether to escape italics.
|
|
*
|
|
* @defaultValue `true`
|
|
*/
|
|
italic?: boolean;
|
|
|
|
/**
|
|
* Whether to escape masked links.
|
|
*
|
|
* @defaultValue `true`
|
|
*/
|
|
maskedLink?: boolean;
|
|
|
|
/**
|
|
* Whether to escape numbered lists.
|
|
*
|
|
* @defaultValue `true`
|
|
*/
|
|
numberedList?: boolean;
|
|
|
|
/**
|
|
* Whether to escape block quotes.
|
|
*
|
|
* @defaultValue `true`
|
|
*/
|
|
quote?: boolean;
|
|
|
|
/**
|
|
* Whether to escape spoilers.
|
|
*
|
|
* @defaultValue `true`
|
|
*/
|
|
spoiler?: boolean;
|
|
|
|
/**
|
|
* Whether to escape strikethroughs.
|
|
*
|
|
* @defaultValue `true`
|
|
*/
|
|
strikethrough?: boolean;
|
|
|
|
/**
|
|
* Whether to escape underlines.
|
|
*
|
|
* @defaultValue `true`
|
|
*/
|
|
underline?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Escapes any Discord-flavored markdown in a string.
|
|
*
|
|
* @param text - Content to escape
|
|
* @param options - Options for escaping the markdown
|
|
*/
|
|
export function escapeMarkdown(text: string, options: EscapeMarkdownOptions = {}): string {
|
|
const {
|
|
codeBlock = true,
|
|
inlineCode = true,
|
|
bold = true,
|
|
italic = true,
|
|
underline = true,
|
|
strikethrough = true,
|
|
spoiler = true,
|
|
codeBlockContent = true,
|
|
inlineCodeContent = true,
|
|
escape = true,
|
|
heading = true,
|
|
bulletedList = true,
|
|
numberedList = true,
|
|
maskedLink = true,
|
|
blockQuote = true,
|
|
quote = true,
|
|
} = options;
|
|
|
|
if (!codeBlockContent) {
|
|
return text
|
|
.split('```')
|
|
.map((subString, index, array) => {
|
|
if (index % 2 && index !== array.length - 1) return subString;
|
|
return escapeMarkdown(subString, {
|
|
inlineCode,
|
|
bold,
|
|
italic,
|
|
underline,
|
|
strikethrough,
|
|
spoiler,
|
|
inlineCodeContent,
|
|
escape,
|
|
heading,
|
|
bulletedList,
|
|
numberedList,
|
|
maskedLink,
|
|
blockQuote,
|
|
quote,
|
|
});
|
|
})
|
|
.join(codeBlock ? '\\`\\`\\`' : '```');
|
|
}
|
|
|
|
if (!inlineCodeContent) {
|
|
return text
|
|
.split(/(?<=^|[^`])`(?=[^`]|$)/g)
|
|
.map((subString, index, array) => {
|
|
if (index % 2 && index !== array.length - 1) return subString;
|
|
return escapeMarkdown(subString, {
|
|
codeBlock,
|
|
bold,
|
|
italic,
|
|
underline,
|
|
strikethrough,
|
|
spoiler,
|
|
escape,
|
|
heading,
|
|
bulletedList,
|
|
numberedList,
|
|
maskedLink,
|
|
blockQuote,
|
|
quote,
|
|
});
|
|
})
|
|
.join(inlineCode ? '\\`' : '`');
|
|
}
|
|
|
|
let res = text;
|
|
if (escape) res = escapeEscape(res);
|
|
if (inlineCode) res = escapeInlineCode(res);
|
|
if (codeBlock) res = escapeCodeBlock(res);
|
|
if (italic) res = escapeItalic(res);
|
|
if (bold) res = escapeBold(res);
|
|
if (underline) res = escapeUnderline(res);
|
|
if (strikethrough) res = escapeStrikethrough(res);
|
|
if (spoiler) res = escapeSpoiler(res);
|
|
if (heading) res = escapeHeading(res);
|
|
if (bulletedList) res = escapeBulletedList(res);
|
|
if (numberedList) res = escapeNumberedList(res);
|
|
if (maskedLink) res = escapeMaskedLink(res);
|
|
if (quote) res = escapeQuote(res);
|
|
if (blockQuote) res = escapeBlockQuote(res);
|
|
return res;
|
|
}
|
|
|
|
/**
|
|
* Escapes code block markdown in a string.
|
|
*
|
|
* @param text - Content to escape
|
|
*/
|
|
export function escapeCodeBlock(text: string): string {
|
|
return text.replaceAll('```', '\\`\\`\\`');
|
|
}
|
|
|
|
/**
|
|
* Escapes inline code markdown in a string.
|
|
*
|
|
* @param text - Content to escape
|
|
*/
|
|
export function escapeInlineCode(text: string): string {
|
|
return text.replaceAll(/(?<=^|[^`])``?(?=[^`]|$)/g, (match) => (match.length === 2 ? '\\`\\`' : '\\`'));
|
|
}
|
|
|
|
/**
|
|
* Escapes italic markdown in a string.
|
|
*
|
|
* @param text - Content to escape
|
|
*/
|
|
export function escapeItalic(text: string): string {
|
|
let idx = 0;
|
|
const newText = text.replaceAll(
|
|
/(?<=^|[^*])(?<!(?<!<)https?:\/\/\S*|<[^\s:]+:\/[^\s>]*)\*([^*]|\*\*|$)/g,
|
|
(_, match) => {
|
|
if (match === '**') return ++idx % 2 ? `\\*${match}` : `${match}\\*`;
|
|
return `\\*${match}`;
|
|
},
|
|
);
|
|
idx = 0;
|
|
return newText.replaceAll(
|
|
/(?<=^|[^_])(?<!<a?:.+|(?<!<)https?:\/\/\S*|<[^\s:]:\/[^\s>]*)_(?!:\d+>)([^_]|__|$)/g,
|
|
(_, match) => {
|
|
if (match === '__') return ++idx % 2 ? `\\_${match}` : `${match}\\_`;
|
|
return `\\_${match}`;
|
|
},
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Escapes bold markdown in a string.
|
|
*
|
|
* @param text - Content to escape
|
|
*/
|
|
export function escapeBold(text: string): string {
|
|
let idx = 0;
|
|
return text.replaceAll(/\*\*(\*)?/g, (_, match) => {
|
|
if (match) return ++idx % 2 ? `${match}\\*\\*` : `\\*\\*${match}`;
|
|
return '\\*\\*';
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Escapes underline markdown in a string.
|
|
*
|
|
* @param text - Content to escape
|
|
*/
|
|
export function escapeUnderline(text: string): string {
|
|
let idx = 0;
|
|
return text.replaceAll(/(?<!<a?:.+|https?:\/\/\S+)__(_)?(?!:\d+>)/g, (_, match) => {
|
|
if (match) return ++idx % 2 ? `${match}\\_\\_` : `\\_\\_${match}`;
|
|
return '\\_\\_';
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Escapes strikethrough markdown in a string.
|
|
*
|
|
* @param text - Content to escape
|
|
*/
|
|
export function escapeStrikethrough(text: string): string {
|
|
return text.replaceAll('~~', '\\~\\~');
|
|
}
|
|
|
|
/**
|
|
* Escapes spoiler markdown in a string.
|
|
*
|
|
* @param text - Content to escape
|
|
*/
|
|
export function escapeSpoiler(text: string): string {
|
|
return text.replaceAll('||', '\\|\\|');
|
|
}
|
|
|
|
/**
|
|
* Escapes escape characters in a string.
|
|
*
|
|
* @param text - Content to escape
|
|
*/
|
|
export function escapeEscape(text: string): string {
|
|
return text.replaceAll('\\', '\\\\');
|
|
}
|
|
|
|
/**
|
|
* Escapes heading characters in a string.
|
|
*
|
|
* @param text - Content to escape
|
|
*/
|
|
export function escapeHeading(text: string): string {
|
|
return text.replaceAll(/^( {0,2})([*-] )?( *)(#{1,3} )/gm, '$1$2$3\\$4');
|
|
}
|
|
|
|
/**
|
|
* Escapes bulleted list characters in a string.
|
|
*
|
|
* @param text - Content to escape
|
|
*/
|
|
export function escapeBulletedList(text: string): string {
|
|
return text.replaceAll(/^( *)([*-])( +)/gm, '$1\\$2$3');
|
|
}
|
|
|
|
/**
|
|
* Escapes numbered list characters in a string.
|
|
*
|
|
* @param text - Content to escape
|
|
*/
|
|
export function escapeNumberedList(text: string): string {
|
|
return text.replaceAll(/^( *\d+)\./gm, '$1\\.');
|
|
}
|
|
|
|
/**
|
|
* Escapes masked link characters in a string.
|
|
*
|
|
* @param text - Content to escape
|
|
*/
|
|
export function escapeMaskedLink(text: string): string {
|
|
return text.replaceAll(/\[.+]\(.+\)/gm, '\\$&');
|
|
}
|
|
|
|
/**
|
|
* Escapes quote characters in a string.
|
|
*
|
|
* @param text - Content to escape
|
|
*/
|
|
export function escapeQuote(text: string): string {
|
|
return text.replaceAll(/^(\s*)>(\s+)/gm, '$1\\>$2');
|
|
}
|
|
|
|
/**
|
|
* Escapes block quote characters in a string.
|
|
*
|
|
* @param text - Content to escape
|
|
*/
|
|
export function escapeBlockQuote(text: string): string {
|
|
return text.replaceAll(/^(\s*)>>>(\s+)/gm, '$1\\>>>$2');
|
|
}
|