detritus

Utils.Markupnamespace

Format and escape text with Discord's markdown.

On this page
import { Utils } from 'detritus-client';

Utils.Markup;
12 interfaces · 4 variables · 16 functions

Interfaces

Object shapes for options, payloads and events.

Functions

Standalone helpers.

trueSlice()

function src/utils/markup.ts:385
function trueSlice(text: string, limit?: number, escapes?: boolean): string
text
string
limit?
number
escapes?
boolean
Default true

Variables

Exported values.

escape

variable const src/utils/markup.ts:449
const escape: Readonly<{ all: (text: string, options?: MarkupFilterOptions) => string; bold: (text: string, options?: MarkupFilterOptions) => string; codeblock: (text: string, options?: CodeblockFilterOptions) => string; codestring: (text: string, options?: MarkupFilterOptions) => string; header: (text: string, options?: HeaderFilterOptions) => string; italics: (text: string, options?: MarkupFilterOptions) => string; links: (text: string, replacement?: string) => string; list: (text: string, options?: ListFilterOptions) => string; mentions: (text: string, replacement?: string) => string; quote: (text: string, options?: QuoteFilterOptions) => string; spoiler: (text: string, options?: MarkupFilterOptions) => string; strike: (text: string, options?: MarkupFilterOptions) => string; … 3 more }>
export const escape = Object.freeze({
  all: (text: string, options: MarkupFilterOptions = {}): string => {
    const filter: MarkupFilter = Object.assign({}, defaultMarkupFilter, options);

    // urls are left alone, the backslashes would end up in the link
    // (the parts after a url get their line-start escapes too, which shows the same)
    text = text.split(Regexes.URL_BARE).map((part, i) => {
      return (i % 2) ? part : escapeAllMarkdown(part);
    }).join('');

    if (filter.links) {
      // not `mentionEscapeCharacter`, setting that to '' would leave links working
      text = escape.links(text);
    }
    if (filter.mentions) {
      text = escape.mentions(text, filter.mentionEscapeCharacter);
    }
    return trueSlice(text, filter.limit);
  },
  bold: (text: string, options: MarkupFilterOptions = {}): string => {
    const filter: MarkupFilter = Object.assign({}, defaultBoldFilter, options);

    text = escapeMarker(text, Regexes[Strings.BOLD], (match: string) => {
      return match.replace(Regexes[Strings.BOLD], filter.replacement);
    });
    if (filter.mentions) {
      text = escape.mentions(text, filter.mentionEscapeCharacter);
    }
    return trueSlice(text, filter.limit);
  },
  codeblock: (text: string, options: CodeblockFilterOptions = {}): string => {
    const filter: CodeblockFilter = Object.assign({}, defaultCodeblockFilter, options);

    while (text.includes(Strings.CODEBLOCK)) {
      text = text.replace(Regexes[Strings.CODEBLOCK], filter.replacement);
    }

    if (options.limit === undefined) {
      filter.limit -= filter.language.length;
    }
    return trueSlice(text, filter.limit, false);
  },
  codestring: (text: string, options: MarkupFilterOptions = {}): string => {
    const filter: MarkupFilter = Object.assign({}, defaultCodestringFilter, options);

    text = text.replace(Regexes[Strings.CODESTRING], filter.replacement);
    if (filter.mentions) {
      text = escape.mentions(text, filter.mentionEscapeCharacter);
    }
    return trueSlice(text, filter.limit, false);
  },
  header: (text: string, options: HeaderFilterOptions = {}): string => {
    const filter: HeaderFilter = Object.assign({}, defaultHeaderFilter, options);

    text = text.replace(Regexes[Strings.HEADER], filter.replacement);
    if (!filter.multiline) {
      text = text.replace(/\n/g, ' ');
    }
    if (filter.mentions) {
      text = escape.mentions(text, filter.mentionEscapeCharacter);
    }
    return trueSlice(text, filter.limit);
  },
  italics: (text: string, options: MarkupFilterOptions = {}): string => {
    const filter: MarkupFilter = Object.assign({}, defaultItalicsFilter, options);

    text = escapeMarker(text, Regexes[Strings.ITALICS], (match: string) => {
      return match.replace(Regexes[Strings.ITALICS], filter.replacement);
    });
    if (filter.mentions) {
      text = escape.mentions(text, filter.mentionEscapeCharacter);
    }
    return trueSlice(text, filter.limit);
  },
  links: (text: string, replacement: string = Replacements.MENTION): string => {
    text = text.replace(Regexes.LINK, `]${replacement}(`);
    return text;
  },
  list: (text: string, options: ListFilterOptions = {}): string => {
    const filter: ListFilter = Object.assign({}, defaultListFilter, options);

    text = text.replace(Regexes[Strings.LIST], filter.replacement);
    if (!filter.multiline) {
      text = text.replace(/\n/g, ' ');
    }
    if (filter.mentions) {
      text = escape.mentions(text, filter.mentionEscapeCharacter);
    }
    return trueSlice(text, filter.limit);
  },
  mentions: (text: string, replacement: string = Replacements.MENTION): string => {
    text = text.replace(Regexes.MENTION_HARDCORE, `@${replacement}`);
    return text;
  },
  quote: (text: string, options: QuoteFilterOptions = {}): string => {
    const filter: QuoteFilter = Object.assign({}, defaultQuoteFilter, options);

    text = text.replace(Regexes.QUOTE_LINE_START, filter.replacement);
    if (!filter.multiline) {
      text = text.replace(/\n/g, ' ');
    }
    if (filter.mentions) {
      text = escape.mentions(text, filter.mentionEscapeCharacter);
    }
    return trueSlice(text, filter.limit);
  },
  spoiler: (text: string, options: MarkupFilterOptions = {}): string => {
    const filter: MarkupFilter = Object.assign({}, defaultSpoilerFilter, options);

    // a lone `|` gets escaped too, `||secret|||` would leave the last one outside of the spoiler
    text = escapeMarker(text, Regexes.PIPES, (match: string) => {
      return (match === Strings.SPOILER) ? filter.replacement : '\\|';
    });
    if (filter.mentions) {
      text = escape.mentions(text, filter.mentionEscapeCharacter);
    }
    return trueSlice(text, filter.limit);
  },
  strike: (text: string, options: MarkupFilterOptions = {}): string => {
    const filter: MarkupFilter = Object.assign({}, defaultStrikeFilter, options);

    // same as spoilers, a lone trailing `~` would end up outside of the strike
    text = escapeMarker(text, Regexes.TILDES, (match: string) => {
      return (match === Strings.STRIKE) ? filter.replacement : '\\~';
    });
    if (filter.mentions) {
      text = escape.mentions(text, filter.mentionEscapeCharacter);
    }
    return trueSlice(text, filter.limit);
  },
  subtext: (text: string, options: SubtextFilterOptions = {}): string => {
    const filter: SubtextFilter = Object.assign({}, defaultSubtextFilter, options);

    text = text.replace(Regexes[Strings.SUBTEXT], filter.replacement);
    if (!filter.multiline) {
      text = text.replace(/\n/g, ' ');
    }
    if (filter.mentions) {
      text = escape.mentions(text, filter.mentionEscapeCharacter);
    }
    return trueSlice(text, filter.limit);
  },
  underline: (text: string, options: MarkupFilterOptions = {}): string => {
    const filter: MarkupFilter = Object.assign({}, defaultUnderlineFilter, options);

    text = escapeMarker(text, Regexes[Strings.UNDERLINE], (match: string) => {
      return match.replace(Regexes[Strings.UNDERLINE], filter.replacement);
    });
    if (filter.mentions) {
      text = escape.mentions(text, filter.mentionEscapeCharacter);
    }
    return trueSlice(text, filter.limit);
  },
  url: (text: string, options: MarkupFilterOptions = {}): string => {
    // a `<url>` wrap (no embed) is the caller's, only encode what's inside of it
    const wrapped = (text.length >= 2 && text.startsWith('<') && text.endsWith('>'));
    if (wrapped) {
      text = text.slice(1, -1);
    }
    // encodes the utf-8 bytes, a char code is wrong for anything past ascii (`\u2028` isn't `%2028`)
    text = text.replace(Regexes.URL, (match: string) => {
      return (match === '(' || match === ')') ? '%' + match.charCodeAt(0).toString(16).toUpperCase() : encodeURIComponent(match);
    });
    return (wrapped) ? `<${text}>` : text;
  },
});

