Apply the input rules (markdown) but also keep the tokens

I’m building a markdown editor and I want to make something similar to StackEdit, where you have a split screen with a text editor at the left and the markdown preview at the right. What I want is to build the part of the left, where you basically write some markdown, it is applied to the same text and also you can see the tokens.

As an example, here’s an image:

image

You see the # or the ** in a grey color, but it is applied to the text and the token is still there. I hope I’m clear! Thanks!

You could fake them with :before style rules and such. But if you want to allow people to edit those characters directly, you might be better off building on top of something like CodeMirror

Thanks @marijn! What about codemirror would make this easier to implement?

It’s a text editor that you can set up to keep a Markdown parse tree of your code on the side, so it’ll make a textual editing approach easier to implement than a rich text editor will. But either way this is not going to be an easy thing to build.

1 Like

Thanks @marijn! I handled to add some of the tokens customizing some input rules, for instance:

export function headingsInputRule (regexp: any, nodeType: any, getAttrs: any) {
  return new InputRule(regexp, (state, match, start, end) => {
    const $start = state.doc.resolve(start);
    const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs;
    if (!$start.node(-1).canReplaceWith($start.index(-1), $start.indexAfter(-1), nodeType)) {
      return null;
    }
    return state.tr
      .delete(start, end)
      .setBlockType(start, start, nodeType, attrs)
      .insertText(`${'#'.repeat(attrs?.level || 1)} `, start);
  });
}

But the are input rules not working when, for instance, I write **some text**, which is supposed to make the text bold. This seems to be related to (please correct me) wrappingInputRule vs textblockTypeInputRule or maybe nodes vs marks. Can you give me a small guidance on this, or where to see to be able to make an input rule for this: **some text**?

Thanks so much in advance, I really appreciate your answers!

Hi, I am trying to create a markInputRule(regex, markType) function, which is symmetrical to textblockTypeInputRule but for marks instead of nodes (this doesn’t address the initial question but the latest comment just above).

The steps would be:

  1. set current selection to the content wrapped into “**”
  2. replace the selection to same text without “**”
  3. apply relevant mark
function markInputRule(regex: RegExp, markType: MarkType) {
    return new InputRule(regex, (state, match, start, end) => {
        const tr = state.tr
        const anchor = state.doc.resolve(start)
        const head = state.doc.resolve(end)
        tr.setSelection(new TextSelection(anchor, head))
        // real code is a tad more complex: 
        // I reuse logic from the toggleMark command
        tr.addStoredMark(markType.create({}))
        return tr
    })

It doesn’t work as I would expect, only the selection part seems to work. I will update if I manage to setup something.

Edit: this seems better:

function markInputRule(regex: RegExp, markType: MarkType) {
    return new InputRule(regex, (state, match, start, end) => {
        const tr = state.tr
        const markedText = schema.text(match[1], [markType.create({})])
        tr.replaceWith(start, end, markedText)
        return tr
    })
}

The problem with toggleMark internal logic is that it relies a lot on the current selection, but you might not want to alter this selection to apply new marks, instead you’ll want to use the matched position.

New update:

This seems actually a bad idea to have such a generic pattern. The problem is that you will hit issues when writing the regex:

  • the regex should contain only the content you want to replace
  • but you also want to differentiate “*foobar” (should not match, it’s a non-finished bold text) from “foobar” (should match and become italic) => to achieve that you have to check the characters before the content you want to match, which makes things messy.

I ended up writing a rule specific to each kind of mark I want to apply, with regex accepting a varying number of separators, for example /(-{1,3})([^-]+)(-{1,3})/, and I check the number of matched separators within the input rule.