diff --git a/__tests__/extensions/MarkdownV2.spec.ts b/__tests__/extensions/MarkdownV2.spec.ts index 6b037cb1..b2709863 100644 --- a/__tests__/extensions/MarkdownV2.spec.ts +++ b/__tests__/extensions/MarkdownV2.spec.ts @@ -1,96 +1,846 @@ -import { MarkdownV2Parser } from "../../gramjs/extensions/markdownv2"; +import { + MarkdownV2Parser, + markdownV2ToHtml, + htmlToMarkdownV2, +} from "../../gramjs/extensions/markdownv2"; import { Api as types } from "../../gramjs/tl/api"; describe("MarkdownV2Parser", () => { - describe(".parse", () => { - test("it should parse bold entities", () => { - const [text, entities] = MarkdownV2Parser.parse("Hello *world*"); - expect(text).toEqual("Hello world"); - expect(entities.length).toEqual(1); - expect(entities[0]).toBeInstanceOf(types.MessageEntityBold); - }); - - test("it should parse italic entities", () => { - const [text, entities] = MarkdownV2Parser.parse("Hello -world-"); - expect(text).toEqual("Hello world"); - expect(entities.length).toEqual(1); - expect(entities[0]).toBeInstanceOf(types.MessageEntityItalic); - }); - - test("it should parse code entities", () => { - const [text, entities] = MarkdownV2Parser.parse("Hello `world`"); - expect(text).toEqual("Hello world"); - expect(entities.length).toEqual(1); - expect(entities[0]).toBeInstanceOf(types.MessageEntityCode); - }); - - test("it should parse pre entities", () => { - const [text, entities] = MarkdownV2Parser.parse("Hello ```world```"); - expect(text).toEqual("Hello world"); - expect(entities.length).toEqual(1); - expect(entities[0]).toBeInstanceOf(types.MessageEntityPre); - }); - - test("it should parse strike entities", () => { - const [text, entities] = MarkdownV2Parser.parse("Hello ~world~"); - expect(text).toEqual("Hello world"); - expect(entities.length).toEqual(1); - expect(entities[0]).toBeInstanceOf(types.MessageEntityStrike); - }); - - test("it should parse link entities", () => { - const [text, entities] = MarkdownV2Parser.parse( - "Hello [world](https://hello.world)" - ); - expect(text).toEqual("Hello world"); - expect(entities.length).toEqual(1); - expect(entities[0]).toBeInstanceOf(types.MessageEntityTextUrl); - expect((entities[0] as types.MessageEntityTextUrl).url).toEqual( - "https://hello.world" - ); - }); - - test("it should parse custom emoji", () => { - const [text, entities] = MarkdownV2Parser.parse( - "![👍](tg://emoji?id=5368324170671202286)" - ); - expect(text).toEqual("👍"); - expect(entities.length).toEqual(1); - expect(entities[0]).toBeInstanceOf(types.MessageEntityCustomEmoji); - expect( - (entities[0] as types.MessageEntityCustomEmoji).documentId - ).toEqual("5368324170671202286"); - }); - - test("it should parse multiple entities", () => { - const [text, entities] = MarkdownV2Parser.parse("-Hello- *world*"); - expect(text).toEqual("Hello world"); - expect(entities.length).toEqual(2); - expect(entities[0]).toBeInstanceOf(types.MessageEntityItalic); - expect(entities[1]).toBeInstanceOf(types.MessageEntityBold); - }); - }); - - describe(".unparse", () => { - // skipped until MarkDownV2 - test.skip("it should create a markdown string from raw text and entities", () => { - const unparsed = - "*hello* -hello- ~hello~ `hello` ```hello``` [hello](https://hello.world)"; - const strippedText = "hello hello hello hello hello hello"; - const rawEntities = [ - new types.MessageEntityBold({ offset: 0, length: 5 }), - new types.MessageEntityItalic({ offset: 6, length: 5 }), - new types.MessageEntityStrike({ offset: 12, length: 5 }), - new types.MessageEntityCode({ offset: 18, length: 5 }), - new types.MessageEntityPre({ offset: 24, length: 5, language: "" }), - new types.MessageEntityTextUrl({ - offset: 30, - length: 5, - url: "https://hello.world", - }), - ]; - const text = MarkdownV2Parser.unparse(strippedText, rawEntities); - expect(text).toEqual(unparsed); - }); - }); + describe(".parse — span markup basics", () => { + test("bold", () => { + const [text, entities] = MarkdownV2Parser.parse("Hello *world*"); + expect(text).toEqual("Hello world"); + expect(entities.length).toEqual(1); + expect(entities[0]).toBeInstanceOf(types.MessageEntityBold); + }); + + test("italic", () => { + const [text, entities] = MarkdownV2Parser.parse("Hello _world_"); + expect(text).toEqual("Hello world"); + expect(entities.length).toEqual(1); + expect(entities[0]).toBeInstanceOf(types.MessageEntityItalic); + }); + + test("underline", () => { + const [text, entities] = MarkdownV2Parser.parse("Hello __world__"); + expect(text).toEqual("Hello world"); + expect(entities.length).toEqual(1); + expect(entities[0]).toBeInstanceOf(types.MessageEntityUnderline); + }); + + test("strikethrough", () => { + const [text, entities] = MarkdownV2Parser.parse("Hello ~world~"); + expect(text).toEqual("Hello world"); + expect(entities.length).toEqual(1); + expect(entities[0]).toBeInstanceOf(types.MessageEntityStrike); + }); + + test("spoiler", () => { + const [text, entities] = MarkdownV2Parser.parse("Hello ||world||"); + expect(text).toEqual("Hello world"); + expect(entities.length).toEqual(1); + expect(entities[0]).toBeInstanceOf(types.MessageEntitySpoiler); + }); + + test("bold spans multiple words", () => { + const [text, entities] = + MarkdownV2Parser.parse("*hello world*"); + expect(text).toEqual("hello world"); + expect(entities.length).toEqual(1); + expect(entities[0].length).toEqual(11); + }); + + test("italic spans newlines", () => { + const [text, entities] = + MarkdownV2Parser.parse("_line1\nline2_"); + expect(text).toEqual("line1\nline2"); + expect(entities.length).toEqual(1); + expect(entities[0]).toBeInstanceOf(types.MessageEntityItalic); + expect(entities[0].length).toEqual(11); + }); + }); + + describe(".parse — span combinations", () => { + test("multiple separate spans on one line", () => { + const [text, entities] = MarkdownV2Parser.parse("_a_ *b*"); + expect(text).toEqual("a b"); + expect(entities.length).toEqual(2); + expect(entities[0]).toBeInstanceOf(types.MessageEntityItalic); + expect(entities[1]).toBeInstanceOf(types.MessageEntityBold); + }); + + test("five distinct spans in a row", () => { + const [text, entities] = MarkdownV2Parser.parse( + "*a* _b_ ~c~ ||d|| __e__" + ); + expect(text).toEqual("a b c d e"); + expect(entities.length).toEqual(5); + const has = (cls: any) => + entities.some((e) => e instanceof cls); + expect(has(types.MessageEntityBold)).toBe(true); + expect(has(types.MessageEntityItalic)).toBe(true); + expect(has(types.MessageEntityStrike)).toBe(true); + expect(has(types.MessageEntitySpoiler)).toBe(true); + expect(has(types.MessageEntityUnderline)).toBe(true); + }); + + test("italic nested inside bold", () => { + const [text, entities] = MarkdownV2Parser.parse( + "*hello _world_ end*" + ); + expect(text).toEqual("hello world end"); + expect(entities.length).toEqual(2); + expect(entities[0]).toBeInstanceOf(types.MessageEntityItalic); + expect(entities[1]).toBeInstanceOf(types.MessageEntityBold); + expect(entities[0].offset).toEqual(6); + expect(entities[0].length).toEqual(5); + expect(entities[1].offset).toEqual(0); + expect(entities[1].length).toEqual(15); + }); + + test("bold nested inside underline", () => { + const [text, entities] = MarkdownV2Parser.parse( + "__hello *bold* end__" + ); + expect(text).toEqual("hello bold end"); + expect(entities.length).toEqual(2); + expect(entities[0]).toBeInstanceOf(types.MessageEntityBold); + expect(entities[1]).toBeInstanceOf(types.MessageEntityUnderline); + expect(entities[0].offset).toEqual(6); + expect(entities[0].length).toEqual(4); + expect(entities[1].offset).toEqual(0); + expect(entities[1].length).toEqual(14); + }); + }); + + describe(".parse — inline code", () => { + test("basic code", () => { + const [text, entities] = MarkdownV2Parser.parse("Hello `world`"); + expect(text).toEqual("Hello world"); + expect(entities.length).toEqual(1); + expect(entities[0]).toBeInstanceOf(types.MessageEntityCode); + }); + + test("code with markup chars stays literal", () => { + const [text, entities] = + MarkdownV2Parser.parse("`*not bold*`"); + expect(text).toEqual("*not bold*"); + expect(entities.length).toEqual(1); + expect(entities[0]).toBeInstanceOf(types.MessageEntityCode); + }); + + test("code with escaped backtick", () => { + const [text, entities] = MarkdownV2Parser.parse("`a\\`b`"); + expect(text).toEqual("a`b"); + expect(entities.length).toEqual(1); + expect(entities[0]).toBeInstanceOf(types.MessageEntityCode); + }); + + test("code with escaped backslash", () => { + const [text, entities] = MarkdownV2Parser.parse("`a\\\\b`"); + expect(text).toEqual("a\\b"); + expect(entities.length).toEqual(1); + expect(entities[0]).toBeInstanceOf(types.MessageEntityCode); + }); + + test("code preserves backslash before non-special char", () => { + // Inside code, only \\ and \` are escapes. \X for any other X + // stays as a literal backslash followed by X. + const [text, entities] = MarkdownV2Parser.parse("`a\\bc`"); + expect(text).toEqual("a\\bc"); + expect(entities.length).toEqual(1); + expect(entities[0]).toBeInstanceOf(types.MessageEntityCode); + }); + + test("code with HTML special chars renders as literal text", () => { + const [text, entities] = + MarkdownV2Parser.parse('`a < b & "c"`'); + expect(text).toEqual('a < b & "c"'); + expect(entities.length).toEqual(1); + expect(entities[0]).toBeInstanceOf(types.MessageEntityCode); + }); + }); + + describe(".parse — pre block", () => { + test("basic pre", () => { + const [text, entities] = + MarkdownV2Parser.parse("Hello ```world```"); + expect(text).toEqual("Hello world"); + expect(entities.length).toEqual(1); + expect(entities[0]).toBeInstanceOf(types.MessageEntityPre); + }); + + test("pre with language tag", () => { + const [text, entities] = + MarkdownV2Parser.parse("```python\nfoo```"); + expect(text).toEqual("foo"); + expect(entities.length).toEqual(1); + expect(entities[0]).toBeInstanceOf(types.MessageEntityPre); + expect((entities[0] as types.MessageEntityPre).language).toEqual( + "python" + ); + }); + + test("pre with non-identifier first line keeps it as content", () => { + const [text, entities] = + MarkdownV2Parser.parse("```hello world\nfoo```"); + expect(text).toEqual("hello world\nfoo"); + expect(entities.length).toEqual(1); + expect(entities[0]).toBeInstanceOf(types.MessageEntityPre); + expect((entities[0] as types.MessageEntityPre).language).toEqual( + "" + ); + }); + + test("pre with backslash-escaped backtick inside", () => { + const [text, entities] = + MarkdownV2Parser.parse("```a\\`b```"); + expect(text).toEqual("a`b"); + expect(entities.length).toEqual(1); + expect(entities[0]).toBeInstanceOf(types.MessageEntityPre); + }); + + test("pre preserves markup chars literally", () => { + const [text, entities] = + MarkdownV2Parser.parse("```*not bold*```"); + expect(text).toEqual("*not bold*"); + expect(entities.length).toEqual(1); + expect(entities[0]).toBeInstanceOf(types.MessageEntityPre); + }); + + test("pre with HTML special chars", () => { + const [text, entities] = + MarkdownV2Parser.parse("```