feat(parse-mode): compile-time formatted string compatibility check

This commit is contained in:
teidesu 2022-05-06 00:11:28 +03:00
parent 28baf50958
commit d8111ea525
5 changed files with 44 additions and 26 deletions

View file

@ -41,12 +41,12 @@ export interface IMessageEntityParser {
* Raw string that will not be escaped when passing * Raw string that will not be escaped when passing
* to tagged template helpers (like `html` and `md`) * to tagged template helpers (like `html` and `md`)
*/ */
export class FormattedString { export class FormattedString<T extends string = never> {
/** /**
* @param value Value that the string holds * @param value Value that the string holds
* @param mode Name of the parse mode used * @param mode Name of the parse mode used
*/ */
constructor (readonly value: string, readonly mode?: string) {} constructor (readonly value: string, readonly mode?: T) {}
toString(): string { toString(): string {
return this.value return this.value

View file

@ -20,8 +20,8 @@ const MENTION_REGEX =
*/ */
export function html( export function html(
strings: TemplateStringsArray, strings: TemplateStringsArray,
...sub: (string | FormattedString)[] ...sub: (string | FormattedString<'html'>)[]
): FormattedString { ): FormattedString<'html'> {
let str = '' let str = ''
sub.forEach((it, idx) => { sub.forEach((it, idx) => {
if (typeof it === 'string') it = HtmlMessageEntityParser.escape(it) if (typeof it === 'string') it = HtmlMessageEntityParser.escape(it)

View file

@ -611,6 +611,8 @@ describe('HtmlMessageEntityParser', () => {
const unsafeString2 = new FormattedString('<&>', 'some-other-mode') const unsafeString2 = new FormattedString('<&>', 'some-other-mode')
expect(() => html`${unsafeString}`.value).not.throw(Error) expect(() => html`${unsafeString}`.value).not.throw(Error)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
expect(() => html`${unsafeString2}`.value).throw(Error) expect(() => html`${unsafeString2}`.value).throw(Error)
}) })
}) })

View file

@ -3,7 +3,8 @@ import { tl } from '@mtcute/tl'
import { FormattedString } from '@mtcute/client' import { FormattedString } from '@mtcute/client'
import Long from 'long' import Long from 'long'
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 TAG_BOLD = '**' const TAG_BOLD = '**'
const TAG_ITALIC = '__' const TAG_ITALIC = '__'
@ -22,12 +23,17 @@ const TO_BE_ESCAPED = /[*_\-~`[\\\]]/g
* const escaped = md`**${user.displayName}**` * const escaped = md`**${user.displayName}**`
* ``` * ```
*/ */
export function md(strings: TemplateStringsArray, ...sub: (string | FormattedString)[]): FormattedString { export function md(
strings: TemplateStringsArray,
...sub: (string | FormattedString<'markdown'>)[]
): FormattedString<'markdown'> {
let str = '' let str = ''
sub.forEach((it, idx) => { sub.forEach((it, idx) => {
if (typeof it === 'string') it = MarkdownMessageEntityParser.escape(it as string) if (typeof it === 'string')
it = MarkdownMessageEntityParser.escape(it as string)
else { else {
if (it.mode && it.mode !== 'markdown') throw new Error(`Incompatible parse mode: ${it.mode}`) if (it.mode && it.mode !== 'markdown')
throw new Error(`Incompatible parse mode: ${it.mode}`)
it = it.value it = it.value
} }
@ -126,7 +132,9 @@ export class MarkdownMessageEntityParser implements IMessageEntityParser {
if (text[pos + 1] !== '(') { if (text[pos + 1] !== '(') {
// [link text] // [link text]
// ignore this, and add opening [ // ignore this, and add opening [
result = `${result.substr(0, ent.offset)}[${result.substr(ent.offset)}]` result = `${result.substr(0, ent.offset)}[${result.substr(
ent.offset
)}]`
pos += 1 pos += 1
insideLink = false insideLink = false
continue continue
@ -151,24 +159,34 @@ export class MarkdownMessageEntityParser implements IMessageEntityParser {
const userId = parseInt(m[1]) const userId = parseInt(m[1])
const accessHash = m[2] const accessHash = m[2]
if (accessHash) { if (accessHash) {
;(ent as tl.Mutable<tl.RawInputMessageEntityMentionName>)._ = ;(
'inputMessageEntityMentionName' ent as tl.Mutable<tl.RawInputMessageEntityMentionName>
;(ent as tl.Mutable<tl.RawInputMessageEntityMentionName>).userId = { )._ = 'inputMessageEntityMentionName'
;(
ent as tl.Mutable<tl.RawInputMessageEntityMentionName>
).userId = {
_: 'inputUser', _: 'inputUser',
userId, userId,
accessHash: Long.fromString(accessHash, false,16), accessHash: Long.fromString(
accessHash,
false,
16
),
} }
} else { } else {
;(ent as tl.Mutable<tl.RawMessageEntityMentionName>)._ = ;(
'messageEntityMentionName' ent as tl.Mutable<tl.RawMessageEntityMentionName>
;(ent as tl.Mutable<tl.RawMessageEntityMentionName>).userId = userId )._ = 'messageEntityMentionName'
;(
ent as tl.Mutable<tl.RawMessageEntityMentionName>
).userId = userId
} }
} else { } else {
if (url.match(/^\/\//)) url = 'http:' + url if (url.match(/^\/\//)) url = 'http:' + url
;(ent as tl.Mutable<tl.RawMessageEntityTextUrl>)._ = ;(ent as tl.Mutable<tl.RawMessageEntityTextUrl>)._ =
'messageEntityTextUrl' 'messageEntityTextUrl'
;(ent as tl.Mutable<tl.RawMessageEntityTextUrl>).url = url ;(ent as tl.Mutable<tl.RawMessageEntityTextUrl>).url =
url
} }
entities.push(ent) entities.push(ent)
} }
@ -230,12 +248,8 @@ export class MarkdownMessageEntityParser implements IMessageEntityParser {
if (c === text[pos + 1]) { if (c === text[pos + 1]) {
// maybe (?) start or end of an entity // maybe (?) start or end of an entity
let type: let type: 'Italic' | 'Bold' | 'Underline' | 'Strike' | null =
| 'Italic' null
| 'Bold'
| 'Underline'
| 'Strike'
| null = null
switch (c) { switch (c) {
case '_': case '_':
type = 'Italic' type = 'Italic'

View file

@ -3,7 +3,7 @@ import { expect } from 'chai'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { MessageEntity, FormattedString } from '@mtcute/client' import { MessageEntity, FormattedString } from '@mtcute/client'
import { MarkdownMessageEntityParser, md } from '../src' import { MarkdownMessageEntityParser, md } from '../src'
import bigInt from 'big-integer' import Long from 'long'
const createEntity = <T extends tl.TypeMessageEntity['_']>( const createEntity = <T extends tl.TypeMessageEntity['_']>(
type: T, type: T,
@ -356,7 +356,7 @@ describe('MarkdownMessageEntityParser', () => {
userId: { userId: {
_: 'inputUser', _: 'inputUser',
userId: 1234567, userId: 1234567,
accessHash: bigInt('aabbccddaabbccdd', 16), accessHash: Long.fromString('aabbccddaabbccdd', 16),
}, },
}), }),
], ],
@ -674,6 +674,8 @@ describe('MarkdownMessageEntityParser', () => {
const unsafeString2 = new FormattedString('<&>', 'some-other-mode') const unsafeString2 = new FormattedString('<&>', 'some-other-mode')
expect(() => md`${unsafeString}`.value).not.throw(Error) expect(() => md`${unsafeString}`.value).not.throw(Error)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
expect(() => md`${unsafeString2}`.value).throw(Error) expect(() => md`${unsafeString2}`.value).throw(Error)
}) })
}) })