Skip to content

Commit

Permalink
Merge pull request #4872 from nextcloud/fix/4602-remove-newlines-in-p…
Browse files Browse the repository at this point in the history
…astes

fix(paste): collapse whitespace before pasting
  • Loading branch information
mejo- authored Oct 24, 2023
2 parents c34d0fb + a958e8a commit 72e9537
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 1 deletion.
4 changes: 3 additions & 1 deletion src/extensions/Markdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@
import { Extension, getExtensionField } from '@tiptap/core'
import { Plugin, PluginKey } from '@tiptap/pm/state'
import { MarkdownSerializer, defaultMarkdownSerializer } from '@tiptap/pm/markdown'
import markdownit from '../markdownit/index.js'
import { DOMParser } from '@tiptap/pm/model'
import markdownit from '../markdownit/index.js'
import transformPastedHTML from './transformPastedHTML.js'

const Markdown = Extension.create({

Expand Down Expand Up @@ -106,6 +107,7 @@ const Markdown = Extension.create({

return parser.parseSlice(dom, { preserveWhitespace: true, context: $context })
},
transformPastedHTML,
},
}),
]
Expand Down
100 changes: 100 additions & 0 deletions src/extensions/transformPastedHTML.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* @copyright Copyright (c) 2023 Max <[email protected]>
*
* @author Max <[email protected]>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

/**
*
* Prepare pasted html for insertion into tiptap
*
* We render paragraphs with `white-space: pre-wrap`
* so newlines are visible and preserved.
*
* Pasted html may contain newlines inside tags with a different `white-space` style.
* They are not visible in the source.
* Strip them so the pasted result wraps nicely.
*
* At the same time we need to preserve whitespace inside `<pre>` tags
* and the like.
*
* @param {string} html Pasted html content
*/
export default function(html) {
const parser = new DOMParser()
const doc = parser.parseFromString(html, 'text/html')
forAllTextNodes(doc, textNode => {
if (collapseWhiteSpace(textNode)) {
textNode.textContent = textNode.textContent.replaceAll('\n', ' ')
}
})
return doc.body.innerHTML
}

/**
*
* Run function for all text nodes in the document.
*
* @param {Document} doc Html document to process
* @param {Function} fn Function to run
*
*/
function forAllTextNodes(doc, fn) {
const nodeIterator = doc.createNodeIterator(
doc.body,
NodeFilter.SHOW_TEXT,
)
let currentNode = nodeIterator.nextNode()
while (currentNode) {
fn(currentNode)
currentNode = nodeIterator.nextNode()
}
}

/**
*
* Check if newlines need to be collapsed based on the applied style
*
* @param {Text} textNode Text to check the style for
*
*/
function collapseWhiteSpace(textNode) {
// Values of `white-space` css that will collapse newline whitespace
// See https://developer.mozilla.org/en-US/docs/Web/CSS/white-space#values
const COLLAPSING_WHITE_SPACE_VALUES = ['normal', 'nowrap']
let ancestor = textNode.parentElement
while (ancestor) {
// Chrome does not support getComputedStyle on detached dom
// https://lists.w3.org/Archives/Public/www-style/2018May/0031.html
// Therefore the following logic only works on Firefox
const style = getComputedStyle(ancestor)
const whiteSpace = style?.getPropertyValue('white-space')
if (whiteSpace) {
// Returns false if white-space has a value not listed in COLLAPSING_WHITE_SPACE_VALUES
return COLLAPSING_WHITE_SPACE_VALUES.includes(whiteSpace)
}

// Check for `tagName` as fallback on Chrome
if (ancestor.tagName === 'PRE') {
return false
}
ancestor = ancestor.parentElement
}
return true
}
73 changes: 73 additions & 0 deletions src/tests/extensions/transformPastedHTML.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import transformPastedHTML from './../../extensions/transformPastedHTML.js'

describe('transformPastedHTML', () => {

// alias so the strings line up nicely
const tr = transformPastedHTML

it('strips newlines from input', () => {
expect(tr('a\nb\n\nc'))
.toBe('a b c')
})

it('strips newlines from input', () => {
expect(tr('a\nb\n\nc'))
.toBe('a b c')
})

it('strips newlines from tags', () => {
expect(tr('<p>a\nb</p>'))
.toBe('<p>a b</p>')
})

it('preserve newlines in pre tags', () => {
expect(tr('<pre>a\nb</pre>'))
.toBe('<pre>a\nb</pre>')
})

it('strips newlines in tags with white-space: normal', () => {
expect(tr('<div style="white-space: normal;">a\nb</div>'))
.toBe('<div style="white-space: normal;">a b</div>')
})

it('strips newlines in tags with white-space: nowrap', () => {
expect(tr('<div style="white-space: nowrap;">a\nb</div>'))
.toBe('<div style="white-space: nowrap;">a b</div>')
})

it('preserves newlines in tags with white-space: pre', () => {
expect(tr('<div style="white-space: pre;">a\nb</div>'))
.toBe('<div style="white-space: pre;">a\nb</div>')
})

it('preserve newlines in tags with white-space: pre-wrap', () => {
expect(tr('<div style="white-space: pre-wrap;">a\nb</div>'))
.toBe('<div style="white-space: pre-wrap;">a\nb</div>')
})

it('preserve newlines in tags with white-space: pre-line', () => {
expect(tr('<div style="white-space: pre-line;">a\nb</div>'))
.toBe('<div style="white-space: pre-line;">a\nb</div>')
})

it('preserve newlines in tags with white-space: break-spaces', () => {
expect(tr('<div style="white-space: break-spaces;">a\nb</div>'))
.toBe('<div style="white-space: break-spaces;">a\nb</div>')
})

it('handles different tags', () => {
expect(tr('<pre>a\nb</pre><p>c\nd</p>'))
.toBe('<pre>a\nb</pre><p>c d</p>')
})

it('preserve newlines in nested code blocks', () => {
expect(tr('<pre><code>this\nis code\nplease preserve\n whitespace!\nThanks</code></pre>'))
.toBe('<pre><code>this\nis code\nplease preserve\n whitespace!\nThanks</code></pre>')
})

it('preserve newlines in deep nested code blocks', () => {
expect(tr('<pre><code><em>this\nis code</em>\nplease preserve\n whitespace!\nThanks</code></pre>'))
.toBe('<pre><code><em>this\nis code</em>\nplease preserve\n whitespace!\nThanks</code></pre>')
})

})

0 comments on commit 72e9537

Please sign in to comment.