Underline ParseRule that excludes Links

My current underline mark schema is

  get schema(): MarkSpec {
    return {
      parseDOM: [{tag: 'u'}, {style: 'text-decoration=underline'}],
      toDOM() { return ['u', 0] },
    }
  }

One issue I’ve found with this schema by inspecting the html of the editor after a link has been pasted, is that an underline mark is created around a link as well.

This is because most links have a text-decoration: underline style applied to them. Is there a way of constructing a ParseRule that still checks for text-decoration=underline but excludes anchor (link) tags?

I think you should be able to add a getAttrs function that returns false for elements where the mark shouldn’t apply.

If I use a ParseRule with style, getAttrs only gets the style’s value as the argument so I wouldn’t be able to detect if the underline came from an anchor or not.

If I use a ParseRule with tag, I would be able to get the html element as an argument, but what tag would I use? Do I use all inline html nodes, or something as simple as “span” only?

With the current ParseRule api with style or tag, how would I say “text-decoration=underline but not in an anchor tag”? Is there an inverse operator like {tag: 'not(a)', style: 'text-decoration=underline'} that can combine both?

Oh, right, getAttrs is only passed the style value in this case, that’s indeed no use. You could use tag: "*" with a low priority as a kludge, I suppose.

Not sure why I didn’t do this earlier, but I dug into the prosemirror-model source code and realized the tag value is just a CSS selector. So to negate anchor tags, we can just use :not(a). :smile:

{
  tag: ':not(a)', 
  getAttrs: dom => dom.style.textDecoration.includes("underline") || dom.style.textDecorationLine.includes("underline")
}

One thing I need to double check though is: we don’t need to account for priority with the tag selector when we’re applying marks right (assuming no conflicts with marks themselves)? Are all matching tags applied, not only the first one found?

Only the first tag rule for a given element will be applied (multiple style rules may match, if they match different style properties), so there is a difference between the old rule and this one in that it’ll ‘consume’ the styled element, and it won’t apply if a higher-precedence rule matches the tag (i.e. <blockquote style="text-decoration: underline"> will just be a blockquote, and not add the mark).

Hmm, why is this the case when using ParseRule and tag for marks?

I understand the reasoning of ParseRule with node consuming the element since a DOM node can only be of one type of node; but there isn’t a same restriction with applying multiple marktypes to a DOM node.

Would it be possible to modify prosemirror-model to allow for this behavior? i.e. node parseRules are consumptive and mark parseRules are not, rather than the current behavior of tags are consumptive and styles are not.

I’m thinking the changes would be centered around addDom:

  addDOM(dom) {
    if (dom.nodeType == 3) {
      this.addTextNode(dom)
    } else if (dom.nodeType == 1) {
      let style = dom.getAttribute("style")
      // parse not only mark rules (rule.mark != null) with styles, but also marks with tags here
      let marks = style ? this.readStyles(parseStyles(style)) : null, top = this.top
      if (marks != null) for (let i = 0; i < marks.length; i++) this.addPendingMark(marks[i])
      this.addElement(dom) // remove mark parsing rules here to avoid re-parsing?
      if (marks != null) for (let i = 0; i < marks.length; i++) this.removePendingMark(marks[i], top)
    }
  }

Two edge cases I’ve thought of with this are:

  1. are parseRules with style and node defined, but rule.mark = null allowed? This question came from looking at readStyles and seeing only marks are returned and rule.mark is used to index into schema.marks.
  2. ensuring backwards compatibility with keeping mark parseRules with tag attributes being consumptive may require an additional boolean like returnOnFirstMatch default set to true

Hmm, why is this the case when using ParseRule and tag for marks?

Because there, too, matching the same node to multiple rules might be nonsense (if the rule uses the node name to create a mark, it’d be weird to use it again in another rule to create a node).

Would an opt-in consuming: false flag on parse rules help you here?

if the rule uses the node name to create a mark, it’d be weird to use it again in another rule to create a node

Ah, that’s a use case I didn’t account for: I was only thinking about parseRules in terms of model output and not html dom inputs.

Would an opt-in consuming: false flag on parse rules help you here?

Yes! This would allow me to use tags for mark pasteRules, and help with the text-underline scenario.

I’ve added an RFC for this to allow community feedback, will follow up in a week.

2 Likes

This has now been implemented in prosemirror-model.

1 Like