teleform
Version:
Format Telegram messages, escape special characters, switch formatting styles, convert special entities to formatted text and vice versa, form Telegram links, use Unicode symbols.
271 lines (270 loc) • 11.6 kB
JavaScript
export { format };
class format {
static _entities = [
'bold',
'italic',
'underline',
'strikethrough',
'spoiler',
'text_link',
'custom_emoji',
'code',
'pre',
'blockquote',
'expandable_blockquote'
];
static _underscore_entities = [
'italic',
'underline'
];
static _blockquote_entities = [
'blockquote',
'expandable_blockquote'
];
static _to_escape = [];
static _tags = (type, optional) => ({}[type]);
static _limits = { length: 4096 };
static _name = name => `telegram-formatting‐${name}`;
static _link_regexp = /tg:\/\/(?<type>user|emoji)\?id=(?<id>.+)/;
static from_entities(...args) {
const [text_name, __, text, entities] = this._args(...args);
const tags = this._tags_from_entities(entities);
return { [text_name]: this._text_from_entities(text, tags) };
}
static to_entities(args, length = false, to_text = '', to_entities = []) {
const [text_name, entities_name, text, __] = this._args(args);
const result = this._text_to_entities(text, to_text.length);
const output = {
text: to_text + result.text,
entities: to_entities.concat(result.entities)
};
if (length === true)
length = this._limits.length;
if (length > 0)
this._clip(output, length);
return { [text_name]: output.text, [entities_name]: output.entities };
}
static _args(args, entities) {
if (typeof args !== 'object')
return ['text', 'entities', args, entities];
const name = args?.caption ? 'caption' : 'text';
return [
name, name === 'text' ? 'entities' : `${name}_entities`,
args?.text ?? args?.caption, args?.entities ?? args?.caption_entities
];
}
static _tags_from_entities(entities, tags = {}) {
for (const entity of entities ?? []) {
const { type, offset, length, url, user, language, custom_emoji_id } = entity;
const data = this._tags(type, url ?? user?.id ?? language ?? custom_emoji_id);
if (!data)
continue;
const tagend = offset + length;
for (const l of [offset, tagend])
if (!tags[l])
tags[l] = [];
tags[offset].push({ type: type, kind: 'opening', data: data[0] });
tags[tagend].push({ type: type, kind: 'closing', data: data[1] });
}
return tags;
}
static _text_from_entities(text, tags) {
let result_text = '', open = [];
const characters = text.split('');
for (let l = 0; l < characters.length + 1; l++) {
if (tags[l])
for (const tag of tags[l]) {
switch (tag.kind) {
case 'opening':
open.push(tag.type);
break;
case 'closing': {
let open_type;
while (open_type !== tag.type) {
open_type = open.pop();
if (open_type !== tag.type) {
const i = tags[l].findLastIndex(tag => (tag.type === open_type &&
tag.kind === 'closing'));
result_text += tags[l][i].data;
tags[l].splice(i, 1);
}
}
}
}
result_text += tag.data;
if (this.name === 'mdv2' &&
tag.kind === 'opening' &&
this._underscore_entities.includes(tag.type))
result_text += '**';
}
if (l === characters.length)
break;
result_text += this.escape(characters[l]);
if (this.name === 'mdv2' &&
open.some(type => {
return this._blockquote_entities.includes(type);
}) && characters[l] === '\n')
result_text += '>';
}
return result_text;
}
static _text_to_entities(text, indent, ii = 0, litter = 0, from = 0) {
let result_text = '', result_entities = [];
let spot, regexp = structuredClone(this._regexp);
if (typeof this._underlined_regexp !== 'undefined')
text = text.replace(this._underlined_regexp, `${this._name(this._underlined_name)}` +
'$<text>' +
`${this._name(this._underlined_name)}`);
while ((spot = regexp.exec(text)) !== null) {
let type = this._entities.find(type => spot.groups[type]);
if (type === 'blockquote' && !spot.groups.blockquote_text) {
const narrow_type = type;
let contents = spot.groups[`${type}_contents`] ?? '';
if (contents.match(/\|\|$/)) {
type = 'expandable_blockquote';
contents = contents.replace(/\|\|$/, '');
}
if (type !== narrow_type)
spot.groups[type] = spot.groups[narrow_type];
spot.groups[`${type}_text`] = contents.replace(/^(\*\*)?>/gm, '');
}
else if (spot.groups.pre2) {
[type, spot.groups.pre, spot.groups.pre_text] = [
'pre', spot.groups.pre2, spot.groups.pre2_text
];
}
const formatted_part = text.slice(from, spot.index);
const unescaped_part = this.unescape(formatted_part);
litter += formatted_part.length - unescaped_part.length;
from = spot.index + spot.groups[type].length;
const part = spot.groups[`${type}_text`];
const ni = ii + spot.index;
const nested = this._text_to_entities(part, indent, ni, litter);
const offset = indent + (ni - litter);
const length = nested.text.length;
litter += spot.groups[type].length - length;
let optional = {};
if (type === 'text_link') {
const link_spot = spot.groups.text_link_data.match(this._link_regexp);
[type, optional] = link_spot === null ? [
'text_link', { url: spot.groups.text_link_data }
] : link_spot.groups.type === 'user' ? [
'text_mention', { user: { id: +link_spot.groups.id } }
] : ['custom_emoji', { custom_emoji_id: link_spot.groups.id }];
}
else {
optional = {
...(!spot.groups.pre_language ? {}
: { language: spot.groups.pre_language }),
...(!spot.groups.custom_emoji_id ? {}
: { custom_emoji_id: spot.groups.custom_emoji_id })
};
}
result_text += unescaped_part + nested.text;
result_entities.push({ type, offset, length, ...optional });
result_entities.push(...nested.entities);
}
result_text += this.unescape(text.slice(from, text.length));
return { text: result_text, entities: result_entities };
}
static _clip(output, length) {
if (output.text.length > length) {
output.text = output.text.substring(0, length);
output.entities = output.entities.filter(entity => {
return entity.offset + entity.length <= length;
});
}
}
static _toggle(string, to) {
const { text, entities } = this.to_entities(string);
return to.from_entities(text, entities).text;
}
static b(string, escape = true) {
return this.bold(string, escape);
}
static i(string, escape = true) {
return this.italic(string, escape);
}
static u(string, escape = true) {
return this.underline(string, escape);
}
static s(string, escape = true) {
return this.strikethrough(string, escape);
}
static link(string, url, escape = true) {
return this.text_link(string, url, escape);
}
static mention(string, uid, escape = true) {
return this.text_mention(string, uid, escape);
}
static emoji(string, eid, escape = true) {
return this.custom_emoji(string, eid, escape);
}
static quote(string, expandable = false, escape = true) {
return this.blockquote(string, expandable, escape);
}
static bold(string, escape = true) {
return this.escape(string, [], escape);
}
static italic(string, escape = true) {
return this.escape(string, [], escape);
}
static underline(string, escape = true) {
return this.escape(string, [], escape);
}
static strikethrough(string, escape = true) {
return this.escape(string, [], escape);
}
static spoiler(string, escape = true) {
return this.escape(string, [], escape);
}
static text_link(string, url, escape = true) {
return this.escape(string, [], escape);
}
static text_mention(string, uid, escape = true) {
return this.escape(string, [], escape);
}
static custom_emoji(string, eid, escape = true) {
return this.escape(string, [], escape);
}
static code(string, escape = true) {
return this.escape(string, [], escape);
}
static pre(string, language = null, escape = true) {
return this.escape(string, [], escape);
}
static blockquote(string, expandable = false, escape = true) {
return this.escape(string, [], escape);
}
static expandable_blockquote(string, escape = true) {
return this.blockquote(string, true, escape);
}
static escape(string, excluded = [], escape = true, only = []) {
const to_escape = this._characters_to_escape(excluded, only, 1);
const escapist = this.name === 'html' ? (result, char) => {
return this._substitute(result, char, this._to_escape[char]);
} : (result, char) => this._substitute(result, char, `\\${char}`);
return this._escape(escape, string, to_escape, escapist);
}
static unescape(string, excluded = [], unescape = true, only = []) {
const to_unescape = this._characters_to_escape(excluded, only, -1);
const escapist = this.name === 'html' ? (result, char) => {
return this._substitute(result, this._to_escape[char], char);
} : (result, char) => this._substitute(result, `\\${char}`, char);
return this._escape(unescape, string, to_unescape, escapist);
}
static _characters_to_escape(excluded, only, vector) {
if (excluded === true)
excluded = ['\\', '*', '_', '~', '|', '`'];
const characters = Array.isArray(this._to_escape) ?
this._to_escape : Object.keys(this._to_escape);
const to_escape = only.length ?
only : this._subtract(characters, excluded);
return vector > 0 ? to_escape : to_escape.toReversed();
}
static _escape(escape, string, to_escape, escapist) {
return escape ? to_escape.reduce(escapist, string) : string;
}
static _subtract(array, extra) { return array.filter(value => !extra.includes(value)); }
static _substitute(string, char, withChar) { return string.split(char).join(withChar); }
}