Browse Source

Added MarkdownV2 support (#580)

* Added MarkdownV2 support

* Added MarkdownV2 test, fixed 'code' and 'pre' conflict

* Add type for markdownv2 to client.setParseMode
エムレ・カン 1 year ago
parent
commit
a3ddb35b6a

+ 96 - 0
__tests__/extensions/MarkdownV2.spec.ts

@@ -0,0 +1,96 @@
+import { MarkdownV2Parser } 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);
+    });
+  });
+});

+ 7 - 1
gramjs/Utils.ts

@@ -7,6 +7,7 @@ import mime from "mime";
 import type { ParseInterface } from "./client/messageParse";
 import { MarkdownParser } from "./extensions/markdown";
 import { CustomFile } from "./client/uploads";
+import { MarkdownV2Parser } from "./extensions/markdownv2";
 
 export function getFileInfo(
     fileLocation:
@@ -1104,6 +1105,10 @@ export function sanitizeParseMode(
     if (mode === "md" || mode === "markdown") {
         return MarkdownParser;
     }
+
+    if (mode === "md2" || mode === "markdownv2") {
+        return MarkdownV2Parser;
+    }
     if (mode == "html") {
         return HTMLParser;
     }
@@ -1115,6 +1120,7 @@ export function sanitizeParseMode(
     throw new Error(`Invalid parse mode type ${mode}`);
 }
 
+
 /**
  Convert the given peer into its marked ID by default.
 
@@ -1356,4 +1362,4 @@ export function  isListLike(item) {
         )
     )
 }
-*/
+*/

+ 2 - 0
gramjs/client/TelegramClient.ts

@@ -556,7 +556,9 @@ export class TelegramClient extends TelegramBaseClient {
     setParseMode(
         mode:
             | "md"
+            | "md2"
             | "markdown"
+            | "markdownv2"
             | "html"
             | parseMethods.ParseInterface
             | undefined

+ 1 - 1
gramjs/client/updates.ts

@@ -218,7 +218,7 @@ export async function _updateLoop(client: TelegramClient) {
                     PING_FAIL_INTERVAL
                 );
             } else {
-                let wakeUpWarningTimeout: Timeout | undefined = setTimeout(
+                let wakeUpWarningTimeout: Timeout | undefined | number = setTimeout(
                     () => {
                         _handleUpdate(
                             client,

+ 4 - 1
gramjs/extensions/html.ts

@@ -226,7 +226,10 @@ export class HTMLParser {
                 html.push(
                     `<a href="tg://user?id=${entity.userId}">${entityText}</a>`
                 );
-            } else {
+            } else if (entity instanceof Api.MessageEntityCustomEmoji) {
+                html.push(`<tg-emoji emoji-id="${entity.documentId}">${entityText}</tg-emoji>`);
+            }
+            else {
                 skipEntity = true;
             }
             lastOffset = relativeOffset + (skipEntity ? 0 : length);

+ 70 - 0
gramjs/extensions/markdownv2.ts

@@ -0,0 +1,70 @@
+import { Api } from "../tl";
+import { HTMLParser } from "./html";
+
+export class MarkdownV2Parser {
+    static parse(message: string): [string, Api.TypeMessageEntity[]] {
+        // Bold
+        message = message.replace(/\*(.*?)\*/g, '<b>$1</b>');
+
+        // underline
+        message = message.replace(/__(.*?)__/g, '<u>$1</u>');
+
+        // strikethrough
+        message = message.replace(/~(.*?)~/g, '<s>$1</s>');
+
+        // italic
+        message = message.replace(/-(.*?)-/g, '<i>$1</i>');
+        
+        // pre
+        message = message.replace(/```(.*?)```/g, '<pre>$1</pre>');
+        
+        // code
+        message = message.replace(/`(.*?)`/g, '<code>$1</code>');
+
+        // Spoiler 
+        message = message.replace(/\|\|(.*?)\|\|/g, '<spoiler>$1</spoiler>');
+
+        // Inline URL
+        message = message.replace(/(?<!\!)\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
+
+        // Emoji 
+        message = message.replace(/!\[([^\]]+)\]\(tg:\/\/emoji\?id=(\d+)\)/g, '<tg-emoji emoji-id="$2">$1</tg-emoji>');
+        return HTMLParser.parse(message)
+    }
+
+    static unparse(
+        text: string,
+        entities: Api.TypeMessageEntity[] | undefined
+    ) {
+        text = HTMLParser.unparse(text, entities)
+
+        // Bold
+        text = text.replace(/<b>(.*?)<\/b>/g, '*$1*');
+
+        // Underline
+        text = text.replace(/<u>(.*?)<\/u>/g, '__$1__');
+
+        // Code
+        text = text.replace(/<code>(.*?)<\/code>/g, '`$1`');
+
+        // Pre
+        text = text.replace(/<pre>(.*?)<\/pre>/g, '```$1```');
+
+        // strikethrough
+        text = text.replace(/<s>(.*?)<\/s>/g, '~$1~');
+
+        // Italic
+        text = text.replace(/<i>(.*?)<\/i>/g, '-$1-');
+
+        // Spoiler
+        text = text.replace(/<spoiler>(.*?)<\/spoiler>/g, '||$1||');
+
+        // Inline URL
+        text = text.replace(/<a href="([^"]+)">([^<]+)<\/a>/g, '[$2]($1)');
+
+        // Emoji
+        text = text.replace(/<tg-emoji emoji-id="(\d+)">([^<]+)<\/tg-emoji>/g, '![$2](tg://emoji?id=$1)');
+
+        return text
+    }
+}

+ 1 - 1
tsconfig.json

@@ -2,7 +2,7 @@
   "compilerOptions": {
     "module": "commonjs",
     "target": "es2017",
-    "lib": ["dom", "es7", "ES2019"],
+    "lib": ["dom", "es7", "ES2019", "ES2020"],
     "sourceMap": false,
     "downlevelIteration": true,
     "allowJs": true,