Skip to content

Commit 82a67b0

Browse files
peyerlukdfreier
authored andcommitted
fix(clipboard): filter plaintext on paste
1 parent ed139eb commit 82a67b0

File tree

3 files changed

+75
-21
lines changed

3 files changed

+75
-21
lines changed

spec/clipboard.spec.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ describe('Clipboard', function () {
1717
return parseContent(div)
1818
}
1919

20+
function extractPlainText (str) {
21+
const div = document.createElement('div')
22+
div.innerHTML = str
23+
return parseContent(div, {plainText: true})
24+
}
25+
2026
function extractSingleBlock (str) {
2127
return extract(str)[0]
2228
}
@@ -377,5 +383,42 @@ describe('Clipboard', function () {
377383
expect(block).to.equal('text outside “<a href="https://livingdocs.io">text inside</a>”')
378384
})
379385
})
386+
387+
// Plain Text
388+
// ----------
389+
390+
describe('plain text option', function () {
391+
it('unwraps a single <b>', function () {
392+
expect(extractPlainText('<b>a</b>')[0]).to.equal('a')
393+
})
394+
395+
it('unwraps a single <strong>', function () {
396+
expect(extractPlainText('<strong>a</strong>')[0]).to.equal('a')
397+
})
398+
399+
it('unwraps nested <b><strong>', function () {
400+
expect(extractPlainText('<b><strong>a</strong></b>')[0]).to.equal('a')
401+
})
402+
403+
it('unwraps nested <span><strong>', function () {
404+
expect(extractPlainText('<span><strong>a</strong></span>')[0]).to.equal('a')
405+
})
406+
407+
it('keeps <br> tags', function () {
408+
expect(extractPlainText('a<br>b')[0]).to.equal('a<br>b')
409+
})
410+
411+
it('creates two blocks from two paragraphs', function () {
412+
const blocks = extractPlainText('<p>a</p><p>b</p>')
413+
expect(blocks[0]).to.equal('a')
414+
expect(blocks[1]).to.equal('b')
415+
})
416+
417+
it('unwraps phrasing tags within blocks', function () {
418+
const blocks = extractPlainText('<p><i>a</i></p><p><em>b</em></p>')
419+
expect(blocks[0]).to.equal('a')
420+
expect(blocks[1]).to.equal('b')
421+
})
422+
})
380423
})
381424
})

src/clipboard.js

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import config from './config'
22
import * as string from './util/string'
33
import * as nodeType from './node-type'
44
import * as quotes from './quotes'
5+
import {isPlainTextBlock} from './block'
56