Regexes

variable const src/utils/markup.ts:20
const Regexes: Readonly<{ _: RegExp; __: RegExp; -: RegExp; -#: RegExp; **: RegExp; \: RegExp; #: RegExp; `: RegExp; ```: RegExp; >: RegExp; ||: RegExp; ~~: RegExp; … 11 more }>
export const Regexes = Object.freeze({
  [Strings.BOLD]: /\*\*/g,
  [Strings.CODEBLOCK]: new RegExp(Strings.CODEBLOCK, 'g'),
  [Strings.CODESTRING]: new RegExp(Strings.CODESTRING, 'g'),
  [Strings.ESCAPE]: /\\/g,
  // `m` so every line's start gets escaped, not just the text's
  [Strings.HEADER]: /^(\s*)#/gm,
  [Strings.ITALICS]: /(_|\*)/g,
  [Strings.LIST]: /(-|\*)/g,
  [Strings.QUOTE]: />/g,
  [Strings.SPOILER]: /\|\|/g,
  [Strings.STRIKE]: new RegExp(Strings.STRIKE, 'g'),
  [Strings.SUBTEXT]: /^(\s*)(-?#)/gm,
  [Strings.UNDERLINE]: new RegExp(Strings.UNDERLINE, 'g'),
  EVERYONE: /@(everyone|here)/g,
  LINK: /\]\(/g,
  LINK_TEXT: /[\[\]]/g,
  // list markers at the start of a line (`- `, `* `, `1. `)
  LIST_LINE_START: /^(\s*)([-*]|\d+\.)(?=\s)/gm,
  MENTION: /<@([!&]?[0-9]{16,21})>/g,
  MENTION_HARDCORE: /@/g,
  // a lone `|`/`~` next to the closing marker would leave one outside of it
  PIPES: /\|\|?/g,
  // only a `>` at the start of a line is a quote, the rest are emojis/channels/timestamps
  QUOTE_LINE_START: /^(\s*)>/gm,
  TILDES: /~~?/g,
  // anything that would end the link's url early (`\` escapes the `)`, `>` gets read as `<url>`'s end)
  URL: /[()<>\s\\]/g,
  // how discord autolinks, markdown inside these isn't parsed so escaping them only puts backslashes in the link
  URL_BARE: /(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/g,

Replacements

variable const src/utils/markup.ts:52
const Replacements: Readonly<{ _: "\\$1"; __: "\\_\\_"; -: "\\$1"; -#: "$1\\$2"; **: "\\*\\*"; \: "\\\\"; #: "$1\\#"; `: "\\`"; ```: "``​`"; >: "\\>"; ||: "\\|\\|"; ~~: "\\~\\~"; … 3 more }>
export const Replacements = Object.freeze({
  [Strings.BOLD]: '\\*\\*',
  [Strings.CODEBLOCK]: '``\u200b`',
  [Strings.CODESTRING]: '\\`',
  [Strings.ESCAPE]: '\\\\',
  LINK_TEXT: '\\$&',
  [Strings.HEADER]: '$1\\#',
  [Strings.ITALICS]: '\\$1',
  [Strings.LIST]: '\\$1',
  [Strings.QUOTE]: '\\>',
  QUOTE_LINE_START: '$1\\>',
  [Strings.SPOILER]: '\\|\\|',
  [Strings.STRIKE]: '\\~\\~',
  [Strings.SUBTEXT]: '$1\\$2',
  [Strings.UNDERLINE]: '\\_\\_',
  MENTION: '\u200b',
});

Strings

variable const src/utils/markup.ts:4
const Strings: Readonly<{ BOLD: "**"; CODEBLOCK: "```"; CODESTRING: "`"; CODESTRING_DOUBLE: "``"; ESCAPE: "\\"; HEADER: "#"; ITALICS: "_"; LIST: "-"; QUOTE: ">"; SPOILER: "||"; STRIKE: "~~"; SUBTEXT: "-#"; … 1 more }>
export const Strings = Object.freeze({
  BOLD: '**',
  CODEBLOCK: '```',
  CODESTRING: '`',
  CODESTRING_DOUBLE: '``',
  ESCAPE: '\\',
  HEADER: '#',
  ITALICS: '_',
  LIST: '-',
  QUOTE: '>',
  SPOILER: '||',
  STRIKE: '~~',
  SUBTEXT: '-#',
  UNDERLINE: '__',
});