mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-12 01:23:31 +01:00
refactor: Moved the escapeX functions from discord.js to @discord.js/formatters (#8957)
* refactor: moved escapeX funcs from discord.js to @discord.js/formatters
- moved escapeX functions from discord.js to @discord.js/formatters
- converted code from JS to TS (including JSDoc and TSDoc)
- made linter happy
- modified the escapeHeading's RegExp to pass the RegExp safety test
- escapeBulletedList now conserves the bullet style (- or *)
* fix: removed useless exports and eslint command
removed useless exports and eslint command
* fix(escapeX): emojis with underlines
porting the fix made in 2c4c5c23d6 into the refactorization PR
Co-authored-by: space <spaceeec@yahoo.com>
This commit is contained in:
@@ -53,233 +53,6 @@ function flatten(obj, ...props) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options used to escape markdown.
|
||||
* @typedef {Object} EscapeMarkdownOptions
|
||||
* @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
|
||||
*/
|
||||
|
||||
/**
|
||||
* Escapes any Discord-flavour markdown in a string.
|
||||
* @param {string} text Content to escape
|
||||
* @param {EscapeMarkdownOptions} [options={}] Options for escaping the markdown
|
||||
* @returns {string}
|
||||
*/
|
||||
function escapeMarkdown(
|
||||
text,
|
||||
{
|
||||
codeBlock = true,
|
||||
inlineCode = true,
|
||||
bold = true,
|
||||
italic = true,
|
||||
underline = true,
|
||||
strikethrough = true,
|
||||
spoiler = true,
|
||||
codeBlockContent = true,
|
||||
inlineCodeContent = true,
|
||||
escape = true,
|
||||
heading = false,
|
||||
bulletedList = false,
|
||||
numberedList = false,
|
||||
maskedLink = false,
|
||||
} = {},
|
||||
) {
|
||||
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,
|
||||
});
|
||||
})
|
||||
.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,
|
||||
});
|
||||
})
|
||||
.join(inlineCode ? '\\`' : '`');
|
||||
}
|
||||
if (escape) text = escapeEscape(text);
|
||||
if (inlineCode) text = escapeInlineCode(text);
|
||||
if (codeBlock) text = escapeCodeBlock(text);
|
||||
if (italic) text = escapeItalic(text);
|
||||
if (bold) text = escapeBold(text);
|
||||
if (underline) text = escapeUnderline(text);
|
||||
if (strikethrough) text = escapeStrikethrough(text);
|
||||
if (spoiler) text = escapeSpoiler(text);
|
||||
if (heading) text = escapeHeading(text);
|
||||
if (bulletedList) text = escapeBulletedList(text);
|
||||
if (numberedList) text = escapeNumberedList(text);
|
||||
if (maskedLink) text = escapeMaskedLink(text);
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes code block markdown in a string.
|
||||
* @param {string} text Content to escape
|
||||
* @returns {string}
|
||||
*/
|
||||
function escapeCodeBlock(text) {
|
||||
return text.replaceAll('```', '\\`\\`\\`');
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes inline code markdown in a string.
|
||||
* @param {string} text Content to escape
|
||||
* @returns {string}
|
||||
*/
|
||||
function escapeInlineCode(text) {
|
||||
return text.replace(/(?<=^|[^`])``?(?=[^`]|$)/g, match => (match.length === 2 ? '\\`\\`' : '\\`'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes italic markdown in a string.
|
||||
* @param {string} text Content to escape
|
||||
* @returns {string}
|
||||
*/
|
||||
function escapeItalic(text) {
|
||||
let i = 0;
|
||||
text = text.replace(/(?<=^|[^*])\*([^*]|\*\*|$)/g, (_, match) => {
|
||||
if (match === '**') return ++i % 2 ? `\\*${match}` : `${match}\\*`;
|
||||
return `\\*${match}`;
|
||||
});
|
||||
i = 0;
|
||||
return text.replace(/(?<=^|[^_])(?<!<a?:.+)_(?!:\d+>)([^_]|__|$)/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}
|
||||
*/
|
||||
function 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}
|
||||
*/
|
||||
function escapeUnderline(text) {
|
||||
let i = 0;
|
||||
return text.replace(/(?<!<a?:.+)__(_)?(?!:\d+>)/g, (_, match) => {
|
||||
if (match) return ++i % 2 ? `${match}\\_\\_` : `\\_\\_${match}`;
|
||||
return '\\_\\_';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes strikethrough markdown in a string.
|
||||
* @param {string} text Content to escape
|
||||
* @returns {string}
|
||||
*/
|
||||
function escapeStrikethrough(text) {
|
||||
return text.replaceAll('~~', '\\~\\~');
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes spoiler markdown in a string.
|
||||
* @param {string} text Content to escape
|
||||
* @returns {string}
|
||||
*/
|
||||
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
|
||||
@@ -600,18 +373,6 @@ function parseWebhookURL(url) {
|
||||
|
||||
module.exports = {
|
||||
flatten,
|
||||
escapeMarkdown,
|
||||
escapeCodeBlock,
|
||||
escapeInlineCode,
|
||||
escapeItalic,
|
||||
escapeBold,
|
||||
escapeUnderline,
|
||||
escapeStrikethrough,
|
||||
escapeSpoiler,
|
||||
escapeHeading,
|
||||
escapeBulletedList,
|
||||
escapeNumberedList,
|
||||
escapeMaskedLink,
|
||||
fetchRecommendedShardCount,
|
||||
parseEmoji,
|
||||
resolvePartialEmoji,
|
||||
|
||||
@@ -1,254 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* eslint-env jest */
|
||||
/* eslint-disable max-len */
|
||||
|
||||
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', () => {
|
||||
expect(Util.escapeCodeBlock(testString)).toEqual(
|
||||
"`_Behold!_`\n||___~~***\\`\\`\\`js\n`use strict`;\nrequire('discord.js');\\`\\`\\`***~~___||",
|
||||
);
|
||||
});
|
||||
|
||||
test('basic', () => {
|
||||
expect(Util.escapeCodeBlock('```test```')).toEqual('\\`\\`\\`test\\`\\`\\`');
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeInlineCode', () => {
|
||||
test('shared', () => {
|
||||
expect(Util.escapeInlineCode(testString)).toEqual(
|
||||
"\\`_Behold!_\\`\n||___~~***```js\n\\`use strict\\`;\nrequire('discord.js');```***~~___||",
|
||||
);
|
||||
});
|
||||
|
||||
test('basic', () => {
|
||||
expect(Util.escapeInlineCode('`test`')).toEqual('\\`test\\`');
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeBold', () => {
|
||||
test('shared', () => {
|
||||
expect(Util.escapeBold(testString)).toEqual(
|
||||
"`_Behold!_`\n||___~~*\\*\\*```js\n`use strict`;\nrequire('discord.js');```\\*\\**~~___||",
|
||||
);
|
||||
});
|
||||
|
||||
test('basic', () => {
|
||||
expect(Util.escapeBold('**test**')).toEqual('\\*\\*test\\*\\*');
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeItalic', () => {
|
||||
test('shared', () => {
|
||||
expect(Util.escapeItalic(testString)).toEqual(
|
||||
"`\\_Behold!\\_`\n||\\___~~\\***```js\n`use strict`;\nrequire('discord.js');```**\\*~~__\\_||",
|
||||
);
|
||||
});
|
||||
|
||||
test('basic (_)', () => {
|
||||
expect(Util.escapeItalic('_test_')).toEqual('\\_test\\_');
|
||||
});
|
||||
|
||||
test('basic (*)', () => {
|
||||
expect(Util.escapeItalic('*test*')).toEqual('\\*test\\*');
|
||||
});
|
||||
|
||||
test('emoji', () => {
|
||||
const testOne = 'This is a test with _emojis_ <:Frost_ed_Wreath:1053399941210443826> and **bold text**.';
|
||||
expect(Util.escapeItalic(testOne)).toEqual(
|
||||
'This is a test with \\_emojis\\_ <:Frost_ed_Wreath:1053399941210443826> and **bold text**.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeUnderline', () => {
|
||||
test('shared', () => {
|
||||
expect(Util.escapeUnderline(testString)).toEqual(
|
||||
"`_Behold!_`\n||_\\_\\_~~***```js\n`use strict`;\nrequire('discord.js');```***~~\\_\\__||",
|
||||
);
|
||||
});
|
||||
|
||||
test('basic', () => {
|
||||
expect(Util.escapeUnderline('__test__')).toEqual('\\_\\_test\\_\\_');
|
||||
});
|
||||
|
||||
test('emoji', () => {
|
||||
const testTwo = 'This is a test with __emojis__ <:Frost__ed__Wreath:1053399939654352978> and **bold text**.';
|
||||
expect(Util.escapeUnderline(testTwo)).toBe(
|
||||
'This is a test with \\_\\_emojis\\_\\_ <:Frost__ed__Wreath:1053399939654352978> and **bold text**.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeStrikethrough', () => {
|
||||
test('shared', () => {
|
||||
expect(Util.escapeStrikethrough(testString)).toEqual(
|
||||
"`_Behold!_`\n||___\\~\\~***```js\n`use strict`;\nrequire('discord.js');```***\\~\\~___||",
|
||||
);
|
||||
});
|
||||
|
||||
test('basic', () => {
|
||||
expect(Util.escapeStrikethrough('~~test~~')).toEqual('\\~\\~test\\~\\~');
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeSpoiler', () => {
|
||||
test('shared', () => {
|
||||
expect(Util.escapeSpoiler(testString)).toEqual(
|
||||
"`_Behold!_`\n\\|\\|___~~***```js\n`use strict`;\nrequire('discord.js');```***~~___\\|\\|",
|
||||
);
|
||||
});
|
||||
|
||||
test('basic', () => {
|
||||
expect(Util.escapeSpoiler('||test||')).toEqual('\\|\\|test\\|\\|');
|
||||
});
|
||||
});
|
||||
|
||||
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(
|
||||
"\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire('discord.js');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|",
|
||||
);
|
||||
});
|
||||
|
||||
test('no codeBlock', () => {
|
||||
expect(Util.escapeMarkdown(testString, { codeBlock: false })).toEqual(
|
||||
"\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*```js\n\\`use strict\\`;\nrequire('discord.js');```\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|",
|
||||
);
|
||||
});
|
||||
|
||||
test('no inlineCode', () => {
|
||||
expect(Util.escapeMarkdown(testString, { inlineCode: false })).toEqual(
|
||||
"`\\_Behold!\\_`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n`use strict`;\nrequire('discord.js');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|",
|
||||
);
|
||||
});
|
||||
|
||||
test('no bold', () => {
|
||||
expect(Util.escapeMarkdown(testString, { bold: false })).toEqual(
|
||||
"\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_\\~\\~\\***\\`\\`\\`js\n\\`use strict\\`;\nrequire('discord.js');\\`\\`\\`**\\*\\~\\~\\_\\_\\_\\|\\|",
|
||||
);
|
||||
});
|
||||
|
||||
test('no italic', () => {
|
||||
expect(Util.escapeMarkdown(testString, { italic: false })).toEqual(
|
||||
"\\`_Behold!_\\`\n\\|\\|_\\_\\_\\~\\~*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire('discord.js');\\`\\`\\`\\*\\**\\~\\~\\_\\__\\|\\|",
|
||||
);
|
||||
});
|
||||
|
||||
test('no underline', () => {
|
||||
expect(Util.escapeMarkdown(testString, { underline: false })).toEqual(
|
||||
"\\`\\_Behold!\\_\\`\n\\|\\|\\___\\~\\~\\*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire('discord.js');\\`\\`\\`\\*\\*\\*\\~\\~__\\_\\|\\|",
|
||||
);
|
||||
});
|
||||
|
||||
test('no strikethrough', () => {
|
||||
expect(Util.escapeMarkdown(testString, { strikethrough: false })).toEqual(
|
||||
"\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_~~\\*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire('discord.js');\\`\\`\\`\\*\\*\\*~~\\_\\_\\_\\|\\|",
|
||||
);
|
||||
});
|
||||
|
||||
test('no spoiler', () => {
|
||||
expect(Util.escapeMarkdown(testString, { spoiler: false })).toEqual(
|
||||
"\\`\\_Behold!\\_\\`\n||\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire('discord.js');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_||",
|
||||
);
|
||||
});
|
||||
|
||||
describe('code content', () => {
|
||||
test('no code block content', () => {
|
||||
expect(Util.escapeMarkdown(testString, { codeBlockContent: false })).toEqual(
|
||||
"\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n`use strict`;\nrequire('discord.js');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|",
|
||||
);
|
||||
});
|
||||
|
||||
test('no inline code content', () => {
|
||||
expect(Util.escapeMarkdown(testString, { inlineCodeContent: false })).toEqual(
|
||||
"\\`_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
|
||||
.toEqual(
|
||||
"\\`_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 })).toEqual(
|
||||
"\\`\\_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 })).toEqual(
|
||||
"`_Behold!_`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n`use strict`;\nrequire('discord.js');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|",
|
||||
);
|
||||
});
|
||||
|
||||
test('edge-case odd number of fences with no code block content', () => {
|
||||
expect(
|
||||
Util.escapeMarkdown('**foo** ```**bar**``` **fizz** ``` **buzz**', {
|
||||
codeBlock: false,
|
||||
codeBlockContent: false,
|
||||
}),
|
||||
).toEqual('\\*\\*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 }),
|
||||
).toEqual('\\*\\*foo\\*\\* `**bar**` \\*\\*fizz\\*\\* ` \\*\\*buzz\\*\\*');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* eslint-enable max-len, no-undef */
|
||||
13
packages/discord.js/typings/index.d.ts
vendored
13
packages/discord.js/typings/index.d.ts
vendored
@@ -3051,19 +3051,6 @@ export function cleanContent(str: string, channel: TextBasedChannel): string;
|
||||
export function discordSort<K, V extends { rawPosition: number; id: Snowflake }>(
|
||||
collection: Collection<K, V>,
|
||||
): Collection<K, V>;
|
||||
export function escapeMarkdown(text: string, options?: EscapeMarkdownOptions): string;
|
||||
export function escapeCodeBlock(text: string): string;
|
||||
export function escapeInlineCode(text: string): string;
|
||||
export function escapeBold(text: string): string;
|
||||
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<number>;
|
||||
export function flatten(obj: unknown, ...props: Record<string, boolean | string>[]): unknown;
|
||||
|
||||
264
packages/formatters/__tests__/escapers.test.ts
Normal file
264
packages/formatters/__tests__/escapers.test.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import {
|
||||
escapeCodeBlock,
|
||||
escapeInlineCode,
|
||||
escapeItalic,
|
||||
escapeBold,
|
||||
escapeUnderline,
|
||||
escapeStrikethrough,
|
||||
escapeMaskedLink,
|
||||
escapeSpoiler,
|
||||
escapeHeading,
|
||||
escapeBulletedList,
|
||||
escapeNumberedList,
|
||||
escapeMarkdown,
|
||||
} from '../src/index.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('Markdown escapers', () => {
|
||||
describe('escapeCodeblock', () => {
|
||||
test('shared', () => {
|
||||
expect(escapeCodeBlock(testString)).toEqual(
|
||||
"`_Behold!_`\n||___~~***\\`\\`\\`js\n`use strict`;\nrequire('discord.js');\\`\\`\\`***~~___||",
|
||||
);
|
||||
});
|
||||
|
||||
test('basic', () => {
|
||||
expect(escapeCodeBlock('```test```')).toEqual('\\`\\`\\`test\\`\\`\\`');
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeInlineCode', () => {
|
||||
test('shared', () => {
|
||||
expect(escapeInlineCode(testString)).toEqual(
|
||||
"\\`_Behold!_\\`\n||___~~***```js\n\\`use strict\\`;\nrequire('discord.js');```***~~___||",
|
||||
);
|
||||
});
|
||||
|
||||
test('basic', () => {
|
||||
expect(escapeInlineCode('`test`')).toEqual('\\`test\\`');
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeBold', () => {
|
||||
test('shared', () => {
|
||||
expect(escapeBold(testString)).toEqual(
|
||||
"`_Behold!_`\n||___~~*\\*\\*```js\n`use strict`;\nrequire('discord.js');```\\*\\**~~___||",
|
||||
);
|
||||
});
|
||||
|
||||
test('basic', () => {
|
||||
expect(escapeBold('**test**')).toEqual('\\*\\*test\\*\\*');
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeItalic', () => {
|
||||
test('shared', () => {
|
||||
expect(escapeItalic(testString)).toEqual(
|
||||
"`\\_Behold!\\_`\n||\\___~~\\***```js\n`use strict`;\nrequire('discord.js');```**\\*~~__\\_||",
|
||||
);
|
||||
});
|
||||
|
||||
test('basic (_)', () => {
|
||||
expect(escapeItalic('_test_')).toEqual('\\_test\\_');
|
||||
});
|
||||
|
||||
test('basic (*)', () => {
|
||||
expect(escapeItalic('*test*')).toEqual('\\*test\\*');
|
||||
});
|
||||
|
||||
test('emoji', () => {
|
||||
const testOne = 'This is a test with _emojis_ <:Frost_ed_Wreath:1053399941210443826> and **bold text**.';
|
||||
expect(escapeItalic(testOne)).toEqual(
|
||||
'This is a test with \\_emojis\\_ <:Frost_ed_Wreath:1053399941210443826> and **bold text**.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeUnderline', () => {
|
||||
test('shared', () => {
|
||||
expect(escapeUnderline(testString)).toEqual(
|
||||
"`_Behold!_`\n||_\\_\\_~~***```js\n`use strict`;\nrequire('discord.js');```***~~\\_\\__||",
|
||||
);
|
||||
});
|
||||
|
||||
test('basic', () => {
|
||||
expect(escapeUnderline('__test__')).toEqual('\\_\\_test\\_\\_');
|
||||
});
|
||||
|
||||
test('emoji', () => {
|
||||
const testTwo = 'This is a test with __emojis__ <:Frost__ed__Wreath:1053399939654352978> and **bold text**.';
|
||||
expect(escapeUnderline(testTwo)).toBe(
|
||||
'This is a test with \\_\\_emojis\\_\\_ <:Frost__ed__Wreath:1053399939654352978> and **bold text**.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeStrikethrough', () => {
|
||||
test('shared', () => {
|
||||
expect(escapeStrikethrough(testString)).toEqual(
|
||||
"`_Behold!_`\n||___\\~\\~***```js\n`use strict`;\nrequire('discord.js');```***\\~\\~___||",
|
||||
);
|
||||
});
|
||||
|
||||
test('basic', () => {
|
||||
expect(escapeStrikethrough('~~test~~')).toEqual('\\~\\~test\\~\\~');
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeSpoiler', () => {
|
||||
test('shared', () => {
|
||||
expect(escapeSpoiler(testString)).toEqual(
|
||||
"`_Behold!_`\n\\|\\|___~~***```js\n`use strict`;\nrequire('discord.js');```***~~___\\|\\|",
|
||||
);
|
||||
});
|
||||
|
||||
test('basic', () => {
|
||||
expect(escapeSpoiler('||test||')).toEqual('\\|\\|test\\|\\|');
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeHeading', () => {
|
||||
test('shared', () => {
|
||||
expect(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(escapeHeading('# test')).toEqual('\\# test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeBulletedList', () => {
|
||||
test('shared', () => {
|
||||
expect(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(escapeBulletedList('- test')).toEqual('\\- test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeNumberedList', () => {
|
||||
test('shared', () => {
|
||||
expect(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(escapeNumberedList('1. test')).toEqual('1\\. test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeMaskedLink', () => {
|
||||
test('basic', () => {
|
||||
expect(escapeMaskedLink('[test](https://discord.js.org)')).toEqual('\\[test](https://discord.js.org)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeMarkdown', () => {
|
||||
test('shared', () => {
|
||||
expect(escapeMarkdown(testString)).toEqual(
|
||||
"\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire('discord.js');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|",
|
||||
);
|
||||
});
|
||||
|
||||
test('no codeBlock', () => {
|
||||
expect(escapeMarkdown(testString, { codeBlock: false })).toEqual(
|
||||
"\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*```js\n\\`use strict\\`;\nrequire('discord.js');```\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|",
|
||||
);
|
||||
});
|
||||
|
||||
test('no inlineCode', () => {
|
||||
expect(escapeMarkdown(testString, { inlineCode: false })).toEqual(
|
||||
"`\\_Behold!\\_`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n`use strict`;\nrequire('discord.js');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|",
|
||||
);
|
||||
});
|
||||
|
||||
test('no bold', () => {
|
||||
expect(escapeMarkdown(testString, { bold: false })).toEqual(
|
||||
"\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_\\~\\~\\***\\`\\`\\`js\n\\`use strict\\`;\nrequire('discord.js');\\`\\`\\`**\\*\\~\\~\\_\\_\\_\\|\\|",
|
||||
);
|
||||
});
|
||||
|
||||
test('no italic', () => {
|
||||
expect(escapeMarkdown(testString, { italic: false })).toEqual(
|
||||
"\\`_Behold!_\\`\n\\|\\|_\\_\\_\\~\\~*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire('discord.js');\\`\\`\\`\\*\\**\\~\\~\\_\\__\\|\\|",
|
||||
);
|
||||
});
|
||||
|
||||
test('no underline', () => {
|
||||
expect(escapeMarkdown(testString, { underline: false })).toEqual(
|
||||
"\\`\\_Behold!\\_\\`\n\\|\\|\\___\\~\\~\\*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire('discord.js');\\`\\`\\`\\*\\*\\*\\~\\~__\\_\\|\\|",
|
||||
);
|
||||
});
|
||||
|
||||
test('no strikethrough', () => {
|
||||
expect(escapeMarkdown(testString, { strikethrough: false })).toEqual(
|
||||
"\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_~~\\*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire('discord.js');\\`\\`\\`\\*\\*\\*~~\\_\\_\\_\\|\\|",
|
||||
);
|
||||
});
|
||||
|
||||
test('no spoiler', () => {
|
||||
expect(escapeMarkdown(testString, { spoiler: false })).toEqual(
|
||||
"\\`\\_Behold!\\_\\`\n||\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire('discord.js');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_||",
|
||||
);
|
||||
});
|
||||
|
||||
describe('code content', () => {
|
||||
test('no code block content', () => {
|
||||
expect(escapeMarkdown(testString, { codeBlockContent: false })).toEqual(
|
||||
"\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n`use strict`;\nrequire('discord.js');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|",
|
||||
);
|
||||
});
|
||||
|
||||
test('no inline code content', () => {
|
||||
expect(escapeMarkdown(testString, { inlineCodeContent: false })).toEqual(
|
||||
"\\`_Behold!_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire('discord.js');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|",
|
||||
);
|
||||
});
|
||||
|
||||
test('neither inline code or code block content', () => {
|
||||
expect(escapeMarkdown(testString, { inlineCodeContent: false, codeBlockContent: false }))
|
||||
// eslint-disable-next-line max-len
|
||||
.toEqual(
|
||||
"\\`_Behold!_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n`use strict`;\nrequire('discord.js');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|",
|
||||
);
|
||||
});
|
||||
|
||||
test('neither code blocks or code block content', () => {
|
||||
expect(escapeMarkdown(testString, { codeBlock: false, codeBlockContent: false })).toEqual(
|
||||
"\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*```js\n`use strict`;\nrequire('discord.js');```\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|",
|
||||
);
|
||||
});
|
||||
|
||||
test('neither inline code or inline code content', () => {
|
||||
expect(escapeMarkdown(testString, { inlineCode: false, inlineCodeContent: false })).toEqual(
|
||||
"`_Behold!_`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n`use strict`;\nrequire('discord.js');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|",
|
||||
);
|
||||
});
|
||||
|
||||
test('edge-case odd number of fences with no code block content', () => {
|
||||
expect(
|
||||
escapeMarkdown('**foo** ```**bar**``` **fizz** ``` **buzz**', {
|
||||
codeBlock: false,
|
||||
codeBlockContent: false,
|
||||
}),
|
||||
).toEqual('\\*\\*foo\\*\\* ```**bar**``` \\*\\*fizz\\*\\* ``` \\*\\*buzz\\*\\*');
|
||||
});
|
||||
|
||||
test('edge-case odd number of backticks with no inline code content', () => {
|
||||
expect(
|
||||
escapeMarkdown('**foo** `**bar**` **fizz** ` **buzz**', { inlineCode: false, inlineCodeContent: false }),
|
||||
).toEqual('\\*\\*foo\\*\\* `**bar**` \\*\\*fizz\\*\\* ` \\*\\*buzz\\*\\*');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
310
packages/formatters/src/escapers.ts
Normal file
310
packages/formatters/src/escapers.ts
Normal file
@@ -0,0 +1,310 @@
|
||||
/* eslint-disable prefer-named-capture-group */
|
||||
|
||||
export interface EscapeMarkdownOptions {
|
||||
/**
|
||||
* Whether to escape bolds
|
||||
*
|
||||
* @defaultValue true
|
||||
*/
|
||||
bold?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to escape bulleted lists
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
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 escape characters
|
||||
*
|
||||
* @defaultValue true
|
||||
*/
|
||||
escape?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to escape headings
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
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 false
|
||||
*/
|
||||
maskedLink?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to escape numbered lists
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
numberedList?: 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-flavour 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 = false,
|
||||
bulletedList = false,
|
||||
numberedList = false,
|
||||
maskedLink = false,
|
||||
} = 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,
|
||||
});
|
||||
})
|
||||
.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,
|
||||
});
|
||||
})
|
||||
.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);
|
||||
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(/(?<=^|[^*])\*([^*]|\*\*|$)/g, (_, match) => {
|
||||
if (match === '**') return ++idx % 2 ? `\\*${match}` : `${match}\\*`;
|
||||
return `\\*${match}`;
|
||||
});
|
||||
idx = 0;
|
||||
return newText.replaceAll(/(?<=^|[^_])(?<!<a?:.+)_(?!:\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?:.+)__(_)?(?!:\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, '\\$&');
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export * from './escapers.js';
|
||||
export * from './formatters.js';
|
||||
|
||||
Reference in New Issue
Block a user