Input rules for wrapping marks

Hi all,

I’m having a hard time creating some custom input rules to match and replace the usual Markdown marks (em, strong, etc.) Here is the one I have so far:

const emRule = wrappingInputRule(/(?:^|[^\*_])(?:\*|_)([^\*_]+)(?:\*|_)$/, schema.marks.em)

It should match any characters (except asterisk and underscore) preceded and followed by one (not two) asterisk or underscore. But those characters aren’t replaced although I use the schema provided by prosemirror-markdown

Any idea? Many thanks in advance!

wrappingInputRule is for wrapping something in a node, marks work differently. I should probably add a utility for wrapping something in a mark too. See #518.

@marijn Thank you very much for your quick reply! Do you have an idea when you’ll have the opportunity to look at it? I’d be more than happy to help, but looking at the wrappingInputRule function, I don’t really know where to start… Let me know if you have any advice :wink:

I’m using something like this:

function textMarkInputRule(regexp, markType, trigger, getAttrs, getLength) {
  return new InputRule(regexp, (state, match, start, end) => {
    const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs;
    const markLength = getLength instanceof Function ? getLength(match) : getLength;
    return state.tr.addMark(start, start + markLength, markType.create(attrs))
                   .insert(end, schema.text(trigger));
  });
}

You should probably remove the “trigger” insert as it’s just something I need for my specific use case.

Wow thank you very much @kiejo! It helps a lot…

Here’s an implementation of what I think you’re trying to do:

function markInputRule(regexp, markType, getAttrs) {
  return new InputRule(regexp, (state, match, start, end) => {
    let attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs
    let tr = state.tr
    if (match[1]) {
      let textStart = start + match[0].indexOf(match[1])
      let textEnd = textStart + match[1].length
      if (textEnd < end) tr.delete(textEnd, end)
      if (textStart > start) tr.delete(start, textStart)
      end = start + match[1].length
    }
    return tr.addMark(start, end, markType.create(attrs))
  })
}

And you can then do something like markInputRule(/_(\S(?:|.*?\S))_$/, schema.marks.em) to auto-emphasize text between undescores, but in practice, I found this wasn’t really usable, because after you type the closing underscore, you’re still directly after emphasized text, and if you continue typing, the new text is still emphasized.

So, for now, I’m not adding this to the prosemirror-inputrules package.

Many thanks @marijn… I’m starting with ProseMirror and it’s good to have such a helpful community!

Hi @marijn and @kiejo, here’s what I got based on your answers… It works fine and might find its place in the prosemirror-inputrules plugin if you find it useable enough:

const markInputRule = (regexp, markType, getAttrs) => {
  const newRegexp = new RegExp(regexp.source.replace(/\$$/, '') + '(.)' + '$')

  return new InputRule(newRegexp, (state, match, start, end) => {
    const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs
    const textStart = start + match[0].indexOf(match[1])
    const textEnd = textStart + match[1].length
    const tr = state.tr

    start = (match[0].match(/^\s/)) ? start + 1 : start

    if (textEnd < end) tr.delete(textEnd, end)
    if (textStart > start) tr.delete(start, textStart)

    end = start + match[1].length

    return tr
      .addMark(start, end, markType.create(attrs))
      .insert(end, schema.text(match[2]))
  })
}

You can then use it like this:

const buildInputRules = (schema) => {
  let result = [], type

  if (type = schema.marks.strong) result.push(markInputRule(/(?:\*\*|__)([^\*_]+)(?:\*\*|__)$/, type))
  if (type = schema.marks.em) result.push(markInputRule(/(?:^|[^\*_])(?:\*|_)([^\*_]+)(?:\*|_)$/, type))
  
  return result
}
1 Like

I considered that approach, but it has the downside that you have to type something after the marked-up text, so if that’s at the end of the line, or you’re just inserting it between other text and then don’t continue typing, the rule doesn’t fire.

Do you have any ideas on how this could be fixed? Would moving the cursor ahead one position work?

@marijn I got it working so the mark is not applied to the new text! It’s your implementation exactly, except for the second last line

function markInputRule(regexp, markType, getAttrs) {
  return new InputRule(regexp, (state, match, start, end) => {
    let attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs
    let tr = state.tr
    if (match[1]) {
      let textStart = start + match[0].indexOf(match[1])
      let textEnd = textStart + match[1].length
      if (textEnd < end) tr.delete(textEnd, end)
      if (textStart > start) tr.delete(start, textStart)
      end = start + match[1].length
    }
    tr.addMark(start, end, markType.create(attrs))
    tr.removeStoredMark(markType) // Do not continue with mark.
    return tr
  })
}
5 Likes

I came up with this:

function markInputRule(regexp, markType, getAttrs, skipStart) {
    return new InputRule(regexp, (state, match, start, end) => {
        let attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs
        let tr = state.tr
        if (match[1]) {
            let skipMatch;
            let skipLen = 0;

            if (skipMatch = skipStart && match[0].match(skipStart)) {
                console.log(skipMatch)
                skipLen = skipMatch[0].length;
                start += skipLen;
            }

            let textStart = start + match[0].indexOf(match[1]) - skipLen
            let textEnd = textStart + match[1].length
            if (textEnd < end) tr.delete(textEnd, end)
            if (textStart > start) tr.delete(start, textStart)
            end = start + match[1].length
        }
        tr.addMark(start, end, markType.create(attrs))
        tr.removeStoredMark(markType)
        return tr
    })
}

(skipStart is a hack for emphasis because some browsers don’t yet support lookbehinds in Regex.)

And, here are the rules I used:

// strong (** AND __)
markInputRule(/(?:\*\*)([^\*]+)(?:\*\*)$/, marks.strong),
markInputRule(/(?:\s__)([^_]+)(?:__)$/, marks.strong),

// em (* and _)
markInputRule(/(?:^|[^\*])(?:\*)([^\*]+)(?:\*)$/, marks.em, {}, /^[^\*]/),
markInputRule(/(?:^|[^_])(?:_)([^_]+)(?:_)$/, marks.em, {}, /^[^_]/),

// links
markInputRule(/(?:\[([^\]]+)\])(\([^\)]+\))$/, marks.link, function(match) {
    return {href: match[2]}
}),

// code (prosemirror-codemark adds this)
markInputRule(/(?:`)([^`]+)(?:`)$/, marks.code),

// strikethrough
markInputRule(/(?:~~)([^~]+)(?:~~)$/, marks.s),

// sup & sub
markInputRule(/(?:\^)([^\^]+)(?:\^)$/, marks.sup),
markInputRule(/(?:~)([^~]+)(?:~)$/, marks.sub),

// mark
markInputRule(/(?:==)([^=]+)(?:==)/, marks.mark),

Instead of the code inputrule, I let prosemirror-codemark handle that (with better cursors).

2 Likes