import { expect } from 'chai' import Long from 'long' import { describe, it } from 'mocha' import { FormattedString, tl } from '@mtcute/client' import { html, HtmlMessageEntityParser } from '../src/index.js' const createEntity = ( type: T, offset: number, length: number, additional?: Omit, '_' | 'offset' | 'length'>, ): tl.TypeMessageEntity => { return { _: type, offset, length, ...(additional ?? {}), } as tl.TypeMessageEntity } describe('HtmlMessageEntityParser', () => { const parser = new HtmlMessageEntityParser() describe('unparse', () => { const test = (text: string, entities: tl.TypeMessageEntity[], expected: string, _parser = parser): void => { expect(_parser.unparse(text, entities)).eq(expected) } it('should return the same text if there are no entities or text', () => { test('', [], '') test('some text', [], 'some text') }) it('should handle , , , tags', () => { test( 'plain bold italic underline strikethrough plain', [ createEntity('messageEntityBold', 6, 4), createEntity('messageEntityItalic', 11, 6), createEntity('messageEntityUnderline', 18, 9), createEntity('messageEntityStrike', 28, 13), ], 'plain bold italic underline strikethrough plain', ) }) it('should handle ,
, 
, tags', () => { test( 'plain code pre blockquote spoiler plain', [ createEntity('messageEntityCode', 6, 4), createEntity('messageEntityPre', 11, 3), createEntity('messageEntityBlockquote', 15, 10), createEntity('messageEntitySpoiler', 26, 7), ], 'plain code
pre
blockquote
spoiler plain', ) }) it('should handle links and text mentions', () => { test( 'plain https://google.com google @durov Pavel Durov mail@mail.ru plain', [ createEntity('messageEntityUrl', 6, 18), createEntity('messageEntityTextUrl', 25, 6, { url: 'https://google.com', }), createEntity('messageEntityMention', 32, 6), createEntity('messageEntityMentionName', 39, 11, { userId: 36265675, }), createEntity('messageEntityEmail', 51, 12), ], 'plain https://google.com google @durov Pavel Durov mail@mail.ru plain', ) }) it('should handle language in
', () => {
            test(
                'plain console.log("Hello, world!") some code plain',
                [
                    createEntity('messageEntityPre', 6, 28, {
                        language: 'javascript',
                    }),
                    createEntity('messageEntityPre', 35, 9, { language: '' }),
                ],
                'plain 
console.log("Hello, world!")
some code
plain', ) }) it('should support entities on the edges', () => { test( 'Hello, world', [createEntity('messageEntityBold', 0, 5), createEntity('messageEntityBold', 7, 5)], 'Hello, world', ) }) it('should clamp out-of-range entities', () => { test( 'Hello, world', [createEntity('messageEntityBold', -2, 7), createEntity('messageEntityBold', 7, 10)], 'Hello, world', ) }) it('should ignore entities outside the length', () => { test('Hello, world', [createEntity('messageEntityBold', 50, 5)], 'Hello, world') }) it('should support entities followed by each other', () => { test( 'plain Hello, world plain', [createEntity('messageEntityBold', 6, 6), createEntity('messageEntityItalic', 12, 6)], 'plain Hello, world plain', ) }) it('should support nested entities', () => { test( 'Welcome to the gym zone!', [createEntity('messageEntityItalic', 0, 24), createEntity('messageEntityBold', 15, 8)], 'Welcome to the gym zone!', ) }) it('should support nested entities with the same edges', () => { test( 'Welcome to the gym zone!', [createEntity('messageEntityItalic', 0, 24), createEntity('messageEntityBold', 15, 9)], 'Welcome to the gym zone!', ) test( 'Welcome to the gym zone!', [createEntity('messageEntityBold', 0, 24), createEntity('messageEntityItalic', 15, 9)], 'Welcome to the gym zone!', ) test( 'Welcome to the gym zone!', [createEntity('messageEntityItalic', 0, 24), createEntity('messageEntityBold', 0, 7)], 'Welcome to the gym zone!', ) test( 'Welcome to the gym zone!', [createEntity('messageEntityItalic', 0, 24), createEntity('messageEntityBold', 0, 24)], 'Welcome to the gym zone!', ) }) it('should support overlapping entities', () => { test( 'Welcome to the gym zone!', [createEntity('messageEntityItalic', 0, 14), createEntity('messageEntityBold', 8, 10)], 'Welcome to the gym zone!', ) test( 'plain bold bold-italic bold-italic-underline underline plain', [ createEntity('messageEntityBold', 6, 38), createEntity('messageEntityItalic', 11, 33), createEntity('messageEntityUnderline', 23, 31), ], 'plain bold bold-italic bold-italic-underline underline plain', ) test( 'plain bold bold-italic bold-italic-underline italic-underline underline plain', [ createEntity('messageEntityBold', 6, 38), createEntity('messageEntityItalic', 11, 50), createEntity('messageEntityUnderline', 23, 48), ], // not the most efficient way (in the second part we could do ......), but whatever 'plain bold bold-italic bold-italic-underline italic-underline underline plain', ) }) it('should properly handle emojis', () => { test( "best flower: 🌸. don't you even doubt it.", [ createEntity('messageEntityItalic', 0, 11), createEntity('messageEntityBold', 13, 2), createEntity('messageEntityItalic', 17, 5), ], "best flower: 🌸. don't you even doubt it.", ) }) it('should escape special symbols', () => { test( '<&> < & > <&>', [createEntity('messageEntityBold', 4, 5)], '<&> < & > <&>', ) }) it('should work with custom syntax highlighter', () => { const parser = new HtmlMessageEntityParser({ syntaxHighlighter: (code, lang) => `lang: ${lang}
${code}`, }) test( 'plain console.log("Hello, world!") some code plain', [ createEntity('messageEntityPre', 6, 28, { language: 'javascript', }), createEntity('messageEntityPre', 35, 9, { language: '' }), ], 'plain
lang: javascript
console.log("Hello, world!")
some code
plain', parser, ) }) it('should replace newlines with
outside pre', () => { test('plain\n\nplain', [], 'plain

plain') test('plain\n\nplain', [createEntity('messageEntityBold', 0, 12)], 'plain

plain
') test('plain\n\nplain', [createEntity('messageEntityPre', 0, 12)], '
plain\n\nplain
') }) it('should replace multiple spaces with  ', () => { test('plain plain', [], 'plain    plain') }) }) describe('parse', () => { const test = (text: string, expectedEntities: tl.TypeMessageEntity[], expectedText: string): void => { const [_text, entities] = parser.parse(text) expect(_text).eql(expectedText) expect(entities).eql(expectedEntities) } it('should handle , , , tags', () => { test( 'plain bold italic underline strikethrough plain', [ createEntity('messageEntityBold', 6, 4), createEntity('messageEntityItalic', 11, 6), createEntity('messageEntityUnderline', 18, 9), createEntity('messageEntityStrike', 28, 13), ], 'plain bold italic underline strikethrough plain', ) }) it('should handle ,
, 
, tags', () => { test( 'plain code
pre
blockquote
spoiler plain', [ createEntity('messageEntityCode', 6, 4), createEntity('messageEntityPre', 11, 3, { language: '' }), createEntity('messageEntityBlockquote', 15, 10), createEntity('messageEntitySpoiler', 26, 7), ], 'plain code pre blockquote spoiler plain', ) }) it('should handle links and text mentions', () => { test( 'plain https://google.com google @durov Pavel Durov plain', [ createEntity('messageEntityTextUrl', 25, 6, { url: 'https://google.com', }), createEntity('messageEntityMentionName', 39, 11, { userId: 36265675, }), ], 'plain https://google.com google @durov Pavel Durov plain', ) test( 'user', [ createEntity('inputMessageEntityMentionName', 0, 4, { userId: { _: 'inputUser', userId: 1234567, accessHash: Long.fromString('aabbccddaabbccdd', 16), }, }), ], 'user', ) }) it('should handle language in
', () => {
            test(
                'plain 
console.log("Hello, world!")
some code
plain', [ createEntity('messageEntityPre', 6, 28, { language: 'javascript', }), createEntity('messageEntityPre', 35, 9, { language: '' }), ], 'plain console.log("Hello, world!") some code plain', ) }) it('should ignore other tags inside
', () => {
            test(
                '
bold and not bold
', [createEntity('messageEntityPre', 0, 17, { language: '' })], 'bold and not bold', ) test( '
pre inside pre
so cool
', [createEntity('messageEntityPre', 0, 22, { language: '' })], 'pre inside pre so cool', ) }) it('should ignore newlines and indentation', () => { test('this is some text\n\nwith newlines', [], 'this is some text with newlines') test( 'this is some text\n\nwith newlines', [createEntity('messageEntityBold', 0, 22)], 'this is some text with newlines', ) test( 'this is some text ending with\n\n newlines', [createEntity('messageEntityBold', 0, 29)], 'this is some text ending with newlines', ) test( ` this is some indented text with newlines and indented tags yeah so cool `, [createEntity('messageEntityBold', 45, 13), createEntity('messageEntityItalic', 64, 7)], 'this is some indented text with newlines and indented tags yeah so cool', ) }) it('should not ignore newlines and indentation in pre', () => { test( '
this is some text\n\nwith newlines
', [createEntity('messageEntityPre', 0, 32, { language: '' })], 'this is some text\n\nwith newlines', ) // fuck my life const indent = ' ' test( `
                this  is  some  indented  text
                with    newlines     and
                
                    indented tags
                 yeah so cool
                
                
`, [createEntity('messageEntityPre', 0, 203, { language: '' })], '\n' + indent + 'this is some indented text\n' + indent + 'with newlines and\n' + indent + '\n' + indent + ' indented tags\n' + indent + ' yeah so cool\n' + indent + '\n' + indent, ) }) it('should handle
', () => { test('this is some text

with actual newlines', [], 'this is some text\n\nwith actual newlines') test( 'this is some text

with actual newlines', // note that the
(i.e. \n) is not included in the entity // this is expected, and the result is the same [createEntity('messageEntityBold', 0, 17)], 'this is some text\n\nwith actual newlines', ) }) it('should handle  ', () => { test( 'one space, many    spaces, and
a newline', [], 'one space, many spaces, and\na newline', ) }) it('should support entities on the edges', () => { test( 'Hello, world', [createEntity('messageEntityBold', 0, 5), createEntity('messageEntityBold', 7, 5)], 'Hello, world', ) }) it('should return empty array if there are no entities', () => { test('Hello, world', [], 'Hello, world') }) it('should support entities followed by each other', () => { test( 'plain Hello, world plain', [createEntity('messageEntityBold', 6, 6), createEntity('messageEntityItalic', 12, 6)], 'plain Hello, world plain', ) }) it('should support nested entities', () => { test( 'Welcome to the gym zone!', [createEntity('messageEntityBold', 15, 8), createEntity('messageEntityItalic', 0, 24)], 'Welcome to the gym zone!', ) }) it('should support nested entities with the same edges', () => { test( 'Welcome to the gym zone!', [createEntity('messageEntityBold', 15, 9), createEntity('messageEntityItalic', 0, 24)], 'Welcome to the gym zone!', ) test( 'Welcome to the gym zone!', [createEntity('messageEntityItalic', 15, 9), createEntity('messageEntityBold', 0, 24)], 'Welcome to the gym zone!', ) test( 'Welcome to the gym zone!', [createEntity('messageEntityBold', 0, 7), createEntity('messageEntityItalic', 0, 24)], 'Welcome to the gym zone!', ) test( 'Welcome to the gym zone!', [createEntity('messageEntityBold', 0, 24), createEntity('messageEntityItalic', 0, 24)], 'Welcome to the gym zone!', ) }) it('should properly handle emojis', () => { test( "best flower: 🌸. don't you even doubt it.", [ createEntity('messageEntityItalic', 0, 11), createEntity('messageEntityBold', 13, 2), createEntity('messageEntityItalic', 17, 5), ], "best flower: 🌸. don't you even doubt it.", ) }) it('should handle non-escaped special symbols', () => { test('<&> < & > <&>', [createEntity('messageEntityBold', 4, 5)], '<&> < & > <&>') }) it('should unescape special symbols', () => { test( '<&> < & > <&> link', [ createEntity('messageEntityBold', 4, 5), createEntity('messageEntityTextUrl', 14, 4, { url: '/?a="hello"&b', }), ], '<&> < & > <&> link', ) }) it('should ignore other tags', () => { test('', [], 'alert(1)') }) it('should ignore empty urls', () => { test('link link', [], 'link link') }) }) describe('template', () => { it('should work as a tagged template literal', () => { const unsafeString = '<&>' expect(html`${unsafeString}`.value).eq('<&>') expect(html`${unsafeString} text`.value).eq('<&> text') expect(html`text ${unsafeString}`.value).eq('text <&>') expect(html`${unsafeString}`.value).eq('<&>') }) it('should skip with FormattedString', () => { const unsafeString2 = '<&>' const unsafeString = new FormattedString('<&>') expect(html`${unsafeString}`.value).eq('<&>') expect(html`${unsafeString} ${unsafeString2}`.value).eq('<&> <&>') expect(html`${unsafeString} text`.value).eq('<&> text') expect(html`text ${unsafeString}`.value).eq('text <&>') expect(html`${unsafeString}`.value).eq('<&>') expect(html`${unsafeString} ${unsafeString2}`.value).eq('<&> <&>') }) it('should error with incompatible FormattedString', () => { const unsafeString = new FormattedString('<&>', 'html') const unsafeString2 = new FormattedString('<&>', 'some-other-mode') expect(() => html`${unsafeString}`.value).not.throw(Error) // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error expect(() => html`${unsafeString2}`.value).throw(Error) }) }) })