feat(Util): escape more markdown characters (#8701)

* feat(Util): escape more markdown characters

Signed-off-by: RedGuy12 <61329810+RedGuy12@users.noreply.github.com>

* types(EscapeMarkdownOptions): update types

Signed-off-by: RedGuy12 <61329810+RedGuy12@users.noreply.github.com>

* fix: lists bulleted with `*`

Signed-off-by: RedGuy12 <61329810+RedGuy12@users.noreply.github.com>

* tests(escapeMarkdown): add tests

Signed-off-by: RedGuy12 <61329810+RedGuy12@users.noreply.github.com>

Signed-off-by: RedGuy12 <61329810+RedGuy12@users.noreply.github.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
RedGuy12
2022-10-10 00:23:05 -05:00
committed by GitHub
parent 3ed668e539
commit 7b8966bca1
3 changed files with 135 additions and 11 deletions

View File

@@ -56,15 +56,20 @@ function flatten(obj, ...props) {
/** /**
* Options used to escape markdown. * Options used to escape markdown.
* @typedef {Object} EscapeMarkdownOptions * @typedef {Object} EscapeMarkdownOptions
* @property {boolean} [codeBlock=true] Whether to escape code blocks or not * @property {boolean} [codeBlock=true] Whether to escape code blocks
* @property {boolean} [inlineCode=true] Whether to escape inline code or not * @property {boolean} [inlineCode=true] Whether to escape inline code
* @property {boolean} [bold=true] Whether to escape bolds or not * @property {boolean} [bold=true] Whether to escape bolds
* @property {boolean} [italic=true] Whether to escape italics or not * @property {boolean} [italic=true] Whether to escape italics
* @property {boolean} [underline=true] Whether to escape underlines or not * @property {boolean} [underline=true] Whether to escape underlines
* @property {boolean} [strikethrough=true] Whether to escape strikethroughs or not * @property {boolean} [strikethrough=true] Whether to escape strikethroughs
* @property {boolean} [spoiler=true] Whether to escape spoilers or not * @property {boolean} [spoiler=true] Whether to escape spoilers
* @property {boolean} [codeBlockContent=true] Whether to escape text inside code blocks or not * @property {boolean} [codeBlockContent=true] Whether to escape text inside code blocks
* @property {boolean} [inlineCodeContent=true] Whether to escape text inside inline code or not * @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, spoiler = true,
codeBlockContent = true, codeBlockContent = true,
inlineCodeContent = true, inlineCodeContent = true,
escape = true,
heading = false,
bulletedList = false,
numberedList = false,
maskedLink = false,
} = {}, } = {},
) { ) {
if (!codeBlockContent) { if (!codeBlockContent) {
@@ -100,6 +110,11 @@ function escapeMarkdown(
strikethrough, strikethrough,
spoiler, spoiler,
inlineCodeContent, inlineCodeContent,
escape,
heading,
bulletedList,
numberedList,
maskedLink,
}); });
}) })
.join(codeBlock ? '\\`\\`\\`' : '```'); .join(codeBlock ? '\\`\\`\\`' : '```');
@@ -116,6 +131,11 @@ function escapeMarkdown(
underline, underline,
strikethrough, strikethrough,
spoiler, spoiler,
escape,
heading,
bulletedList,
numberedList,
maskedLink,
}); });
}) })
.join(inlineCode ? '\\`' : '`'); .join(inlineCode ? '\\`' : '`');
@@ -127,6 +147,11 @@ function escapeMarkdown(
if (underline) text = escapeUnderline(text); if (underline) text = escapeUnderline(text);
if (strikethrough) text = escapeStrikethrough(text); if (strikethrough) text = escapeStrikethrough(text);
if (spoiler) text = escapeSpoiler(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; return text;
} }
@@ -210,6 +235,51 @@ function escapeSpoiler(text) {
return text.replaceAll('||', '\\|\\|'); 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 * @typedef {Object} FetchRecommendedShardCountOptions
* @property {number} [guildsPerShard=1000] Number of guilds assigned per shard * @property {number} [guildsPerShard=1000] Number of guilds assigned per shard

View File

@@ -5,6 +5,8 @@
const Util = require('../src/util/Util'); const Util = require('../src/util/Util');
const testString = "`_Behold!_`\n||___~~***```js\n`use strict`;\nrequire('discord.js');```***~~___||"; 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', () => { describe('escapeCodeblock', () => {
test('shared', () => { 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', () => { describe('escapeMarkdown', () => {
test('shared', () => { test('shared', () => {
expect(Util.escapeMarkdown(testString)).toEqual( 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( expect(
Util.escapeMarkdown('**foo** ```**bar**``` **fizz** ``` **buzz**', { Util.escapeMarkdown('**foo** ```**bar**``` **fizz** ``` **buzz**', {
codeBlock: false, codeBlock: false,

View File

@@ -2737,6 +2737,11 @@ export function escapeItalic(text: string): string;
export function escapeUnderline(text: string): string; export function escapeUnderline(text: string): string;
export function escapeStrikethrough(text: string): string; export function escapeStrikethrough(text: string): string;
export function escapeSpoiler(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 cleanCodeBlockContent(text: string): string;
export function fetchRecommendedShardCount(token: string, options?: FetchRecommendedShardCountOptions): Promise<number>; export function fetchRecommendedShardCount(token: string, options?: FetchRecommendedShardCountOptions): Promise<number>;
export function flatten(obj: unknown, ...props: Record<string, boolean | string>[]): unknown; export function flatten(obj: unknown, ...props: Record<string, boolean | string>[]): unknown;
@@ -4660,8 +4665,13 @@ export interface EscapeMarkdownOptions {
underline?: boolean; underline?: boolean;
strikethrough?: boolean; strikethrough?: boolean;
spoiler?: boolean; spoiler?: boolean;
inlineCodeContent?: boolean;
codeBlockContent?: boolean; codeBlockContent?: boolean;
inlineCodeContent?: boolean;
escape?: boolean;
heading?: boolean;
bulletedList?: boolean;
numberedList?: boolean;
maskedLink?: boolean;
} }
export interface FetchApplicationCommandOptions extends BaseFetchOptions { export interface FetchApplicationCommandOptions extends BaseFetchOptions {