refactor: no more parse modes!
This commit is contained in:
parent
cfa7e8ef5c
commit
23a0e69942
37 changed files with 1022 additions and 1389 deletions
|
@ -12,8 +12,8 @@
|
||||||
"test:all:ci": "pnpm run -r test",
|
"test:all:ci": "pnpm run -r test",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint:ci": "NODE_OPTIONS=\"--max_old_space_size=8192\" eslint --config .eslintrc.ci.js .",
|
"lint:ci": "NODE_OPTIONS=\"--max_old_space_size=8192\" eslint --config .eslintrc.ci.js .",
|
||||||
"lint:tsc": "pnpm -r --filter=!crypto --parallel exec tsc --build",
|
"lint:tsc": "pnpm -r --parallel exec tsc --build",
|
||||||
"lint:tsc:ci": "pnpm -r --filter=!crypto exec tsc --build",
|
"lint:tsc:ci": "pnpm -r exec tsc --build",
|
||||||
"lint:dpdm": "dpdm -T --no-warning --no-tree --exit-code circular:1 packages/*",
|
"lint:dpdm": "dpdm -T --no-warning --no-tree --exit-code circular:1 packages/*",
|
||||||
"lint:fix": "eslint --fix .",
|
"lint:fix": "eslint --fix .",
|
||||||
"format": "prettier --write \"packages/**/*.ts\"",
|
"format": "prettier --write \"packages/**/*.ts\"",
|
||||||
|
|
|
@ -167,12 +167,6 @@ import { unpinAllMessages } from './methods/messages/unpin-all-messages.js'
|
||||||
import { unpinMessage } from './methods/messages/unpin-message.js'
|
import { unpinMessage } from './methods/messages/unpin-message.js'
|
||||||
import { initTakeoutSession } from './methods/misc/init-takeout-session.js'
|
import { initTakeoutSession } from './methods/misc/init-takeout-session.js'
|
||||||
import { _normalizePrivacyRules } from './methods/misc/normalize-privacy-rules.js'
|
import { _normalizePrivacyRules } from './methods/misc/normalize-privacy-rules.js'
|
||||||
import {
|
|
||||||
getParseMode,
|
|
||||||
registerParseMode,
|
|
||||||
setDefaultParseMode,
|
|
||||||
unregisterParseMode,
|
|
||||||
} from './methods/parse-modes/parse-modes.js'
|
|
||||||
import { changeCloudPassword } from './methods/password/change-cloud-password.js'
|
import { changeCloudPassword } from './methods/password/change-cloud-password.js'
|
||||||
import { enableCloudPassword } from './methods/password/enable-cloud-password.js'
|
import { enableCloudPassword } from './methods/password/enable-cloud-password.js'
|
||||||
import { cancelPasswordEmail, resendPasswordEmail, verifyPasswordEmail } from './methods/password/password-email.js'
|
import { cancelPasswordEmail, resendPasswordEmail, verifyPasswordEmail } from './methods/password/password-email.js'
|
||||||
|
@ -271,11 +265,9 @@ import {
|
||||||
Dialog,
|
Dialog,
|
||||||
FileDownloadLocation,
|
FileDownloadLocation,
|
||||||
FileDownloadParameters,
|
FileDownloadParameters,
|
||||||
FormattedString,
|
|
||||||
ForumTopic,
|
ForumTopic,
|
||||||
GameHighScore,
|
GameHighScore,
|
||||||
HistoryReadUpdate,
|
HistoryReadUpdate,
|
||||||
IMessageEntityParser,
|
|
||||||
InlineQuery,
|
InlineQuery,
|
||||||
InputChatEventFilters,
|
InputChatEventFilters,
|
||||||
InputDialogFolder,
|
InputDialogFolder,
|
||||||
|
@ -288,6 +280,7 @@ import {
|
||||||
InputReaction,
|
InputReaction,
|
||||||
InputStickerSet,
|
InputStickerSet,
|
||||||
InputStickerSetItem,
|
InputStickerSetItem,
|
||||||
|
InputText,
|
||||||
MaybeDynamic,
|
MaybeDynamic,
|
||||||
Message,
|
Message,
|
||||||
MessageEntity,
|
MessageEntity,
|
||||||
|
@ -852,18 +845,6 @@ export interface TelegramClient extends BaseTelegramClient {
|
||||||
*/
|
*/
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse mode to use when parsing inline message text.
|
|
||||||
*
|
|
||||||
* Passing `null` will explicitly disable formatting.
|
|
||||||
*
|
|
||||||
* **Note**: inline results themselves *can not* have markup
|
|
||||||
* entities, only the messages that are sent once a result is clicked.
|
|
||||||
*
|
|
||||||
* @default current default parse mode (if any).
|
|
||||||
*/
|
|
||||||
parseMode?: string | null
|
|
||||||
},
|
},
|
||||||
): Promise<void>
|
): Promise<void>
|
||||||
/**
|
/**
|
||||||
|
@ -2232,8 +2213,7 @@ export interface TelegramClient extends BaseTelegramClient {
|
||||||
*/
|
*/
|
||||||
_normalizeInputMedia(
|
_normalizeInputMedia(
|
||||||
media: InputMediaLike,
|
media: InputMediaLike,
|
||||||
params: {
|
params?: {
|
||||||
parseMode?: string | null
|
|
||||||
progressCallback?: (uploaded: number, total: number) => void
|
progressCallback?: (uploaded: number, total: number) => void
|
||||||
uploadPeer?: tl.TypeInputPeer
|
uploadPeer?: tl.TypeInputPeer
|
||||||
},
|
},
|
||||||
|
@ -2929,26 +2909,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 | FormattedString<string>
|
text?: InputText
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse mode to use to parse entities before sending the message.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* Passing `null` will explicitly disable formatting.
|
|
||||||
* @default current default parse mode (if any).
|
|
||||||
*/
|
|
||||||
parseMode?: string | null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of formatting entities to use instead of parsing via a
|
|
||||||
* parse mode.
|
|
||||||
*
|
|
||||||
* **Note:** Passing this makes the method ignore {@link parseMode}
|
|
||||||
*
|
|
||||||
* When `media` is passed, `media.entities` is used instead
|
|
||||||
*/
|
|
||||||
entities?: tl.TypeMessageEntity[]
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* New message media
|
* New message media
|
||||||
|
@ -2998,26 +2959,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 | FormattedString<string>
|
text?: InputText
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse mode to use to parse entities before sending the message.
|
|
||||||
*
|
|
||||||
* Passing `null` will explicitly disable formatting.
|
|
||||||
*
|
|
||||||
* @default current default parse mode (if any).
|
|
||||||
*/
|
|
||||||
parseMode?: string | null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of formatting entities to use instead of parsing via a
|
|
||||||
* parse mode.
|
|
||||||
*
|
|
||||||
* **Note:** Passing this makes the method ignore {@link parseMode}
|
|
||||||
*
|
|
||||||
* When `media` is passed, `media.entities` is used instead
|
|
||||||
*/
|
|
||||||
entities?: tl.TypeMessageEntity[]
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* New message media
|
* New message media
|
||||||
|
@ -3068,8 +3010,6 @@ export interface TelegramClient extends BaseTelegramClient {
|
||||||
* Forward one or more messages by their IDs.
|
* Forward one or more messages by their IDs.
|
||||||
* You can forward no more than 100 messages at once.
|
* You can forward no more than 100 messages at once.
|
||||||
*
|
*
|
||||||
* If a caption message was sent, it will be the first message in the resulting array.
|
|
||||||
*
|
|
||||||
* **Available**: ✅ both users and bots
|
* **Available**: ✅ both users and bots
|
||||||
*
|
*
|
||||||
* @param toChatId Destination chat ID, username, phone, `"me"` or `"self"`
|
* @param toChatId Destination chat ID, username, phone, `"me"` or `"self"`
|
||||||
|
@ -3784,15 +3724,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 | FormattedString<string>
|
caption?: InputText
|
||||||
|
|
||||||
/**
|
|
||||||
* Override entities for `media`.
|
|
||||||
*
|
|
||||||
* Can be used, for example. when using File IDs
|
|
||||||
* or when using existing InputMedia objects.
|
|
||||||
*/
|
|
||||||
entities?: tl.TypeMessageEntity[]
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function that will be called after some part has been uploaded.
|
* Function that will be called after some part has been uploaded.
|
||||||
|
@ -3883,7 +3815,7 @@ export interface TelegramClient extends BaseTelegramClient {
|
||||||
*/
|
*/
|
||||||
sendText(
|
sendText(
|
||||||
chatId: InputPeerLike,
|
chatId: InputPeerLike,
|
||||||
text: string | FormattedString<string>,
|
text: InputText,
|
||||||
params?: CommonSendParams & {
|
params?: CommonSendParams & {
|
||||||
/**
|
/**
|
||||||
* For bots: inline or reply markup or an instruction
|
* For bots: inline or reply markup or an instruction
|
||||||
|
@ -3891,14 +3823,6 @@ export interface TelegramClient extends BaseTelegramClient {
|
||||||
*/
|
*/
|
||||||
replyMarkup?: ReplyMarkup
|
replyMarkup?: ReplyMarkup
|
||||||
|
|
||||||
/**
|
|
||||||
* List of formatting entities to use instead of parsing via a
|
|
||||||
* parse mode.
|
|
||||||
*
|
|
||||||
* **Note:** Passing this makes the method ignore {@link parseMode}
|
|
||||||
*/
|
|
||||||
entities?: tl.TypeMessageEntity[]
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to disable links preview in this message
|
* Whether to disable links preview in this message
|
||||||
*/
|
*/
|
||||||
|
@ -4027,47 +3951,6 @@ export interface TelegramClient extends BaseTelegramClient {
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
_normalizePrivacyRules(rules: InputPrivacyRule[]): Promise<tl.TypeInputPrivacyRule[]>
|
_normalizePrivacyRules(rules: InputPrivacyRule[]): Promise<tl.TypeInputPrivacyRule[]>
|
||||||
/**
|
|
||||||
* Register a given {@link IMessageEntityParser} as a parse mode
|
|
||||||
* for messages. When this method is first called, given parse
|
|
||||||
* mode is also set as default.
|
|
||||||
*
|
|
||||||
* **Available**: ✅ both users and bots
|
|
||||||
*
|
|
||||||
* @param parseMode Parse mode to register
|
|
||||||
* @throws MtClientError When the parse mode with a given name is already registered.
|
|
||||||
*/
|
|
||||||
registerParseMode(parseMode: IMessageEntityParser): void
|
|
||||||
/**
|
|
||||||
* Unregister a parse mode by its name.
|
|
||||||
* Will silently fail if given parse mode does not exist.
|
|
||||||
*
|
|
||||||
* Also updates the default parse mode to the next one available, if any
|
|
||||||
*
|
|
||||||
* **Available**: ✅ both users and bots
|
|
||||||
*
|
|
||||||
* @param name Name of the parse mode to unregister
|
|
||||||
*/
|
|
||||||
unregisterParseMode(name: string): void
|
|
||||||
/**
|
|
||||||
* Get a {@link IMessageEntityParser} registered under a given name (or a default one).
|
|
||||||
*
|
|
||||||
* **Available**: ✅ both users and bots
|
|
||||||
*
|
|
||||||
* @param name Name of the parse mode which parser to get.
|
|
||||||
* @throws MtClientError When the provided parse mode is not registered
|
|
||||||
* @throws MtClientError When `name` is omitted and there is no default parse mode
|
|
||||||
*/
|
|
||||||
getParseMode(name?: string | null): IMessageEntityParser
|
|
||||||
/**
|
|
||||||
* Set a given parse mode as a default one.
|
|
||||||
*
|
|
||||||
* **Available**: ✅ both users and bots
|
|
||||||
*
|
|
||||||
* @param name Name of the parse mode
|
|
||||||
* @throws MtClientError When given parse mode is not registered.
|
|
||||||
*/
|
|
||||||
setDefaultParseMode(name: string): void
|
|
||||||
/**
|
/**
|
||||||
* Change your 2FA password
|
* Change your 2FA password
|
||||||
* **Available**: 👤 users only
|
* **Available**: 👤 users only
|
||||||
|
@ -4422,20 +4305,7 @@ export interface TelegramClient extends BaseTelegramClient {
|
||||||
/**
|
/**
|
||||||
* Override caption for {@link media}
|
* Override caption for {@link media}
|
||||||
*/
|
*/
|
||||||
caption?: string | FormattedString<string>
|
caption?: InputText
|
||||||
|
|
||||||
/**
|
|
||||||
* Override entities for {@link media}
|
|
||||||
*/
|
|
||||||
entities?: tl.TypeMessageEntity[]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse mode to use to parse entities before sending the message.
|
|
||||||
* Passing `null` will explicitly disable formatting.
|
|
||||||
*
|
|
||||||
* @default current default parse mode (if any).
|
|
||||||
*/
|
|
||||||
parseMode?: string | null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interactive elements to add to the story
|
* Interactive elements to add to the story
|
||||||
|
@ -4806,20 +4676,7 @@ export interface TelegramClient extends BaseTelegramClient {
|
||||||
/**
|
/**
|
||||||
* Override caption for {@link media}
|
* Override caption for {@link media}
|
||||||
*/
|
*/
|
||||||
caption?: string | FormattedString<string>
|
caption?: InputText
|
||||||
|
|
||||||
/**
|
|
||||||
* Override entities for {@link media}
|
|
||||||
*/
|
|
||||||
entities?: tl.TypeMessageEntity[]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse mode to use to parse entities before sending the message.
|
|
||||||
* Passing `null` will explicitly disable formatting.
|
|
||||||
*
|
|
||||||
* @default current default parse mode (if any).
|
|
||||||
*/
|
|
||||||
parseMode?: string | null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to automatically pin this story to the profile
|
* Whether to automatically pin this story to the profile
|
||||||
|
@ -5422,10 +5279,6 @@ export class TelegramClient extends BaseTelegramClient {
|
||||||
unpinMessage = unpinMessage.bind(null, this)
|
unpinMessage = unpinMessage.bind(null, this)
|
||||||
initTakeoutSession = initTakeoutSession.bind(null, this)
|
initTakeoutSession = initTakeoutSession.bind(null, this)
|
||||||
_normalizePrivacyRules = _normalizePrivacyRules.bind(null, this)
|
_normalizePrivacyRules = _normalizePrivacyRules.bind(null, this)
|
||||||
registerParseMode = registerParseMode.bind(null, this)
|
|
||||||
unregisterParseMode = unregisterParseMode.bind(null, this)
|
|
||||||
getParseMode = getParseMode.bind(null, this)
|
|
||||||
setDefaultParseMode = setDefaultParseMode.bind(null, this)
|
|
||||||
changeCloudPassword = changeCloudPassword.bind(null, this)
|
changeCloudPassword = changeCloudPassword.bind(null, this)
|
||||||
enableCloudPassword = enableCloudPassword.bind(null, this)
|
enableCloudPassword = enableCloudPassword.bind(null, this)
|
||||||
verifyPasswordEmail = verifyPasswordEmail.bind(null, this)
|
verifyPasswordEmail = verifyPasswordEmail.bind(null, this)
|
||||||
|
|
|
@ -38,11 +38,9 @@ import {
|
||||||
Dialog,
|
Dialog,
|
||||||
FileDownloadLocation,
|
FileDownloadLocation,
|
||||||
FileDownloadParameters,
|
FileDownloadParameters,
|
||||||
FormattedString,
|
|
||||||
ForumTopic,
|
ForumTopic,
|
||||||
GameHighScore,
|
GameHighScore,
|
||||||
HistoryReadUpdate,
|
HistoryReadUpdate,
|
||||||
IMessageEntityParser,
|
|
||||||
InlineQuery,
|
InlineQuery,
|
||||||
InputChatEventFilters,
|
InputChatEventFilters,
|
||||||
InputDialogFolder,
|
InputDialogFolder,
|
||||||
|
@ -55,6 +53,7 @@ import {
|
||||||
InputReaction,
|
InputReaction,
|
||||||
InputStickerSet,
|
InputStickerSet,
|
||||||
InputStickerSetItem,
|
InputStickerSetItem,
|
||||||
|
InputText,
|
||||||
MaybeDynamic,
|
MaybeDynamic,
|
||||||
Message,
|
Message,
|
||||||
MessageEntity,
|
MessageEntity,
|
||||||
|
|
|
@ -96,23 +96,11 @@ export async function answerInlineQuery(
|
||||||
*/
|
*/
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse mode to use when parsing inline message text.
|
|
||||||
*
|
|
||||||
* Passing `null` will explicitly disable formatting.
|
|
||||||
*
|
|
||||||
* **Note**: inline results themselves *can not* have markup
|
|
||||||
* entities, only the messages that are sent once a result is clicked.
|
|
||||||
*
|
|
||||||
* @default current default parse mode (if any).
|
|
||||||
*/
|
|
||||||
parseMode?: string | null
|
|
||||||
},
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { cacheTime = 300, gallery, private: priv, nextOffset, switchPm, switchWebview, parseMode } = params ?? {}
|
const { cacheTime = 300, gallery, private: priv, nextOffset, switchPm, switchWebview } = params ?? {}
|
||||||
|
|
||||||
const [defaultGallery, tlResults] = await BotInline._convertToTl(client, results, parseMode)
|
const [defaultGallery, tlResults] = await BotInline._convertToTl(client, results)
|
||||||
|
|
||||||
await client.call({
|
await client.call({
|
||||||
_: 'messages.setInlineBotResults',
|
_: 'messages.setInlineBotResults',
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { InputMediaLike } from '../../types/media/input-media.js'
|
||||||
import { extractFileName } from '../../utils/file-utils.js'
|
import { extractFileName } from '../../utils/file-utils.js'
|
||||||
import { normalizeDate } from '../../utils/misc-utils.js'
|
import { normalizeDate } from '../../utils/misc-utils.js'
|
||||||
import { encodeWaveform } from '../../utils/voice-utils.js'
|
import { encodeWaveform } from '../../utils/voice-utils.js'
|
||||||
import { _parseEntities } from '../messages/parse-entities.js'
|
import { _normalizeInputText } from '../misc/normalize-text.js'
|
||||||
import { resolvePeer } from '../users/resolve-peer.js'
|
import { resolvePeer } from '../users/resolve-peer.js'
|
||||||
import { _normalizeInputFile } from './normalize-input-file.js'
|
import { _normalizeInputFile } from './normalize-input-file.js'
|
||||||
import { uploadFile } from './upload-file.js'
|
import { uploadFile } from './upload-file.js'
|
||||||
|
@ -21,10 +21,9 @@ export async function _normalizeInputMedia(
|
||||||
client: BaseTelegramClient,
|
client: BaseTelegramClient,
|
||||||
media: InputMediaLike,
|
media: InputMediaLike,
|
||||||
params: {
|
params: {
|
||||||
parseMode?: string | null
|
|
||||||
progressCallback?: (uploaded: number, total: number) => void
|
progressCallback?: (uploaded: number, total: number) => void
|
||||||
uploadPeer?: tl.TypeInputPeer
|
uploadPeer?: tl.TypeInputPeer
|
||||||
},
|
} = {},
|
||||||
uploadMedia = false,
|
uploadMedia = false,
|
||||||
): Promise<tl.TypeInputMedia> {
|
): Promise<tl.TypeInputMedia> {
|
||||||
// my condolences to those poor souls who are going to maintain this (myself included)
|
// my condolences to those poor souls who are going to maintain this (myself included)
|
||||||
|
@ -165,12 +164,7 @@ export async function _normalizeInputMedia(
|
||||||
})
|
})
|
||||||
|
|
||||||
if (media.solution) {
|
if (media.solution) {
|
||||||
[solution, solutionEntities] = await _parseEntities(
|
[solution, solutionEntities] = await _normalizeInputText(client, media.solution)
|
||||||
client,
|
|
||||||
media.solution,
|
|
||||||
params.parseMode,
|
|
||||||
media.solutionEntities,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { BaseTelegramClient, tl } from '@mtcute/core'
|
import { BaseTelegramClient, tl } from '@mtcute/core'
|
||||||
|
|
||||||
import { BotKeyboard, FormattedString, InputMediaLike, ReplyMarkup } from '../../types/index.js'
|
import { BotKeyboard, InputMediaLike, InputText, ReplyMarkup } from '../../types/index.js'
|
||||||
import { normalizeInlineId } from '../../utils/inline-utils.js'
|
import { normalizeInlineId } from '../../utils/inline-utils.js'
|
||||||
import { _normalizeInputMedia } from '../files/normalize-input-media.js'
|
import { _normalizeInputMedia } from '../files/normalize-input-media.js'
|
||||||
import { _parseEntities } from './parse-entities.js'
|
import { _normalizeInputText } from '../misc/normalize-text.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edit sent inline message text, media and reply markup.
|
* Edit sent inline message text, media and reply markup.
|
||||||
|
@ -27,26 +27,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 | FormattedString<string>
|
text?: InputText
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse mode to use to parse entities before sending the message.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* Passing `null` will explicitly disable formatting.
|
|
||||||
* @default current default parse mode (if any).
|
|
||||||
*/
|
|
||||||
parseMode?: string | null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of formatting entities to use instead of parsing via a
|
|
||||||
* parse mode.
|
|
||||||
*
|
|
||||||
* **Note:** Passing this makes the method ignore {@link parseMode}
|
|
||||||
*
|
|
||||||
* When `media` is passed, `media.entities` is used instead
|
|
||||||
*/
|
|
||||||
entities?: tl.TypeMessageEntity[]
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* New message media
|
* New message media
|
||||||
|
@ -93,15 +74,10 @@ export async function editInlineMessage(
|
||||||
// if there's no caption in input media (i.e. not present or undefined),
|
// if there's no caption in input media (i.e. not present or undefined),
|
||||||
// user wants to keep current caption, thus `content` needs to stay `undefined`
|
// user wants to keep current caption, thus `content` needs to stay `undefined`
|
||||||
if ('caption' in params.media && params.media.caption !== undefined) {
|
if ('caption' in params.media && params.media.caption !== undefined) {
|
||||||
[content, entities] = await _parseEntities(
|
[content, entities] = await _normalizeInputText(client, params.media.caption)
|
||||||
client,
|
|
||||||
params.media.caption,
|
|
||||||
params.parseMode,
|
|
||||||
params.media.entities,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else if (params.text) {
|
} else if (params.text) {
|
||||||
[content, entities] = await _parseEntities(client, params.text, params.parseMode, params.entities)
|
[content, entities] = await _normalizeInputText(client, params.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
let retries = 3
|
let retries = 3
|
||||||
|
|
|
@ -2,17 +2,17 @@ import { BaseTelegramClient, tl } from '@mtcute/core'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BotKeyboard,
|
BotKeyboard,
|
||||||
FormattedString,
|
|
||||||
InputMediaLike,
|
InputMediaLike,
|
||||||
InputMessageId,
|
InputMessageId,
|
||||||
|
InputText,
|
||||||
Message,
|
Message,
|
||||||
normalizeInputMessageId,
|
normalizeInputMessageId,
|
||||||
ReplyMarkup,
|
ReplyMarkup,
|
||||||
} from '../../types/index.js'
|
} from '../../types/index.js'
|
||||||
import { _normalizeInputMedia } from '../files/normalize-input-media.js'
|
import { _normalizeInputMedia } from '../files/normalize-input-media.js'
|
||||||
|
import { _normalizeInputText } from '../misc/normalize-text.js'
|
||||||
import { resolvePeer } from '../users/resolve-peer.js'
|
import { resolvePeer } from '../users/resolve-peer.js'
|
||||||
import { _findMessageInUpdate } from './find-in-update.js'
|
import { _findMessageInUpdate } from './find-in-update.js'
|
||||||
import { _parseEntities } from './parse-entities.js'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edit message text, media, reply markup and schedule date.
|
* Edit message text, media, reply markup and schedule date.
|
||||||
|
@ -29,26 +29,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 | FormattedString<string>
|
text?: InputText
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse mode to use to parse entities before sending the message.
|
|
||||||
*
|
|
||||||
* Passing `null` will explicitly disable formatting.
|
|
||||||
*
|
|
||||||
* @default current default parse mode (if any).
|
|
||||||
*/
|
|
||||||
parseMode?: string | null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of formatting entities to use instead of parsing via a
|
|
||||||
* parse mode.
|
|
||||||
*
|
|
||||||
* **Note:** Passing this makes the method ignore {@link parseMode}
|
|
||||||
*
|
|
||||||
* When `media` is passed, `media.entities` is used instead
|
|
||||||
*/
|
|
||||||
entities?: tl.TypeMessageEntity[]
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* New message media
|
* New message media
|
||||||
|
@ -106,17 +87,12 @@ export async function editMessage(
|
||||||
// if there's no caption in input media (i.e. not present or undefined),
|
// if there's no caption in input media (i.e. not present or undefined),
|
||||||
// user wants to keep current caption, thus `content` needs to stay `undefined`
|
// user wants to keep current caption, thus `content` needs to stay `undefined`
|
||||||
if ('caption' in params.media && params.media.caption !== undefined) {
|
if ('caption' in params.media && params.media.caption !== undefined) {
|
||||||
[content, entities] = await _parseEntities(
|
[content, entities] = await _normalizeInputText(client, params.media.caption)
|
||||||
client,
|
|
||||||
params.media.caption,
|
|
||||||
params.parseMode,
|
|
||||||
params.media.entities,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.text) {
|
if (params.text) {
|
||||||
[content, entities] = await _parseEntities(client, params.text, params.parseMode, params.entities)
|
[content, entities] = await _normalizeInputText(client, params.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await client.call({
|
const res = await client.call({
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { BaseTelegramClient, MtArgumentError, tl } from '@mtcute/core'
|
import { BaseTelegramClient, MtArgumentError } from '@mtcute/core'
|
||||||
import { randomLong } from '@mtcute/core/utils.js'
|
import { randomLong } from '@mtcute/core/utils.js'
|
||||||
|
|
||||||
import { FormattedString, InputMediaLike, InputPeerLike, Message, PeersIndex } from '../../types/index.js'
|
import { InputPeerLike, Message, PeersIndex } from '../../types/index.js'
|
||||||
import { normalizeDate } from '../../utils/misc-utils.js'
|
import { normalizeDate } from '../../utils/misc-utils.js'
|
||||||
import { assertIsUpdatesGroup } from '../../utils/updates-utils.js'
|
import { assertIsUpdatesGroup } from '../../utils/updates-utils.js'
|
||||||
import { resolvePeer } from '../users/resolve-peer.js'
|
import { resolvePeer } from '../users/resolve-peer.js'
|
||||||
|
@ -11,41 +11,6 @@ export interface ForwardMessageOptions {
|
||||||
/** Destination chat ID, username, phone, `"me"` or `"self"` */
|
/** Destination chat ID, username, phone, `"me"` or `"self"` */
|
||||||
toChatId: InputPeerLike
|
toChatId: InputPeerLike
|
||||||
|
|
||||||
/**
|
|
||||||
* Optionally, a caption for your forwarded message(s).
|
|
||||||
* It will be sent as a separate message before the forwarded messages.
|
|
||||||
*
|
|
||||||
* You can either pass `caption` or `captionMedia`, passing both will
|
|
||||||
* result in an error
|
|
||||||
*/
|
|
||||||
caption?: string | FormattedString<string>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optionally, a media caption for your forwarded message(s).
|
|
||||||
* It will be sent as a separate message before the forwarded messages.
|
|
||||||
*
|
|
||||||
* You can either pass `caption` or `captionMedia`, passing both will
|
|
||||||
* result in an error
|
|
||||||
*/
|
|
||||||
captionMedia?: InputMediaLike
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse mode to use to parse entities in caption.
|
|
||||||
*
|
|
||||||
* Passing `null` will explicitly disable formatting.
|
|
||||||
*
|
|
||||||
* @default current default parse mode (if any).
|
|
||||||
*/
|
|
||||||
parseMode?: string | null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of formatting entities in caption to use instead
|
|
||||||
* of parsing via a parse mode.
|
|
||||||
*
|
|
||||||
* **Note:** Passing this makes the method ignore {@link parseMode}
|
|
||||||
*/
|
|
||||||
entities?: tl.TypeMessageEntity[]
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to forward silently (also applies to caption message).
|
* Whether to forward silently (also applies to caption message).
|
||||||
*/
|
*/
|
||||||
|
@ -102,8 +67,6 @@ export interface ForwardMessageOptions {
|
||||||
* Forward one or more messages by their IDs.
|
* Forward one or more messages by their IDs.
|
||||||
* You can forward no more than 100 messages at once.
|
* You can forward no more than 100 messages at once.
|
||||||
*
|
*
|
||||||
* If a caption message was sent, it will be the first message in the resulting array.
|
|
||||||
*
|
|
||||||
* @param toChatId Destination chat ID, username, phone, `"me"` or `"self"`
|
* @param toChatId Destination chat ID, username, phone, `"me"` or `"self"`
|
||||||
* @param fromChatId Source chat ID, username, phone, `"me"` or `"self"`
|
* @param fromChatId Source chat ID, username, phone, `"me"` or `"self"`
|
||||||
* @param messages Message IDs
|
* @param messages Message IDs
|
||||||
|
|
|
@ -1,60 +0,0 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import { BaseTelegramClient, MtArgumentError, tl } from '@mtcute/core'
|
|
||||||
|
|
||||||
import { FormattedString } from '../../types/index.js'
|
|
||||||
import { normalizeToInputUser } from '../../utils/peer-utils.js'
|
|
||||||
import { getParseModesState } from '../parse-modes/_state.js'
|
|
||||||
import { resolvePeer } from '../users/resolve-peer.js'
|
|
||||||
|
|
||||||
const empty: [string, undefined] = ['', undefined]
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
export async function _parseEntities(
|
|
||||||
client: BaseTelegramClient,
|
|
||||||
text?: string | FormattedString<string>,
|
|
||||||
mode?: string | null,
|
|
||||||
entities?: tl.TypeMessageEntity[],
|
|
||||||
): Promise<[string, tl.TypeMessageEntity[] | undefined]> {
|
|
||||||
if (!text) {
|
|
||||||
return empty
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof text === 'object') {
|
|
||||||
mode = text.mode
|
|
||||||
text = text.value
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!entities) {
|
|
||||||
const parseModesState = getParseModesState(client)
|
|
||||||
|
|
||||||
if (mode === undefined) {
|
|
||||||
mode = parseModesState.defaultParseMode
|
|
||||||
}
|
|
||||||
// either explicitly disabled or no available parser
|
|
||||||
if (!mode) return [text, []]
|
|
||||||
|
|
||||||
const modeImpl = parseModesState.parseModes.get(mode)
|
|
||||||
|
|
||||||
if (!modeImpl) {
|
|
||||||
throw new MtArgumentError(`Parse mode ${mode} is not registered.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
[text, entities] = modeImpl.parse(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
// replace mentionName entities with input ones
|
|
||||||
for (const ent of entities) {
|
|
||||||
if (ent._ === 'messageEntityMentionName') {
|
|
||||||
try {
|
|
||||||
const inputPeer = normalizeToInputUser(await resolvePeer(client, ent.userId), ent.userId)
|
|
||||||
|
|
||||||
// not a user
|
|
||||||
if (!inputPeer) continue
|
|
||||||
(ent as any)._ = 'inputMessageEntityMentionName'
|
|
||||||
;(ent as any).userId = inputPeer
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [text, entities]
|
|
||||||
}
|
|
|
@ -2,8 +2,10 @@ import { BaseTelegramClient, getMarkedPeerId, MtArgumentError, tl } from '@mtcut
|
||||||
|
|
||||||
import { MtMessageNotFoundError } from '../../types/errors.js'
|
import { MtMessageNotFoundError } from '../../types/errors.js'
|
||||||
import { Message } from '../../types/messages/message.js'
|
import { Message } from '../../types/messages/message.js'
|
||||||
|
import { TextWithEntities } from '../../types/misc/entities.js'
|
||||||
import { InputPeerLike } from '../../types/peers/index.js'
|
import { InputPeerLike } from '../../types/peers/index.js'
|
||||||
import { normalizeMessageId, normalizeToInputUser } from '../../utils/index.js'
|
import { normalizeMessageId, normalizeToInputUser } from '../../utils/index.js'
|
||||||
|
import { _normalizeInputText } from '../misc/normalize-text.js'
|
||||||
import { resolvePeer } from '../users/resolve-peer.js'
|
import { resolvePeer } from '../users/resolve-peer.js'
|
||||||
import { _getDiscussionMessage } from './get-discussion-message.js'
|
import { _getDiscussionMessage } from './get-discussion-message.js'
|
||||||
import { getMessages } from './get-messages.js'
|
import { getMessages } from './get-messages.js'
|
||||||
|
@ -50,24 +52,9 @@ export interface CommonSendParams {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Quoted text. Must be exactly contained in the message
|
* Quoted text. Must be exactly contained in the message
|
||||||
* being quoted to be accepted by the server
|
* being quoted to be accepted by the server (as well as entities)
|
||||||
*/
|
*/
|
||||||
quoteText?: string
|
quote?: TextWithEntities
|
||||||
|
|
||||||
/**
|
|
||||||
* Entities contained in the quoted text.
|
|
||||||
* Must be exactly contained in the message
|
|
||||||
* being quoted to be accepted by the server
|
|
||||||
*/
|
|
||||||
quoteEntities?: tl.TypeMessageEntity[]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse mode to use to parse entities before sending the message.
|
|
||||||
* Passing `null` will explicitly disable formatting.
|
|
||||||
*
|
|
||||||
* @default current default parse mode (if any).
|
|
||||||
*/
|
|
||||||
parseMode?: string | null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to send this message silently.
|
* Whether to send this message silently.
|
||||||
|
@ -151,8 +138,8 @@ export async function _processCommonSendParameters(
|
||||||
_: 'inputReplyToMessage',
|
_: 'inputReplyToMessage',
|
||||||
replyToMsgId: replyTo,
|
replyToMsgId: replyTo,
|
||||||
replyToPeerId: replyToPeer,
|
replyToPeerId: replyToPeer,
|
||||||
quoteText: params.quoteText,
|
quoteText: params.quote?.text,
|
||||||
quoteEntities: params.quoteEntities,
|
quoteEntities: params.quote?.entities as tl.TypeMessageEntity[],
|
||||||
}
|
}
|
||||||
} else if (params.replyToStory) {
|
} else if (params.replyToStory) {
|
||||||
tlReplyTo = {
|
tlReplyTo = {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { BaseTelegramClient, getMarkedPeerId, MtArgumentError, tl } from '@mtcute/core'
|
import { BaseTelegramClient, getMarkedPeerId, MtArgumentError } from '@mtcute/core'
|
||||||
|
|
||||||
import { FormattedString, InputPeerLike, Message, MtMessageNotFoundError, ReplyMarkup } from '../../types/index.js'
|
import { InputPeerLike, InputText, Message, MtMessageNotFoundError, ReplyMarkup } from '../../types/index.js'
|
||||||
import { resolvePeer } from '../users/resolve-peer.js'
|
import { resolvePeer } from '../users/resolve-peer.js'
|
||||||
import { getMessages } from './get-messages.js'
|
import { getMessages } from './get-messages.js'
|
||||||
import { CommonSendParams } from './send-common.js'
|
import { CommonSendParams } from './send-common.js'
|
||||||
|
@ -15,24 +15,7 @@ export interface SendCopyParams extends CommonSendParams {
|
||||||
/**
|
/**
|
||||||
* New message caption (only used for media)
|
* New message caption (only used for media)
|
||||||
*/
|
*/
|
||||||
caption?: string | FormattedString<string>
|
caption?: InputText
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse mode to use to parse `text` entities before sending
|
|
||||||
* the message.
|
|
||||||
* Passing `null` will explicitly disable formatting.
|
|
||||||
*
|
|
||||||
* @default current default parse mode (if any).
|
|
||||||
*/
|
|
||||||
parseMode?: string | null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of formatting entities to use instead of parsing via a
|
|
||||||
* parse mode.
|
|
||||||
*
|
|
||||||
* **Note:** Passing this makes the method ignore {@link parseMode}
|
|
||||||
*/
|
|
||||||
entities?: tl.TypeMessageEntity[]
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For bots: inline or reply markup or an instruction
|
* For bots: inline or reply markup or an instruction
|
||||||
|
@ -83,15 +66,26 @@ export async function sendCopy(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.media && msg.media.type !== 'web_page' && msg.media.type !== 'invoice') {
|
if (msg.media && msg.media.type !== 'web_page' && msg.media.type !== 'invoice') {
|
||||||
|
let caption: InputText | undefined = params.caption
|
||||||
|
|
||||||
|
if (!caption) {
|
||||||
|
if (msg.raw.entities?.length) {
|
||||||
|
caption = {
|
||||||
|
text: msg.raw.message,
|
||||||
|
entities: msg.raw.entities,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
caption = msg.raw.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return sendMedia(
|
return sendMedia(
|
||||||
client,
|
client,
|
||||||
toChatId,
|
toChatId,
|
||||||
{
|
{
|
||||||
type: 'auto',
|
type: 'auto',
|
||||||
file: msg.media.inputMedia,
|
file: msg.media.inputMedia,
|
||||||
caption: params.caption ?? msg.raw.message,
|
caption,
|
||||||
// we shouldn't use original entities if the user wants custom text
|
|
||||||
entities: params.entities ?? params.caption ? undefined : msg.raw.entities,
|
|
||||||
},
|
},
|
||||||
rest,
|
rest,
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,9 +7,9 @@ import { InputPeerLike, PeersIndex } from '../../types/peers/index.js'
|
||||||
import { normalizeDate } from '../../utils/misc-utils.js'
|
import { normalizeDate } from '../../utils/misc-utils.js'
|
||||||
import { assertIsUpdatesGroup } from '../../utils/updates-utils.js'
|
import { assertIsUpdatesGroup } from '../../utils/updates-utils.js'
|
||||||
import { _normalizeInputMedia } from '../files/normalize-input-media.js'
|
import { _normalizeInputMedia } from '../files/normalize-input-media.js'
|
||||||
|
import { _normalizeInputText } from '../misc/normalize-text.js'
|
||||||
import { resolvePeer } from '../users/resolve-peer.js'
|
import { resolvePeer } from '../users/resolve-peer.js'
|
||||||
import { _getDiscussionMessage } from './get-discussion-message.js'
|
import { _getDiscussionMessage } from './get-discussion-message.js'
|
||||||
import { _parseEntities } from './parse-entities.js'
|
|
||||||
import { _processCommonSendParameters, CommonSendParams } from './send-common.js'
|
import { _processCommonSendParameters, CommonSendParams } from './send-common.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -77,14 +77,11 @@ export async function sendMediaGroup(
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
|
||||||
const [message, entities] = await _parseEntities(
|
const [message, entities] = await _normalizeInputText(
|
||||||
client,
|
client,
|
||||||
// some types dont have `caption` field, and ts warns us,
|
// some types dont have `caption` field, and ts warns us,
|
||||||
// but since it's JS, they'll just be `undefined` and properly
|
// but since it's JS, they'll just be `undefined` and properly handled by the method
|
||||||
// handled by _parseEntities method
|
|
||||||
(media as Extract<typeof media, { caption?: unknown }>).caption,
|
(media as Extract<typeof media, { caption?: unknown }>).caption,
|
||||||
params.parseMode,
|
|
||||||
(media as Extract<typeof media, { entities?: unknown }>).entities,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
multiMedia.push({
|
multiMedia.push({
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import { BaseTelegramClient, tl } from '@mtcute/core'
|
import { BaseTelegramClient } from '@mtcute/core'
|
||||||
import { randomLong } from '@mtcute/core/utils.js'
|
import { randomLong } from '@mtcute/core/utils.js'
|
||||||
|
|
||||||
import { BotKeyboard, ReplyMarkup } from '../../types/bots/keyboards.js'
|
import { BotKeyboard, ReplyMarkup } from '../../types/bots/keyboards.js'
|
||||||
import { InputMediaLike } from '../../types/media/input-media.js'
|
import { InputMediaLike } from '../../types/media/input-media.js'
|
||||||
import { Message } from '../../types/messages/message.js'
|
import { Message } from '../../types/messages/message.js'
|
||||||
import { FormattedString } from '../../types/parser.js'
|
import { InputText } from '../../types/misc/entities.js'
|
||||||
import { InputPeerLike } from '../../types/peers/index.js'
|
import { InputPeerLike } from '../../types/peers/index.js'
|
||||||
import { normalizeDate } from '../../utils/misc-utils.js'
|
import { normalizeDate } from '../../utils/misc-utils.js'
|
||||||
import { _normalizeInputMedia } from '../files/normalize-input-media.js'
|
import { _normalizeInputMedia } from '../files/normalize-input-media.js'
|
||||||
|
import { _normalizeInputText } from '../misc/normalize-text.js'
|
||||||
import { resolvePeer } from '../users/resolve-peer.js'
|
import { resolvePeer } from '../users/resolve-peer.js'
|
||||||
import { _findMessageInUpdate } from './find-in-update.js'
|
import { _findMessageInUpdate } from './find-in-update.js'
|
||||||
import { _getDiscussionMessage } from './get-discussion-message.js'
|
import { _getDiscussionMessage } from './get-discussion-message.js'
|
||||||
import { _parseEntities } from './parse-entities.js'
|
|
||||||
import { _processCommonSendParameters, CommonSendParams } from './send-common.js'
|
import { _processCommonSendParameters, CommonSendParams } from './send-common.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -50,15 +50,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 | FormattedString<string>
|
caption?: InputText
|
||||||
|
|
||||||
/**
|
|
||||||
* Override entities for `media`.
|
|
||||||
*
|
|
||||||
* Can be used, for example. when using File IDs
|
|
||||||
* or when using existing InputMedia objects.
|
|
||||||
*/
|
|
||||||
entities?: tl.TypeMessageEntity[]
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function that will be called after some part has been uploaded.
|
* Function that will be called after some part has been uploaded.
|
||||||
|
@ -82,14 +74,11 @@ export async function sendMedia(
|
||||||
|
|
||||||
const inputMedia = await _normalizeInputMedia(client, media, params)
|
const inputMedia = await _normalizeInputMedia(client, media, params)
|
||||||
|
|
||||||
const [message, entities] = await _parseEntities(
|
const [message, entities] = await _normalizeInputText(
|
||||||
client,
|
client,
|
||||||
// some types dont have `caption` field, and ts warns us,
|
// some types dont have `caption` field, and ts warns us,
|
||||||
// but since it's JS, they'll just be `undefined` and properly
|
// but since it's JS, they'll just be `undefined` and properly handled by the method
|
||||||
// handled by _parseEntities method
|
|
||||||
params.caption || (media as Extract<typeof media, { caption?: unknown }>).caption,
|
params.caption || (media as Extract<typeof media, { caption?: unknown }>).caption,
|
||||||
params.parseMode,
|
|
||||||
params.entities || (media as Extract<typeof media, { entities?: unknown }>).entities,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const replyMarkup = BotKeyboard._convertToTl(params.replyMarkup)
|
const replyMarkup = BotKeyboard._convertToTl(params.replyMarkup)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { BaseTelegramClient, MtArgumentError, tl } from '@mtcute/core'
|
import { BaseTelegramClient, MtArgumentError, tl } from '@mtcute/core'
|
||||||
|
|
||||||
import { InputPeerLike } from '../../index.js'
|
import { InputPeerLike, TextWithEntities } from '../../index.js'
|
||||||
import { Message } from '../../types/messages/message.js'
|
import { Message } from '../../types/messages/message.js'
|
||||||
import { sendMedia } from './send-media.js'
|
import { sendMedia } from './send-media.js'
|
||||||
import { sendMediaGroup } from './send-media-group.js'
|
import { sendMediaGroup } from './send-media-group.js'
|
||||||
|
@ -22,7 +22,7 @@ export type QuoteParamsFrom<T> = Omit<NonNullable<T>, 'quoteText' | 'quoteEntiti
|
||||||
end: number
|
end: number
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractQuote(message: Message, from: number, to: number): [string, tl.TypeMessageEntity[] | undefined] {
|
function extractQuote(message: Message, from: number, to: number): TextWithEntities {
|
||||||
const { raw } = message
|
const { raw } = message
|
||||||
if (raw._ === 'messageService') throw new MtArgumentError('Cannot quote service message')
|
if (raw._ === 'messageService') throw new MtArgumentError('Cannot quote service message')
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ function extractQuote(message: Message, from: number, to: number): [string, tl.T
|
||||||
|
|
||||||
if (from >= to) throw new MtArgumentError('Invalid quote range')
|
if (from >= to) throw new MtArgumentError('Invalid quote range')
|
||||||
|
|
||||||
if (!raw.entities) return [text.slice(from, to), undefined]
|
if (!raw.entities) return { text: text.slice(from, to), entities: undefined }
|
||||||
|
|
||||||
const entities: tl.TypeMessageEntity[] = []
|
const entities: tl.TypeMessageEntity[] = []
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ function extractQuote(message: Message, from: number, to: number): [string, tl.T
|
||||||
entities.push(newEnt)
|
entities.push(newEnt)
|
||||||
}
|
}
|
||||||
|
|
||||||
return [text.slice(from, to), entities]
|
return { text: text.slice(from, to), entities }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Send a text in reply to a given quote */
|
/** Send a text in reply to a given quote */
|
||||||
|
@ -66,7 +66,7 @@ export function quoteWithText(
|
||||||
const { toChatId = message.chat, start, end, text, ...params__ } = params
|
const { toChatId = message.chat, start, end, text, ...params__ } = params
|
||||||
const params_ = params__ as NonNullable<Parameters<typeof sendText>[3]>
|
const params_ = params__ as NonNullable<Parameters<typeof sendText>[3]>
|
||||||
params_.replyTo = message
|
params_.replyTo = message
|
||||||
;[params_.quoteText, params_.quoteEntities] = extractQuote(message, params.start, params.end)
|
params_.quote = extractQuote(message, params.start, params.end)
|
||||||
|
|
||||||
return sendText(client, toChatId, text, params_)
|
return sendText(client, toChatId, text, params_)
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,7 @@ export function quoteWithMedia(
|
||||||
const { toChatId = message.chat, start, end, media, ...params__ } = params
|
const { toChatId = message.chat, start, end, media, ...params__ } = params
|
||||||
const params_ = params__ as NonNullable<Parameters<typeof sendMedia>[3]>
|
const params_ = params__ as NonNullable<Parameters<typeof sendMedia>[3]>
|
||||||
params_.replyTo = message
|
params_.replyTo = message
|
||||||
;[params_.quoteText, params_.quoteEntities] = extractQuote(message, params.start, params.end)
|
params_.quote = extractQuote(message, params.start, params.end)
|
||||||
|
|
||||||
return sendMedia(client, toChatId, media, params_)
|
return sendMedia(client, toChatId, media, params_)
|
||||||
}
|
}
|
||||||
|
@ -100,7 +100,7 @@ export function quoteWithMediaGroup(
|
||||||
const { toChatId, start, end, medias, ...params__ } = params
|
const { toChatId, start, end, medias, ...params__ } = params
|
||||||
const params_ = params__ as NonNullable<Parameters<typeof sendMediaGroup>[3]>
|
const params_ = params__ as NonNullable<Parameters<typeof sendMediaGroup>[3]>
|
||||||
params_.replyTo = message
|
params_.replyTo = message
|
||||||
;[params_.quoteText, params_.quoteEntities] = extractQuote(message, params.start, params.end)
|
params_.quote = extractQuote(message, params.start, params.end)
|
||||||
|
|
||||||
return sendMediaGroup(client, message.chat.inputPeer, medias, params_)
|
return sendMediaGroup(client, message.chat.inputPeer, medias, params_)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,16 +3,16 @@ import { randomLong } from '@mtcute/core/utils.js'
|
||||||
|
|
||||||
import { BotKeyboard, ReplyMarkup } from '../../types/bots/keyboards.js'
|
import { BotKeyboard, ReplyMarkup } from '../../types/bots/keyboards.js'
|
||||||
import { Message } from '../../types/messages/message.js'
|
import { Message } from '../../types/messages/message.js'
|
||||||
import { FormattedString } from '../../types/parser.js'
|
import { InputText } from '../../types/misc/entities.js'
|
||||||
import { InputPeerLike, PeersIndex } from '../../types/peers/index.js'
|
import { InputPeerLike, PeersIndex } from '../../types/peers/index.js'
|
||||||
import { normalizeDate } from '../../utils/misc-utils.js'
|
import { normalizeDate } from '../../utils/misc-utils.js'
|
||||||
import { inputPeerToPeer } from '../../utils/peer-utils.js'
|
import { inputPeerToPeer } from '../../utils/peer-utils.js'
|
||||||
import { createDummyUpdate } from '../../utils/updates-utils.js'
|
import { createDummyUpdate } from '../../utils/updates-utils.js'
|
||||||
import { getAuthState } from '../auth/_state.js'
|
import { getAuthState } from '../auth/_state.js'
|
||||||
|
import { _normalizeInputText } from '../misc/normalize-text.js'
|
||||||
import { resolvePeer } from '../users/resolve-peer.js'
|
import { resolvePeer } from '../users/resolve-peer.js'
|
||||||
import { _findMessageInUpdate } from './find-in-update.js'
|
import { _findMessageInUpdate } from './find-in-update.js'
|
||||||
import { _getDiscussionMessage } from './get-discussion-message.js'
|
import { _getDiscussionMessage } from './get-discussion-message.js'
|
||||||
import { _parseEntities } from './parse-entities.js'
|
|
||||||
import { _processCommonSendParameters, CommonSendParams } from './send-common.js'
|
import { _processCommonSendParameters, CommonSendParams } from './send-common.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -25,7 +25,7 @@ import { _processCommonSendParameters, CommonSendParams } from './send-common.js
|
||||||
export async function sendText(
|
export async function sendText(
|
||||||
client: BaseTelegramClient,
|
client: BaseTelegramClient,
|
||||||
chatId: InputPeerLike,
|
chatId: InputPeerLike,
|
||||||
text: string | FormattedString<string>,
|
text: InputText,
|
||||||
params?: CommonSendParams & {
|
params?: CommonSendParams & {
|
||||||
/**
|
/**
|
||||||
* For bots: inline or reply markup or an instruction
|
* For bots: inline or reply markup or an instruction
|
||||||
|
@ -33,14 +33,6 @@ export async function sendText(
|
||||||
*/
|
*/
|
||||||
replyMarkup?: ReplyMarkup
|
replyMarkup?: ReplyMarkup
|
||||||
|
|
||||||
/**
|
|
||||||
* List of formatting entities to use instead of parsing via a
|
|
||||||
* parse mode.
|
|
||||||
*
|
|
||||||
* **Note:** Passing this makes the method ignore {@link parseMode}
|
|
||||||
*/
|
|
||||||
entities?: tl.TypeMessageEntity[]
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to disable links preview in this message
|
* Whether to disable links preview in this message
|
||||||
*/
|
*/
|
||||||
|
@ -57,7 +49,7 @@ export async function sendText(
|
||||||
): Promise<Message> {
|
): Promise<Message> {
|
||||||
if (!params) params = {}
|
if (!params) params = {}
|
||||||
|
|
||||||
const [message, entities] = await _parseEntities(client, text, params.parseMode, params.entities)
|
const [message, entities] = await _normalizeInputText(client, text)
|
||||||
|
|
||||||
const replyMarkup = BotKeyboard._convertToTl(params.replyMarkup)
|
const replyMarkup = BotKeyboard._convertToTl(params.replyMarkup)
|
||||||
const { peer, replyTo } = await _processCommonSendParameters(client, chatId, params)
|
const { peer, replyTo } = await _processCommonSendParameters(client, chatId, params)
|
||||||
|
|
41
packages/client/src/methods/misc/normalize-text.ts
Normal file
41
packages/client/src/methods/misc/normalize-text.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { BaseTelegramClient, tl } from '@mtcute/core'
|
||||||
|
|
||||||
|
import { InputText } from '../../types/misc/entities.js'
|
||||||
|
import { normalizeToInputUser } from '../../utils/peer-utils.js'
|
||||||
|
import { resolvePeer } from '../users/resolve-peer.js'
|
||||||
|
|
||||||
|
const empty: [string, undefined] = ['', undefined]
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
export async function _normalizeInputText(
|
||||||
|
client: BaseTelegramClient,
|
||||||
|
input?: InputText,
|
||||||
|
): Promise<[string, tl.TypeMessageEntity[] | undefined]> {
|
||||||
|
if (!input) {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof input === 'string') {
|
||||||
|
return [input, undefined]
|
||||||
|
}
|
||||||
|
|
||||||
|
const { text, entities } = input
|
||||||
|
if (!entities) return [text, undefined]
|
||||||
|
|
||||||
|
// replace mentionName entities with input ones
|
||||||
|
for (const ent of entities) {
|
||||||
|
if (ent._ === 'messageEntityMentionName') {
|
||||||
|
try {
|
||||||
|
const inputPeer = normalizeToInputUser(await resolvePeer(client, ent.userId), ent.userId)
|
||||||
|
|
||||||
|
const ent_ = ent as unknown as tl.RawInputMessageEntityMentionName
|
||||||
|
ent_._ = 'inputMessageEntityMentionName'
|
||||||
|
ent_.userId = inputPeer
|
||||||
|
} catch (e) {
|
||||||
|
client.log.warn('Failed to resolve mention entity for %s: %s', ent.userId, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [text, entities]
|
||||||
|
}
|
|
@ -1,21 +0,0 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
import { BaseTelegramClient } from '@mtcute/core'
|
|
||||||
|
|
||||||
import { IMessageEntityParser } from '../../types/index.js'
|
|
||||||
|
|
||||||
const STATE_SYMBOL = Symbol('parseModesState')
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
export interface ParseModesState {
|
|
||||||
parseModes: Map<string, IMessageEntityParser>
|
|
||||||
defaultParseMode: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
export function getParseModesState(client: BaseTelegramClient): ParseModesState {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
return ((client as any)[STATE_SYMBOL] ??= {
|
|
||||||
parseModes: new Map(),
|
|
||||||
defaultParseMode: null,
|
|
||||||
} satisfies ParseModesState)
|
|
||||||
}
|
|
|
@ -1,89 +0,0 @@
|
||||||
import { BaseTelegramClient, MtArgumentError } from '@mtcute/core'
|
|
||||||
|
|
||||||
import { IMessageEntityParser } from '../../types/index.js'
|
|
||||||
import { getParseModesState } from './_state.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a given {@link IMessageEntityParser} as a parse mode
|
|
||||||
* for messages. When this method is first called, given parse
|
|
||||||
* mode is also set as default.
|
|
||||||
*
|
|
||||||
* @param parseMode Parse mode to register
|
|
||||||
* @throws MtClientError When the parse mode with a given name is already registered.
|
|
||||||
*/
|
|
||||||
export function registerParseMode(client: BaseTelegramClient, parseMode: IMessageEntityParser): void {
|
|
||||||
const name = parseMode.name
|
|
||||||
|
|
||||||
const state = getParseModesState(client)
|
|
||||||
|
|
||||||
if (state.parseModes.has(name)) {
|
|
||||||
throw new MtArgumentError(`Parse mode ${name} is already registered. Unregister it first!`)
|
|
||||||
}
|
|
||||||
state.parseModes.set(name, parseMode)
|
|
||||||
|
|
||||||
if (!state.defaultParseMode) {
|
|
||||||
state.defaultParseMode = name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unregister a parse mode by its name.
|
|
||||||
* Will silently fail if given parse mode does not exist.
|
|
||||||
*
|
|
||||||
* Also updates the default parse mode to the next one available, if any
|
|
||||||
*
|
|
||||||
* @param name Name of the parse mode to unregister
|
|
||||||
*/
|
|
||||||
export function unregisterParseMode(client: BaseTelegramClient, name: string): void {
|
|
||||||
const state = getParseModesState(client)
|
|
||||||
|
|
||||||
state.parseModes.delete(name)
|
|
||||||
|
|
||||||
if (state.defaultParseMode === name) {
|
|
||||||
const [first] = state.parseModes.keys()
|
|
||||||
state.defaultParseMode = first ?? null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a {@link IMessageEntityParser} registered under a given name (or a default one).
|
|
||||||
*
|
|
||||||
* @param name Name of the parse mode which parser to get.
|
|
||||||
* @throws MtClientError When the provided parse mode is not registered
|
|
||||||
* @throws MtClientError When `name` is omitted and there is no default parse mode
|
|
||||||
*/
|
|
||||||
export function getParseMode(client: BaseTelegramClient, name?: string | null): IMessageEntityParser {
|
|
||||||
const state = getParseModesState(client)
|
|
||||||
|
|
||||||
if (!name) {
|
|
||||||
if (!state.defaultParseMode) {
|
|
||||||
throw new MtArgumentError('There is no default parse mode')
|
|
||||||
}
|
|
||||||
|
|
||||||
name = state.defaultParseMode
|
|
||||||
}
|
|
||||||
|
|
||||||
const mode = state.parseModes.get(name)
|
|
||||||
|
|
||||||
if (!mode) {
|
|
||||||
throw new MtArgumentError(`Parse mode ${name} is not registered.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return mode
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a given parse mode as a default one.
|
|
||||||
*
|
|
||||||
* @param name Name of the parse mode
|
|
||||||
* @throws MtClientError When given parse mode is not registered.
|
|
||||||
*/
|
|
||||||
export function setDefaultParseMode(client: BaseTelegramClient, name: string): void {
|
|
||||||
const state = getParseModesState(client)
|
|
||||||
|
|
||||||
if (!state.parseModes.has(name)) {
|
|
||||||
throw new MtArgumentError(`Parse mode ${name} is not registered.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
state.defaultParseMode = name
|
|
||||||
}
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { BaseTelegramClient, tl } from '@mtcute/core'
|
import { BaseTelegramClient, tl } from '@mtcute/core'
|
||||||
|
|
||||||
import { FormattedString, InputMediaLike, InputPeerLike, InputPrivacyRule, Story } from '../../types/index.js'
|
import { InputMediaLike, InputPeerLike, InputPrivacyRule, InputText, Story } from '../../types/index.js'
|
||||||
import { _normalizeInputMedia } from '../files/normalize-input-media.js'
|
import { _normalizeInputMedia } from '../files/normalize-input-media.js'
|
||||||
import { _parseEntities } from '../messages/parse-entities.js'
|
|
||||||
import { _normalizePrivacyRules } from '../misc/normalize-privacy-rules.js'
|
import { _normalizePrivacyRules } from '../misc/normalize-privacy-rules.js'
|
||||||
|
import { _normalizeInputText } from '../misc/normalize-text.js'
|
||||||
import { resolvePeer } from '../users/resolve-peer.js'
|
import { resolvePeer } from '../users/resolve-peer.js'
|
||||||
import { _findStoryInUpdate } from './find-in-update.js'
|
import { _findStoryInUpdate } from './find-in-update.js'
|
||||||
|
|
||||||
|
@ -35,20 +35,7 @@ export async function editStory(
|
||||||
/**
|
/**
|
||||||
* Override caption for {@link media}
|
* Override caption for {@link media}
|
||||||
*/
|
*/
|
||||||
caption?: string | FormattedString<string>
|
caption?: InputText
|
||||||
|
|
||||||
/**
|
|
||||||
* Override entities for {@link media}
|
|
||||||
*/
|
|
||||||
entities?: tl.TypeMessageEntity[]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse mode to use to parse entities before sending the message.
|
|
||||||
* Passing `null` will explicitly disable formatting.
|
|
||||||
*
|
|
||||||
* @default current default parse mode (if any).
|
|
||||||
*/
|
|
||||||
parseMode?: string | null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interactive elements to add to the story
|
* Interactive elements to add to the story
|
||||||
|
@ -70,22 +57,17 @@ export async function editStory(
|
||||||
let media: tl.TypeInputMedia | undefined = undefined
|
let media: tl.TypeInputMedia | undefined = undefined
|
||||||
|
|
||||||
if (params.media) {
|
if (params.media) {
|
||||||
media = await _normalizeInputMedia(client, params.media, params)
|
media = await _normalizeInputMedia(client, params.media)
|
||||||
|
|
||||||
// if there's no caption in input media (i.e. not present or undefined),
|
// if there's no caption in input media (i.e. not present or undefined),
|
||||||
// user wants to keep current caption, thus `content` needs to stay `undefined`
|
// user wants to keep current caption, thus `content` needs to stay `undefined`
|
||||||
if ('caption' in params.media && params.media.caption !== undefined) {
|
if ('caption' in params.media && params.media.caption !== undefined) {
|
||||||
[caption, entities] = await _parseEntities(
|
[caption, entities] = await _normalizeInputText(client, params.media.caption)
|
||||||
client,
|
|
||||||
params.media.caption,
|
|
||||||
params.parseMode,
|
|
||||||
params.media.entities,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.caption) {
|
if (params.caption) {
|
||||||
[caption, entities] = await _parseEntities(client, params.caption, params.parseMode, params.entities)
|
[caption, entities] = await _normalizeInputText(client, params.caption)
|
||||||
}
|
}
|
||||||
|
|
||||||
const privacyRules = params.privacyRules ? await _normalizePrivacyRules(client, params.privacyRules) : undefined
|
const privacyRules = params.privacyRules ? await _normalizePrivacyRules(client, params.privacyRules) : undefined
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { BaseTelegramClient, tl } from '@mtcute/core'
|
import { BaseTelegramClient, tl } from '@mtcute/core'
|
||||||
import { randomLong } from '@mtcute/core/utils.js'
|
import { randomLong } from '@mtcute/core/utils.js'
|
||||||
|
|
||||||
import { FormattedString, InputMediaLike, InputPeerLike, InputPrivacyRule, Story } from '../../types/index.js'
|
import { InputMediaLike, InputPeerLike, InputPrivacyRule, InputText, Story } from '../../types/index.js'
|
||||||
import { _normalizeInputMedia } from '../files/normalize-input-media.js'
|
import { _normalizeInputMedia } from '../files/normalize-input-media.js'
|
||||||
import { _parseEntities } from '../messages/parse-entities.js'
|
|
||||||
import { _normalizePrivacyRules } from '../misc/normalize-privacy-rules.js'
|
import { _normalizePrivacyRules } from '../misc/normalize-privacy-rules.js'
|
||||||
|
import { _normalizeInputText } from '../misc/normalize-text.js'
|
||||||
import { resolvePeer } from '../users/resolve-peer.js'
|
import { resolvePeer } from '../users/resolve-peer.js'
|
||||||
import { _findStoryInUpdate } from './find-in-update.js'
|
import { _findStoryInUpdate } from './find-in-update.js'
|
||||||
|
|
||||||
|
@ -34,20 +34,7 @@ export async function sendStory(
|
||||||
/**
|
/**
|
||||||
* Override caption for {@link media}
|
* Override caption for {@link media}
|
||||||
*/
|
*/
|
||||||
caption?: string | FormattedString<string>
|
caption?: InputText
|
||||||
|
|
||||||
/**
|
|
||||||
* Override entities for {@link media}
|
|
||||||
*/
|
|
||||||
entities?: tl.TypeMessageEntity[]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse mode to use to parse entities before sending the message.
|
|
||||||
* Passing `null` will explicitly disable formatting.
|
|
||||||
*
|
|
||||||
* @default current default parse mode (if any).
|
|
||||||
*/
|
|
||||||
parseMode?: string | null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to automatically pin this story to the profile
|
* Whether to automatically pin this story to the profile
|
||||||
|
@ -89,19 +76,16 @@ export async function sendStory(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputMedia = await _normalizeInputMedia(client, media, params)
|
const inputMedia = await _normalizeInputMedia(client, media)
|
||||||
const privacyRules = params.privacyRules ?
|
const privacyRules = params.privacyRules ?
|
||||||
await _normalizePrivacyRules(client, params.privacyRules) :
|
await _normalizePrivacyRules(client, params.privacyRules) :
|
||||||
[{ _: 'inputPrivacyValueAllowAll' } as const]
|
[{ _: 'inputPrivacyValueAllowAll' } as const]
|
||||||
|
|
||||||
const [caption, entities] = await _parseEntities(
|
const [caption, entities] = await _normalizeInputText(
|
||||||
client,
|
client,
|
||||||
// some types dont have `caption` field, and ts warns us,
|
// some types dont have `caption` field, and ts warns us,
|
||||||
// but since it's JS, they'll just be `undefined` and properly
|
// but since it's JS, they'll just be `undefined` and properly handled by the method
|
||||||
// handled by _parseEntities method
|
|
||||||
params.caption || (media as Extract<typeof media, { caption?: unknown }>).caption,
|
params.caption || (media as Extract<typeof media, { caption?: unknown }>).caption,
|
||||||
params.parseMode,
|
|
||||||
params.entities || (media as Extract<typeof media, { entities?: unknown }>).entities,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const res = await client.call({
|
const res = await client.call({
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { assertNever, BaseTelegramClient, tl } from '@mtcute/core'
|
import { assertNever, BaseTelegramClient, tl } from '@mtcute/core'
|
||||||
|
|
||||||
import { _parseEntities } from '../../../methods/messages/parse-entities.js'
|
import { _normalizeInputText } from '../../../methods/misc/normalize-text.js'
|
||||||
|
import { InputText } from '../../../types/misc/entities.js'
|
||||||
import {
|
import {
|
||||||
InputMediaContact,
|
InputMediaContact,
|
||||||
InputMediaGeo,
|
InputMediaGeo,
|
||||||
|
@ -8,7 +9,6 @@ import {
|
||||||
InputMediaVenue,
|
InputMediaVenue,
|
||||||
InputMediaWebpage,
|
InputMediaWebpage,
|
||||||
} from '../../media/index.js'
|
} from '../../media/index.js'
|
||||||
import { FormattedString } from '../../parser.js'
|
|
||||||
import { BotKeyboard, ReplyMarkup } from '../keyboards.js'
|
import { BotKeyboard, ReplyMarkup } from '../keyboards.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -20,13 +20,7 @@ export interface InputInlineMessageText {
|
||||||
/**
|
/**
|
||||||
* Text of the message
|
* Text of the message
|
||||||
*/
|
*/
|
||||||
text: string | FormattedString<string>
|
text: InputText
|
||||||
|
|
||||||
/**
|
|
||||||
* Text markup entities.
|
|
||||||
* If passed, parse mode is ignored
|
|
||||||
*/
|
|
||||||
entities?: tl.TypeMessageEntity[]
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Message reply markup
|
* Message reply markup
|
||||||
|
@ -57,13 +51,7 @@ export interface InputInlineMessageMedia {
|
||||||
/**
|
/**
|
||||||
* Caption for the media
|
* Caption for the media
|
||||||
*/
|
*/
|
||||||
text?: string | FormattedString<string>
|
text?: InputText
|
||||||
|
|
||||||
/**
|
|
||||||
* Caption markup entities.
|
|
||||||
* If passed, parse mode is ignored
|
|
||||||
*/
|
|
||||||
entities?: tl.TypeMessageEntity[]
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Message reply markup
|
* Message reply markup
|
||||||
|
@ -135,13 +123,7 @@ export interface InputInlineMessageWebpage extends InputMediaWebpage {
|
||||||
/**
|
/**
|
||||||
* Text of the message
|
* Text of the message
|
||||||
*/
|
*/
|
||||||
text: string | FormattedString<string>
|
text: InputText
|
||||||
|
|
||||||
/**
|
|
||||||
* Text markup entities.
|
|
||||||
* If passed, parse mode is ignored
|
|
||||||
*/
|
|
||||||
entities?: tl.TypeMessageEntity[]
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Message reply markup
|
* Message reply markup
|
||||||
|
@ -176,7 +158,7 @@ export namespace BotInlineMessage {
|
||||||
* @param params
|
* @param params
|
||||||
*/
|
*/
|
||||||
export function text(
|
export function text(
|
||||||
text: string | FormattedString<string>,
|
text: InputText,
|
||||||
params: Omit<InputInlineMessageText, 'type' | 'text'> = {},
|
params: Omit<InputInlineMessageText, 'type' | 'text'> = {},
|
||||||
): InputInlineMessageText {
|
): InputInlineMessageText {
|
||||||
const ret = params as tl.Mutable<InputInlineMessageText>
|
const ret = params as tl.Mutable<InputInlineMessageText>
|
||||||
|
@ -266,11 +248,10 @@ export namespace BotInlineMessage {
|
||||||
export async function _convertToTl(
|
export async function _convertToTl(
|
||||||
client: BaseTelegramClient,
|
client: BaseTelegramClient,
|
||||||
obj: InputInlineMessage,
|
obj: InputInlineMessage,
|
||||||
parseMode?: string | null,
|
|
||||||
): Promise<tl.TypeInputBotInlineMessage> {
|
): Promise<tl.TypeInputBotInlineMessage> {
|
||||||
switch (obj.type) {
|
switch (obj.type) {
|
||||||
case 'text': {
|
case 'text': {
|
||||||
const [message, entities] = await _parseEntities(client, obj.text, parseMode, obj.entities)
|
const [message, entities] = await _normalizeInputText(client, obj.text)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_: 'inputBotInlineMessageText',
|
_: 'inputBotInlineMessageText',
|
||||||
|
@ -281,7 +262,7 @@ export namespace BotInlineMessage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'media': {
|
case 'media': {
|
||||||
const [message, entities] = await _parseEntities(client, obj.text, parseMode, obj.entities)
|
const [message, entities] = await _normalizeInputText(client, obj.text)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_: 'inputBotInlineMessageMediaAuto',
|
_: 'inputBotInlineMessageMediaAuto',
|
||||||
|
@ -336,7 +317,7 @@ export namespace BotInlineMessage {
|
||||||
replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup),
|
replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup),
|
||||||
}
|
}
|
||||||
case 'webpage': {
|
case 'webpage': {
|
||||||
const [message, entities] = await _parseEntities(client, obj.text, parseMode, obj.entities)
|
const [message, entities] = await _normalizeInputText(client, obj.text)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_: 'inputBotInlineMessageMediaWebPage',
|
_: 'inputBotInlineMessageMediaWebPage',
|
||||||
|
|
|
@ -725,7 +725,6 @@ export namespace BotInline {
|
||||||
export async function _convertToTl(
|
export async function _convertToTl(
|
||||||
client: BaseTelegramClient,
|
client: BaseTelegramClient,
|
||||||
results: InputInlineResult[],
|
results: InputInlineResult[],
|
||||||
parseMode?: string | null,
|
|
||||||
): Promise<[boolean, tl.TypeInputBotInlineResult[]]> {
|
): Promise<[boolean, tl.TypeInputBotInlineResult[]]> {
|
||||||
const normalizeThumb = (obj: InputInlineResult, fallback?: string): tl.RawInputWebDocument | undefined => {
|
const normalizeThumb = (obj: InputInlineResult, fallback?: string): tl.RawInputWebDocument | undefined => {
|
||||||
if (obj.type !== 'voice' && obj.type !== 'audio' && obj.type !== 'sticker' && obj.type !== 'game') {
|
if (obj.type !== 'voice' && obj.type !== 'audio' && obj.type !== 'sticker' && obj.type !== 'game') {
|
||||||
|
@ -760,7 +759,7 @@ export namespace BotInline {
|
||||||
let sendMessage: tl.TypeInputBotInlineMessage
|
let sendMessage: tl.TypeInputBotInlineMessage
|
||||||
|
|
||||||
if (obj.message) {
|
if (obj.message) {
|
||||||
sendMessage = await BotInlineMessage._convertToTl(client, obj.message, parseMode)
|
sendMessage = await BotInlineMessage._convertToTl(client, obj.message)
|
||||||
} else {
|
} else {
|
||||||
let message = obj.title
|
let message = obj.title
|
||||||
const entities: tl.TypeMessageEntity[] = [
|
const entities: tl.TypeMessageEntity[] = [
|
||||||
|
@ -817,7 +816,7 @@ export namespace BotInline {
|
||||||
let sendMessage: tl.TypeInputBotInlineMessage
|
let sendMessage: tl.TypeInputBotInlineMessage
|
||||||
|
|
||||||
if (obj.message) {
|
if (obj.message) {
|
||||||
sendMessage = await BotInlineMessage._convertToTl(client, obj.message, parseMode)
|
sendMessage = await BotInlineMessage._convertToTl(client, obj.message)
|
||||||
|
|
||||||
if (sendMessage._ !== 'inputBotInlineMessageGame') {
|
if (sendMessage._ !== 'inputBotInlineMessageGame') {
|
||||||
throw new MtArgumentError('game inline result must contain a game inline message')
|
throw new MtArgumentError('game inline result must contain a game inline message')
|
||||||
|
@ -850,7 +849,7 @@ export namespace BotInline {
|
||||||
let sendMessage: tl.TypeInputBotInlineMessage
|
let sendMessage: tl.TypeInputBotInlineMessage
|
||||||
|
|
||||||
if (obj.message) {
|
if (obj.message) {
|
||||||
sendMessage = await BotInlineMessage._convertToTl(client, obj.message, parseMode)
|
sendMessage = await BotInlineMessage._convertToTl(client, obj.message)
|
||||||
} else if (obj.type === 'venue') {
|
} else if (obj.type === 'venue') {
|
||||||
if (obj.latitude && obj.longitude) {
|
if (obj.latitude && obj.longitude) {
|
||||||
sendMessage = {
|
sendMessage = {
|
||||||
|
|
|
@ -7,7 +7,6 @@ export * from './files/index.js'
|
||||||
export * from './media/index.js'
|
export * from './media/index.js'
|
||||||
export * from './messages/index.js'
|
export * from './messages/index.js'
|
||||||
export * from './misc/index.js'
|
export * from './misc/index.js'
|
||||||
export * from './parser.js'
|
|
||||||
export * from './peers/index.js'
|
export * from './peers/index.js'
|
||||||
export * from './reactions/index.js'
|
export * from './reactions/index.js'
|
||||||
export * from './stories/index.js'
|
export * from './stories/index.js'
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { MaybeArray, tl } from '@mtcute/core'
|
import { MaybeArray, tl } from '@mtcute/core'
|
||||||
|
|
||||||
|
import { InputText } from '../../types/misc/entities.js'
|
||||||
import { InputFileLike } from '../files/index.js'
|
import { InputFileLike } from '../files/index.js'
|
||||||
import { FormattedString } from '../parser.js'
|
|
||||||
import { InputPeerLike } from '../peers/index.js'
|
import { InputPeerLike } from '../peers/index.js'
|
||||||
import { VenueSource } from './venue.js'
|
import { VenueSource } from './venue.js'
|
||||||
|
|
||||||
|
@ -9,13 +9,7 @@ export interface CaptionMixin {
|
||||||
/**
|
/**
|
||||||
* Caption of the media
|
* Caption of the media
|
||||||
*/
|
*/
|
||||||
caption?: string | FormattedString<string>
|
caption?: InputText
|
||||||
|
|
||||||
/**
|
|
||||||
* Caption entities of the media.
|
|
||||||
* If passed, parse mode is ignored
|
|
||||||
*/
|
|
||||||
entities?: tl.TypeMessageEntity[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileMixin {
|
export interface FileMixin {
|
||||||
|
@ -541,13 +535,7 @@ export interface InputMediaQuiz extends Omit<InputMediaPoll, 'type'> {
|
||||||
/**
|
/**
|
||||||
* Explanation of the quiz solution
|
* Explanation of the quiz solution
|
||||||
*/
|
*/
|
||||||
solution?: string | FormattedString<string>
|
solution?: InputText
|
||||||
|
|
||||||
/**
|
|
||||||
* Format entities for `solution`.
|
|
||||||
* If used, parse mode is ignored.
|
|
||||||
*/
|
|
||||||
solutionEntities?: tl.TypeMessageEntity[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
import { tl } from '@mtcute/core'
|
import { tl } from '@mtcute/core'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface describing some text with entities.
|
* Formatted text with entities
|
||||||
*
|
|
||||||
* Primarily used as a return type for parsers.
|
|
||||||
*/
|
*/
|
||||||
export interface TextWithEntities {
|
export interface TextWithEntities {
|
||||||
readonly text: string
|
readonly text: string
|
||||||
readonly entities: tl.TypeMessageEntity[]
|
readonly entities?: tl.TypeMessageEntity[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type to be used as a parameter for methods that accept
|
||||||
|
* a formatted text with entities.
|
||||||
|
*
|
||||||
|
* Can be either a plain string or an object with `text` and `entities` fields.
|
||||||
|
*/
|
||||||
|
export type InputText = string | TextWithEntities
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
export * from './entities.js'
|
||||||
export * from './input-privacy-rule.js'
|
export * from './input-privacy-rule.js'
|
||||||
export * from './sticker-set.js'
|
export * from './sticker-set.js'
|
||||||
export * from './takeout-session.js'
|
export * from './takeout-session.js'
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
import { tl } from '@mtcute/core'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface describing a message entity parser.
|
|
||||||
*
|
|
||||||
* mtcute comes with HTML parser inside `@mtcute/html-parser`
|
|
||||||
* and Markdown parser inside `@mtcute/markdown-parser`.
|
|
||||||
*
|
|
||||||
* You are also free to implement your own parser and register it with
|
|
||||||
* {@link TelegramClient.registerParseMode}.
|
|
||||||
*/
|
|
||||||
export interface IMessageEntityParser {
|
|
||||||
/**
|
|
||||||
* Parser name, which will be used when registering it.
|
|
||||||
*/
|
|
||||||
name: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a string containing some text with formatting to plain text
|
|
||||||
* and message entities
|
|
||||||
*
|
|
||||||
* @param text Formatted text
|
|
||||||
* @returns A tuple containing plain text and a list of entities
|
|
||||||
*/
|
|
||||||
parse(text: string): [string, tl.TypeMessageEntity[]]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add formatting to the text given the plain text and the entities.
|
|
||||||
*
|
|
||||||
* > **Note**: `unparse(parse(text)) === text` is not always true!
|
|
||||||
*
|
|
||||||
* @param text Plain text
|
|
||||||
* @param entities Message entities that should be added to the text
|
|
||||||
*/
|
|
||||||
unparse(text: string, entities: ReadonlyArray<tl.TypeMessageEntity>): string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Raw string that will not be escaped when passing
|
|
||||||
* to tagged template helpers (like `html` and `md`)
|
|
||||||
*/
|
|
||||||
export class FormattedString<T extends string = never> {
|
|
||||||
/**
|
|
||||||
* @param value Value that the string holds
|
|
||||||
* @param mode Name of the parse mode used
|
|
||||||
*/
|
|
||||||
constructor(
|
|
||||||
readonly value: string,
|
|
||||||
readonly mode?: T,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
toString(): string {
|
|
||||||
return this.value
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
📖 [API Reference](https://ref.mtcute.dev/modules/_mtcute_html_parser.html)
|
📖 [API Reference](https://ref.mtcute.dev/modules/_mtcute_html_parser.html)
|
||||||
|
|
||||||
|
|
||||||
HTML entities parser for mtcute
|
HTML entities parser for mtcute
|
||||||
|
|
||||||
> **NOTE**: The syntax implemented here is **incompatible** with Bot API _HTML_.
|
> **NOTE**: The syntax implemented here is **incompatible** with Bot API _HTML_.
|
||||||
|
@ -12,21 +11,20 @@ HTML entities parser for mtcute
|
||||||
## Features
|
## Features
|
||||||
- Supports all entities that Telegram supports
|
- Supports all entities that Telegram supports
|
||||||
- Supports nested entities
|
- Supports nested entities
|
||||||
- Proper newline handling (just like in real HTML)
|
- Proper newline/whitespace handling (just like in real HTML)
|
||||||
- Automatic escaping of user input
|
- [Interpolation](#interpolation)!
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { TelegramClient } from '@mtcute/client'
|
import { html } from '@mtcute/html-parser'
|
||||||
import { HtmlMessageEntityParser, html } from '@mtcute/html-parser'
|
|
||||||
|
|
||||||
const tg = new TelegramClient({ ... })
|
|
||||||
tg.registerParseMode(new HtmlMessageEntityParser())
|
|
||||||
|
|
||||||
tg.sendText(
|
tg.sendText(
|
||||||
'me',
|
'me',
|
||||||
html`Hello, <b>me</b>! Updates from the feed:<br>${await getUpdatesFromFeed()}`
|
html`
|
||||||
|
Hello, <b>me</b>! Updates from the feed:<br>
|
||||||
|
${await getUpdatesFromFeed()}
|
||||||
|
`
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -97,18 +95,20 @@ Overlapping entities are supported in `unparse()`, though.
|
||||||
| `<b>bold <i>and</b> italic</i>` | **bold _and_** italic<br>⚠️ <i>word "italic" is not actually italic!</i> |
|
| `<b>bold <i>and</b> italic</i>` | **bold _and_** italic<br>⚠️ <i>word "italic" is not actually italic!</i> |
|
||||||
| `<b>bold <i>and</i></b><i> italic</i>`<br>⚠️ <i>this is how <code>unparse()</code> handles overlapping entities</i> | **bold _and_** _italic_ |
|
| `<b>bold <i>and</i></b><i> italic</i>`<br>⚠️ <i>this is how <code>unparse()</code> handles overlapping entities</i> | **bold _and_** _italic_ |
|
||||||
|
|
||||||
## Escaping
|
## Interpolation
|
||||||
|
|
||||||
Escaping in this parser works exactly the same as in `htmlparser2`.
|
Being a tagged template literal, `html` supports interpolation.
|
||||||
|
|
||||||
This means that you can keep `<>&` symbols as-is in some cases. However, when dealing with user input, it is always
|
You can interpolate one of the following:
|
||||||
better to use [`HtmlMessageEntityParser.escape`](./classes/htmlmessageentityparser.html#escape) or, even better,
|
- `string` - **will not** be parsed, and appended to plain text as-is
|
||||||
`html` helper:
|
- In case you want the string to be parsed, use `html` as a simple function: <code>html\`... ${html('**bold**')} ...\`</code>
|
||||||
|
- `number` - will be converted to string and appended to plain text as-is
|
||||||
|
- `TextWithEntities` or `MessageEntity` - will add the text and its entities to the output. This is the type returned by `html` itself:
|
||||||
|
```ts
|
||||||
|
const bold = html`**bold**`
|
||||||
|
const text = html`Hello, ${bold}!`
|
||||||
|
```
|
||||||
|
- falsy value (i.e. `null`, `undefined`, `false`) - will be ignored
|
||||||
|
|
||||||
```typescript
|
Note that because of interpolation, you almost never need to think about escaping anything,
|
||||||
import { html } from '@mtcute/html-parser'
|
since the values are not even parsed as HTML, and are appended to the output as-is.
|
||||||
|
|
||||||
const username = 'Boris <&>'
|
|
||||||
const text = html`Hi, ${username}!`
|
|
||||||
console.log(text) // Hi, Boris &lt;&amp;&gt;!
|
|
||||||
```
|
|
|
@ -1,84 +1,27 @@
|
||||||
import { Parser } from 'htmlparser2'
|
import { Parser } from 'htmlparser2'
|
||||||
import Long from 'long'
|
import Long from 'long'
|
||||||
|
|
||||||
import type { FormattedString, IMessageEntityParser, MessageEntity, tl } from '@mtcute/client'
|
import type { InputText, MessageEntity, TextWithEntities, tl } 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]+)(?:&|$)|&|$)/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tagged template based helper for escaping entities in HTML
|
* Escape a string to be safely used in HTML.
|
||||||
*
|
*
|
||||||
* @example
|
* > **Note**: this function is in most cases not needed, as `html` function
|
||||||
* ```typescript
|
* > handles all `string`s passed to it automatically as plain text.
|
||||||
* const escaped = html`<b>${user.displayName}</b>`
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
export function html(
|
function escape(str: string, quote = false): string {
|
||||||
strings: TemplateStringsArray,
|
|
||||||
...sub: (string | FormattedString<'html'> | MessageEntity | boolean | undefined | null)[]
|
|
||||||
): FormattedString<'html'> {
|
|
||||||
let str = ''
|
|
||||||
sub.forEach((it, idx) => {
|
|
||||||
if (typeof it === 'boolean' || !it) return
|
|
||||||
|
|
||||||
if (typeof it === 'string') {
|
|
||||||
it = HtmlMessageEntityParser.escape(it, Boolean(str.match(/=['"]$/)))
|
|
||||||
} else if ('raw' in it) {
|
|
||||||
it = new HtmlMessageEntityParser().unparse(it.text, [it.raw])
|
|
||||||
} else {
|
|
||||||
if (it.mode && it.mode !== 'html') {
|
|
||||||
throw new Error(`Incompatible parse mode: ${it.mode}`)
|
|
||||||
}
|
|
||||||
it = it.value
|
|
||||||
}
|
|
||||||
|
|
||||||
str += strings[idx] + it
|
|
||||||
})
|
|
||||||
|
|
||||||
return { value: str + strings[strings.length - 1], mode: 'html' }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Syntax highlighter function used in {@link HtmlMessageEntityParser.unparse}
|
|
||||||
*
|
|
||||||
* Must be sync (this might change in the future) and must return valid HTML.
|
|
||||||
*/
|
|
||||||
export type SyntaxHighlighter = (code: string, language: string) => string
|
|
||||||
|
|
||||||
export interface HtmlMessageEntityParserOptions {
|
|
||||||
syntaxHighlighter?: SyntaxHighlighter
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTML MessageEntity parser.
|
|
||||||
*
|
|
||||||
* This class implements syntax very similar to one available
|
|
||||||
* in the Bot API ([documented here](https://core.telegram.org/bots/api#html-style))
|
|
||||||
* with some slight differences.
|
|
||||||
*/
|
|
||||||
export class HtmlMessageEntityParser implements IMessageEntityParser {
|
|
||||||
name = 'html'
|
|
||||||
|
|
||||||
private readonly _syntaxHighlighter?: SyntaxHighlighter
|
|
||||||
|
|
||||||
constructor(options?: HtmlMessageEntityParserOptions) {
|
|
||||||
this._syntaxHighlighter = options?.syntaxHighlighter
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Escape the string so it can be safely used inside HTML
|
|
||||||
*
|
|
||||||
* @param str String to be escaped
|
|
||||||
* @param quote Whether `"` (double quote) should be escaped as `"`
|
|
||||||
*/
|
|
||||||
static escape(str: string, quote = false): string {
|
|
||||||
str = str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
str = str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
if (quote) str = str.replace(/"/g, '"')
|
if (quote) str = str.replace(/"/g, '"')
|
||||||
|
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
parse(text: string): [string, tl.TypeMessageEntity[]] {
|
function parse(
|
||||||
|
strings: TemplateStringsArray | string,
|
||||||
|
...sub: (InputText | MessageEntity | boolean | number | undefined | null)[]
|
||||||
|
): TextWithEntities {
|
||||||
const stacks: Record<string, tl.Mutable<tl.TypeMessageEntity>[]> = {}
|
const stacks: Record<string, tl.Mutable<tl.TypeMessageEntity>[]> = {}
|
||||||
const entities: tl.TypeMessageEntity[] = []
|
const entities: tl.TypeMessageEntity[] = []
|
||||||
let plainText = ''
|
let plainText = ''
|
||||||
|
@ -271,29 +214,63 @@ export class HtmlMessageEntityParser implements IMessageEntityParser {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
parser.write(text)
|
if (typeof strings === 'string') strings = [strings] as unknown as TemplateStringsArray
|
||||||
|
|
||||||
|
sub.forEach((it, idx) => {
|
||||||
|
parser.write(strings[idx])
|
||||||
|
|
||||||
|
if (typeof it === 'boolean' || !it) return
|
||||||
|
|
||||||
|
if (typeof it === 'string' || typeof it === 'number') {
|
||||||
|
pendingText += it
|
||||||
|
} else {
|
||||||
|
// TextWithEntities or MessageEntity
|
||||||
|
const text = it.text
|
||||||
|
const innerEntities = 'raw' in it ? [it.raw] : it.entities
|
||||||
|
|
||||||
|
processPendingText()
|
||||||
|
const baseOffset = plainText.length
|
||||||
|
pendingText += text
|
||||||
|
|
||||||
|
if (innerEntities) {
|
||||||
|
for (const ent of innerEntities) {
|
||||||
|
entities.push({ ...ent, offset: ent.offset + baseOffset })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
parser.write(strings[strings.length - 1])
|
||||||
|
|
||||||
processPendingText(true)
|
processPendingText(true)
|
||||||
|
|
||||||
return [plainText.replace(/\u00A0/g, ' '), entities]
|
return {
|
||||||
|
text: plainText.replace(/\u00A0/g, ' '),
|
||||||
|
entities,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
unparse(text: string, entities: ReadonlyArray<tl.TypeMessageEntity>): string {
|
/** Options passed to `html.unparse` */
|
||||||
return this._unparse(text, entities)
|
export interface HtmlUnparseOptions {
|
||||||
}
|
/**
|
||||||
|
* Syntax highlighter to use when un-parsing `pre` tags with language
|
||||||
|
*/
|
||||||
|
syntaxHighlighter?: (code: string, language: string) => string
|
||||||
|
}
|
||||||
|
|
||||||
// internal function that uses recursion to correctly process nested & overlapping entities
|
// internal function that uses recursion to correctly process nested & overlapping entities
|
||||||
private _unparse(
|
function _unparse(
|
||||||
text: string,
|
text: string,
|
||||||
entities: ReadonlyArray<tl.TypeMessageEntity>,
|
entities: ReadonlyArray<tl.TypeMessageEntity>,
|
||||||
|
params: HtmlUnparseOptions,
|
||||||
entitiesOffset = 0,
|
entitiesOffset = 0,
|
||||||
offset = 0,
|
offset = 0,
|
||||||
length = text.length,
|
length = text.length,
|
||||||
): string {
|
): string {
|
||||||
if (!text) return text
|
if (!text) return text
|
||||||
|
|
||||||
if (!entities.length || entities.length === entitiesOffset) {
|
if (!entities.length || entities.length === entitiesOffset) {
|
||||||
return HtmlMessageEntityParser.escape(text)
|
return escape(text)
|
||||||
.replace(/\n/g, '<br>')
|
.replace(/\n/g, '<br>')
|
||||||
.replace(/ {2,}/g, (match) => {
|
.replace(/ {2,}/g, (match) => {
|
||||||
return ' '.repeat(match.length)
|
return ' '.repeat(match.length)
|
||||||
|
@ -321,7 +298,7 @@ export class HtmlMessageEntityParser implements IMessageEntityParser {
|
||||||
|
|
||||||
if (relativeOffset > lastOffset) {
|
if (relativeOffset > lastOffset) {
|
||||||
// add missing plain text
|
// add missing plain text
|
||||||
html.push(HtmlMessageEntityParser.escape(text.substring(lastOffset, relativeOffset)))
|
html.push(escape(text.substring(lastOffset, relativeOffset)))
|
||||||
} else if (relativeOffset < lastOffset) {
|
} else if (relativeOffset < lastOffset) {
|
||||||
length -= lastOffset - relativeOffset
|
length -= lastOffset - relativeOffset
|
||||||
relativeOffset = lastOffset
|
relativeOffset = lastOffset
|
||||||
|
@ -343,7 +320,7 @@ export class HtmlMessageEntityParser implements IMessageEntityParser {
|
||||||
if (type === 'messageEntityPre') {
|
if (type === 'messageEntityPre') {
|
||||||
entityText = substr
|
entityText = substr
|
||||||
} else {
|
} else {
|
||||||
entityText = this._unparse(substr, entities, i + 1, offset + relativeOffset, length)
|
entityText = _unparse(substr, entities, params, i + 1, offset + relativeOffset, length)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
@ -372,8 +349,8 @@ export class HtmlMessageEntityParser implements IMessageEntityParser {
|
||||||
case 'messageEntityPre':
|
case 'messageEntityPre':
|
||||||
html.push(
|
html.push(
|
||||||
`<pre${entity.language ? ` language="${entity.language}"` : ''}>${
|
`<pre${entity.language ? ` language="${entity.language}"` : ''}>${
|
||||||
this._syntaxHighlighter && entity.language ?
|
params.syntaxHighlighter && entity.language ?
|
||||||
this._syntaxHighlighter(entityText, entity.language) :
|
params.syntaxHighlighter(entityText, entity.language) :
|
||||||
entityText
|
entityText
|
||||||
}</pre>`,
|
}</pre>`,
|
||||||
)
|
)
|
||||||
|
@ -385,7 +362,7 @@ export class HtmlMessageEntityParser implements IMessageEntityParser {
|
||||||
html.push(`<a href="${entityText}">${entityText}</a>`)
|
html.push(`<a href="${entityText}">${entityText}</a>`)
|
||||||
break
|
break
|
||||||
case 'messageEntityTextUrl':
|
case 'messageEntityTextUrl':
|
||||||
html.push(`<a href="${HtmlMessageEntityParser.escape(entity.url, true)}">${entityText}</a>`)
|
html.push(`<a href="${escape(entity.url, true)}">${entityText}</a>`)
|
||||||
break
|
break
|
||||||
case 'messageEntityMentionName':
|
case 'messageEntityMentionName':
|
||||||
html.push(`<a href="tg://user?id=${entity.userId}">${entityText}</a>`)
|
html.push(`<a href="tg://user?id=${entity.userId}">${entityText}</a>`)
|
||||||
|
@ -398,8 +375,61 @@ export class HtmlMessageEntityParser implements IMessageEntityParser {
|
||||||
lastOffset = relativeOffset + (skip ? 0 : length)
|
lastOffset = relativeOffset + (skip ? 0 : length)
|
||||||
}
|
}
|
||||||
|
|
||||||
html.push(HtmlMessageEntityParser.escape(text.substr(lastOffset)))
|
html.push(escape(text.substr(lastOffset)))
|
||||||
|
|
||||||
return html.join('')
|
return html.join('')
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add HTML formatting to the text given the plain text and entities contained in it.
|
||||||
|
*/
|
||||||
|
function unparse(input: InputText, options?: HtmlUnparseOptions): string {
|
||||||
|
if (typeof input === 'string') {
|
||||||
|
return _unparse(input, [], options ?? {})
|
||||||
|
}
|
||||||
|
|
||||||
|
return _unparse(input.text, input.entities ?? [], options ?? {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// typedoc doesn't support this yet, so we'll have to do it manually
|
||||||
|
// https://github.com/TypeStrong/typedoc/issues/2436
|
||||||
|
|
||||||
|
export const html: {
|
||||||
|
/**
|
||||||
|
* Tagged template based HTML-to-entities parser function
|
||||||
|
*
|
||||||
|
* Additionally, `md` function has two static methods:
|
||||||
|
* - `html.escape` - escape a string to be safely used in HTML
|
||||||
|
* (should not be needed in most cases, as `html` function itself handles all `string`s
|
||||||
|
* passed to it automatically as plain text)
|
||||||
|
* - `html.unparse` - add HTML formatting to the text given the plain text and entities contained in it
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const text = html`<b>${user.displayName}</b>`
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
(
|
||||||
|
strings: TemplateStringsArray,
|
||||||
|
...sub: (InputText | MessageEntity | boolean | number | undefined | null)[]
|
||||||
|
): TextWithEntities
|
||||||
|
/**
|
||||||
|
* A variant taking a plain JS string as input
|
||||||
|
* and parsing it.
|
||||||
|
*
|
||||||
|
* Useful for cases when you already have a string
|
||||||
|
* (e.g. from some server) and want to parse it.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const string = '<b>hello</b>'
|
||||||
|
* const text = html(string)
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
(string: string): TextWithEntities
|
||||||
|
escape: typeof escape
|
||||||
|
unparse: typeof unparse
|
||||||
|
} = Object.assign(parse, {
|
||||||
|
escape,
|
||||||
|
unparse,
|
||||||
|
})
|
||||||
|
|
|
@ -2,9 +2,12 @@ import { expect } from 'chai'
|
||||||
import Long from 'long'
|
import Long from 'long'
|
||||||
import { describe, it } from 'mocha'
|
import { describe, it } from 'mocha'
|
||||||
|
|
||||||
import { FormattedString, tl } from '@mtcute/client'
|
import { MessageEntity, TextWithEntities, tl } from '@mtcute/client'
|
||||||
|
|
||||||
import { html, HtmlMessageEntityParser } from '../src/index.js'
|
// prettier has "html" special-cased which breaks the formatting
|
||||||
|
// this is not an issue when using normally, since we properly handle newlines/spaces,
|
||||||
|
// but here we want to test everything as it is
|
||||||
|
import { html as htm, HtmlUnparseOptions } from '../src/index.js'
|
||||||
|
|
||||||
const createEntity = <T extends tl.TypeMessageEntity['_']>(
|
const createEntity = <T extends tl.TypeMessageEntity['_']>(
|
||||||
type: T,
|
type: T,
|
||||||
|
@ -21,11 +24,14 @@ const createEntity = <T extends tl.TypeMessageEntity['_']>(
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('HtmlMessageEntityParser', () => {
|
describe('HtmlMessageEntityParser', () => {
|
||||||
const parser = new HtmlMessageEntityParser()
|
|
||||||
|
|
||||||
describe('unparse', () => {
|
describe('unparse', () => {
|
||||||
const test = (text: string, entities: tl.TypeMessageEntity[], expected: string, _parser = parser): void => {
|
const test = (
|
||||||
expect(_parser.unparse(text, entities)).eq(expected)
|
text: string,
|
||||||
|
entities: tl.TypeMessageEntity[],
|
||||||
|
expected: string,
|
||||||
|
params?: HtmlUnparseOptions,
|
||||||
|
): void => {
|
||||||
|
expect(htm.unparse({ text, entities }, params)).eq(expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should return the same text if there are no entities or text', () => {
|
it('should return the same text if there are no entities or text', () => {
|
||||||
|
@ -197,10 +203,6 @@ describe('HtmlMessageEntityParser', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should work with custom syntax highlighter', () => {
|
it('should work with custom syntax highlighter', () => {
|
||||||
const parser = new HtmlMessageEntityParser({
|
|
||||||
syntaxHighlighter: (code, lang) => `lang: <b>${lang}</b><br>${code}`,
|
|
||||||
})
|
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'plain console.log("Hello, world!") some code plain',
|
'plain console.log("Hello, world!") some code plain',
|
||||||
[
|
[
|
||||||
|
@ -210,7 +212,9 @@ describe('HtmlMessageEntityParser', () => {
|
||||||
createEntity('messageEntityPre', 35, 9, { language: '' }),
|
createEntity('messageEntityPre', 35, 9, { language: '' }),
|
||||||
],
|
],
|
||||||
'plain <pre language="javascript">lang: <b>javascript</b><br>console.log("Hello, world!")</pre> <pre>some code</pre> plain',
|
'plain <pre language="javascript">lang: <b>javascript</b><br>console.log("Hello, world!")</pre> <pre>some code</pre> plain',
|
||||||
parser,
|
{
|
||||||
|
syntaxHighlighter: (code, lang) => `lang: <b>${lang}</b><br>${code}`,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -226,15 +230,14 @@ describe('HtmlMessageEntityParser', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('parse', () => {
|
describe('parse', () => {
|
||||||
const test = (text: string, expectedEntities: tl.TypeMessageEntity[], expectedText: string): void => {
|
const test = (text: TextWithEntities, expectedEntities: tl.TypeMessageEntity[], expectedText: string): void => {
|
||||||
const [_text, entities] = parser.parse(text)
|
expect(text.text).eql(expectedText)
|
||||||
expect(_text).eql(expectedText)
|
expect(text.entities ?? []).eql(expectedEntities)
|
||||||
expect(entities).eql(expectedEntities)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should handle <b>, <i>, <u>, <s> tags', () => {
|
it('should handle <b>, <i>, <u>, <s> tags', () => {
|
||||||
test(
|
test(
|
||||||
'plain <b>bold</b> <i>italic</i> <u>underline</u> <s>strikethrough</s> plain',
|
htm`plain <b>bold</b> <i>italic</i> <u>underline</u> <s>strikethrough</s> plain`,
|
||||||
[
|
[
|
||||||
createEntity('messageEntityBold', 6, 4),
|
createEntity('messageEntityBold', 6, 4),
|
||||||
createEntity('messageEntityItalic', 11, 6),
|
createEntity('messageEntityItalic', 11, 6),
|
||||||
|
@ -247,7 +250,7 @@ describe('HtmlMessageEntityParser', () => {
|
||||||
|
|
||||||
it('should handle <code>, <pre>, <blockquote>, <spoiler> tags', () => {
|
it('should handle <code>, <pre>, <blockquote>, <spoiler> tags', () => {
|
||||||
test(
|
test(
|
||||||
'plain <code>code</code> <pre>pre</pre> <blockquote>blockquote</blockquote> <spoiler>spoiler</spoiler> plain',
|
htm`plain <code>code</code> <pre>pre</pre> <blockquote>blockquote</blockquote> <spoiler>spoiler</spoiler> plain`,
|
||||||
[
|
[
|
||||||
createEntity('messageEntityCode', 6, 4),
|
createEntity('messageEntityCode', 6, 4),
|
||||||
createEntity('messageEntityPre', 11, 3, { language: '' }),
|
createEntity('messageEntityPre', 11, 3, { language: '' }),
|
||||||
|
@ -260,7 +263,7 @@ describe('HtmlMessageEntityParser', () => {
|
||||||
|
|
||||||
it('should handle links and text mentions', () => {
|
it('should handle links and text mentions', () => {
|
||||||
test(
|
test(
|
||||||
'plain https://google.com <a href="https://google.com">google</a> @durov <a href="tg://user?id=36265675">Pavel Durov</a> plain',
|
htm`plain https://google.com <a href="https://google.com">google</a> @durov <a href="tg://user?id=36265675">Pavel Durov</a> plain`,
|
||||||
[
|
[
|
||||||
createEntity('messageEntityTextUrl', 25, 6, {
|
createEntity('messageEntityTextUrl', 25, 6, {
|
||||||
url: 'https://google.com',
|
url: 'https://google.com',
|
||||||
|
@ -273,7 +276,7 @@ describe('HtmlMessageEntityParser', () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'<a href="tg://user?id=1234567&hash=aabbccddaabbccdd">user</a>',
|
htm`<a href="tg://user?id=1234567&hash=aabbccddaabbccdd">user</a>`,
|
||||||
[
|
[
|
||||||
createEntity('inputMessageEntityMentionName', 0, 4, {
|
createEntity('inputMessageEntityMentionName', 0, 4, {
|
||||||
userId: {
|
userId: {
|
||||||
|
@ -289,7 +292,7 @@ describe('HtmlMessageEntityParser', () => {
|
||||||
|
|
||||||
it('should handle language in <pre>', () => {
|
it('should handle language in <pre>', () => {
|
||||||
test(
|
test(
|
||||||
'plain <pre language="javascript">console.log("Hello, world!")</pre> <pre>some code</pre> plain',
|
htm`plain <pre language="javascript">console.log("Hello, world!")</pre> <pre>some code</pre> plain`,
|
||||||
[
|
[
|
||||||
createEntity('messageEntityPre', 6, 28, {
|
createEntity('messageEntityPre', 6, 28, {
|
||||||
language: 'javascript',
|
language: 'javascript',
|
||||||
|
@ -302,31 +305,31 @@ describe('HtmlMessageEntityParser', () => {
|
||||||
|
|
||||||
it('should ignore other tags inside <pre>', () => {
|
it('should ignore other tags inside <pre>', () => {
|
||||||
test(
|
test(
|
||||||
'<pre><b>bold</b> and not bold</pre>',
|
htm`<pre><b>bold</b> and not bold</pre>`,
|
||||||
[createEntity('messageEntityPre', 0, 17, { language: '' })],
|
[createEntity('messageEntityPre', 0, 17, { language: '' })],
|
||||||
'bold and not bold',
|
'bold and not bold',
|
||||||
)
|
)
|
||||||
test(
|
test(
|
||||||
'<pre><pre>pre inside pre</pre> so cool</pre>',
|
htm`<pre><pre>pre inside pre</pre> so cool</pre>`,
|
||||||
[createEntity('messageEntityPre', 0, 22, { language: '' })],
|
[createEntity('messageEntityPre', 0, 22, { language: '' })],
|
||||||
'pre inside pre so cool',
|
'pre inside pre so cool',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should ignore newlines and indentation', () => {
|
it('should ignore newlines and indentation', () => {
|
||||||
test('this is some text\n\nwith newlines', [], 'this is some text with newlines')
|
test(htm`this is some text\n\nwith newlines`, [], 'this is some text with newlines')
|
||||||
test(
|
test(
|
||||||
'<b>this is some text\n\nwith</b> newlines',
|
htm`<b>this is some text\n\nwith</b> newlines`,
|
||||||
[createEntity('messageEntityBold', 0, 22)],
|
[createEntity('messageEntityBold', 0, 22)],
|
||||||
'this is some text with newlines',
|
'this is some text with newlines',
|
||||||
)
|
)
|
||||||
test(
|
test(
|
||||||
'<b>this is some text ending with\n\n</b> newlines',
|
htm`<b>this is some text ending with\n\n</b> newlines`,
|
||||||
[createEntity('messageEntityBold', 0, 29)],
|
[createEntity('messageEntityBold', 0, 29)],
|
||||||
'this is some text ending with newlines',
|
'this is some text ending with newlines',
|
||||||
)
|
)
|
||||||
test(
|
test(
|
||||||
`
|
htm`
|
||||||
this is some indented text
|
this is some indented text
|
||||||
with newlines and
|
with newlines and
|
||||||
<b>
|
<b>
|
||||||
|
@ -341,7 +344,7 @@ describe('HtmlMessageEntityParser', () => {
|
||||||
|
|
||||||
it('should not ignore newlines and indentation in pre', () => {
|
it('should not ignore newlines and indentation in pre', () => {
|
||||||
test(
|
test(
|
||||||
'<pre>this is some text\n\nwith newlines</pre>',
|
htm`<pre>this is some text\n\nwith newlines</pre>`,
|
||||||
[createEntity('messageEntityPre', 0, 32, { language: '' })],
|
[createEntity('messageEntityPre', 0, 32, { language: '' })],
|
||||||
'this is some text\n\nwith newlines',
|
'this is some text\n\nwith newlines',
|
||||||
)
|
)
|
||||||
|
@ -349,7 +352,7 @@ describe('HtmlMessageEntityParser', () => {
|
||||||
// fuck my life
|
// fuck my life
|
||||||
const indent = ' '
|
const indent = ' '
|
||||||
test(
|
test(
|
||||||
`<pre>
|
htm`<pre>
|
||||||
this is some indented text
|
this is some indented text
|
||||||
with newlines and
|
with newlines and
|
||||||
<b>
|
<b>
|
||||||
|
@ -376,9 +379,9 @@ describe('HtmlMessageEntityParser', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle <br>', () => {
|
it('should handle <br>', () => {
|
||||||
test('this is some text<br><br>with actual newlines', [], 'this is some text\n\nwith actual newlines')
|
test(htm`this is some text<br><br>with actual newlines`, [], 'this is some text\n\nwith actual newlines')
|
||||||
test(
|
test(
|
||||||
'<b>this is some text<br><br></b>with actual newlines',
|
htm`<b>this is some text<br><br></b>with actual newlines`,
|
||||||
// note that the <br> (i.e. \n) is not included in the entity
|
// note that the <br> (i.e. \n) is not included in the entity
|
||||||
// this is expected, and the result is the same
|
// this is expected, and the result is the same
|
||||||
[createEntity('messageEntityBold', 0, 17)],
|
[createEntity('messageEntityBold', 0, 17)],
|
||||||
|
@ -388,7 +391,7 @@ describe('HtmlMessageEntityParser', () => {
|
||||||
|
|
||||||
it('should handle ', () => {
|
it('should handle ', () => {
|
||||||
test(
|
test(
|
||||||
'one space, many spaces, and<br>a newline',
|
htm`one space, many spaces, and<br>a newline`,
|
||||||
[],
|
[],
|
||||||
'one space, many spaces, and\na newline',
|
'one space, many spaces, and\na newline',
|
||||||
)
|
)
|
||||||
|
@ -396,19 +399,19 @@ describe('HtmlMessageEntityParser', () => {
|
||||||
|
|
||||||
it('should support entities on the edges', () => {
|
it('should support entities on the edges', () => {
|
||||||
test(
|
test(
|
||||||
'<b>Hello</b>, <b>world</b>',
|
htm`<b>Hello</b>, <b>world</b>`,
|
||||||
[createEntity('messageEntityBold', 0, 5), createEntity('messageEntityBold', 7, 5)],
|
[createEntity('messageEntityBold', 0, 5), createEntity('messageEntityBold', 7, 5)],
|
||||||
'Hello, world',
|
'Hello, world',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return empty array if there are no entities', () => {
|
it('should return empty array if there are no entities', () => {
|
||||||
test('Hello, world', [], 'Hello, world')
|
test(htm`Hello, world`, [], 'Hello, world')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should support entities followed by each other', () => {
|
it('should support entities followed by each other', () => {
|
||||||
test(
|
test(
|
||||||
'plain <b>Hello,</b><i> world</i> plain',
|
htm`plain <b>Hello,</b><i> world</i> plain`,
|
||||||
[createEntity('messageEntityBold', 6, 6), createEntity('messageEntityItalic', 12, 6)],
|
[createEntity('messageEntityBold', 6, 6), createEntity('messageEntityItalic', 12, 6)],
|
||||||
'plain Hello, world plain',
|
'plain Hello, world plain',
|
||||||
)
|
)
|
||||||
|
@ -416,7 +419,7 @@ describe('HtmlMessageEntityParser', () => {
|
||||||
|
|
||||||
it('should support nested entities', () => {
|
it('should support nested entities', () => {
|
||||||
test(
|
test(
|
||||||
'<i>Welcome to the <b>gym zone</b>!</i>',
|
htm`<i>Welcome to the <b>gym zone</b>!</i>`,
|
||||||
[createEntity('messageEntityBold', 15, 8), createEntity('messageEntityItalic', 0, 24)],
|
[createEntity('messageEntityBold', 15, 8), createEntity('messageEntityItalic', 0, 24)],
|
||||||
'Welcome to the gym zone!',
|
'Welcome to the gym zone!',
|
||||||
)
|
)
|
||||||
|
@ -424,22 +427,22 @@ describe('HtmlMessageEntityParser', () => {
|
||||||
|
|
||||||
it('should support nested entities with the same edges', () => {
|
it('should support nested entities with the same edges', () => {
|
||||||
test(
|
test(
|
||||||
'<i>Welcome to the <b>gym zone!</b></i>',
|
htm`<i>Welcome to the <b>gym zone!</b></i>`,
|
||||||
[createEntity('messageEntityBold', 15, 9), createEntity('messageEntityItalic', 0, 24)],
|
[createEntity('messageEntityBold', 15, 9), createEntity('messageEntityItalic', 0, 24)],
|
||||||
'Welcome to the gym zone!',
|
'Welcome to the gym zone!',
|
||||||
)
|
)
|
||||||
test(
|
test(
|
||||||
'<b>Welcome to the <i>gym zone!</i></b>',
|
htm`<b>Welcome to the <i>gym zone!</i></b>`,
|
||||||
[createEntity('messageEntityItalic', 15, 9), createEntity('messageEntityBold', 0, 24)],
|
[createEntity('messageEntityItalic', 15, 9), createEntity('messageEntityBold', 0, 24)],
|
||||||
'Welcome to the gym zone!',
|
'Welcome to the gym zone!',
|
||||||
)
|
)
|
||||||
test(
|
test(
|
||||||
'<i><b>Welcome</b> to the gym zone!</i>',
|
htm`<i><b>Welcome</b> to the gym zone!</i>`,
|
||||||
[createEntity('messageEntityBold', 0, 7), createEntity('messageEntityItalic', 0, 24)],
|
[createEntity('messageEntityBold', 0, 7), createEntity('messageEntityItalic', 0, 24)],
|
||||||
'Welcome to the gym zone!',
|
'Welcome to the gym zone!',
|
||||||
)
|
)
|
||||||
test(
|
test(
|
||||||
'<i><b>Welcome to the gym zone!</b></i>',
|
htm`<i><b>Welcome to the gym zone!</b></i>`,
|
||||||
[createEntity('messageEntityBold', 0, 24), createEntity('messageEntityItalic', 0, 24)],
|
[createEntity('messageEntityBold', 0, 24), createEntity('messageEntityItalic', 0, 24)],
|
||||||
'Welcome to the gym zone!',
|
'Welcome to the gym zone!',
|
||||||
)
|
)
|
||||||
|
@ -447,7 +450,7 @@ describe('HtmlMessageEntityParser', () => {
|
||||||
|
|
||||||
it('should properly handle emojis', () => {
|
it('should properly handle emojis', () => {
|
||||||
test(
|
test(
|
||||||
"<i>best flower</i>: <b>🌸</b>. <i>don't</i> you even doubt it.",
|
htm`<i>best flower</i>: <b>🌸</b>. <i>don't</i> you even doubt it.`,
|
||||||
[
|
[
|
||||||
createEntity('messageEntityItalic', 0, 11),
|
createEntity('messageEntityItalic', 0, 11),
|
||||||
createEntity('messageEntityBold', 13, 2),
|
createEntity('messageEntityBold', 13, 2),
|
||||||
|
@ -458,12 +461,12 @@ describe('HtmlMessageEntityParser', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle non-escaped special symbols', () => {
|
it('should handle non-escaped special symbols', () => {
|
||||||
test('<&> <b>< & ></b> <&>', [createEntity('messageEntityBold', 4, 5)], '<&> < & > <&>')
|
test(htm`<&> <b>< & ></b> <&>`, [createEntity('messageEntityBold', 4, 5)], '<&> < & > <&>')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should unescape special symbols', () => {
|
it('should unescape special symbols', () => {
|
||||||
test(
|
test(
|
||||||
'<&> <b>< & ></b> <&> <a href="/?a="hello"&b">link</a>',
|
htm`<&> <b>< & ></b> <&> <a href="/?a="hello"&b">link</a>`,
|
||||||
[
|
[
|
||||||
createEntity('messageEntityBold', 4, 5),
|
createEntity('messageEntityBold', 4, 5),
|
||||||
createEntity('messageEntityTextUrl', 14, 4, {
|
createEntity('messageEntityTextUrl', 14, 4, {
|
||||||
|
@ -475,44 +478,96 @@ describe('HtmlMessageEntityParser', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should ignore other tags', () => {
|
it('should ignore other tags', () => {
|
||||||
test('<script>alert(1)</script>', [], 'alert(1)')
|
test(htm`<script>alert(1)</script>`, [], 'alert(1)')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should ignore empty urls', () => {
|
it('should ignore empty urls', () => {
|
||||||
test('<a href="">link</a> <a>link</a>', [], 'link link')
|
test(htm`<a href="">link</a> <a>link</a>`, [], 'link link')
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('template', () => {
|
describe('template', () => {
|
||||||
it('should work as a tagged template literal', () => {
|
it('should add plain strings as is', () => {
|
||||||
const unsafeString = '<&>'
|
test(
|
||||||
|
htm`some text ${'<b>not bold yea</b>'} some more text`,
|
||||||
expect(html`${unsafeString}`.value).eq('<&>')
|
[],
|
||||||
expect(html`${unsafeString} <b>text</b>`.value).eq('<&> <b>text</b>')
|
'some text <b>not bold yea</b> some more text',
|
||||||
expect(html`<b>text</b> ${unsafeString}`.value).eq('<b>text</b> <&>')
|
)
|
||||||
expect(html`<b>${unsafeString}</b>`.value).eq('<b><&></b>')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should skip with FormattedString', () => {
|
it('should skip falsy values', () => {
|
||||||
const unsafeString2 = '<&>'
|
test(htm`some text ${null} some ${false} more text`, [], 'some text some more text')
|
||||||
const unsafeString = new FormattedString('<&>')
|
|
||||||
|
|
||||||
expect(html`${unsafeString}`.value).eq('<&>')
|
|
||||||
expect(html`${unsafeString} ${unsafeString2}`.value).eq('<&> <&>')
|
|
||||||
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><&> <&></b>')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should error with incompatible FormattedString', () => {
|
it('should process entities', () => {
|
||||||
const unsafeString = new FormattedString('<&>', 'html')
|
const inner = htm`<b>bold</b>`
|
||||||
const unsafeString2 = new FormattedString('<&>', 'some-other-mode')
|
test(
|
||||||
|
htm`some text ${inner} some more text`,
|
||||||
|
[createEntity('messageEntityBold', 10, 4)],
|
||||||
|
'some text bold some more text',
|
||||||
|
)
|
||||||
|
test(
|
||||||
|
htm`some text ${inner} some more ${inner} text`,
|
||||||
|
[createEntity('messageEntityBold', 10, 4), createEntity('messageEntityBold', 25, 4)],
|
||||||
|
'some text bold some more bold text',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
expect(() => html`${unsafeString}`.value).not.throw(Error)
|
it('should process entities on edges', () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
test(
|
||||||
// @ts-expect-error
|
htm`${htm`<b>bold</b>`} and ${htm`<i>italic</i>`}`,
|
||||||
expect(() => html`${unsafeString2}`.value).throw(Error)
|
[createEntity('messageEntityBold', 0, 4), createEntity('messageEntityItalic', 9, 6)],
|
||||||
|
'bold and italic',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should process nested entities', () => {
|
||||||
|
test(
|
||||||
|
htm`<b>bold ${htm`<i>bold italic</i>`} more bold</b>`,
|
||||||
|
[createEntity('messageEntityItalic', 5, 11), createEntity('messageEntityBold', 0, 26)],
|
||||||
|
'bold bold italic more bold',
|
||||||
|
)
|
||||||
|
test(
|
||||||
|
htm`<b>bold ${htm`<i>bold italic</i> <u>and some underline</u>`} more bold</b>`,
|
||||||
|
[
|
||||||
|
createEntity('messageEntityItalic', 5, 11),
|
||||||
|
createEntity('messageEntityUnderline', 17, 18),
|
||||||
|
createEntity('messageEntityBold', 0, 45),
|
||||||
|
],
|
||||||
|
'bold bold italic and some underline more bold',
|
||||||
|
)
|
||||||
|
test(
|
||||||
|
htm`<b>${htm`<i>bold italic <u>underline</u></i>`}</b>`,
|
||||||
|
[
|
||||||
|
createEntity('messageEntityUnderline', 12, 9),
|
||||||
|
createEntity('messageEntityItalic', 0, 21),
|
||||||
|
createEntity('messageEntityBold', 0, 21),
|
||||||
|
],
|
||||||
|
'bold italic underline',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should process MessageEntity', () => {
|
||||||
|
test(
|
||||||
|
htm`<b>bold ${new MessageEntity(
|
||||||
|
createEntity('messageEntityItalic', 0, 11),
|
||||||
|
'bold italic',
|
||||||
|
)} more bold</b>`,
|
||||||
|
[createEntity('messageEntityItalic', 5, 11), createEntity('messageEntityBold', 0, 26)],
|
||||||
|
'bold bold italic more bold',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support simple function usage', () => {
|
||||||
|
// assuming we are receiving it e.g. from a server
|
||||||
|
const someHtml = '<b>bold</b>'
|
||||||
|
|
||||||
|
test(htm(someHtml), [createEntity('messageEntityBold', 0, 4)], 'bold')
|
||||||
|
test(
|
||||||
|
htm`text ${htm(someHtml)} more text`,
|
||||||
|
[createEntity('messageEntityBold', 5, 4)],
|
||||||
|
'text bold more text',
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
import type { FormattedString } from '@mtcute/client'
|
import type { tl } from '@mtcute/client'
|
||||||
|
|
||||||
type Values<T> = T[keyof T]
|
type Values<T> = T[keyof T]
|
||||||
type SafeGet<T, K extends string> = T extends Record<K, unknown> ? T[K] : never
|
type SafeGet<T, K extends string> = T extends Record<K, unknown> ? T[K] : never
|
||||||
|
@ -8,7 +8,18 @@ type SafeGet<T, K extends string> = T extends Record<K, unknown> ? T[K] : never
|
||||||
/**
|
/**
|
||||||
* Literal translated value, represented by (optionally formatted) string
|
* Literal translated value, represented by (optionally formatted) string
|
||||||
*/
|
*/
|
||||||
export type I18nValueLiteral = string | FormattedString<string>
|
export type I18nValueLiteral =
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
readonly text: string
|
||||||
|
readonly entities?: tl.TypeMessageEntity[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ^ we're not using InputText from @mtcute/client because it's a type-only dependency
|
||||||
|
// and may not be available at runtime, and we don't want it to be `any`
|
||||||
|
//
|
||||||
|
// we check if this is assignable to InputText in tests, so it's fine
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dynamic translated value, represented by a
|
* Dynamic translated value, represented by a
|
||||||
* function resolving to a literal one
|
* function resolving to a literal one
|
||||||
|
@ -59,7 +70,7 @@ export type MtcuteI18nFunction<Strings, Input> = <K extends NestedKeysDelimited<
|
||||||
lang: Input | string | null,
|
lang: Input | string | null,
|
||||||
key: K,
|
key: K,
|
||||||
...params: ExtractParameter<Strings, K>
|
...params: ExtractParameter<Strings, K>
|
||||||
) => string | FormattedString<string>
|
) => I18nValueLiteral
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper type for i18n object containing strings for a language
|
* Wrapper type for i18n object containing strings for a language
|
||||||
|
|
|
@ -14,7 +14,7 @@ export function createI18nStringsIndex(strings: I18nStrings): Record<string, I18
|
||||||
for (const key in obj) {
|
for (const key in obj) {
|
||||||
const val = obj[key]
|
const val = obj[key]
|
||||||
|
|
||||||
if (typeof val === 'object' && !('value' in val)) {
|
if (typeof val === 'object' && !('text' in val)) {
|
||||||
add(val, prefix + key + '.')
|
add(val, prefix + key + '.')
|
||||||
} else {
|
} else {
|
||||||
ret[prefix + key] = val as string
|
ret[prefix + key] = val as string
|
||||||
|
|
|
@ -2,14 +2,17 @@
|
||||||
// This is a test for TypeScript typings
|
// This is a test for TypeScript typings
|
||||||
// This file is never executed, only compiled
|
// This file is never executed, only compiled
|
||||||
|
|
||||||
import { Message } from '@mtcute/client'
|
import { InputText, Message } from '@mtcute/client'
|
||||||
import { createMtcuteI18n, OtherLanguageWrap } from '../src/index.js'
|
import { createMtcuteI18n, OtherLanguageWrap } from '../src/index.js'
|
||||||
|
|
||||||
|
declare const someInputText: InputText
|
||||||
|
|
||||||
const en = {
|
const en = {
|
||||||
basic: {
|
basic: {
|
||||||
hello: 'Hello',
|
hello: 'Hello',
|
||||||
world: () => 'World',
|
world: () => 'World',
|
||||||
welcome: (name: string) => `Welcome ${name}`,
|
welcome: (name: string) => `Welcome ${name}`,
|
||||||
|
test: someInputText,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,22 +8,23 @@ Markdown entities parser for mtcute
|
||||||
>
|
>
|
||||||
> Please read [Syntax](#syntax) below for a detailed explanation
|
> Please read [Syntax](#syntax) below for a detailed explanation
|
||||||
|
|
||||||
> **Note**:
|
## Features
|
||||||
> It is generally recommended to use `@mtcute/html-parser` instead,
|
- Supports all entities that Telegram supports
|
||||||
> as it is easier to use and is more readable in most cases
|
- Supports nested and overlapping entities
|
||||||
|
- Supports dedentation
|
||||||
|
- [Interpolation](#interpolation)!
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { TelegramClient } from '@mtcute/client'
|
import { md } from '@mtcute/markdown-parser'
|
||||||
import { MarkdownMessageEntityParser, md } from '@mtcute/markdown-parser'
|
|
||||||
|
|
||||||
const tg = new TelegramClient({ ... })
|
|
||||||
tg.registerParseMode(new MarkdownMessageEntityParser())
|
|
||||||
|
|
||||||
tg.sendText(
|
tg.sendText(
|
||||||
'me',
|
'me',
|
||||||
md`Hello, **me**! Updates from the feed:\n${await getUpdatesFromFeed()}`
|
md`
|
||||||
|
Hello, **me**! Updates from the feed:
|
||||||
|
${await getUpdatesFromFeed()}
|
||||||
|
`
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -118,27 +119,20 @@ tags just as start/end markers, and not in terms of nesting.
|
||||||
| `**Welcome back, __User__!**` | **Welcome back, _User_!** | `<b>Welcome back, <i>User</i>!</b>` |
|
| `**Welcome back, __User__!**` | **Welcome back, _User_!** | `<b>Welcome back, <i>User</i>!</b>` |
|
||||||
| `**bold __and** italic__` | **bold _and_** _italic_ | `<b>bold <i>and</i></b><i> italic</i>` |
|
| `**bold __and** italic__` | **bold _and_** _italic_ | `<b>bold <i>and</i></b><i> italic</i>` |
|
||||||
|
|
||||||
## Escaping
|
## Interpolation
|
||||||
|
|
||||||
Often, you may want to escape the text in a way it is not processed as an entity.
|
Being a tagged template literal, `md` supports interpolation.
|
||||||
|
|
||||||
To escape any character, prepend it with ` \ ` (backslash). Escaped characters are added to output as-is.
|
You can interpolate one of the following:
|
||||||
|
- `string` - **will not** be parsed, and appended to plain text as-is
|
||||||
|
- In case you want the string to be parsed, use `md` as a simple function: <code>md\`... ${md('**bold**')} ...\`</code>
|
||||||
|
- `number` - will be converted to string and appended to plain text as-is
|
||||||
|
- `TextWithEntities` or `MessageEntity` - will add the text and its entities to the output. This is the type returned by `md` itself:
|
||||||
|
```ts
|
||||||
|
const bold = md`**bold**`
|
||||||
|
const text = md`Hello, ${bold}!`
|
||||||
|
```
|
||||||
|
- falsy value (i.e. `null`, `undefined`, `false`) - will be ignored
|
||||||
|
|
||||||
Inline entities and links inside code entities (both inline and pre) are not processed, so you only need to escape
|
Because of interpolation, you almost never need to think about escaping anything,
|
||||||
closing tags.
|
since the values are not even parsed as Markdown, and are appended to the output as-is.
|
||||||
|
|
||||||
> **Note**: backslash itself must be escaped like this: ` \\ ` (double backslash).
|
|
||||||
>
|
|
||||||
> This will look pretty bad in real code, so use escaping only when really needed, and use
|
|
||||||
> [`MarkdownMessageEntityParser.escape`](./classes/markdownmessageentityparser.html#escape) or `md` or
|
|
||||||
> other parse modes (like HTML one provided by [`@mtcute/html-parser`](../html-parser/index.html))) instead.
|
|
||||||
|
|
||||||
> In theory, you could escape every single non-markup character, but why would you want to do that 😜
|
|
||||||
|
|
||||||
| Code | Result (visual) | Result (as HTML) |
|
|
||||||
|----------------------------------------|--------------------------------|---------------------------------------------------------|
|
|
||||||
| `\_\_not italic\_\_` | \_\_not italic\_\_ | `__not italic__` |
|
|
||||||
| `__italic \_ text__` | _italic \_ text_ | `<i>italic _ text </i>` |
|
|
||||||
| <code>\`__not italic__\`</code> | `__not italic__` | `<code>__not italic__</code>` |
|
|
||||||
| <code>C:\\\\Users\\\\Guest</code> | C:\Users\Guest | `C:\Users\Guest` |
|
|
||||||
| <code>\`var a = \\\`hello\\\`\`</code> | <code>var a = \`hello\`</code> | <code><code>var a = \`hello\`</code></code> |
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Long from 'long'
|
import Long from 'long'
|
||||||
|
|
||||||
import type { FormattedString, IMessageEntityParser, MessageEntity, tl } from '@mtcute/client'
|
import type { InputText, MessageEntity, TextWithEntities, tl } 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]+)(?:&|$)|&|$)/
|
||||||
const EMOJI_REGEX = /^tg:\/\/emoji\?id=(-?\d+)/
|
const EMOJI_REGEX = /^tg:\/\/emoji\?id=(-?\d+)/
|
||||||
|
@ -16,61 +16,128 @@ const TAG_PRE = '```'
|
||||||
const TO_BE_ESCAPED = /[*_\-~`[\\\]|]/g
|
const TO_BE_ESCAPED = /[*_\-~`[\\\]|]/g
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tagged template based helper for escaping entities in Markdown
|
* Escape a string to be safely used in Markdown.
|
||||||
*
|
*
|
||||||
* @example
|
* > **Note**: this function is in most cases not needed, as `md` function
|
||||||
* ```typescript
|
* > handles all `string`s passed to it automatically as plain text.
|
||||||
* const escaped = md`**${user.displayName}**`
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
export function md(
|
function escape(str: string): string {
|
||||||
strings: TemplateStringsArray,
|
return str.replace(TO_BE_ESCAPED, (s) => '\\' + s)
|
||||||
...sub: (string | FormattedString<'markdown'> | MessageEntity | boolean | undefined | null)[]
|
|
||||||
): FormattedString<'markdown'> {
|
|
||||||
let str = ''
|
|
||||||
sub.forEach((it, idx) => {
|
|
||||||
if (typeof it === 'boolean' || !it) return
|
|
||||||
|
|
||||||
if (typeof it === 'string') it = MarkdownMessageEntityParser.escape(it)
|
|
||||||
else if ('raw' in it) {
|
|
||||||
it = new MarkdownMessageEntityParser().unparse(it.text, [it.raw])
|
|
||||||
} else {
|
|
||||||
if (it.mode && it.mode !== 'markdown') {
|
|
||||||
throw new Error(`Incompatible parse mode: ${it.mode}`)
|
|
||||||
}
|
|
||||||
it = it.value
|
|
||||||
}
|
|
||||||
|
|
||||||
str += strings[idx] + it
|
|
||||||
})
|
|
||||||
|
|
||||||
return { value: str + strings[strings.length - 1], mode: 'markdown' }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Markdown MessageEntity parser.
|
* Add Markdown formatting to the text given the plain text and entities contained in it.
|
||||||
*
|
|
||||||
* This class is **not** compatible with the Bot API Markdown nor MarkdownV2,
|
|
||||||
* please read the [documentation](../) to learn about syntax.
|
|
||||||
*/
|
*/
|
||||||
export class MarkdownMessageEntityParser implements IMessageEntityParser {
|
function unparse(input: InputText): string {
|
||||||
name = 'markdown'
|
if (typeof input === 'string') return escape(input)
|
||||||
|
|
||||||
/**
|
let text = input.text
|
||||||
*
|
const entities = input.entities ?? []
|
||||||
* @param str String to be escaped
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* istanbul ignore next */
|
// keep track of positions of inserted escape symbols
|
||||||
static escape(str: string): string {
|
const escaped: number[] = []
|
||||||
// this code doesn't really need to be tested since it's just
|
text = text.replace(TO_BE_ESCAPED, (s, pos: number) => {
|
||||||
// a simplified version of what is used in .unparse()
|
escaped.push(pos)
|
||||||
return str.replace(TO_BE_ESCAPED, (s) => '\\' + s)
|
|
||||||
|
return '\\' + s
|
||||||
|
})
|
||||||
|
const hasEscaped = escaped.length > 0
|
||||||
|
|
||||||
|
type InsertLater = [number, string]
|
||||||
|
const insert: InsertLater[] = []
|
||||||
|
|
||||||
|
for (const entity of entities) {
|
||||||
|
const type = entity._
|
||||||
|
|
||||||
|
let start = entity.offset
|
||||||
|
let end = start + entity.length
|
||||||
|
|
||||||
|
if (start > text.length) continue
|
||||||
|
if (start < 0) start = 0
|
||||||
|
if (end > text.length) end = text.length
|
||||||
|
|
||||||
|
if (hasEscaped) {
|
||||||
|
// determine number of escape chars since the beginning of the string
|
||||||
|
let escapedPos = 0
|
||||||
|
|
||||||
|
while (escapedPos < escaped.length && escaped[escapedPos] < start) {
|
||||||
|
escapedPos += 1
|
||||||
|
}
|
||||||
|
start += escapedPos
|
||||||
|
|
||||||
|
while (escapedPos < escaped.length && escaped[escapedPos] <= end) {
|
||||||
|
escapedPos += 1
|
||||||
|
}
|
||||||
|
end += escapedPos
|
||||||
}
|
}
|
||||||
|
|
||||||
parse(text: string): [string, tl.TypeMessageEntity[]] {
|
let startTag
|
||||||
|
let endTag: string
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'messageEntityBold':
|
||||||
|
startTag = endTag = TAG_BOLD
|
||||||
|
break
|
||||||
|
case 'messageEntityItalic':
|
||||||
|
startTag = endTag = TAG_ITALIC
|
||||||
|
break
|
||||||
|
case 'messageEntityUnderline':
|
||||||
|
startTag = endTag = TAG_UNDERLINE
|
||||||
|
break
|
||||||
|
case 'messageEntityStrike':
|
||||||
|
startTag = endTag = TAG_STRIKE
|
||||||
|
break
|
||||||
|
case 'messageEntitySpoiler':
|
||||||
|
startTag = endTag = TAG_SPOILER
|
||||||
|
break
|
||||||
|
case 'messageEntityCode':
|
||||||
|
startTag = endTag = TAG_CODE
|
||||||
|
break
|
||||||
|
case 'messageEntityPre':
|
||||||
|
startTag = TAG_PRE
|
||||||
|
|
||||||
|
if (entity.language) {
|
||||||
|
startTag += entity.language
|
||||||
|
}
|
||||||
|
|
||||||
|
startTag += '\n'
|
||||||
|
endTag = '\n' + TAG_PRE
|
||||||
|
break
|
||||||
|
case 'messageEntityTextUrl':
|
||||||
|
startTag = '['
|
||||||
|
endTag = `](${entity.url})`
|
||||||
|
break
|
||||||
|
case 'messageEntityMentionName':
|
||||||
|
startTag = '['
|
||||||
|
endTag = `](tg://user?id=${entity.userId})`
|
||||||
|
break
|
||||||
|
case 'messageEntityCustomEmoji':
|
||||||
|
startTag = '['
|
||||||
|
endTag = `](tg://emoji?id=${entity.documentId.toString()})`
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
insert.push([start, startTag])
|
||||||
|
insert.push([end, endTag])
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort by offset desc
|
||||||
|
insert.sort((a, b) => b[0] - a[0])
|
||||||
|
|
||||||
|
for (const [offset, tag] of insert) {
|
||||||
|
text = text.substr(0, offset) + tag + text.substr(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
function parse(
|
||||||
|
strings: TemplateStringsArray | string,
|
||||||
|
...sub: (InputText | MessageEntity | boolean | number | undefined | null)[]
|
||||||
|
): TextWithEntities {
|
||||||
const entities: tl.TypeMessageEntity[] = []
|
const entities: tl.TypeMessageEntity[] = []
|
||||||
const len = text.length
|
|
||||||
let result = ''
|
let result = ''
|
||||||
|
|
||||||
const stacks: Record<string, tl.Mutable<tl.TypeMessageEntity>[]> = {}
|
const stacks: Record<string, tl.Mutable<tl.TypeMessageEntity>[]> = {}
|
||||||
|
@ -79,6 +146,8 @@ export class MarkdownMessageEntityParser implements IMessageEntityParser {
|
||||||
let insidePre = false
|
let insidePre = false
|
||||||
let insideLink = false
|
let insideLink = false
|
||||||
|
|
||||||
|
function feed(text: string) {
|
||||||
|
const len = text.length
|
||||||
let pos = 0
|
let pos = 0
|
||||||
|
|
||||||
while (pos < len) {
|
while (pos < len) {
|
||||||
|
@ -297,111 +366,106 @@ export class MarkdownMessageEntityParser implements IMessageEntityParser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (c === '\n') {
|
||||||
|
if (pos !== 0) {
|
||||||
|
result += '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
const nonWhitespace = text.slice(pos + 1).search(/\S/)
|
||||||
|
|
||||||
|
if (nonWhitespace !== -1) {
|
||||||
|
pos += nonWhitespace + 1
|
||||||
|
} else {
|
||||||
|
pos = len
|
||||||
|
result = result.trimEnd()
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// nothing matched => normal character
|
// nothing matched => normal character
|
||||||
result += c
|
result += c
|
||||||
pos += 1
|
pos += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
return [result, entities]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unparse(text: string, entities: ReadonlyArray<tl.TypeMessageEntity>): string {
|
if (typeof strings === 'string') strings = [strings] as unknown as TemplateStringsArray
|
||||||
// keep track of positions of inserted escape symbols
|
|
||||||
const escaped: number[] = []
|
|
||||||
text = text.replace(TO_BE_ESCAPED, (s, pos: number) => {
|
|
||||||
escaped.push(pos)
|
|
||||||
|
|
||||||
return '\\' + s
|
sub.forEach((it, idx) => {
|
||||||
|
feed(strings[idx])
|
||||||
|
|
||||||
|
if (typeof it === 'boolean' || !it) return
|
||||||
|
|
||||||
|
if (typeof it === 'string' || typeof it === 'number') {
|
||||||
|
result += it
|
||||||
|
} else {
|
||||||
|
// TextWithEntities or MessageEntity
|
||||||
|
const text = it.text
|
||||||
|
const innerEntities = 'raw' in it ? [it.raw] : it.entities
|
||||||
|
|
||||||
|
const baseOffset = result.length
|
||||||
|
result += text
|
||||||
|
|
||||||
|
if (innerEntities) {
|
||||||
|
for (const ent of innerEntities) {
|
||||||
|
entities.push({ ...ent, offset: ent.offset + baseOffset })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
const hasEscaped = escaped.length > 0
|
|
||||||
|
|
||||||
type InsertLater = [number, string]
|
feed(strings[strings.length - 1])
|
||||||
const insert: InsertLater[] = []
|
|
||||||
|
|
||||||
for (const entity of entities) {
|
for (const [name, stack] of Object.entries(stacks)) {
|
||||||
const type = entity._
|
if (stack.length) {
|
||||||
|
throw new Error(`Unterminated ${name} entity`)
|
||||||
let start = entity.offset
|
|
||||||
let end = start + entity.length
|
|
||||||
|
|
||||||
if (start > text.length) continue
|
|
||||||
if (start < 0) start = 0
|
|
||||||
if (end > text.length) end = text.length
|
|
||||||
|
|
||||||
if (hasEscaped) {
|
|
||||||
// determine number of escape chars since the beginning of the string
|
|
||||||
let escapedPos = 0
|
|
||||||
|
|
||||||
while (escapedPos < escaped.length && escaped[escapedPos] < start) {
|
|
||||||
escapedPos += 1
|
|
||||||
}
|
}
|
||||||
start += escapedPos
|
|
||||||
|
|
||||||
while (escapedPos < escaped.length && escaped[escapedPos] <= end) {
|
|
||||||
escapedPos += 1
|
|
||||||
}
|
|
||||||
end += escapedPos
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let startTag
|
return {
|
||||||
let endTag: string
|
text: result,
|
||||||
|
entities,
|
||||||
switch (type) {
|
|
||||||
case 'messageEntityBold':
|
|
||||||
startTag = endTag = TAG_BOLD
|
|
||||||
break
|
|
||||||
case 'messageEntityItalic':
|
|
||||||
startTag = endTag = TAG_ITALIC
|
|
||||||
break
|
|
||||||
case 'messageEntityUnderline':
|
|
||||||
startTag = endTag = TAG_UNDERLINE
|
|
||||||
break
|
|
||||||
case 'messageEntityStrike':
|
|
||||||
startTag = endTag = TAG_STRIKE
|
|
||||||
break
|
|
||||||
case 'messageEntitySpoiler':
|
|
||||||
startTag = endTag = TAG_SPOILER
|
|
||||||
break
|
|
||||||
case 'messageEntityCode':
|
|
||||||
startTag = endTag = TAG_CODE
|
|
||||||
break
|
|
||||||
case 'messageEntityPre':
|
|
||||||
startTag = TAG_PRE
|
|
||||||
|
|
||||||
if (entity.language) {
|
|
||||||
startTag += entity.language
|
|
||||||
}
|
|
||||||
|
|
||||||
startTag += '\n'
|
|
||||||
endTag = '\n' + TAG_PRE
|
|
||||||
break
|
|
||||||
case 'messageEntityTextUrl':
|
|
||||||
startTag = '['
|
|
||||||
endTag = `](${entity.url})`
|
|
||||||
break
|
|
||||||
case 'messageEntityMentionName':
|
|
||||||
startTag = '['
|
|
||||||
endTag = `](tg://user?id=${entity.userId})`
|
|
||||||
break
|
|
||||||
case 'messageEntityCustomEmoji':
|
|
||||||
startTag = '['
|
|
||||||
endTag = `](tg://emoji?id=${entity.documentId.toString()})`
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
insert.push([start, startTag])
|
|
||||||
insert.push([end, endTag])
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort by offset desc
|
|
||||||
insert.sort((a, b) => b[0] - a[0])
|
|
||||||
|
|
||||||
for (const [offset, tag] of insert) {
|
|
||||||
text = text.substr(0, offset) + tag + text.substr(offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
return text
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// typedoc doesn't support this yet, so we'll have to do it manually
|
||||||
|
// https://github.com/TypeStrong/typedoc/issues/2436
|
||||||
|
|
||||||
|
export const md: {
|
||||||
|
/**
|
||||||
|
* Tagged template based Markdown-to-entities parser function
|
||||||
|
*
|
||||||
|
* Additionally, `md` function has two static methods:
|
||||||
|
* - `md.escape` - escape a string to be safely used in Markdown
|
||||||
|
* (should not be needed in most cases, as `md` function itself handles all `string`s
|
||||||
|
* passed to it automatically as plain text)
|
||||||
|
* - `md.unparse` - add Markdown formatting to the text given the plain text and entities contained in it
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const text = md`**${user.displayName}**`
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
(
|
||||||
|
strings: TemplateStringsArray,
|
||||||
|
...sub: (InputText | MessageEntity | boolean | number | undefined | null)[]
|
||||||
|
): TextWithEntities
|
||||||
|
/**
|
||||||
|
* A variant taking a plain JS string as input
|
||||||
|
* and parsing it.
|
||||||
|
*
|
||||||
|
* Useful for cases when you already have a string
|
||||||
|
* (e.g. from some server) and want to parse it.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const string = '**hello**'
|
||||||
|
* const text = md(string)
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
(string: string): TextWithEntities
|
||||||
|
escape: typeof escape
|
||||||
|
unparse: typeof unparse
|
||||||
|
} = Object.assign(parse, {
|
||||||
|
escape,
|
||||||
|
unparse,
|
||||||
|
})
|
||||||
|
|
|
@ -2,9 +2,10 @@ import { expect } from 'chai'
|
||||||
import Long from 'long'
|
import Long from 'long'
|
||||||
import { describe, it } from 'mocha'
|
import { describe, it } from 'mocha'
|
||||||
|
|
||||||
import { FormattedString, tl } from '@mtcute/client'
|
import { MessageEntity, TextWithEntities, tl } from '@mtcute/client'
|
||||||
|
|
||||||
import { MarkdownMessageEntityParser, md } from '../src/index.js'
|
// md is special cased in prettier, we don't want that here
|
||||||
|
import { md as md_ } from '../src/index.js'
|
||||||
|
|
||||||
const createEntity = <T extends tl.TypeMessageEntity['_']>(
|
const createEntity = <T extends tl.TypeMessageEntity['_']>(
|
||||||
type: T,
|
type: T,
|
||||||
|
@ -21,16 +22,9 @@ const createEntity = <T extends tl.TypeMessageEntity['_']>(
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('MarkdownMessageEntityParser', () => {
|
describe('MarkdownMessageEntityParser', () => {
|
||||||
const parser = new MarkdownMessageEntityParser()
|
|
||||||
|
|
||||||
describe('unparse', () => {
|
describe('unparse', () => {
|
||||||
const test = (
|
const test = (text: string, entities: tl.TypeMessageEntity[], expected: string | string[]): void => {
|
||||||
text: string,
|
const result = md_.unparse({ text, entities })
|
||||||
entities: tl.TypeMessageEntity[],
|
|
||||||
expected: string | string[],
|
|
||||||
_parser = parser,
|
|
||||||
): void => {
|
|
||||||
const result = _parser.unparse(text, entities)
|
|
||||||
|
|
||||||
if (Array.isArray(expected)) {
|
if (Array.isArray(expected)) {
|
||||||
expect(expected).to.include(result)
|
expect(expected).to.include(result)
|
||||||
|
@ -240,9 +234,9 @@ describe('MarkdownMessageEntityParser', () => {
|
||||||
if (!Array.isArray(texts)) texts = [texts]
|
if (!Array.isArray(texts)) texts = [texts]
|
||||||
|
|
||||||
for (const text of texts) {
|
for (const text of texts) {
|
||||||
const [_text, entities] = parser.parse(text)
|
const res = md_(text)
|
||||||
expect(_text).eql(expectedText)
|
expect(res.text).eql(expectedText)
|
||||||
expect(entities).eql(expectedEntities)
|
expect(res.entities ?? []).eql(expectedEntities)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -492,29 +486,8 @@ describe('MarkdownMessageEntityParser', () => {
|
||||||
test('[link]() [link]', [], 'link [link]')
|
test('[link]() [link]', [], 'link [link]')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should ignore unclosed tags', () => {
|
|
||||||
test('plain ```\npre closed with single backtick`', [], 'plain pre closed with single backtick`')
|
|
||||||
test('plain ```\npre closed with single backtick\n`', [], 'plain pre closed with single backtick\n`')
|
|
||||||
|
|
||||||
test('plain ```\npre closed with double backticks`', [], 'plain pre closed with double backticks`')
|
|
||||||
test('plain ```\npre closed with double backticks\n`', [], 'plain pre closed with double backticks\n`')
|
|
||||||
|
|
||||||
test('plain __italic but unclosed', [], 'plain italic but unclosed')
|
|
||||||
test('plain __italic and **also bold but both unclosed', [], 'plain italic and also bold but both unclosed')
|
|
||||||
test(
|
|
||||||
'plain __italic and **also bold but italic closed__',
|
|
||||||
[createEntity('messageEntityItalic', 6, 38)],
|
|
||||||
'plain italic and also bold but italic closed',
|
|
||||||
)
|
|
||||||
test(
|
|
||||||
'plain __italic and **also bold but bold closed**',
|
|
||||||
[createEntity('messageEntityBold', 17, 25)],
|
|
||||||
'plain italic and also bold but bold closed',
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('malformed input', () => {
|
describe('malformed input', () => {
|
||||||
const testThrows = (input: string) => expect(() => parser.parse(input)).throws(Error)
|
const testThrows = (input: string) => expect(() => md_(input)).throws(Error)
|
||||||
|
|
||||||
it('should throw an error on malformed links', () => {
|
it('should throw an error on malformed links', () => {
|
||||||
testThrows('plain [link](https://google.com but unclosed')
|
testThrows('plain [link](https://google.com but unclosed')
|
||||||
|
@ -524,37 +497,95 @@ describe('MarkdownMessageEntityParser', () => {
|
||||||
testThrows('plain ```pre without linebreaks```')
|
testThrows('plain ```pre without linebreaks```')
|
||||||
testThrows('plain ``` pre without linebreaks but with spaces instead ```')
|
testThrows('plain ``` pre without linebreaks but with spaces instead ```')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should throw an error on unterminated entity', () => {
|
||||||
|
testThrows('plain **bold but unclosed')
|
||||||
|
testThrows('plain **bold and __also italic but unclosed')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('template', () => {
|
describe('template', () => {
|
||||||
it('should work as a tagged template literal', () => {
|
const test = (text: TextWithEntities, expectedEntities: tl.TypeMessageEntity[], expectedText: string): void => {
|
||||||
const unsafeString = '__[]__'
|
expect(text.text).eql(expectedText)
|
||||||
|
expect(text.entities ?? []).eql(expectedEntities)
|
||||||
|
}
|
||||||
|
|
||||||
expect(md`${unsafeString}`.value).eq('\\_\\_\\[\\]\\_\\_')
|
it('should add plain strings as is', () => {
|
||||||
expect(md`${unsafeString} **text**`.value).eq('\\_\\_\\[\\]\\_\\_ **text**')
|
test(md_`${'**plain**'}`, [], '**plain**')
|
||||||
expect(md`**text** ${unsafeString}`.value).eq('**text** \\_\\_\\[\\]\\_\\_')
|
|
||||||
expect(md`**${unsafeString}**`.value).eq('**\\_\\_\\[\\]\\_\\_**')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should skip with FormattedString', () => {
|
it('should skip falsy values', () => {
|
||||||
const unsafeString2 = '__[]__'
|
test(md_`some text ${null} more text ${false}`, [], 'some text more text ')
|
||||||
const unsafeString = new FormattedString('__[]__')
|
|
||||||
|
|
||||||
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', () => {
|
it('should properly dedent', () => {
|
||||||
const unsafeString = new FormattedString('<&>', 'markdown')
|
test(
|
||||||
const unsafeString2 = new FormattedString('<&>', 'some-other-mode')
|
md_`
|
||||||
|
some text
|
||||||
|
**bold**
|
||||||
|
more text
|
||||||
|
`,
|
||||||
|
[createEntity('messageEntityBold', 10, 4)],
|
||||||
|
'some text\nbold\nmore text',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
expect(() => md`${unsafeString}`.value).not.throw(Error)
|
it('should process entities', () => {
|
||||||
// @ts-expect-error this is intentional
|
const inner = md_`**bold**`
|
||||||
expect(() => md`${unsafeString2}`.value).throw(Error)
|
|
||||||
|
test(
|
||||||
|
md_`some text ${inner} some more text`,
|
||||||
|
[createEntity('messageEntityBold', 10, 4)],
|
||||||
|
'some text bold some more text',
|
||||||
|
)
|
||||||
|
test(
|
||||||
|
md_`some text ${inner} some more ${inner} text`,
|
||||||
|
[createEntity('messageEntityBold', 10, 4), createEntity('messageEntityBold', 25, 4)],
|
||||||
|
'some text bold some more bold text',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should process entities on edges', () => {
|
||||||
|
test(
|
||||||
|
md_`${md_`**bold**`} and ${md_`__italic__`}`,
|
||||||
|
[createEntity('messageEntityBold', 0, 4), createEntity('messageEntityItalic', 9, 6)],
|
||||||
|
'bold and italic',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should process nested entities', () => {
|
||||||
|
test(
|
||||||
|
md_`**bold ${md_`__bold italic__`} more bold**`,
|
||||||
|
[createEntity('messageEntityItalic', 5, 11), createEntity('messageEntityBold', 0, 26)],
|
||||||
|
'bold bold italic more bold',
|
||||||
|
)
|
||||||
|
test(
|
||||||
|
md_`**bold ${md_`__bold italic__ --and some underline--`} more bold**`,
|
||||||
|
[
|
||||||
|
createEntity('messageEntityItalic', 5, 11),
|
||||||
|
createEntity('messageEntityUnderline', 17, 18),
|
||||||
|
createEntity('messageEntityBold', 0, 45),
|
||||||
|
],
|
||||||
|
'bold bold italic and some underline more bold',
|
||||||
|
)
|
||||||
|
test(
|
||||||
|
md_`**${md_`__bold italic --underline--__`}**`,
|
||||||
|
[
|
||||||
|
createEntity('messageEntityUnderline', 12, 9),
|
||||||
|
createEntity('messageEntityItalic', 0, 21),
|
||||||
|
createEntity('messageEntityBold', 0, 21),
|
||||||
|
],
|
||||||
|
'bold italic underline',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should process MessageEntity', () => {
|
||||||
|
test(
|
||||||
|
md_`**bold ${new MessageEntity(createEntity('messageEntityItalic', 0, 11), 'bold italic')} more bold**`,
|
||||||
|
[createEntity('messageEntityItalic', 5, 11), createEntity('messageEntityBold', 0, 26)],
|
||||||
|
'bold bold italic more bold',
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,8 +2,6 @@ import { createRequire } from 'module'
|
||||||
import { createInterface, Interface as RlInterface } from 'readline'
|
import { createInterface, Interface as RlInterface } from 'readline'
|
||||||
|
|
||||||
import { TelegramClient, TelegramClientOptions } from '@mtcute/client'
|
import { TelegramClient, TelegramClientOptions } from '@mtcute/client'
|
||||||
import { HtmlMessageEntityParser } from '@mtcute/html-parser'
|
|
||||||
import { MarkdownMessageEntityParser } from '@mtcute/markdown-parser'
|
|
||||||
import { SqliteStorage } from '@mtcute/sqlite'
|
import { SqliteStorage } from '@mtcute/sqlite'
|
||||||
|
|
||||||
export * from '@mtcute/client'
|
export * from '@mtcute/client'
|
||||||
|
@ -23,16 +21,6 @@ try {
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
export interface NodeTelegramClientOptions extends Omit<TelegramClientOptions, 'storage'> {
|
export interface NodeTelegramClientOptions extends Omit<TelegramClientOptions, 'storage'> {
|
||||||
/**
|
|
||||||
* Default parse mode to use.
|
|
||||||
*
|
|
||||||
* Both HTML and Markdown parse modes are
|
|
||||||
* registered automatically.
|
|
||||||
*
|
|
||||||
* @default `html`
|
|
||||||
*/
|
|
||||||
defaultParseMode?: 'html' | 'markdown'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Storage to use.
|
* Storage to use.
|
||||||
*
|
*
|
||||||
|
@ -66,13 +54,6 @@ export class NodeTelegramClient extends TelegramClient {
|
||||||
new SqliteStorage(opts.storage) :
|
new SqliteStorage(opts.storage) :
|
||||||
opts.storage ?? new SqliteStorage('client.session'),
|
opts.storage ?? new SqliteStorage('client.session'),
|
||||||
})
|
})
|
||||||
|
|
||||||
this.registerParseMode(new HtmlMessageEntityParser())
|
|
||||||
this.registerParseMode(new MarkdownMessageEntityParser())
|
|
||||||
|
|
||||||
if (opts.defaultParseMode) {
|
|
||||||
this.setDefaultParseMode(opts.defaultParseMode)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _rl?: RlInterface
|
private _rl?: RlInterface
|
||||||
|
|
Loading…
Reference in a new issue