mtcute/packages/markdown-parser/tests/markdown-parser.spec.ts

591 lines
23 KiB
TypeScript

import { expect } from 'chai'
import Long from 'long'
import { describe, it } from 'mocha'
import { MessageEntity, TextWithEntities, tl } from '@mtcute/client'
// 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['_']>(
type: T,
offset: number,
length: number,
additional?: Omit<tl.FindByName<tl.TypeMessageEntity, T>, '_' | 'offset' | 'length'>,
): tl.TypeMessageEntity => {
return {
_: type,
offset,
length,
...(additional ?? {}),
} as tl.TypeMessageEntity // idc really, its not that important
}
describe('MarkdownMessageEntityParser', () => {
describe('unparse', () => {
const test = (text: string, entities: tl.TypeMessageEntity[], expected: string | string[]): void => {
const result = md_.unparse({ text, entities })
if (Array.isArray(expected)) {
expect(expected).to.include(result)
} else {
expect(result).eq(expected)
}
}
it('should return the same text if there are no entities or text', () => {
test('', [], '')
test('some text', [], 'some text')
})
it('should handle bold, italic, underline, strikethrough and spoiler', () => {
test(
'plain bold italic underline strikethrough spoiler plain',
[
createEntity('messageEntityBold', 6, 4),
createEntity('messageEntityItalic', 11, 6),
createEntity('messageEntityUnderline', 18, 9),
createEntity('messageEntityStrike', 28, 13),
createEntity('messageEntitySpoiler', 42, 7),
],
'plain **bold** __italic__ --underline-- ~~strikethrough~~ ||spoiler|| plain',
)
})
it('should handle code and pre', () => {
test(
'plain code pre __ignored__ plain',
[
createEntity('messageEntityCode', 6, 4),
createEntity('messageEntityPre', 11, 3),
createEntity('messageEntityCode', 15, 11),
],
'plain `code` ```\npre\n``` `\\_\\_ignored\\_\\_` plain',
)
})
it('should handle links and text mentions', () => {
test(
'plain https://google.com google @durov Pavel Durov mail@mail.ru plain',
[
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](https://google.com) @durov [Pavel Durov](tg://user?id=36265675) mail@mail.ru plain',
)
})
it('should handle language in pre', () => {
test(
'plain console.log("Hello, world!") some code plain',
[
createEntity('messageEntityPre', 6, 28, {
language: 'javascript',
}),
createEntity('messageEntityPre', 35, 9, { language: '' }),
],
'plain ```javascript\nconsole.log("Hello, world!")\n``` ```\nsome code\n``` 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',
// not the most obvious order, but who cares :D
// we support this syntax in parse()
'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!**__', '__Welcome to the **gym zone!__**'],
)
test(
'Welcome to the gym zone!',
[createEntity('messageEntityBold', 0, 24), createEntity('messageEntityItalic', 15, 9)],
['**Welcome to the __gym zone!__**', '**Welcome to the __gym zone!**__'],
)
test(
'Welcome to the gym zone!',
[createEntity('messageEntityItalic', 0, 24), createEntity('messageEntityBold', 0, 7)],
['__**Welcome** to the gym zone!__', '**__Welcome** to the gym zone!__'],
)
test(
'Welcome to the gym zone!',
[createEntity('messageEntityItalic', 0, 24), createEntity('messageEntityBold', 0, 24)],
[
'__**Welcome to the gym zone!**__',
'__**Welcome to the gym zone!__**',
'**__Welcome to the gym zone!**__',
'**__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',
'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),
],
'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 reserved symbols', () => {
test(
'* ** *** _ __ ___ - -- --- ~ ~~ ~~~ [ [[ ` `` ``` ```` \\ \\\\',
[createEntity('messageEntityItalic', 9, 8)],
// holy shit
'/* /*/* /*/*/* __/_ /_/_ /_/_/___ /- /-/- /-/-/- /~ /~/~ /~/~/~ /[ /[/[ /` /`/` /`/`/` /`/`/`/` // ////'
// so we don't have to escape every single backslash lol
.replace(/\//g, '\\'),
)
test(
'* ** *** _ __ ___ - -- ---',
[
// here we test that the order of the entities does not matter
createEntity('messageEntityItalic', 18, 4),
createEntity('messageEntityItalic', 9, 8),
],
'/* /*/* /*/*/* __/_ /_/_ /_/_/___ __/- /-/-__ /-/-/-'.replace(/\//g, '\\'),
)
})
})
describe('parse', () => {
const test = (
texts: string | string[],
expectedEntities: tl.TypeMessageEntity[],
expectedText: string,
): void => {
if (!Array.isArray(texts)) texts = [texts]
for (const text of texts) {
const res = md_(text)
expect(res.text).eql(expectedText)
expect(res.entities ?? []).eql(expectedEntities)
}
}
it('should handle bold, italic, underline, spoiler and strikethrough', () => {
test(
'plain **bold** __italic__ --underline-- ~~strikethrough~~ ||spoiler|| plain',
[
createEntity('messageEntityBold', 6, 4),
createEntity('messageEntityItalic', 11, 6),
createEntity('messageEntityUnderline', 18, 9),
createEntity('messageEntityStrike', 28, 13),
createEntity('messageEntitySpoiler', 42, 7),
],
'plain bold italic underline strikethrough spoiler plain',
)
})
it('should handle code and pre', () => {
test(
[
'plain `code` ```\npre\n``` `__ignored__` plain',
'plain `code` ```\npre\n``` `\\_\\_ignored\\_\\_` plain',
'plain `code` ```\npre``` `\\_\\_ignored\\_\\_` plain',
],
[
createEntity('messageEntityCode', 6, 4),
createEntity('messageEntityPre', 11, 3, { language: '' }),
createEntity('messageEntityCode', 15, 11),
],
'plain code pre __ignored__ plain',
)
test(
'plain ```\npre with ` and ``\n``` plain',
[createEntity('messageEntityPre', 6, 17, { language: '' })],
'plain pre with ` and `` plain',
)
test(
'plain ```\npre with \n`\n and \n``\nend\n``` plain',
[createEntity('messageEntityPre', 6, 24, { language: '' })],
'plain pre with \n`\n and \n``\nend plain',
)
})
it('should handle links and text mentions', () => {
test(
'plain https://google.com [google](https://google.com) @durov [Pavel Durov](tg://user?id=36265675) 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](tg://user?id=1234567&hash=aabbccddaabbccdd)',
[
createEntity('inputMessageEntityMentionName', 0, 4, {
userId: {
_: 'inputUser',
userId: 1234567,
accessHash: Long.fromString('aabbccddaabbccdd', 16),
},
}),
],
'user',
)
})
it('should handle language in pre', () => {
test(
'plain ```javascript\nconsole.log("Hello, world!")\n``` ```\nsome code\n``` 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 return empty array if there are no entities', () => {
test('Hello, world', [], 'Hello, world')
})
it('should support overlapping entities', () => {
test(
'__Welcome **to the__ gym** zone!',
[createEntity('messageEntityItalic', 0, 14), createEntity('messageEntityBold', 8, 10)],
'Welcome to the gym zone!',
)
// resulting order will depend on the order in which the closing ** or __ are passed,
// thus we use separate tests
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__** underline-- plain',
[
createEntity('messageEntityItalic', 11, 33),
createEntity('messageEntityBold', 6, 38),
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),
],
'plain bold bold-italic bold-italic-underline italic-underline underline plain',
)
})
it('should support entities followed by each other', () => {
test(
['plain **Hello,**__ world__ plain', '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!',
)
test(
'plain [__google__](https://google.com) plain',
[
createEntity('messageEntityItalic', 6, 6),
createEntity('messageEntityTextUrl', 6, 6, {
url: 'https://google.com',
}),
],
'plain google plain',
)
test(
'plain [plain __google__ plain](https://google.com) plain',
[
createEntity('messageEntityItalic', 12, 6),
createEntity('messageEntityTextUrl', 6, 18, {
url: 'https://google.com',
}),
],
'plain plain google plain plain',
)
})
it('should support nested entities with the same edges', () => {
// again, order of the entities depends on which closing tag goes first.
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', 0, 24), createEntity('messageEntityBold', 15, 9)],
'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, 24), createEntity('messageEntityItalic', 15, 9)],
'Welcome to the gym zone!',
)
test(
['__**Welcome** to the gym zone!__', '**__Welcome** to the gym zone!__'],
[createEntity('messageEntityBold', 0, 7), createEntity('messageEntityItalic', 0, 24)],
'Welcome to the gym zone!',
)
test(
['__**Welcome to the gym zone!**__', '**__Welcome to the gym zone!**__'],
[createEntity('messageEntityBold', 0, 24), createEntity('messageEntityItalic', 0, 24)],
'Welcome to the gym zone!',
)
test(
['__**Welcome to the gym zone!__**', '**__Welcome to the gym zone!__**'],
[createEntity('messageEntityItalic', 0, 24), createEntity('messageEntityBold', 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 escaped reserved symbols', () => {
test(
'/* /*/* /*/*/* __/_ /_/_ /_/_/___ /- /-/- /-/-/- /~ /~/~ /~/~/~ /[ /[/[ /` /`/` /`/`/` /`/`/`/` // ////'.replace(
/\//g,
'\\',
),
[createEntity('messageEntityItalic', 9, 8)],
'* ** *** _ __ ___ - -- --- ~ ~~ ~~~ [ [[ ` `` ``` ```` \\ \\\\',
)
test(
'/* /*/* /*/*/* __/_ /_/_ /_/_/___ __/- /-/-__ /-/-/-'.replace(/\//g, '\\'),
[createEntity('messageEntityItalic', 9, 8), createEntity('messageEntityItalic', 18, 4)],
'* ** *** _ __ ___ - -- ---',
)
})
it('should ignore empty urls', () => {
test('[link]() [link]', [], 'link [link]')
})
describe('malformed input', () => {
const testThrows = (input: string) => expect(() => md_(input)).throws(Error)
it('should throw an error on malformed links', () => {
testThrows('plain [link](https://google.com but unclosed')
})
it('should throw an error on malformed pres', () => {
testThrows('plain ```pre without linebreaks```')
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', () => {
const test = (text: TextWithEntities, expectedEntities: tl.TypeMessageEntity[], expectedText: string): void => {
expect(text.text).eql(expectedText)
expect(text.entities ?? []).eql(expectedEntities)
}
it('should add plain strings as is', () => {
test(md_`${'**plain**'}`, [], '**plain**')
})
it('should skip falsy values', () => {
test(md_`some text ${null} more text ${false}`, [], 'some text more text ')
})
it('should properly dedent', () => {
test(
md_`
some text
**bold**
more text
`,
[createEntity('messageEntityBold', 10, 4)],
'some text\nbold\nmore text',
)
})
it('should process entities', () => {
const inner = md_`**bold**`
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',
)
})
})
})