feat: parse mode template literals override default/passed parse mode

This commit is contained in:
teidesu 2021-07-09 16:39:45 +03:00
parent be8ffe5b5b
commit d611f91f19
20 changed files with 123 additions and 83 deletions

View file

@ -171,6 +171,7 @@ import {
ChatsIndex,
Dialog,
FileDownloadParameters,
FormattedString,
GameHighScore,
IMessageEntityParser,
InputFileLike,
@ -1829,7 +1830,7 @@ export interface TelegramClient extends BaseTelegramClient {
*
* When `media` is passed, `media.caption` is used instead
*/
text?: string
text?: string | FormattedString
/**
* Parse mode to use to parse entities before sending
@ -1890,7 +1891,7 @@ export interface TelegramClient extends BaseTelegramClient {
*
* When `media` is passed, `media.caption` is used instead
*/
text?: string
text?: string | FormattedString
/**
* Parse mode to use to parse entities before sending
@ -1998,7 +1999,7 @@ export interface TelegramClient extends BaseTelegramClient {
* You can either pass `caption` or `captionMedia`, passing both will
* result in an error
*/
caption?: string
caption?: string | FormattedString
/**
* Optionally, a media caption for your forwarded message(s).
@ -2384,7 +2385,7 @@ export interface TelegramClient extends BaseTelegramClient {
/**
* New message caption (only used for media)
*/
caption?: string
caption?: string | FormattedString
/**
* Parse mode to use to parse `text` entities before sending
@ -2546,7 +2547,7 @@ export interface TelegramClient extends BaseTelegramClient {
* Can be used, for example. when using File IDs
* or when using existing InputMedia objects.
*/
caption?: string
caption?: string | FormattedString
/**
* Override entities for `media`.
@ -2636,7 +2637,7 @@ export interface TelegramClient extends BaseTelegramClient {
*/
sendText(
chatId: InputPeerLike,
text: string,
text: string | FormattedString,
params?: {
/**
* Message to reply to. Either a message object or message ID.
@ -2785,10 +2786,9 @@ export interface TelegramClient extends BaseTelegramClient {
* mode is also set as default.
*
* @param parseMode Parse mode to register
* @param name (default: `parseMode.name`) Parse mode name. By default is taken from the object.
* @throws MtCuteError When the parse mode with a given name is already registered.
*/
registerParseMode(parseMode: IMessageEntityParser, name?: string): void
registerParseMode(parseMode: IMessageEntityParser): void
/**
* Unregister a parse mode by its name.
* Will silently fail if given parse mode does not exist.

View file

@ -39,7 +39,8 @@ import {
BotCommands,
MessageMedia,
RawDocument,
IMessageEntityParser
IMessageEntityParser,
FormattedString
} from '../types'
// @copy

View file

@ -1,5 +1,5 @@
import { TelegramClient } from '../../client'
import { BotKeyboard, InputMediaLike, ReplyMarkup } from '../../types'
import { BotKeyboard, FormattedString, InputMediaLike, ReplyMarkup } from '../../types'
import { tl } from '@mtcute/tl'
/**
@ -20,7 +20,7 @@ export async function editInlineMessage(
*
* When `media` is passed, `media.caption` is used instead
*/
text?: string
text?: string | FormattedString
/**
* Parse mode to use to parse entities before sending

View file

@ -1,6 +1,6 @@
import { TelegramClient } from '../../client'
import {
BotKeyboard,
BotKeyboard, FormattedString,
InputMediaLike,
InputPeerLike,
Message,
@ -26,7 +26,7 @@ export async function editMessage(
*
* When `media` is passed, `media.caption` is used instead
*/
text?: string
text?: string | FormattedString
/**
* Parse mode to use to parse entities before sending

View file

@ -1,5 +1,6 @@
import { TelegramClient } from '../../client'
import {
FormattedString,
InputMediaLike,
InputPeerLike,
Message,
@ -74,7 +75,7 @@ export async function forwardMessages(
* You can either pass `caption` or `captionMedia`, passing both will
* result in an error
*/
caption?: string
caption?: string | FormattedString
/**
* Optionally, a media caption for your forwarded message(s).
@ -139,7 +140,7 @@ export async function forwardMessages(
* You can either pass `caption` or `captionMedia`, passing both will
* result in an error
*/
caption?: string
caption?: string | FormattedString
/**
* Optionally, a media caption for your forwarded message(s).

View file

@ -1,13 +1,14 @@
import { tl } from '@mtcute/tl'
import { TelegramClient } from '../../client'
import { normalizeToInputUser } from '../../utils/peer-utils'
import { FormattedString, MtCuteError } from '../../types'
const empty: [string, undefined] = ['', undefined]
/** @internal */
export async function _parseEntities(
this: TelegramClient,
text?: string,
text?: string | FormattedString,
mode?: string | null,
entities?: tl.TypeMessageEntity[]
): Promise<[string, tl.TypeMessageEntity[] | undefined]> {
@ -15,12 +16,22 @@ export async function _parseEntities(
return empty
}
if (typeof text === 'object') {
mode = text.mode
text = text.value
}
if (!entities) {
if (mode === undefined) {
mode = this._defaultParseMode
}
// either explicitly disabled or no available parser
if (!mode) return [text, []]
if (!(mode in this._parseModes)) {
throw new MtCuteError(`Parse mode ${mode} is not registered.`)
}
;[text, entities] = await this._parseModes[mode].parse(text)
}

View file

@ -1,5 +1,5 @@
import { TelegramClient } from '../../client'
import { InputPeerLike, Message, ReplyMarkup } from '../../types'
import { InputPeerLike, Message, FormattedString, ReplyMarkup } from '../../types'
import { tl } from '@mtcute/tl'
import { MessageNotFoundError } from '@mtcute/tl/errors'
@ -45,7 +45,7 @@ export async function sendCopy(
/**
* New message caption (only used for media)
*/
caption?: string
caption?: string | FormattedString
/**
* Parse mode to use to parse `text` entities before sending

View file

@ -1,6 +1,6 @@
import { TelegramClient } from '../../client'
import {
BotKeyboard,
BotKeyboard, FormattedString,
InputMediaLike,
InputPeerLike,
Message, MtCuteArgumentError,
@ -37,7 +37,7 @@ export async function sendMedia(
* Can be used, for example. when using File IDs
* or when using existing InputMedia objects.
*/
caption?: string
caption?: string | FormattedString
/**
* Override entities for `media`.

View file

@ -14,7 +14,7 @@ import {
UsersIndex,
MtCuteTypeAssertionError,
ChatsIndex,
MtCuteArgumentError,
MtCuteArgumentError, FormattedString,
} from '../../types'
import { getMarkedPeerId, MessageNotFoundError } from '@mtcute/core'
import { createDummyUpdate } from '../../utils/updates-utils'
@ -30,7 +30,7 @@ import { createDummyUpdate } from '../../utils/updates-utils'
export async function sendText(
this: TelegramClient,
chatId: InputPeerLike,
text: string,
text: string | FormattedString,
params?: {
/**
* Message to reply to. Either a message object or message ID.

View file

@ -7,15 +7,15 @@ import { MtCuteError, IMessageEntityParser } from '../../types'
* mode is also set as default.
*
* @param parseMode Parse mode to register
* @param name Parse mode name. By default is taken from the object.
* @throws MtCuteError When the parse mode with a given name is already registered.
* @internal
*/
export function registerParseMode(
this: TelegramClient,
parseMode: IMessageEntityParser,
name: string = parseMode.name
parseMode: IMessageEntityParser
): void {
const name = parseMode.name
if (name in this._parseModes) {
throw new MtCuteError(
`Parse mode ${name} is already registered. Unregister it first!`

View file

@ -7,6 +7,7 @@ import {
InputMediaGeoLive,
InputMediaVenue,
} from '../../media'
import { FormattedString } from '../../parser'
/**
* Inline message containing only text
@ -17,7 +18,7 @@ export interface InputInlineMessageText {
/**
* Text of the message
*/
text: string
text: string | FormattedString
/**
* Text markup entities.
@ -46,7 +47,7 @@ export interface InputInlineMessageMedia {
/**
* Caption for the media
*/
text?: string
text?: string | FormattedString
/**
* Caption markup entities.

View file

@ -2,6 +2,7 @@ import { InputFileLike } from '../files'
import { tl } from '@mtcute/tl'
import { Venue } from './venue'
import { MaybeArray } from '@mtcute/core'
import { FormattedString } from '../parser'
interface BaseInputMedia {
/**
@ -12,7 +13,7 @@ interface BaseInputMedia {
/**
* Caption of the media
*/
caption?: string
caption?: string | FormattedString
/**
* Caption entities of the media.
@ -517,7 +518,7 @@ export interface InputMediaQuiz extends Omit<InputMediaPoll, 'type'> {
/**
* Explanation of the quiz solution
*/
solution?: string
solution?: string | FormattedString
/**
* Format entities for `solution`.

View file

@ -12,6 +12,7 @@ import { makeInspectable } from '../utils'
import { InputMediaLike, WebPage } from '../media'
import { _messageActionFromTl, MessageAction } from './message-action'
import { _messageMediaFromTl, MessageMedia } from './message-media'
import { FormattedString } from '../parser'
/**
* A message or a service message
@ -557,7 +558,7 @@ export class Message {
* @param params
*/
answerText(
text: string,
text: string | FormattedString,
params?: Parameters<TelegramClient['sendText']>[2]
): ReturnType<TelegramClient['sendText']> {
return this.client.sendText(this.chat.inputPeer, text, params)
@ -602,7 +603,7 @@ export class Message {
* @param params
*/
replyText(
text: string,
text: string | FormattedString,
params?: Parameters<TelegramClient['sendText']>[2]
): ReturnType<TelegramClient['sendText']> {
if (!params) params = {}
@ -657,7 +658,7 @@ export class Message {
* @param params
*/
commentText(
text: string,
text: string | FormattedString,
params?: Parameters<TelegramClient['sendText']>[2]
): ReturnType<TelegramClient['sendText']> {
if (this.chat.type !== 'channel') {
@ -790,7 +791,7 @@ export class Message {
* @link TelegramClient.editMessage
*/
editText(
text: string,
text: string | FormattedString,
params?: Omit<Parameters<TelegramClient['editMessage']>[2], 'text'>
): Promise<Message> {
return this.edit({

View file

@ -13,9 +13,7 @@ import { MessageEntity } from '../types'
*/
export interface IMessageEntityParser {
/**
* Default name for the parser.
*
* Used when registering the parser as a fallback value for `name`
* Parser name, which will be used when registering it.
*/
name: string
@ -29,9 +27,9 @@ export interface IMessageEntityParser {
parse(text: string): [string, tl.TypeMessageEntity[]]
/**
* Add formating to the text given the plain text and the entities.
* Add formatting to the text given the plain text and the entities.
*
* **Note** that `unparse(parse(text)) === text` is not always true!
* > **Note**: `unparse(parse(text)) === text` is not always true!
*
* @param text Plain text
* @param entities Message entities that should be added to the text
@ -43,14 +41,14 @@ export interface IMessageEntityParser {
* Raw string that will not be escaped when passing
* to tagged template helpers (like `html` and `md`)
*/
export class RawString {
raw!: true
constructor (readonly value: string) {}
export class FormattedString {
/**
* @param value Value that the string holds
* @param mode Name of the parse mode used
*/
constructor (readonly value: string, readonly mode?: string) {}
toString(): string {
return this.value
}
}
RawString.prototype.raw = true

View file

@ -8,7 +8,7 @@ import { makeInspectable } from '../utils'
import { ChatsIndex, InputPeerLike, User, UsersIndex } from './index'
import { ChatLocation } from './chat-location'
import { InputMediaLike } from '../media'
import { RawString } from '../parser'
import { FormattedString } from '../parser'
export namespace Chat {
/**
@ -552,7 +552,7 @@ export class Chat {
* msg.replyText(`Hello, ${msg.chat.mention()`)
* ```
*/
mention(text?: string | null, parseMode?: string | null): string | RawString {
mention(text?: string | null, parseMode?: string | null): string | FormattedString {
if (this.user) return this.user.mention(text, parseMode)
if (text === undefined && this.username) {
@ -564,7 +564,7 @@ export class Chat {
if (!parseMode) parseMode = this.client['_defaultParseMode']
return new RawString(this.client.getParseMode(parseMode).unparse(text, [
return new FormattedString(this.client.getParseMode(parseMode).unparse(text, [
{
raw: undefined as any,
type: 'text_link',
@ -628,7 +628,7 @@ export class Chat {
* @param params
*/
sendText(
text: string,
text: string | FormattedString,
params?: Parameters<TelegramClient['sendText']>[2]
): ReturnType<TelegramClient['sendText']> {
return this.client.sendText(this.inputPeer, text, params)

View file

@ -5,7 +5,7 @@ import { MtCuteArgumentError } from '../errors'
import { makeInspectable } from '../utils'
import { assertTypeIs } from '../../utils/type-assertion'
import { InputMediaLike } from '../media'
import { RawString } from '../parser'
import { FormattedString } from '../parser'
export namespace User {
/**
@ -294,7 +294,7 @@ export class User {
* msg.replyText(`Hello, ${msg.sender.mention()`)
* ```
*/
mention(text?: string | null, parseMode?: string | null): string | RawString {
mention(text?: string | null, parseMode?: string | null): string | FormattedString {
if (text === undefined && this.username) {
return `@${this.username}`
}
@ -302,7 +302,7 @@ export class User {
if (!text) text = this.displayName
if (!parseMode) parseMode = this.client['_defaultParseMode']
return new RawString(this.client.getParseMode(parseMode).unparse(text, [
return new FormattedString(this.client.getParseMode(parseMode).unparse(text, [
{
raw: undefined as any,
type: 'text_mention',
@ -369,7 +369,7 @@ export class User {
* @param params
*/
sendText(
text: string,
text: string | FormattedString,
params?: Parameters<TelegramClient['sendText']>[2]
): ReturnType<TelegramClient['sendText']> {
return this.client.sendText(this.inputPeer, text, params)

View file

@ -1,4 +1,4 @@
import type { IMessageEntityParser, MessageEntity, RawString } from '@mtcute/client'
import type { IMessageEntityParser, MessageEntity, FormattedString } from '@mtcute/client'
import { tl } from '@mtcute/tl'
import { Parser } from 'htmlparser2'
import bigInt from 'big-integer'
@ -13,13 +13,18 @@ const MENTION_REGEX = /^tg:\/\/user\?id=(\d+)(?:&hash=(-?[0-9a-fA-F]+)(?:&|$)|&|
* const escaped = html`<b>${user.displayName}</b>`
* ```
*/
export function html(strings: TemplateStringsArray, ...sub: (string | RawString)[]): string {
export function html(strings: TemplateStringsArray, ...sub: (string | FormattedString)[]): FormattedString {
let str = ''
sub.forEach((it, idx) => {
if (typeof it === 'string') it = HtmlMessageEntityParser.escape(it)
else {
if (it.mode && it.mode !== 'html') throw new Error(`Incompatible parse mode: ${it.mode}`)
it = it.value
}
str += strings[idx] + it
})
return str + strings[strings.length - 1]
return { value: str + strings[strings.length - 1], mode: 'html' }
}
export namespace HtmlMessageEntityParser {

View file

@ -2,7 +2,7 @@ import { describe, it } from 'mocha'
import { expect } from 'chai'
import { tl } from '@mtcute/tl'
import { HtmlMessageEntityParser, html } from '../src'
import { MessageEntity, RawString } from '@mtcute/client'
import { MessageEntity, FormattedString } from '@mtcute/client'
import bigInt from 'big-integer'
const createEntity = <T extends tl.TypeMessageEntity['_']>(
@ -455,22 +455,30 @@ describe('HtmlMessageEntityParser', () => {
it('should work as a tagged template literal', () => {
const unsafeString = '<&>'
expect(html`${unsafeString}`).eq('&lt;&amp;&gt;')
expect(html`${unsafeString} <b>text</b>`).eq('&lt;&amp;&gt; <b>text</b>')
expect(html`<b>text</b> ${unsafeString}`).eq('<b>text</b> &lt;&amp;&gt;')
expect(html`<b>${unsafeString}</b>`).eq('<b>&lt;&amp;&gt;</b>')
expect(html`${unsafeString}`.value).eq('&lt;&amp;&gt;')
expect(html`${unsafeString} <b>text</b>`.value).eq('&lt;&amp;&gt; <b>text</b>')
expect(html`<b>text</b> ${unsafeString}`.value).eq('<b>text</b> &lt;&amp;&gt;')
expect(html`<b>${unsafeString}</b>`.value).eq('<b>&lt;&amp;&gt;</b>')
})
it('should skip with RawString', () => {
it('should skip with FormattedString', () => {
const unsafeString2 = '<&>'
const unsafeString = new RawString('<&>')
const unsafeString = new FormattedString('<&>')
expect(html`${unsafeString}`).eq('<&>')
expect(html`${unsafeString} ${unsafeString2}`).eq('<&> &lt;&amp;&gt;')
expect(html`${unsafeString} <b>text</b>`).eq('<&> <b>text</b>')
expect(html`<b>text</b> ${unsafeString}`).eq('<b>text</b> <&>')
expect(html`<b>${unsafeString}</b>`).eq('<b><&></b>')
expect(html`<b>${unsafeString} ${unsafeString2}</b>`).eq('<b><&> &lt;&amp;&gt;</b>')
expect(html`${unsafeString}`.value).eq('<&>')
expect(html`${unsafeString} ${unsafeString2}`.value).eq('<&> &lt;&amp;&gt;')
expect(html`${unsafeString} <b>text</b>`.value).eq('<&> <b>text</b>')
expect(html`<b>text</b> ${unsafeString}`.value).eq('<b>text</b> <&>')
expect(html`<b>${unsafeString}</b>`.value).eq('<b><&></b>')
expect(html`<b>${unsafeString} ${unsafeString2}</b>`.value).eq('<b><&> &lt;&amp;&gt;</b>')
})
it('should error with incompatible FormattedString', () => {
const unsafeString = new FormattedString('<&>', 'html')
const unsafeString2 = new FormattedString('<&>', 'some-other-mode')
expect(() => html`${unsafeString}`.value).not.throw(Error)
expect(() => html`${unsafeString2}`.value).throw(Error)
})
})
})

View file

@ -1,7 +1,7 @@
import type { IMessageEntityParser, MessageEntity } from '@mtcute/client'
import { tl } from '@mtcute/tl'
import bigInt from 'big-integer'
import { RawString } from '@mtcute/client'
import { FormattedString } from '@mtcute/client'
const MENTION_REGEX = /^tg:\/\/user\?id=(\d+)(?:&hash=(-?[0-9a-fA-F]+)(?:&|$)|&|$)/
@ -22,13 +22,18 @@ const TO_BE_ESCAPED = /[*_\-~`[\\\]]/g
* const escaped = md`**${user.displayName}**`
* ```
*/
export function md(strings: TemplateStringsArray, ...sub: (string | RawString)[]): string {
export function md(strings: TemplateStringsArray, ...sub: (string | FormattedString)[]): FormattedString {
let str = ''
sub.forEach((it, idx) => {
if (typeof it === 'string') it = MarkdownMessageEntityParser.escape(it as string)
else {
if (it.mode && it.mode !== 'markdown') throw new Error(`Incompatible parse mode: ${it.mode}`)
it = it.value
}
str += strings[idx] + it
})
return str + strings[strings.length - 1]
return { value: str + strings[strings.length - 1], mode: 'markdown' }
}
/**

View file

@ -1,7 +1,7 @@
import { describe, it } from 'mocha'
import { expect } from 'chai'
import { tl } from '@mtcute/tl'
import { MessageEntity, RawString } from '@mtcute/client'
import { MessageEntity, FormattedString } from '@mtcute/client'
import { MarkdownMessageEntityParser, md } from '../src'
import bigInt from 'big-integer'
@ -652,21 +652,29 @@ describe('MarkdownMessageEntityParser', () => {
it('should work as a tagged template literal', () => {
const unsafeString = '__[]__'
expect(md`${unsafeString}`).eq('\\_\\_\\[\\]\\_\\_')
expect(md`${unsafeString} **text**`).eq('\\_\\_\\[\\]\\_\\_ **text**')
expect(md`**text** ${unsafeString}`).eq('**text** \\_\\_\\[\\]\\_\\_')
expect(md`**${unsafeString}**`).eq('**\\_\\_\\[\\]\\_\\_**')
expect(md`${unsafeString}`.value).eq('\\_\\_\\[\\]\\_\\_')
expect(md`${unsafeString} **text**`.value).eq('\\_\\_\\[\\]\\_\\_ **text**')
expect(md`**text** ${unsafeString}`.value).eq('**text** \\_\\_\\[\\]\\_\\_')
expect(md`**${unsafeString}**`.value).eq('**\\_\\_\\[\\]\\_\\_**')
})
it('should skip with RawString', () => {
it('should skip with FormattedString', () => {
const unsafeString2 = '__[]__'
const unsafeString = new RawString('__[]__')
const unsafeString = new FormattedString('__[]__')
expect(md`${unsafeString}`).eq('__[]__')
expect(md`${unsafeString} ${unsafeString2}`).eq('__[]__ \\_\\_\\[\\]\\_\\_')
expect(md`${unsafeString} **text**`).eq('__[]__ **text**')
expect(md`**text** ${unsafeString}`).eq('**text** __[]__')
expect(md`**${unsafeString} ${unsafeString2}**`).eq('**__[]__ \\_\\_\\[\\]\\_\\_**')
expect(md`${unsafeString}`.value).eq('__[]__')
expect(md`${unsafeString} ${unsafeString2}`.value).eq('__[]__ \\_\\_\\[\\]\\_\\_')
expect(md`${unsafeString} **text**`.value).eq('__[]__ **text**')
expect(md`**text** ${unsafeString}`.value).eq('**text** __[]__')
expect(md`**${unsafeString} ${unsafeString2}**`.value).eq('**__[]__ \\_\\_\\[\\]\\_\\_**')
})
it('should error with incompatible FormattedString', () => {
const unsafeString = new FormattedString('<&>', 'markdown')
const unsafeString2 = new FormattedString('<&>', 'some-other-mode')
expect(() => md`${unsafeString}`.value).not.throw(Error)
expect(() => md`${unsafeString2}`.value).throw(Error)
})
})
})