How do I add rich text editor support to my markdown extension?

I’ve created an extension that adds some bbcode tags to render snapblocks. When the rich text editor was introduced, this kind of broke my extension, or at least it’s unusable with the rich text editor. I want to add support for the rich text editor, so I looked into the spoiled extension to see how that does it, and I’ve currently got it somewhat working. I can actually insert the snapblocks into the post in the rich text editor, but I can’t seem to get existing snapblocks tags to convert to the rich text editor.

Here’s what I’ve got so far.

Right now I’m specifically focused on the actual conversion. I have a basic guess on how it works, but so far I’m not getting any results. So how do get the parsing to work?

1 Like

After a lot of head banging and adding many console.log()s to discourse, I have discovered why it wasn’t working, and how to get it to work.

The parsing is not done with the rendered html, but instead with the markdownit tokens. I had been using state.push(html_raw) in the replace() function to render my [snapblocks] bbcode tags.

// 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>`
      },
    });
}

The issue was that the html_raw token type is ignored by the rich text editor convertor. Which means that you can’t use this if you want rich text editor support.

I found out that by using bbcode_open, text and bbcode_close, I can get it to work.

// 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']] // needed for later checks
      },
    });
}

Once that is done, this will not be ignored anymore.

Moving to rich-text-editor-extension.js, you then do this.

// 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*", // This is needed
      createGapCursor: true,
      parseDOM: [{ tag: "pre.snapblocks-blocks" }],
      toDOM: () => ["pre", { class: "snapblocks-blocks" }, 0],
    },
  },
  parse: {
    bbcode_open(state, token) {
      // The token here is the same `bbcode_open` token object
      // from the previous code
      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) {
      // This just converts it back to plain text markdown
      state.write("[snapblocks]\n");
      state.renderContent(node);
      state.write("\n[/snapblocks]\n\n");
    },
  },
}

The keys in the parse property are parsers for the token type that can be used in the markdown parser.

There’s definitely more to it than just this, but at least this should clear things up a lot more, and lower the amount of head banging I need to do.

1 Like

Not exactly, but we already have the html-block extension handling the html_raw markdown-it tokens, which is a generic way to handle a passthrough content.

You can check all the types of extensions allowed via registerRichEditorExtension, and there are many extensions from plugins as well as the ones that are registered by default that can be used as inspiration.

Please let us know if you have any question.

3 Likes

Thanks for the clarification.


I am now running into an issue where toggling the block version isn’t working. I can toggle inline blocks just not the block level node.

At some point, I would like it render the blocks when the cursor isn’t on it, and also add a style selector, but I feel like those can come after just getting basic editing working.

Sorry, I don’t follow. What do you mean by “toggling the block version”?

My bbcode tag has [snapblocks] and [sb], where [sb] is inline, and [snapblocks] is block level. When pressing the toolbare button, it’s toggling the inline tag well, but it’s not toggling the block level tag when I select a block of text.