Add support for sending spoiler markdown (#267)
authorginnyTheCat <ginnythecat@lelux.net>
Mon, 31 Jan 2022 15:58:42 +0000 (16:58 +0100)
committerGitHub <noreply@github.com>
Mon, 31 Jan 2022 15:58:42 +0000 (21:28 +0530)
* Basic spoiler markdown plugin

* Remove console.log statement

package-lock.json
package.json
src/client/state/RoomsInput.js
src/util/markdown.js [new file with mode: 0644]

index 90312a89c8959be1ccf2bedd144211c7578c9bec..8acb3d65e6d560e3521bafa97ad6ec1664665db0 100644 (file)
@@ -23,6 +23,9 @@
         "matrix-js-sdk": "^15.4.0",
         "micromark": "^3.0.3",
         "micromark-extension-gfm": "^1.0.0",
+        "micromark-util-chunked": "^1.0.0",
+        "micromark-util-resolve-all": "^1.0.0",
+        "micromark-util-symbol": "^1.0.1",
         "prop-types": "^15.8.1",
         "react": "^17.0.2",
         "react-autosize-textarea": "^7.1.0",
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.9.0.tgz",
       "integrity": "sha512-qOKJyNj/h+OWx7s5DePL6Zu1KeM9jPZhwBqs+7DzP6bGOvqzVCSf0xueYmVuaC/oQ/VtS2zLMLHdQFbkka+XDQ==",
       "dev": true,
+      "optional": true,
+      "peer": true,
       "dependencies": {
         "fast-deep-equal": "^3.1.1",
         "json-schema-traverse": "^1.0.0",
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
       "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
-      "dev": true
+      "dev": true,
+      "optional": true,
+      "peer": true
     },
     "node_modules/ajv-keywords": {
       "version": "3.5.2",
       }
     },
     "node_modules/micromark-util-symbol": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.0.0.tgz",
-      "integrity": "sha512-NZA01jHRNCt4KlOROn8/bGi6vvpEmlXld7EHcRH+aYWUfL3Wc8JLUNNlqUMKa0hhz6GrpUWsHtzPmKof57v0gQ==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.0.1.tgz",
+      "integrity": "sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ==",
       "funding": [
         {
           "type": "GitHub Sponsors",
           "type": "OpenCollective",
           "url": "https://opencollective.com/unified"
         }
-      ]
+      ],
+      "license": "MIT"
     },
     "node_modules/micromark-util-types": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
       "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
       "dev": true,
-      "requires": {
-        "ajv": "^8.0.0"
-      },
+      "requires": {},
       "dependencies": {
         "ajv": {
-          "version": "8.9.0",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.9.0.tgz",
+          "version": "https://registry.npmjs.org/ajv/-/ajv-8.9.0.tgz",
           "integrity": "sha512-qOKJyNj/h+OWx7s5DePL6Zu1KeM9jPZhwBqs+7DzP6bGOvqzVCSf0xueYmVuaC/oQ/VtS2zLMLHdQFbkka+XDQ==",
           "dev": true,
+          "optional": true,
+          "peer": true,
           "requires": {
             "fast-deep-equal": "^3.1.1",
             "json-schema-traverse": "^1.0.0",
           "version": "1.0.0",
           "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
           "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
-          "dev": true
+          "dev": true,
+          "optional": true,
+          "peer": true
         }
       }
     },
       }
     },
     "micromark-util-symbol": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.0.0.tgz",
