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, ChatsIndex,
Dialog, Dialog,
FileDownloadParameters, FileDownloadParameters,
FormattedString,
GameHighScore, GameHighScore,
IMessageEntityParser, IMessageEntityParser,
InputFileLike, InputFileLike,
@ -1829,7 +1830,7 @@ export interface TelegramClient extends BaseTelegramClient {
* *
* When `media` is passed, `media.caption` is used instead * When `media` is passed, `media.caption` is used instead
*/ */
text?: string text?: string | FormattedString
/** /**
* Parse mode to use to parse entities before sending * 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 * When `media` is passed, `media.caption` is used instead
*/ */
text?: string text?: string | FormattedString
/** /**
* Parse mode to use to parse entities before sending * 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 * You can either pass `caption` or `captionMedia`, passing both will
* result in an error * result in an error
*/ */
caption?: string caption?: string | FormattedString
/** /**
* Optionally, a media caption for your forwarded message(s). * 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) * New message caption (only used for media)
*/ */
caption?: string caption?: string | FormattedString
/** /**
* Parse mode to use to parse `text` entities before sending * 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 * Can be used, for example. when using File IDs
* or when using existing InputMedia objects. * or when using existing InputMedia objects.
*/ */
caption?: string caption?: string | FormattedString
/** /**
* Override entities for `media`. * Override entities for `media`.
@ -2636,7 +2637,7 @@ export interface TelegramClient extends BaseTelegramClient {
*/ */
sendText( sendText(
chatId: InputPeerLike, chatId: InputPeerLike,
text: string, text: string | FormattedString,
params?: { params?: {
/** /**
* Message to reply to. Either a message object or message ID. * 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. * mode is also set as default.
* *
* @param parseMode Parse mode to register * @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. * @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. * Unregister a parse mode by its name.
* Will silently fail if given parse mode does not exist. * Will silently fail if given parse mode does not exist.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -14,7 +14,7 @@ import {
UsersIndex, UsersIndex,
MtCuteTypeAssertionError, MtCuteTypeAssertionError,
ChatsIndex, ChatsIndex,
MtCuteArgumentError, MtCuteArgumentError, FormattedString,
} from '../../types' } from '../../types'
import { getMarkedPeerId, MessageNotFoundError } from '@mtcute/core' import { getMarkedPeerId, MessageNotFoundError } from '@mtcute/core'
import { createDummyUpdate } from '../../utils/updates-utils' import { createDummyUpdate } from '../../utils/updates-utils'
@ -30,7 +30,7 @@ import { createDummyUpdate } from '../../utils/updates-utils'
export async function sendText( export async function sendText(
this: TelegramClient, this: TelegramClient,
chatId: InputPeerLike, chatId: InputPeerLike,
text: string, text: string | FormattedString,
params?: { params?: {
/** /**
* Message to reply to. Either a message object or message ID. * 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. * mode is also set as default.
* *
* @param parseMode Parse mode to register * @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. * @throws MtCuteError When the parse mode with a given name is already registered.
* @internal * @internal
*/ */
export function registerParseMode( export function registerParseMode(
this: TelegramClient, this: TelegramClient,
parseMode: IMessageEntityParser, parseMode: IMessageEntityParser
name: string = parseMode.name
): void { ): void {
const name = parseMode.name
if (name in this._parseModes) { if (name in this._parseModes) {
throw new MtCuteError( throw new MtCuteError(
`Parse mode ${name} is already registered. Unregister it first!` `Parse mode ${name} is already registered. Unregister it first!`

View file

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

View file

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

View file

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

View file

@ -13,9 +13,7 @@ import { MessageEntity } from '../types'
*/ */
export interface IMessageEntityParser { export interface IMessageEntityParser {
/** /**
* Default name for the parser. * Parser name, which will be used when registering it.
*
* Used when registering the parser as a fallback value for `name`
*/ */
name: string name: string
@ -29,9 +27,9 @@ export interface IMessageEntityParser {
parse(text: string): [string, tl.TypeMessageEntity[]] 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 text Plain text
* @param entities Message entities that should be added to the 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 * Raw string that will not be escaped when passing
* to tagged template helpers (like `html` and `md`) * to tagged template helpers (like `html` and `md`)
*/ */
export class RawString { export class FormattedString {
raw!: true /**
* @param value Value that the string holds
constructor (readonly value: string) {} * @param mode Name of the parse mode used
*/
constructor (readonly value: string, readonly mode?: string) {}
toString(): string { toString(): string {
return this.value 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 { ChatsIndex, InputPeerLike, User, UsersIndex } from './index'
import { ChatLocation } from './chat-location' import { ChatLocation } from './chat-location'
import { InputMediaLike } from '../media' import { InputMediaLike } from '../media'
import { RawString } from '../parser' import { FormattedString } from '../parser'
export namespace Chat { export namespace Chat {
/** /**
@ -552,7 +552,7 @@ export class Chat {
* msg.replyText(`Hello, ${msg.chat.mention()`) * 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 (this.user) return this.user.mention(text, parseMode)
if (text === undefined && this.username) { if (text === undefined && this.username) {
@ -564,7 +564,7 @@ export class Chat {
if (!parseMode) parseMode = this.client['_defaultParseMode'] 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, raw: undefined as any,
type: 'text_link', type: 'text_link',
@ -628,7 +628,7 @@ export class Chat {
* @param params * @param params
*/ */
sendText( sendText(
text: string, text: string | FormattedString,
params?: Parameters<TelegramClient['sendText']>[2] params?: Parameters<TelegramClient['sendText']>[2]
): ReturnType<TelegramClient['sendText']> { ): ReturnType<TelegramClient['sendText']> {
return this.client.sendText(this.inputPeer, text, params) return this.client.sendText(this.inputPeer, text, params)

View file

@ -5,7 +5,7 @@ import { MtCuteArgumentError } from '../errors'
import { makeInspectable } from '../utils' import { makeInspectable } from '../utils'
import { assertTypeIs } from '../../utils/type-assertion' import { assertTypeIs } from '../../utils/type-assertion'
import { InputMediaLike } from '../media' import { InputMediaLike } from '../media'
import { RawString } from '../parser' import { FormattedString } from '../parser'
export namespace User { export namespace User {
/** /**
@ -294,7 +294,7 @@ export class User {
* msg.replyText(`Hello, ${msg.sender.mention()`) * 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) { if (text === undefined && this.username) {
return `@${this.username}` return `@${this.username}`
} }
@ -302,7 +302,7 @@ export class User {
if (!text) text = this.displayName if (!text) text = this.displayName
if (!parseMode) parseMode = this.client['_defaultParseMode'] 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, raw: undefined as any,
type: 'text_mention', type: 'text_mention',
@ -369,7 +369,7 @@ export class User {
* @param params * @param params
*/ */
sendText( sendText(
text: string, text: string | FormattedString,
params?: Parameters<TelegramClient['sendText']>[2] params?: Parameters<TelegramClient['sendText']>[2]
): ReturnType<TelegramClient['sendText']> { ): ReturnType<TelegramClient['sendText']> {
return this.client.sendText(this.inputPeer, text, params) 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 { tl } from '@mtcute/tl'
import { Parser } from 'htmlparser2' import { Parser } from 'htmlparser2'
import bigInt from 'big-integer' 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>` * 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 = '' let str = ''
sub.forEach((it, idx) => { sub.forEach((it, idx) => {
if (typeof it === 'string') it = HtmlMessageEntityParser.escape(it) 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 str += strings[idx] + it
}) })
return str + strings[strings.length - 1] return { value: str + strings[strings.length - 1], mode: 'html' }
} }
export namespace HtmlMessageEntityParser { export namespace HtmlMessageEntityParser {

View file

@ -2,7 +2,7 @@ import { describe, it } from 'mocha'
import { expect } from 'chai' import { expect } from 'chai'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { HtmlMessageEntityParser, html } from '../src' import { HtmlMessageEntityParser, html } from '../src'
import { MessageEntity, RawString } from '@mtcute/client' import { MessageEntity, FormattedString } from '@mtcute/client'
import bigInt from 'big-integer' import bigInt from 'big-integer'
const createEntity = <T extends tl.TypeMessageEntity['_']>( const createEntity = <T extends tl.TypeMessageEntity['_']>(
@ -455,22 +455,30 @@ describe('HtmlMessageEntityParser', () => {
it('should work as a tagged template literal', () => { it('should work as a tagged template literal', () => {
const unsafeString = '<&>' const unsafeString = '<&>'
expect(html`${unsafeString}`).eq('&lt;&amp;&gt;') expect(html`${unsafeString}`.value).eq('&lt;&amp;&gt;')
expect(html`${unsafeString} <b>text</b>`).eq('&lt;&amp;&gt; <b>text</b>') expect(html`${unsafeString} <b>text</b>`.value).eq('&lt;&amp;&gt; <b>text</b>')
expect(html`<b>text</b> ${unsafeString}`).eq('<b>text</b> &lt;&amp;&gt;') expect(html`<b>text</b> ${unsafeString}`.value).eq('<b>text</b> &lt;&amp;&gt;')
expect(html`<b>${unsafeString}</b>`).eq('<b>&lt;&amp;&gt;</b>') 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 unsafeString2 = '<&>'
const unsafeString = new RawString('<&>') const unsafeString = new FormattedString('<&>')
expect(html`${unsafeString}`).eq('<&>') expect(html`${unsafeString}`.value).eq('<&>')
expect(html`${unsafeString} ${unsafeString2}`).eq('<&> &lt;&amp;&gt;') expect(html`${unsafeString} ${unsafeString2}`.value).eq('<&> &lt;&amp;&gt;')
expect(html`${unsafeString} <b>text</b>`).eq('<&> <b>text</b>') expect(html`${unsafeString} <b>text</b>`.value).eq('<&> <b>text</b>')
expect(html`<b>text</b> ${unsafeString}`).eq('<b>text</b> <&>') expect(html`<b>text</b> ${unsafeString}`.value).eq('<b>text</b> <&>')
expect(html`<b>${unsafeString}</b>`).eq('<b><&></b>') expect(html`<b>${unsafeString}</b>`.value).eq('<b><&></b>')
expect(html`<b>${unsafeString} ${unsafeString2}</b>`).eq('<b><&> &lt;&amp;&gt;</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 type { IMessageEntityParser, MessageEntity } from '@mtcute/client'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import bigInt from 'big-integer' 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]+)(?:&|$)|&|$)/ 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}**` * 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 = '' let str = ''
sub.forEach((it, idx) => { sub.forEach((it, idx) => {
if (typeof it === 'string') it = MarkdownMessageEntityParser.escape(it as string) 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 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 { describe, it } from 'mocha'
import { expect } from 'chai' import { expect } from 'chai'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { MessageEntity, RawString } from '@mtcute/client' import { MessageEntity, FormattedString } from '@mtcute/client'
import { MarkdownMessageEntityParser, md } from '../src' import { MarkdownMessageEntityParser, md } from '../src'
import bigInt from 'big-integer' import bigInt from 'big-integer'
@ -652,21 +652,29 @@ describe('MarkdownMessageEntityParser', () => {
it('should work as a tagged template literal', () => { it('should work as a tagged template literal', () => {
const unsafeString = '__[]__' const unsafeString = '__[]__'
expect(md`${unsafeString}`).eq('\\_\\_\\[\\]\\_\\_') expect(md`${unsafeString}`.value).eq('\\_\\_\\[\\]\\_\\_')
expect(md`${unsafeString} **text**`).eq('\\_\\_\\[\\]\\_\\_ **text**') expect(md`${unsafeString} **text**`.value).eq('\\_\\_\\[\\]\\_\\_ **text**')
expect(md`**text** ${unsafeString}`).eq('**text** \\_\\_\\[\\]\\_\\_') expect(md`**text** ${unsafeString}`.value).eq('**text** \\_\\_\\[\\]\\_\\_')
expect(md`**${unsafeString}**`).eq('**\\_\\_\\[\\]\\_\\_**') expect(md`**${unsafeString}**`.value).eq('**\\_\\_\\[\\]\\_\\_**')
}) })
it('should skip with RawString', () => { it('should skip with FormattedString', () => {
const unsafeString2 = '__[]__' const unsafeString2 = '__[]__'
const unsafeString = new RawString('__[]__') const unsafeString = new FormattedString('__[]__')
expect(md`${unsafeString}`).eq('__[]__') expect(md`${unsafeString}`.value).eq('__[]__')
expect(md`${unsafeString} ${unsafeString2}`).eq('__[]__ \\_\\_\\[\\]\\_\\_') expect(md`${unsafeString} ${unsafeString2}`.value).eq('__[]__ \\_\\_\\[\\]\\_\\_')
expect(md`${unsafeString} **text**`).eq('__[]__ **text**') expect(md`${unsafeString} **text**`.value).eq('__[]__ **text**')
expect(md`**text** ${unsafeString}`).eq('**text** __[]__') expect(md`**text** ${unsafeString}`.value).eq('**text** __[]__')
expect(md`**${unsafeString} ${unsafeString2}**`).eq('**__[]__ \\_\\_\\[\\]\\_\\_**') 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)
}) })
}) })
}) })