6-
let allowedElements, requiredAttributes, transformElements, blockLevelElements, replaceQuotes
7+
let allowedElements, allowedPlainTextElements, requiredAttributes, transformElements, blockLevelElements, replaceQuotes
78
let splitIntoBlocks, blacklistedElements
89
const whitespaceOnly = /^\s*$/
910
const blockPlaceholder = '<!-- BLOCK -->'
@@ -13,6 +14,7 @@ updateConfig(config)
1314
export function updateConfig (conf) {
1415
const rules = conf.pastedHtmlRules
1516
allowedElements = rules.allowedElements || {}
17+
allowedPlainTextElements = rules.allowedPlainTextElements || {}
1618
requiredAttributes = rules.requiredAttributes || {}
1719
transformElements = rules.transformElements || {}
1820
blacklistedElements = rules.blacklistedElements || []
@@ -25,9 +27,9 @@ export function updateConfig (conf) {
2527
rules.splitIntoBlocks.forEach((name) => { splitIntoBlocks[name] = true })
2628
}
2729

28-
export function paste (element, cursor, clipboardContent) {
29-
const document = element.ownerDocument
30-
element.setAttribute(config.pastingAttribute, true)
30+
export function paste (block, cursor, clipboardContent) {
31+
const document = block.ownerDocument
32+
block.setAttribute(config.pastingAttribute, true)
3133

3234
if (cursor.isSelection) {
3335
cursor = cursor.deleteExactSurroundingTags()
@@ -39,9 +41,10 @@ export function paste (element, cursor, clipboardContent) {
3941
const pasteHolder = document.createElement('div')
4042
pasteHolder.innerHTML = clipboardContent
4143

42-
const blocks = parseContent(pasteHolder)
44+
const isPlainText = isPlainTextBlock(block)
45+
const blocks = parseContent(pasteHolder, {plainText: isPlainText})
4346

44-
element.removeAttribute(config.pastingAttribute)
47+
block.removeAttribute(config.pastingAttribute)
4548
return {blocks, cursor}
4649
}
4750

@@ -55,30 +58,35 @@ export function paste (element, cursor, clipboardContent) {
5558
* @param {DOM node} A container where the pasted content is located.
5659
* @returns {Array of Strings} An array of cleaned innerHTML like strings.
5760
*/
58-
export function parseContent (element) {
61+
export function parseContent (element, {plainText = false} = {}) {
62+
const options = {
63+
allowedElements: plainText ? allowedPlainTextElements : allowedElements,
64+
keepInternalRelativeLinks: plainText ? false : keepInternalRelativeLinks
65+
}
66+
5967
// Filter pasted content
60-
return filterHtmlElements(element)
68+
return filterHtmlElements(element, options)
6169
// Handle Blocks
6270
.split(blockPlaceholder)
6371
.map((entry) => string.trim(cleanWhitespace(replaceAllQuotes(entry))))
6472
.filter((entry) => !whitespaceOnly.test(entry))
6573
}
6674

67-
export function filterHtmlElements (elem) {
75+
function filterHtmlElements (elem, options) {
6876
return Array.from(elem.childNodes).reduce((content, child) => {
6977
if (blacklistedElements.indexOf(child.nodeName.toLowerCase()) !== -1) {
7078
return ''
7179
}
7280

7381
// Keep internal relative links relative (on paste).
74-
if (keepInternalRelativeLinks && child.nodeName === 'A' && child.href) {
82+
if (options.keepInternalRelativeLinks && child.nodeName === 'A' && child.href) {
7583
const stripInternalHost = child.getAttribute('href').replace(window.location.origin, '')
7684
child.setAttribute('href', stripInternalHost)
7785
}
7886

7987
if (child.nodeType === nodeType.elementNode) {
80-
const childContent = filterHtmlElements(child)
81-
return content + conditionalNodeWrap(child, childContent)
88+
const childContent = filterHtmlElements(child, options)
89+
return content + conditionalNodeWrap(child, childContent, options)
8290
}
8391

8492
// Escape HTML characters <, > and &
@@ -87,11 +95,11 @@ export function filterHtmlElements (elem) {
8795
}, '')
8896
}
8997

90-
export function conditionalNodeWrap (child, content) {
98+
function conditionalNodeWrap (child, content, options) {
9199
let nodeName = child.nodeName.toLowerCase()
92100
nodeName = transformNodeName(nodeName)
93101

94-
if (shouldKeepNode(nodeName, child)) {
102+
if (shouldKeepNode(nodeName, child, options)) {
95103
const attributes = filterAttributes(nodeName, child)
96104

97105
if (nodeName === 'br') return `<${nodeName + attributes}>`
@@ -115,7 +123,7 @@ export function conditionalNodeWrap (child, content) {
115123
}
116124

117125
// returns string of concatenated attributes e.g. 'target="_blank" rel="nofollow" href="/test.com"'
118-
export function filterAttributes (nodeName, node) {
126+
function filterAttributes (nodeName, node) {
119127
return Array.from(node.attributes).reduce((attributes, {name, value}) => {
120128
if (allowedElements[nodeName][name] && value) {
121129
return `${attributes} ${name}="${value}"`
@@ -124,22 +132,22 @@ export function filterAttributes (nodeName, node) {
124132
}, '')
125133
}
126134

127-
export function transformNodeName (nodeName) {
135+
function transformNodeName (nodeName) {
128136
return transformElements[nodeName] || nodeName
129137
}
130138

131-
export function hasRequiredAttributes (nodeName, node) {
139+
function hasRequiredAttributes (nodeName, node) {
132140
const requiredAttrs = requiredAttributes[nodeName]
133141
if (!requiredAttrs) return true
134142

135143
return !requiredAttrs.some((name) => !node.getAttribute(name))
136144
}
137145

138-
export function shouldKeepNode (nodeName, node) {
139-
return allowedElements[nodeName] && hasRequiredAttributes(nodeName, node)
146+
function shouldKeepNode (nodeName, node, options) {
147+
return options.allowedElements[nodeName] && hasRequiredAttributes(nodeName, node)
140148
}
141149

142-
export function cleanWhitespace (str) {
150+
function cleanWhitespace (str) {
143151
return str
144152
.replace(/\n/g, ' ')
145153
.replace(/ {2,}/g, ' ')
@@ -149,7 +157,7 @@ export function cleanWhitespace (str) {
149157
))
150158
}
151159

152-
export function replaceAllQuotes (str) {
160+
function replaceAllQuotes (str) {
153161
if (replaceQuotes.quotes || replaceQuotes.singleQuotes || replaceQuotes.apostrophe) {
154162
return quotes.replaceAllQuotes(str, replaceQuotes)
155163
}

src/config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ export default {
5252
'em': {},
5353
'br': {}
5454
},
55+
allowedPlainTextElements: {
56+
'br': {}
57+
},
5558

5659
// Elements that have required attributes.
5760
// If these are not present the elements are filtered out.

0 commit comments

Comments
 (0)