import Long from 'long' import { describe, expect, it } from 'vitest' import { MessageEntity, TextWithEntities, tl } from '@mtcute/client' // 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 './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', () => { describe('unparse', () => { const test = ( 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', () => { 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', () => { 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', { syntaxHighlighter: (code, lang) => `lang: ${lang}
${code}`, }, ) }) 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: TextWithEntities, expectedEntities: tl.TypeMessageEntity[], expectedText: string): void => { expect(text.text).eql(expectedText) expect(text.entities ?? []).eql(expectedEntities) } it('should handle , , , tags', () => { test( htm`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( htm`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( htm`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( htm`user`, [ createEntity('inputMessageEntityMentionName', 0, 4, { userId: { _: 'inputUser', userId: 1234567, accessHash: Long.fromString('aabbccddaabbccdd', 16), }, }), ], 'user', ) }) it('should handle language in
', () => {
            test(
                htm`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(
                htm`
bold and not bold
`, [createEntity('messageEntityPre', 0, 17, { language: '' })], 'bold and not bold', ) test( htm`
pre inside pre
so cool
`, [createEntity('messageEntityPre', 0, 22, { language: '' })], 'pre inside pre so cool', ) }) it('should ignore newlines and indentation', () => { test(htm`this is some text\n\nwith newlines`, [], 'this is some text with newlines') test( htm`this is some text\n\nwith newlines`, [createEntity('messageEntityBold', 0, 22)], 'this is some text with newlines', ) test( htm`this is some text ending with\n\n newlines`, [createEntity('messageEntityBold', 0, 29)], 'this is some text ending with newlines', ) test( htm` 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( htm`
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( htm`
                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(htm`this is some text

with actual newlines`, [], 'this is some text\n\nwith actual newlines') test( htm`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( htm`one space, many    spaces, and
a newline`, [], 'one space, many spaces, and\na newline', ) }) it('should support entities on the edges', () => { test( htm`Hello, world`, [createEntity('messageEntityBold', 0, 5), createEntity('messageEntityBold', 7, 5)], 'Hello, world', ) }) it('should return empty array if there are no entities', () => { test(htm`Hello, world`, [], 'Hello, world') }) it('should support entities followed by each other', () => { test( htm`plain Hello, world plain`, [createEntity('messageEntityBold', 6, 6), createEntity('messageEntityItalic', 12, 6)], 'plain Hello, world plain', ) }) it('should support nested entities', () => { test( htm`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( htm`Welcome to the gym zone!`, [createEntity('messageEntityBold', 15, 9), createEntity('messageEntityItalic', 0, 24)], 'Welcome to the gym zone!', ) test( htm`Welcome to the gym zone!`, [createEntity('messageEntityItalic', 15, 9), createEntity('messageEntityBold', 0, 24)], 'Welcome to the gym zone!', ) test( htm`Welcome to the gym zone!`, [createEntity('messageEntityBold', 0, 7), createEntity('messageEntityItalic', 0, 24)], 'Welcome to the gym zone!', ) test( htm`Welcome to the gym zone!`, [createEntity('messageEntityBold', 0, 24), createEntity('messageEntityItalic', 0, 24)], 'Welcome to the gym zone!', ) }) it('should properly handle emojis', () => { test( htm`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(htm`<&> < & > <&>`, [createEntity('messageEntityBold', 4, 5)], '<&> < & > <&>') }) it('should unescape special symbols', () => { test( htm`<&> < & > <&> link`, [ createEntity('messageEntityBold', 4, 5), createEntity('messageEntityTextUrl', 14, 4, { url: '/?a="hello"&b', }), ], '<&> < & > <&> link', ) }) it('should ignore other tags', () => { test(htm``, [], 'alert(1)') }) it('should ignore empty urls', () => { test(htm`link link`, [], 'link link') }) describe('template', () => { it('should add plain strings as is', () => { test( htm`some text ${'not bold yea'} some more text`, [], 'some text not bold yea some more text', ) }) it('should skip falsy values', () => { test(htm`some text ${null} some ${false} more text`, [], 'some text some more text') }) it('should process entities', () => { const inner = htm`bold` 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', ) }) it('should process entities on edges', () => { test( htm`${htm`bold`} and ${htm`italic`}`, [createEntity('messageEntityBold', 0, 4), createEntity('messageEntityItalic', 9, 6)], 'bold and italic', ) }) it('should process nested entities', () => { test( htm`bold ${htm`bold italic`} more bold`, [createEntity('messageEntityItalic', 5, 11), createEntity('messageEntityBold', 0, 26)], 'bold bold italic more bold', ) test( htm`bold ${htm`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( htm`${htm`bold italic underline`}`, [ createEntity('messageEntityUnderline', 12, 9), createEntity('messageEntityItalic', 0, 21), createEntity('messageEntityBold', 0, 21), ], 'bold italic underline', ) }) it('should process MessageEntity', () => { test( htm`bold ${new MessageEntity( createEntity('messageEntityItalic', 0, 11), 'bold italic', )} more bold`, [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 = 'bold' test(htm(someHtml), [createEntity('messageEntityBold', 0, 4)], 'bold') test( htm`text ${htm(someHtml)} more text`, [createEntity('messageEntityBold', 5, 4)], 'text bold more text', ) }) }) }) })