Как добавить поддержку текстового редактора с форматированием к моему расширению Markdown?

После множества ударов головой и добавления множества console.log() в Discourse я наконец понял, почему это не работало, и как заставить это работать.

Парсинг выполняется не на основе рендеринга HTML, а на основе токенов markdownit. Я использовал state.push(html_raw) в функции replace(), чтобы рендерить теги bbcode [snapblocks].

// assets/javascripts/lib/discourse-markdown/snapblocks-discourse.js

export function(helper) {
  md.block.bbcode.ruler.push("snapblocks", {
      tag: "snapblocks",
      replace(state, tagInfo, content) {
        let token = state.push('html_raw', '', 0)
        token.content = `<pre class="snapblocks-blocks">${content}</pre>`
      },
    });
}

Проблема заключалась в том, что тип токена html_raw игнорируется конвертером богатого текстового редактора. Это означает, что его нельзя использовать, если требуется поддержка богатого текстового редактора.

Я выяснил, что использование bbcode_open, text и bbcode_close позволяет решить эту проблему.

// assets/javascripts/lib/discourse-markdown/snapblocks-discourse.js

export function(helper) {
  md.block.bbcode.ruler.push("snapblocks", {
      tag: "snapblocks",
      replace(state, tagInfo, content) {
        let token = state.push('bbcode_open', 'pre', 1)
        token.attrs = [['class', 'snapblocks-discourse']]

        token = state.push('text', '', 0)
        token.content = content

        token = token.push('bbcode_close', 'pre', -1)
        token.attrs = [['class', 'snapblocks-discourse']] // необходимо для последующих проверок
      },
    });
}

После этого он больше не будет игнорироваться.

Перейдя к rich-text-editor-extension.js, вы делаете следующее.

// assets/javascripts/lib/rich-text-editor-extension.js

import { i18n } from "discourse-i18n";

const SNAPBLOCKS_NODES = ["inline_snapblocks", "snapblocks"];

/** @type {RichEditorExtension} */
const extension = {
    snapblocks: {
      attrs: { rendered: { default: true } },
      code: true,
      group: "block",
      content: "text*", // Это необходимо
      createGapCursor: true,
      parseDOM: [{ tag: "pre.snapblocks-blocks" }],
      toDOM: () => ["pre", { class: "snapblocks-blocks" }, 0],
    },
  },
  parse: {
    bbcode_open(state, token) {
      // Здесь токен — это тот же объект токена `bbcode_open`,
      // что и в предыдущем коде
      if (token.attrGet('class') === 'snapblocks-blocks') {
        state.openNode(state.schema.nodes.snapblocks, {
          open: token.attrGet("open") !== null,
        });
        return true;
      }
    },
    bbcode_close(state, token) {
      if (token.attrGet('class') === 'snapblocks-blocks') {
        state.closeNode();
        return true;
      }
    },
  },
  serializeNode: {
    snapblocks(state, node) {
      // Здесь он просто преобразуется обратно в обычный markdown
      state.write("[snapblocks]\n");
      state.renderContent(node);
      state.write("\n[/snapblocks]\n\n");
    },
  },
}

Ключи в свойстве parse — это парсеры для типа токена, который может использоваться в парсере markdown.

Очевидно, что здесь есть гораздо больше нюансов, но, по крайней мере, это должно значительно прояснить ситуацию и сократить количество ударов головой, которые мне приходится совершать.