Skip to content

Commit d5a82e5

Browse files
committed
Implement nonconsuming parse rules (RFC 11)
See https://discuss.prosemirror.net/t/underline-parserule-that-excludes-links/3302/9 See https://github.com/ProseMirror/rfcs/blob/master/text/0011-nonconsuming-parse-rules.md FEATURE: Parse rules can now have a `consuming: false` property which allows other rules to match their tag or style even when they apply.
1 parent 5564b0e commit d5a82e5

File tree

2 files changed

+40
-15
lines changed

2 files changed

+40
-15
lines changed

src/from_dom.js

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ import {Mark} from "./mark"
6868
// property is only meaningful in a schema—when directly
6969
// constructing a parser, the order of the rule array is used.
7070
//
71+
// consuming:: ?boolean
72+
// By default, when a rule matches an element or style, no further
73+
// rules get a chance to match it. By setting this to `false`, you
74+
// indicate that even when this rule matches, other rules that come
75+
// after it should also run.
76+
//
7177
// context:: ?string
7278
// When given, restricts this rule to only match when the current
7379
// context—the parent nodes into which the content is being
@@ -189,8 +195,8 @@ export class DOMParser {
189195
return Slice.maxOpen(context.finish())
190196
}
191197

192-
matchTag(dom, context) {
193-
for (let i = 0; i < this.tags.length; i++) {
198+
matchTag(dom, context, after) {
199+
for (let i = after ? this.tags.indexOf(after) + 1 : 0; i < this.tags.length; i++) {
194200
let rule = this.tags[i]
195201
if (matches(dom, rule.tag) &&
196202
(rule.namespace === undefined || dom.namespaceURI == rule.namespace) &&
@@ -205,8 +211,8 @@ export class DOMParser {
205211
}
206212
}
207213

208-
matchStyle(prop, value, context) {
209-
for (let i = 0; i < this.styles.length; i++) {
214+
matchStyle(prop, value, context, after) {
215+
for (let i = after ? this.styles.indexOf(after) + 1 : 0; i < this.styles.length; i++) {
210216
let rule = this.styles[i]
211217
if (rule.style.indexOf(prop) != 0 ||
212218
rule.context && !context.matchesContext(rule.context) ||
@@ -429,13 +435,14 @@ class ParseContext {
429435
}
430436
}
431437

432-
// : (dom.Element)
438+
// : (dom.Element, ?ParseRule)
433439
// Try to find a handler for the given tag and use that to parse. If
434440
// none is found, the element's content nodes are added directly.
435-
addElement(dom) {
436-
let name = dom.nodeName.toLowerCase()
441+
addElement(dom, matchAfter) {
442+
let name = dom.nodeName.toLowerCase(), ruleID
437443
if (listTags.hasOwnProperty(name) && this.parser.normalizeLists) normalizeList(dom)
438-
let rule = (this.options.ruleFromNode && this.options.ruleFromNode(dom)) || this.parser.matchTag(dom, this)
444+
let rule = (this.options.ruleFromNode && this.options.ruleFromNode(dom)) ||
445+
(ruleID = this.parser.matchTag(dom, this, matchAfter))
439446
if (rule ? rule.ignore : ignoreTags.hasOwnProperty(name)) {
440447
this.findInside(dom)
441448
} else if (!rule || rule.skip || rule.closeParent) {
@@ -453,7 +460,7 @@ class ParseContext {
453460
if (sync) this.sync(top)
454461
this.needsBlock = oldNeedsBlock
455462
} else {
456-
this.addElementByRule(dom, rule)
463+
this.addElementByRule(dom, rule, rule.consuming === false ? ruleID : null)
457464
}
458465
}
459466

@@ -468,11 +475,15 @@ class ParseContext {
468475
// had a rule with `ignore` set.
469476
readStyles(styles) {
470477
let marks = Mark.none
471-
for (let i = 0; i < styles.length; i += 2) {
472-
let rule = this.parser.matchStyle(styles[i], styles[i + 1], this)
473-
if (!rule) continue
474-
if (rule.ignore) return null
475-
marks = this.parser.schema.marks[rule.mark].create(rule.attrs).addToSet(marks)
478+
style: for (let i = 0; i < styles.length; i += 2) {
479+
for (let after = null;;) {
480+
let rule = this.parser.matchStyle(styles[i], styles[i + 1], this, after)
481+
if (!rule) continue style
482+
if (rule.ignore) return null
483+
marks = this.parser.schema.marks[rule.mark].create(rule.attrs).addToSet(marks)
484+
if (rule.consuming === false) after = rule
485+
else break
486+
}
476487
}
477488
return marks
478489
}
@@ -481,7 +492,7 @@ class ParseContext {
481492
// Look up a handler for the given node. If none are found, return
482493
// false. Otherwise, apply it, use its return value to drive the way
483494
// the node's content is wrapped, and return true.
484-
addElementByRule(dom, rule) {
495+
addElementByRule(dom, rule, continueAfter) {
485496
let sync, nodeType, markType, mark
486497
if (rule.node) {
487498
nodeType = this.parser.schema.nodes[rule.node]
@@ -499,6 +510,8 @@ class ParseContext {
499510

500511
if (nodeType && nodeType.isLeaf) {
501512
this.findInside(dom)
513+
} else if (continueAfter) {
514+
this.addElement(dom, continueAfter)
502515
} else if (rule.getContent) {
503516
this.findInside(dom)
504517
rule.getContent(dom, this.parser.schema).forEach(node => this.insertNode(node))

test/test-dom.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,18 @@ describe("DOMParser", () => {
478478
let closeParser = new DOMParser(schema, [{tag: "br", closeParent: true}].concat(DOMParser.schemaRules(schema)))
479479
ist(closeParser.parse(domFrom("<p>one<br>two</p>")), doc(p("one"), p("two")), eq)
480480
})
481+
482+
it("supports non-consuming node rules", () => {
483+
let parser = new DOMParser(schema, [{tag: "ol", consuming: false, node: "blockquote"}]
484+
.concat(DOMParser.schemaRules(schema)))
485+
ist(parser.parse(domFrom("<ol><p>one</p></ol>")), doc(blockquote(ol(li(p("one"))))), eq)
486+
})
487+
488+
it("supports non-consuming style rules", () => {
489+
let parser = new DOMParser(schema, [{style: "font-weight", consuming: false, mark: "em"}]
490+
.concat(DOMParser.schemaRules(schema)))
491+
ist(parser.parse(domFrom("<p><span style='font-weight: 800'>one</span></p>")), doc(p(em(strong("one")))), eq)
492+
})
481493
})
482494

483495
describe("schemaRules", () => {

0 commit comments

Comments
 (0)