Skip to content

Commit

Permalink
Improve normalizing of external pasted HTML
Browse files Browse the repository at this point in the history
  • Loading branch information
marijnh committed Nov 14, 2016
1 parent ec7cb35 commit 4985040
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 19 deletions.
61 changes: 43 additions & 18 deletions src/clipboard.js
Expand Up @@ -85,29 +85,54 @@ exports.fromClipboard = fromClipboard
// fit anywhere in the schema.
function normalizeSiblings(slice, $context) {
if (slice.content.childCount < 2) return slice
let firstNode
slice.content.forEach(node => {
if (!node.isText) { firstNode = node; return false }
})
if (!firstNode) return slice
for (let d = $context.depth; d >= 0; d--) {
let parent = $context.node(d), expr = parent.type.contentExpr, match
if (match = expr.atType(parent.attrs, firstNode.type, firstNode.attrs, firstNode.marks)) {
if (firstNode != slice.content.firstChild) match = expr.start(parent.attrs)
let content = []
slice.content.forEach(node => {
let wrap = match.findWrappingFor(node)
if (!wrap) { content = null; return false }
for (let i = wrap.length - 1; i >= 0; i--)
node = wrap[i].type.create(wrap[i].attrs, Fragment.from(node))
content.push(node)
})
if (content) return Slice.maxOpen(Fragment.from(content))
}
let parent = $context.node(d)
let match = parent.contentMatchAt($context.index(d))
let lastWrap, result = []
slice.content.forEach(node => {
if (!result) return
let wrap = match.findWrappingFor(node), inLast
if (!wrap) return result = null
if (inLast = result.length && lastWrap.length && addToSibling(wrap, lastWrap, node, result[result.length - 1], 0)) {
result[result.length - 1] = inLast
} else {
if (result.length) result[result.length - 1] = closeRight(result[result.length - 1], lastWrap.length)
let wrapped = withWrappers(node, wrap)
result.push(wrapped)
match = match.matchType(wrapped.type, wrapped.attrs)
lastWrap = wrap
}
})
if (result) return Slice.maxOpen(Fragment.from(result))
}
return slice
}

function withWrappers(node, wrap, from = 0) {
for (let i = wrap.length - 1; i >= from; i--)
node = wrap[i].type.create(wrap[i].attrs, Fragment.from(node))
return node
}

// Used to group adjacent nodes wrapped in similar parents by
// normalizeSiblings into the same parent node
function addToSibling(wrap, lastWrap, node, sibling, depth) {
if (depth < wrap.length && depth < lastWrap.length && wrap[depth].type == lastWrap[depth].type) {
let inner = addToSibling(wrap, lastWrap, node, sibling.lastChild, depth + 1)
if (inner) return sibling.copy(sibling.content.replaceChild(sibling.childCount - 1, inner))
let match = sibling.contentMatchAt(sibling.childCount)
if (depth == wrap.length - 1 ? match.matchNode(node) : match.matchType(wrap[depth + 1].type, wrap[depth + 1].attrs))
return sibling.copy(sibling.content.append(Fragment.from(withWrappers(node, wrap, depth + 1))))
}
}

function closeRight(node, depth) {
if (depth == 0) return node
let fragment = node.content.replaceChild(node.childCount - 1, closeRight(node.lastChild, depth - 1))
let fill = node.contentMatchAt(node.childCount).fillBefore(Fragment.empty, true)
return node.copy(fragment.append(fill))
}

// Trick from jQuery -- some elements must be wrapped in other
// elements for innerHTML to work. I.e. if you do `div.innerHTML =
// "<td>..</td>"` the table cells are ignored.
Expand Down
12 changes: 11 additions & 1 deletion test/test-clipboard.js
@@ -1,5 +1,5 @@
const ist = require("ist")
const {eq, doc, p, blockquote, ul, li, hr} = require("prosemirror-model/test/build")
const {eq, doc, p, blockquote, ul, ol, li, hr, br} = require("prosemirror-model/test/build")
const {NodeSelection, TextSelection} = require("prosemirror-state")
const {Slice} = require("prosemirror-model")
const {tempEditor} = require("./view")
Expand Down Expand Up @@ -38,4 +38,14 @@ describe("Clipboard interface", () => {
ist(fromClipboard(view, transfer("<p>hello</p><hr>"), false, $p), new Slice(doc(p("hello"), hr).content, 1, 0), eq)
ist(fromClipboard(view, transfer("<p>hello</p>bar"), false, $p), new Slice(doc(p("hello"), p("bar")).content, 1, 1), eq)
})

it("will sanely clean up top-level nodes in HTML", () => {
let view = tempEditor(), $p = view.state.doc.resolve(1)
ist(fromClipboard(view, transfer("<ul><li>foo</li></ul>bar<br>"), false, $p),
new Slice(doc(ul(li(p("foo"))), p("bar", br)).content, 3, 1), eq)
ist(fromClipboard(view, transfer("<ul><li>foo</li></ul>bar<br><p>x</p>"), false, $p),
new Slice(doc(ul(li(p("foo"))), p("bar", br), p("x")).content, 3, 1), eq)
ist(fromClipboard(view, transfer("<li>foo</li><li>bar</li><p>x</p>"), false, $p),
new Slice(doc(ol(li(p("foo")), li(p("bar"))), p("x")).content, 3, 1), eq)
})
})

0 comments on commit 4985040

Please sign in to comment.