mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-09 16:13:31 +01:00
refactor(Util.escapeMarkdown): allow separate escaping and add tests (#3241)
* wip refactor * add escapeMarkdown tests * italics can be done with a single underscore too * more refined * fix test name * unnecessary eslint ignores * use jest * make eslint less annoying in this test file * more testing * fix lib usage * more tests and a small fix
This commit is contained in:
@@ -57,6 +57,7 @@
|
||||
"@types/ws": "^6.0.1",
|
||||
"discord.js-docgen": "discordjs/docgen",
|
||||
"eslint": "^5.13.0",
|
||||
"jest": "^24.7.1",
|
||||
"json-filter-loader": "^1.0.0",
|
||||
"terser-webpack-plugin": "^1.2.2",
|
||||
"tslint": "^5.12.1",
|
||||
|
||||
@@ -93,7 +93,7 @@ class APIMessage {
|
||||
if (content || mentionPart) {
|
||||
if (isCode) {
|
||||
const codeName = typeof this.options.code === 'string' ? this.options.code : '';
|
||||
content = `${mentionPart}\`\`\`${codeName}\n${Util.escapeMarkdown(content || '', true)}\n\`\`\``;
|
||||
content = `${mentionPart}\`\`\`${codeName}\n${Util.cleanCodeBlockContent(content || '')}\n\`\`\``;
|
||||
if (isSplit) {
|
||||
splitOptions.prepend = `${splitOptions.prepend || ''}\`\`\`${codeName}\n`;
|
||||
splitOptions.append = `\n\`\`\`${splitOptions.append || ''}`;
|
||||
|
||||
151
src/util/Util.js
151
src/util/Util.js
@@ -75,14 +75,144 @@ class Util {
|
||||
/**
|
||||
* Escapes any Discord-flavour markdown in a string.
|
||||
* @param {string} text Content to escape
|
||||
* @param {boolean} [onlyCodeBlock=false] Whether to only escape codeblocks (takes priority)
|
||||
* @param {boolean} [onlyInlineCode=false] Whether to only escape inline code
|
||||
* @param {Object} [options={}] What types of markdown to escape
|
||||
* @param {boolean} [options.codeBlock=true] Whether to escape code blocks or not
|
||||
* @param {boolean} [options.inlineCode=true] Whether to escape inline code or not
|
||||
* @param {boolean} [options.bold=true] Whether to escape bolds or not
|
||||
* @param {boolean} [options.italic=true] Whether to escape italics or not
|
||||
* @param {boolean} [options.underline=true] Whether to escape underlines or not
|
||||
* @param {boolean} [options.strikethrough=true] Whether to escape strikethroughs or not
|
||||
* @param {boolean} [options.spoiler=true] Whether to escape spoilers or not
|
||||
* @param {boolean} [options.codeBlockContent=true] Whether to escape text inside code blocks or not
|
||||
* @param {boolean} [options.inlineCodeContent=true] Whether to escape text inside inline code or not
|
||||
* @returns {string}
|
||||
*/
|
||||
static escapeMarkdown(text, onlyCodeBlock = false, onlyInlineCode = false) {
|
||||
if (onlyCodeBlock) return text.replace(/```/g, '`\u200b``');
|
||||
if (onlyInlineCode) return text.replace(/\\(`|\\)/g, '$1').replace(/(`|\\)/g, '\\$1');
|
||||
return text.replace(/\\(\*|_|`|~|\\)/g, '$1').replace(/(\*|_|`|~|\\)/g, '\\$1');
|
||||
static escapeMarkdown(text, {
|
||||
codeBlock = true,
|
||||
inlineCode = true,
|
||||
bold = true,
|
||||
italic = true,
|
||||
underline = true,
|
||||
strikethrough = true,
|
||||
spoiler = true,
|
||||
codeBlockContent = true,
|
||||
inlineCodeContent = true,
|
||||
} = {}) {
|
||||
if (!codeBlockContent) {
|
||||
return text.split('```').map((subString, index, array) => {
|
||||
if ((index % 2) && index !== array.length - 1) return subString;
|
||||
return Util.escapeMarkdown(subString, {
|
||||
inlineCode,
|
||||
bold,
|
||||
italic,
|
||||
underline,
|
||||
strikethrough,
|
||||
spoiler,
|
||||
inlineCodeContent,
|
||||
});
|
||||
}).join(codeBlock ? '\\`\\`\\`' : '```');
|
||||
}
|
||||
if (!inlineCodeContent) {
|
||||
return text.split(/(?<=^|[^`])`(?=[^`]|$)/g).map((subString, index, array) => {
|
||||
if ((index % 2) && index !== array.length - 1) return subString;
|
||||
return Util.escapeMarkdown(subString, {
|
||||
codeBlock,
|
||||
bold,
|
||||
italic,
|
||||
underline,
|
||||
strikethrough,
|
||||
spoiler,
|
||||
});
|
||||
}).join(inlineCode ? '\\`' : '`');
|
||||
}
|
||||
if (inlineCode) text = Util.escapeInlineCode(text);
|
||||
if (codeBlock) text = Util.escapeCodeBlock(text);
|
||||
if (italic) text = Util.escapeItalic(text);
|
||||
if (bold) text = Util.escapeBold(text);
|
||||
if (underline) text = Util.escapeUnderline(text);
|
||||
if (strikethrough) text = Util.escapeStrikethrough(text);
|
||||
if (spoiler) text = Util.escapeSpoiler(text);
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes code block markdown in a string.
|
||||
* @param {string} text Content to escape
|
||||
* @returns {string}
|
||||
*/
|
||||
static escapeCodeBlock(text) {
|
||||
return text.replace(/```/g, '\\`\\`\\`');
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes inline code markdown in a string.
|
||||
* @param {string} text Content to escape
|
||||
* @returns {string}
|
||||
*/
|
||||
static escapeInlineCode(text) {
|
||||
return text.replace(/(?<=^|[^`])`(?=[^`]|$)/g, '\\`');
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes italic markdown in a string.
|
||||
* @param {string} text Content to escape
|
||||
* @returns {string}
|
||||
*/
|
||||
static escapeItalic(text) {
|
||||
let i = 0;
|
||||
text = text.replace(/(?<=^|[^*])\*([^*]|\*\*|$)/g, (_, match) => {
|
||||
if (match === '**') return ++i % 2 ? `\\*${match}` : `${match}\\*`;
|
||||
return `\\*${match}`;
|
||||
});
|
||||
i = 0;
|
||||
return text.replace(/(?<=^|[^_])_([^_]|__|$)/g, (_, match) => {
|
||||
if (match === '__') return ++i % 2 ? `\\_${match}` : `${match}\\_`;
|
||||
return `\\_${match}`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes bold markdown in a string.
|
||||
* @param {string} text Content to escape
|
||||
* @returns {string}
|
||||
*/
|
||||
static escapeBold(text) {
|
||||
let i = 0;
|
||||
return text.replace(/\*\*(\*)?/g, (_, match) => {
|
||||
if (match) return ++i % 2 ? `${match}\\*\\*` : `\\*\\*${match}`;
|
||||
return '\\*\\*';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes underline markdown in a string.
|
||||
* @param {string} text Content to escape
|
||||
* @returns {string}
|
||||
*/
|
||||
static escapeUnderline(text) {
|
||||
let i = 0;
|
||||
return text.replace(/__(_)?/g, (_, match) => {
|
||||
if (match) return ++i % 2 ? `${match}\\_\\_` : `\\_\\_${match}`;
|
||||
return '\\_\\_';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes strikethrough markdown in a string.
|
||||
* @param {string} text Content to escape
|
||||
* @returns {string}
|
||||
*/
|
||||
static escapeStrikethrough(text) {
|
||||
return text.replace(/~~/g, '\\~\\~');
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes spoiler markdown in a string.
|
||||
* @param {string} text Content to escape
|
||||
* @returns {string}
|
||||
*/
|
||||
static escapeSpoiler(text) {
|
||||
return text.replace(/\|\|/g, '\\|\\|');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -421,6 +551,15 @@ class Util {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The content to put in a codeblock with all codeblock fences replaced by the equivalent backticks.
|
||||
* @param {string} text The string to be converted
|
||||
* @returns {string}
|
||||
*/
|
||||
static cleanCodeBlockContent(text) {
|
||||
return text.replace('```', '`\u200b``');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Promise that resolves after a specified duration.
|
||||
* @param {number} ms How long to wait before resolving (in milliseconds)
|
||||
|
||||
184
test/escapeMarkdown.test.js
Normal file
184
test/escapeMarkdown.test.js
Normal file
@@ -0,0 +1,184 @@
|
||||
'use strict';
|
||||
|
||||
/* eslint-disable max-len, no-undef */
|
||||
|
||||
const Util = require('../src/util/Util');
|
||||
const testString = '`_Behold!_`\n||___~~***```js\n`use strict`;\nrequire(\'discord.js\');```***~~___||';
|
||||
|
||||
describe('escapeCodeblock', () => {
|
||||
test('shared', () => {
|
||||
expect(Util.escapeCodeBlock(testString))
|
||||
.toBe('`_Behold!_`\n||___~~***\\`\\`\\`js\n`use strict`;\nrequire(\'discord.js\');\\`\\`\\`***~~___||');
|
||||
});
|
||||
|
||||
test('basic', () => {
|
||||
expect(Util.escapeCodeBlock('```test```'))
|
||||
.toBe('\\`\\`\\`test\\`\\`\\`');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('escapeInlineCode', () => {
|
||||
test('shared', () => {
|
||||
expect(Util.escapeInlineCode(testString))
|
||||
.toBe('\\`_Behold!_\\`\n||___~~***```js\n\\`use strict\\`;\nrequire(\'discord.js\');```***~~___||');
|
||||
});
|
||||
|
||||
test('basic', () => {
|
||||
expect(Util.escapeInlineCode('`test`'))
|
||||
.toBe('\\`test\\`');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('escapeBold', () => {
|
||||
test('shared', () => {
|
||||
expect(Util.escapeBold(testString))
|
||||
.toBe('`_Behold!_`\n||___~~*\\*\\*```js\n`use strict`;\nrequire(\'discord.js\');```\\*\\**~~___||');
|
||||
});
|
||||
|
||||
test('basic', () => {
|
||||
expect(Util.escapeBold('**test**'))
|
||||
.toBe('\\*\\*test\\*\\*');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('escapeItalic', () => {
|
||||
test('shared', () => {
|
||||
expect(Util.escapeItalic(testString))
|
||||
.toBe('`\\_Behold!\\_`\n||\\___~~\\***```js\n`use strict`;\nrequire(\'discord.js\');```**\\*~~__\\_||');
|
||||
});
|
||||
|
||||
test('basic (_)', () => {
|
||||
expect(Util.escapeItalic('_test_'))
|
||||
.toBe('\\_test\\_');
|
||||
});
|
||||
|
||||
test('basic (*)', () => {
|
||||
expect(Util.escapeItalic('*test*'))
|
||||
.toBe('\\*test\\*');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('escapeUnderline', () => {
|
||||
test('shared', () => {
|
||||
expect(Util.escapeUnderline(testString))
|
||||
.toBe('`_Behold!_`\n||_\\_\\_~~***```js\n`use strict`;\nrequire(\'discord.js\');```***~~\\_\\__||');
|
||||
});
|
||||
|
||||
test('basic', () => {
|
||||
expect(Util.escapeUnderline('__test__'))
|
||||
.toBe('\\_\\_test\\_\\_');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('escapeStrikethrough', () => {
|
||||
test('shared', () => {
|
||||
expect(Util.escapeStrikethrough(testString))
|
||||
.toBe('`_Behold!_`\n||___\\~\\~***```js\n`use strict`;\nrequire(\'discord.js\');```***\\~\\~___||');
|
||||
});
|
||||
|
||||
test('basic', () => {
|
||||
expect(Util.escapeStrikethrough('~~test~~'))
|
||||
.toBe('\\~\\~test\\~\\~');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('escapeSpoiler', () => {
|
||||
test('shared', () => {
|
||||
expect(Util.escapeSpoiler(testString))
|
||||
.toBe('`_Behold!_`\n\\|\\|___~~***```js\n`use strict`;\nrequire(\'discord.js\');```***~~___\\|\\|');
|
||||
});
|
||||
|
||||
test('basic', () => {
|
||||
expect(Util.escapeSpoiler('||test||'))
|
||||
.toBe('\\|\\|test\\|\\|');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('escapeMarkdown', () => {
|
||||
test('shared', () => {
|
||||
expect(Util.escapeMarkdown(testString))
|
||||
.toBe('\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|');
|
||||
});
|
||||
|
||||
test('no codeBlock', () => {
|
||||
expect(Util.escapeMarkdown(testString, { codeBlock: false }))
|
||||
.toBe('\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*```js\n\\`use strict\\`;\nrequire(\'discord.js\');```\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|');
|
||||
});
|
||||
|
||||
test('no inlineCode', () => {
|
||||
expect(Util.escapeMarkdown(testString, { inlineCode: false }))
|
||||
.toBe('`\\_Behold!\\_`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n`use strict`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|');
|
||||
});
|
||||
|
||||
test('no bold', () => {
|
||||
expect(Util.escapeMarkdown(testString, { bold: false }))
|
||||
.toBe('\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_\\~\\~\\***\\`\\`\\`js\n\\`use strict\\`;\nrequire(\'discord.js\');\\`\\`\\`**\\*\\~\\~\\_\\_\\_\\|\\|');
|
||||
});
|
||||
|
||||
test('no italic', () => {
|
||||
expect(Util.escapeMarkdown(testString, { italic: false }))
|
||||
.toBe('\\`_Behold!_\\`\n\\|\\|_\\_\\_\\~\\~*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\**\\~\\~\\_\\__\\|\\|');
|
||||
});
|
||||
|
||||
test('no underline', () => {
|
||||
expect(Util.escapeMarkdown(testString, { underline: false }))
|
||||
.toBe('\\`\\_Behold!\\_\\`\n\\|\\|\\___\\~\\~\\*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*\\~\\~__\\_\\|\\|');
|
||||
});
|
||||
|
||||
test('no strikethrough', () => {
|
||||
expect(Util.escapeMarkdown(testString, { strikethrough: false }))
|
||||
.toBe('\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_~~\\*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*~~\\_\\_\\_\\|\\|');
|
||||
});
|
||||
|
||||
test('no spoiler', () => {
|
||||
expect(Util.escapeMarkdown(testString, { spoiler: false }))
|
||||
.toBe('\\`\\_Behold!\\_\\`\n||\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_||');
|
||||
});
|
||||
|
||||
describe('code content', () => {
|
||||
test('no code block content', () => {
|
||||
expect(Util.escapeMarkdown(testString, { codeBlockContent: false }))
|
||||
.toBe('\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n`use strict`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|');
|
||||
});
|
||||
|
||||
test('no inline code content', () => {
|
||||
expect(Util.escapeMarkdown(testString, { inlineCodeContent: false }))
|
||||
.toBe('\\`_Behold!_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|');
|
||||
});
|
||||
|
||||
test('neither inline code or code block content', () => {
|
||||
expect(Util.escapeMarkdown(testString, { inlineCodeContent: false, codeBlockContent: false }))
|
||||
// eslint-disable-next-line max-len
|
||||
.toBe('\\`_Behold!_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n`use strict`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|');
|
||||
});
|
||||
|
||||
test('neither code blocks or code block content', () => {
|
||||
expect(Util.escapeMarkdown(testString, { codeBlock: false, codeBlockContent: false }))
|
||||
.toBe('\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*```js\n`use strict`;\nrequire(\'discord.js\');```\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|');
|
||||
});
|
||||
|
||||
test('neither inline code or inline code content', () => {
|
||||
expect(Util.escapeMarkdown(testString, { inlineCode: false, inlineCodeContent: false }))
|
||||
.toBe('`_Behold!_`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n`use strict`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|');
|
||||
});
|
||||
|
||||
test('edge-case odd number of fenses with no code block content', () => {
|
||||
expect(Util.escapeMarkdown('**foo** ```**bar**``` **fizz** ``` **buzz**', { codeBlock: false, codeBlockContent: false }))
|
||||
.toBe('\\*\\*foo\\*\\* ```**bar**``` \\*\\*fizz\\*\\* ``` \\*\\*buzz\\*\\*');
|
||||
});
|
||||
|
||||
test('edge-case odd number of backticks with no inline code content', () => {
|
||||
expect(Util.escapeMarkdown('**foo** `**bar**` **fizz** ` **buzz**', { inlineCode: false, inlineCodeContent: false }))
|
||||
.toBe('\\*\\*foo\\*\\* `**bar**` \\*\\*fizz\\*\\* ` \\*\\*buzz\\*\\*');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* eslint-enable max-len, no-undef */
|
||||
10
typings/index.d.ts
vendored
10
typings/index.d.ts
vendored
@@ -1160,7 +1160,15 @@ declare module 'discord.js' {
|
||||
public static convertToBuffer(ab: ArrayBuffer | string): Buffer;
|
||||
public static delayFor(ms: number): Promise<void>;
|
||||
public static discordSort<K, V extends { rawPosition: number; id: string; }>(collection: Collection<K, V>): Collection<K, V>;
|
||||
public static escapeMarkdown(text: string, onlyCodeBlock?: boolean, onlyInlineCode?: boolean): string;
|
||||
public static escapeMarkdown(text: string, options?: { codeBlock?: boolean, inlineCode?: boolean, bold?: boolean, italic?: boolean, underline?: boolean, strikethrough?: boolean, spoiler?: boolean, inlineCodeContent?: boolean, codeBlockContent?: boolean }): string;
|
||||
public static escapeCodeBlock(text: string): string;
|
||||
public static escapeInlineCode(text: string): string;
|
||||
public static escapeBold(text: string): string;
|
||||
public static escapeItalic(text: string): string;
|
||||
public static escapeUnderline(text: string): string;
|
||||
public static escapeStrikethrough(text: string): string;
|
||||
public static escapeSpoiler(text: string): string;
|
||||
public static cleanCodeBlockContent(text: string): string;
|
||||
public static fetchRecommendedShards(token: string, guildsPerShard?: number): Promise<number>;
|
||||
public static flatten(obj: object, ...props: { [key: string]: boolean | string }[]): object;
|
||||
public static idToBinary(num: Snowflake): string;
|
||||
|
||||
Reference in New Issue
Block a user