diff --git a/packages/discord.js/src/util/Util.js b/packages/discord.js/src/util/Util.js index 3f68bbedfec4..dc722e13f636 100644 --- a/packages/discord.js/src/util/Util.js +++ b/packages/discord.js/src/util/Util.js @@ -56,15 +56,20 @@ function flatten(obj, ...props) { /** * Options used to escape markdown. * @typedef {Object} EscapeMarkdownOptions - * @property {boolean} [codeBlock=true] Whether to escape code blocks or not - * @property {boolean} [inlineCode=true] Whether to escape inline code or not - * @property {boolean} [bold=true] Whether to escape bolds or not - * @property {boolean} [italic=true] Whether to escape italics or not - * @property {boolean} [underline=true] Whether to escape underlines or not - * @property {boolean} [strikethrough=true] Whether to escape strikethroughs or not - * @property {boolean} [spoiler=true] Whether to escape spoilers or not - * @property {boolean} [codeBlockContent=true] Whether to escape text inside code blocks or not - * @property {boolean} [inlineCodeContent=true] Whether to escape text inside inline code or not + * @property {boolean} [codeBlock=true] Whether to escape code blocks + * @property {boolean} [inlineCode=true] Whether to escape inline code + * @property {boolean} [bold=true] Whether to escape bolds + * @property {boolean} [italic=true] Whether to escape italics + * @property {boolean} [underline=true] Whether to escape underlines + * @property {boolean} [strikethrough=true] Whether to escape strikethroughs + * @property {boolean} [spoiler=true] Whether to escape spoilers + * @property {boolean} [codeBlockContent=true] Whether to escape text inside code blocks + * @property {boolean} [inlineCodeContent=true] Whether to escape text inside inline code + * @property {boolean} [escape=true] Whether to escape escape characters + * @property {boolean} [heading=false] Whether to escape headings + * @property {boolean} [bulletedList=false] Whether to escape bulleted lists + * @property {boolean} [numberedList=false] Whether to escape numbered lists + * @property {boolean} [maskedLink=false] Whether to escape masked links */ /** @@ -85,6 +90,11 @@ function escapeMarkdown( spoiler = true, codeBlockContent = true, inlineCodeContent = true, + escape = true, + heading = false, + bulletedList = false, + numberedList = false, + maskedLink = false, } = {}, ) { if (!codeBlockContent) { @@ -100,6 +110,11 @@ function escapeMarkdown( strikethrough, spoiler, inlineCodeContent, + escape, + heading, + bulletedList, + numberedList, + maskedLink, }); }) .join(codeBlock ? '\\`\\`\\`' : '```'); @@ -116,6 +131,11 @@ function escapeMarkdown( underline, strikethrough, spoiler, + escape, + heading, + bulletedList, + numberedList, + maskedLink, }); }) .join(inlineCode ? '\\`' : '`'); @@ -127,6 +147,11 @@ function escapeMarkdown( if (underline) text = escapeUnderline(text); if (strikethrough) text = escapeStrikethrough(text); if (spoiler) text = escapeSpoiler(text); + if (escape) text = escapeEscape(text); + if (heading) text = escapeHeading(text); + if (bulletedList) text = escapeBulletedList(text); + if (numberedList) text = escapeNumberedList(text); + if (maskedLink) text = escapeMaskedLink(text); return text; } @@ -210,6 +235,51 @@ function escapeSpoiler(text) { return text.replaceAll('||', '\\|\\|'); } +/** + * Escapes escape characters in a string. + * @param {string} text Content to escape + * @returns {string} + */ +function escapeEscape(text) { + return text.replaceAll('\\', '\\\\'); +} + +/** + * Escapes heading characters in a string. + * @param {string} text Content to escape + * @returns {string} + */ +function escapeHeading(text) { + return text.replaceAll(/^( {0,2}[*-] +)?(#{1,3} )/gm, '$1\\$2'); +} + +/** + * Escapes bulleted list characters in a string. + * @param {string} text Content to escape + * @returns {string} + */ +function escapeBulletedList(text) { + return text.replaceAll(/^( *)[*-]( +)/gm, '$1\\-$2'); +} + +/** + * Escapes numbered list characters in a string. + * @param {string} text Content to escape + * @returns {string} + */ +function escapeNumberedList(text) { + return text.replaceAll(/^( *\d+)\./gm, '$1\\.'); +} + +/** + * Escapes masked link characters in a string. + * @param {string} text Content to escape + * @returns {string} + */ +function escapeMaskedLink(text) { + return text.replaceAll(/\[.+\]\(.+\)/gm, '\\$&'); +} + /** * @typedef {Object} FetchRecommendedShardCountOptions * @property {number} [guildsPerShard=1000] Number of guilds assigned per shard diff --git a/packages/discord.js/test/escapeMarkdown.test.js b/packages/discord.js/test/escapeMarkdown.test.js index 316cfdf9625c..246efce19982 100644 --- a/packages/discord.js/test/escapeMarkdown.test.js +++ b/packages/discord.js/test/escapeMarkdown.test.js @@ -5,6 +5,8 @@ const Util = require('../src/util/Util'); const testString = "`_Behold!_`\n||___~~***```js\n`use strict`;\nrequire('discord.js');```***~~___||"; +const testStringForums = + '# Title\n## Subtitle\n### Subsubtitle\n- Bullet list\n - # Title with bullet\n * Subbullet\n1. Number list\n 1. Sub number list'; describe('escapeCodeblock', () => { test('shared', () => { @@ -94,6 +96,48 @@ describe('escapeSpoiler', () => { }); }); +describe('escapeHeading', () => { + test('shared', () => { + expect(Util.escapeHeading(testStringForums)).toEqual( + '\\# Title\n\\## Subtitle\n\\### Subsubtitle\n- Bullet list\n - \\# Title with bullet\n * Subbullet\n1. Number list\n 1. Sub number list', + ); + }); + + test('basic', () => { + expect(Util.escapeHeading('# test')).toEqual('\\# test'); + }); +}); + +describe('escapeBulletedList', () => { + test('shared', () => { + expect(Util.escapeBulletedList(testStringForums)).toEqual( + '# Title\n## Subtitle\n### Subsubtitle\n\\- Bullet list\n \\- # Title with bullet\n \\* Subbullet\n1. Number list\n 1. Sub number list', + ); + }); + + test('basic', () => { + expect(Util.escapeBulletedList('- test')).toEqual('\\- test'); + }); +}); + +describe('escapeNumberedList', () => { + test('shared', () => { + expect(Util.escapeNumberedList(testStringForums)).toEqual( + '# Title\n## Subtitle\n### Subsubtitle\n- Bullet list\n - # Title with bullet\n * Subbullet\n1\\. Number list\n 1\\. Sub number list', + ); + }); + + test('basic', () => { + expect(Util.escapeNumberedList('1. test')).toEqual('1\\. test'); + }); +}); + +describe('escapeMaskedLink', () => { + test('basic', () => { + expect(Util.escapeMaskedLink('[test](https://discord.js.org)')).toEqual('\\[test](https://discord.js.org)'); + }); +}); + describe('escapeMarkdown', () => { test('shared', () => { expect(Util.escapeMarkdown(testString)).toEqual( @@ -176,7 +220,7 @@ describe('escapeMarkdown', () => { ); }); - test('edge-case odd number of fenses with no code block content', () => { + test('edge-case odd number of fences with no code block content', () => { expect( Util.escapeMarkdown('**foo** ```**bar**``` **fizz** ``` **buzz**', { codeBlock: false, diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index f7dae2ba6fa0..5c207c57e18e 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -2737,6 +2737,11 @@ export function escapeItalic(text: string): string; export function escapeUnderline(text: string): string; export function escapeStrikethrough(text: string): string; export function escapeSpoiler(text: string): string; +export function escapeEscape(text: string): string; +export function escapeHeading(text: string): string; +export function escapeBulletedList(text: string): string; +export function escapeNumberedList(text: string): string; +export function escapeMaskedLink(text: string): string; export function cleanCodeBlockContent(text: string): string; export function fetchRecommendedShardCount(token: string, options?: FetchRecommendedShardCountOptions): Promise; export function flatten(obj: unknown, ...props: Record[]): unknown; @@ -4660,8 +4665,13 @@ export interface EscapeMarkdownOptions { underline?: boolean; strikethrough?: boolean; spoiler?: boolean; - inlineCodeContent?: boolean; codeBlockContent?: boolean; + inlineCodeContent?: boolean; + escape?: boolean; + heading?: boolean; + bulletedList?: boolean; + numberedList?: boolean; + maskedLink?: boolean; } export interface FetchApplicationCommandOptions extends BaseFetchOptions {