-      "integrity": "sha512-NZA01jHRNCt4KlOROn8/bGi6vvpEmlXld7EHcRH+aYWUfL3Wc8JLUNNlqUMKa0hhz6GrpUWsHtzPmKof57v0gQ=="
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.0.1.tgz",
+      "integrity": "sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ=="
     },
     "micromark-util-types": {
       "version": "1.0.1",
index cef77b91a00e64cc182a36e29efce5f0b2b60138..011555c2e8217e5583fc8b184095efcf860a4b05 100644 (file)
@@ -29,6 +29,9 @@
     "matrix-js-sdk": "^15.4.0",
     "micromark": "^3.0.3",
     "micromark-extension-gfm": "^1.0.0",
+    "micromark-util-chunked": "^1.0.0",
+    "micromark-util-resolve-all": "^1.0.0",
+    "micromark-util-symbol": "^1.0.1",
     "prop-types": "^15.8.1",
     "react": "^17.0.2",
     "react-autosize-textarea": "^7.1.0",
index 1f0f8a4037ca178dfe241abc3e412fc228b6a20e..bc01f22e078dbf01c8ee935b1134c97c228e7931 100644 (file)
@@ -3,6 +3,7 @@ import { micromark } from 'micromark';
 import { gfm, gfmHtml } from 'micromark-extension-gfm';
 import encrypt from 'browser-encrypt-attachment';
 import { getShortcodeToEmoji } from '../../app/organisms/emoji-board/custom-emoji';
+import { spoilerExtension, spoilerExtensionHtml } from '../../util/markdown';
 import cons from './cons';
 import settings from './settings';
 
@@ -84,8 +85,8 @@ function getVideoThumbnail(video, width, height, mimeType) {
 
 function getFormattedBody(markdown) {
   const result = micromark(markdown, {
-    extensions: [gfm()],
-    htmlExtensions: [gfmHtml],
+    extensions: [gfm(), spoilerExtension()],
+    htmlExtensions: [gfmHtml, spoilerExtensionHtml],
   });
   const bodyParts = result.match(/^(<p>)(.*)(<\/p>)$/);
   if (bodyParts === null) return result;
@@ -406,7 +407,7 @@ class RoomsInput extends EventEmitter {
     // Apply formatting if relevant
     const formattedBody = formatAndEmojifyText(
       this.matrixClient.getRoom(roomId),
-      editedBody
+      editedBody,
     );
     if (formattedBody !== editedBody) {
       content.formatted_body = ` * ${formattedBody}`;
diff --git a/src/util/markdown.js b/src/util/markdown.js
new file mode 100644 (file)
index 0000000..0a6472e
--- /dev/null
@@ -0,0 +1,143 @@
+/* eslint-disable no-param-reassign */
+/* eslint-disable no-plusplus */
+/* eslint-disable no-continue */
+
+import { codes } from 'micromark-util-symbol/codes';
+import { types } from 'micromark-util-symbol/types';
+import { resolveAll } from 'micromark-util-resolve-all';
+import { splice } from 'micromark-util-chunked';
+
+function inlineExtension(marker, len, key) {
+  const keySeq = `${key}Sequence`;
+  const keySeqTmp = `${keySeq}Temporary`;
+
+  return () => {
+    function tokenize(effects, ok, nok) {
+      const { previous, events } = this;
+
+      let size = 0;
+
+      function more(code) {
+        // consume more markers if the maximum length hasn't been reached yet
+        if (code === marker && size < len) {
+          effects.consume(code);
+          size += 1;
+          return more;
+        }
+
+        // check for minimum length
+        if (size < len) return nok(code);
+
+        effects.exit(keySeqTmp);
+        return ok(code);
+      }
+
+      function start(code) {
+        // ignore code if it's not a marker
+        if (code !== marker) return nok(code);
+
+        if (previous === marker
+            && events[events.length - 1][1].type !== types.characterEscape) return nok(code);
+
+        effects.enter(keySeqTmp);
+        return more(code);
+      }
+
+      return start;
+    }
+
+    function resolve(events, context) {
+      let i = -1;
+
+      while (++i < events.length) {
+        if (events[i][0] !== 'enter' || events[i][1].type !== keySeqTmp) continue;
+
+        let open = i;
+        while (open--) {
+          if (events[open][0] !== 'exit' || events[open][1].type !== keySeqTmp) continue;
+
+          events[i][1].type = keySeq;
+          events[open][1].type = keySeq;
+
+          const border = {
+            type: key,
+            start: { ...events[open][1].start },
+            end: { ...events[i][1].end },
+          };
+
+          const text = {
+            type: `${key}Text`,
+            start: { ...events[open][1].end },
+            end: { ...events[i][1].start },
+          };
+
+          const nextEvents = [
+            ['enter', border, context],
+            ['enter', events[open][1], context],
+            ['exit', events[open][1], context],
+            ['enter', text, context],
+          ];
+
+          splice(
+            nextEvents,
+            nextEvents.length,
+            0,
+            resolveAll(
+              context.parser.constructs.insideSpan.null,
+              events.slice(open + 1, i),
+              context,
+            ),
+          );
+
+          splice(nextEvents, nextEvents.length, 0, [
+            ['exit', text, context],
+            ['enter', events[i][1], context],
+            ['exit', events[i][1], context],
+            ['exit', border, context],
+          ]);
+
+          splice(events, open - 1, i - open + 3, nextEvents);
+
+          i = open + nextEvents.length - 2;
+          break;
+        }
+      }
+
+      events.forEach((event) => {
+        if (event[1].type === keySeqTmp) {
+          event[1].type = types.data;
+        }
+      });
+
+      return events;
+    }
+
+    const tokenizer = {
+      tokenize,
+      resolveAll: resolve,
+    };
+
+    return {
+      text: { [marker]: tokenizer },
+      insideSpan: { null: [tokenizer] },
+      attentionMarkers: { null: [marker] },
+    };
+  };
+}
+
+const spoilerExtension = inlineExtension(codes.verticalBar, 2, 'spoiler');
+
+const spoilerExtensionHtml = {
+  enter: {
+    spoiler() {
+      this.tag('<span data-mx-spoiler>');
+    },
+  },
+  exit: {
+    spoiler() {
+      this.tag('</span>');
+    },
+  },
+};
+
+export { inlineExtension, spoilerExtension, spoilerExtensionHtml };