@contember/react-slate-editor-base 2.0.4 → 2.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/development/src/plugins/behaviour/paste/HtmlDeserializer.cjs +35 -2
- package/dist/development/src/plugins/behaviour/paste/HtmlDeserializer.cjs.map +1 -1
- package/dist/development/src/plugins/behaviour/paste/HtmlDeserializer.js +35 -2
- package/dist/development/src/plugins/behaviour/paste/HtmlDeserializer.js.map +1 -1
- package/dist/production/src/plugins/behaviour/paste/HtmlDeserializer.cjs +35 -2
- package/dist/production/src/plugins/behaviour/paste/HtmlDeserializer.cjs.map +1 -1
- package/dist/production/src/plugins/behaviour/paste/HtmlDeserializer.js +35 -2
- package/dist/production/src/plugins/behaviour/paste/HtmlDeserializer.js.map +1 -1
- package/dist/types/plugins/behaviour/paste/HtmlDeserializer.d.ts +1 -0
- package/dist/types/plugins/behaviour/paste/HtmlDeserializer.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/plugins/behaviour/paste/HtmlDeserializer.ts +35 -2
- package/tests/cases/htmlDeserializer.test.ts +78 -0
- package/tests/tsconfig.json +10 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contember/react-slate-editor-base",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.7",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"main": "./dist/production/index.js",
|
|
@@ -30,12 +30,12 @@
|
|
|
30
30
|
},
|
|
31
31
|
"repository": {
|
|
32
32
|
"type": "git",
|
|
33
|
-
"url": "https://github.com/contember/
|
|
33
|
+
"url": "git+https://github.com/contember/contember.git",
|
|
34
34
|
"directory": "packages/react-slate-editor-base"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@contember/react-binding": "2.0.
|
|
38
|
-
"@contember/utilities": "2.0.
|
|
37
|
+
"@contember/react-binding": "2.0.7",
|
|
38
|
+
"@contember/utilities": "2.0.7",
|
|
39
39
|
"@radix-ui/primitive": "^1.1.0",
|
|
40
40
|
"@radix-ui/react-slot": "^1.1.0",
|
|
41
41
|
"is-hotkey": "^0.2.0",
|
|
@@ -83,7 +83,7 @@ export class HtmlDeserializer {
|
|
|
83
83
|
return {
|
|
84
84
|
elements: processed.flatMap(item => {
|
|
85
85
|
if (item.text !== undefined) {
|
|
86
|
-
return item.isWhiteSpace ? [] : [this.createDefaultElement([item.text])]
|
|
86
|
+
return item.isWhiteSpace ? [] : [this.createDefaultElement(this.trimBlockBoundaryWhitespace([item.text]))]
|
|
87
87
|
} else if (item.element !== undefined) {
|
|
88
88
|
return [item.element]
|
|
89
89
|
}
|
|
@@ -111,7 +111,40 @@ export class HtmlDeserializer {
|
|
|
111
111
|
|
|
112
112
|
deserializeBlocks(list: Node[], cumulativeTextAttrs: TextAttrs): Descendant[] {
|
|
113
113
|
const result = this.processNodeListPaste(list, cumulativeTextAttrs)
|
|
114
|
-
|
|
114
|
+
if (result?.texts !== undefined) {
|
|
115
|
+
return this.trimBlockBoundaryWhitespace(result.texts)
|
|
116
|
+
}
|
|
117
|
+
return result?.elements ?? []
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Collapsible whitespace at the start/end of a block renders as nothing in HTML,
|
|
121
|
+
// so strip it from the edges of a block's inline content. Non-breaking spaces are kept.
|
|
122
|
+
private trimBlockBoundaryWhitespace(texts: Descendant[]): Descendant[] {
|
|
123
|
+
const trimmed = [...texts]
|
|
124
|
+
const trimEdge = (direction: 'start' | 'end') => {
|
|
125
|
+
for (let i = direction === 'start' ? 0 : trimmed.length - 1; i >= 0 && i < trimmed.length;) {
|
|
126
|
+
const node = trimmed[i]
|
|
127
|
+
if (!SlateText.isText(node)) {
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
const text = direction === 'start' ? node.text.replace(/^[ \t\r\n]+/, '') : node.text.replace(/[ \t\r\n]+$/, '')
|
|
131
|
+
if (text !== '') {
|
|
132
|
+
trimmed[i] = { ...node, text }
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
if (trimmed.length === 1) {
|
|
136
|
+
trimmed[i] = { ...node, text }
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
trimmed.splice(i, 1)
|
|
140
|
+
if (direction === 'end') {
|
|
141
|
+
i--
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
trimEdge('start')
|
|
146
|
+
trimEdge('end')
|
|
147
|
+
return trimmed
|
|
115
148
|
}
|
|
116
149
|
|
|
117
150
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { Descendant, Element as SlateElement, Text as SlateText } from 'slate'
|
|
3
|
+
import { HtmlDeserializer } from '../../src/plugins/behaviour/paste/HtmlDeserializer'
|
|
4
|
+
import { paragraphHtmlDeserializer } from '../../src/plugins/element/paragraphs/ParagraphHtmlDeserializer'
|
|
5
|
+
|
|
6
|
+
const paragraph = (children: Descendant[]): SlateElement => ({ type: 'paragraph', children } as SlateElement)
|
|
7
|
+
|
|
8
|
+
const createDeserializer = () => new HtmlDeserializer(
|
|
9
|
+
children => paragraph(children),
|
|
10
|
+
[paragraphHtmlDeserializer],
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
const deserialize = (html: string) => {
|
|
14
|
+
const document = new DOMParser().parseFromString(html, 'text/html')
|
|
15
|
+
return createDeserializer().deserializeBlocks(Array.from(document.body.childNodes), {})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const childrenText = (node: unknown): string => {
|
|
19
|
+
if (SlateText.isText(node)) {
|
|
20
|
+
return node.text
|
|
21
|
+
}
|
|
22
|
+
return (node as SlateElement).children.map(childrenText).join('')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('html deserializer', () => {
|
|
26
|
+
it('trims block boundary whitespace in LibreOffice paste', () => {
|
|
27
|
+
const html = '<p style="line-height: 100%; margin-bottom: 0cm">\nFirst.</p>\n'
|
|
28
|
+
+ '<p style="line-height: 100%; margin-bottom: 0cm"><br>\n\n</p>\n'
|
|
29
|
+
+ '<p style="line-height: 100%; margin-bottom: 0cm">Second.</p>'
|
|
30
|
+
expect(deserialize(html)).toEqual([
|
|
31
|
+
paragraph([{ text: 'First.' }]),
|
|
32
|
+
paragraph([{ text: '' }]),
|
|
33
|
+
paragraph([{ text: 'Second.' }]),
|
|
34
|
+
])
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('trims leading and trailing whitespace inside a paragraph', () => {
|
|
38
|
+
expect(deserialize('<p>\n\tfoo\n</p>')).toEqual([
|
|
39
|
+
paragraph([{ text: 'foo' }]),
|
|
40
|
+
])
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('preserves inner whitespace around inline elements', () => {
|
|
44
|
+
const result = deserialize('<p>foo <span>bar</span> baz</p>')
|
|
45
|
+
expect(result).toHaveLength(1)
|
|
46
|
+
expect(childrenText(result[0])).toBe('foo bar baz')
|
|
47
|
+
expect((result[0] as SlateElement).children).toEqual([
|
|
48
|
+
{ text: 'foo ' },
|
|
49
|
+
{ text: 'bar' },
|
|
50
|
+
{ text: ' baz' },
|
|
51
|
+
])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('maps <br> to a single space and keeps a lone <br> paragraph', () => {
|
|
55
|
+
const result = deserialize('<p>foo<br>bar</p>')
|
|
56
|
+
expect(result).toHaveLength(1)
|
|
57
|
+
expect(childrenText(result[0])).toBe('foo bar')
|
|
58
|
+
|
|
59
|
+
expect(deserialize('<p><br></p>')).toEqual([
|
|
60
|
+
paragraph([{ text: '' }]),
|
|
61
|
+
])
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('keeps non-breaking space at block edge', () => {
|
|
65
|
+
const result = deserialize('<p> foo</p>')
|
|
66
|
+
expect(result).toHaveLength(1)
|
|
67
|
+
expect(childrenText(result[0])).toBe(' foo')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('does not produce extra elements from whitespace between paragraphs', () => {
|
|
71
|
+
const result = deserialize('<p>a</p>\n<p>b</p>')
|
|
72
|
+
expect(result).toHaveLength(2)
|
|
73
|
+
expect(result).toEqual([
|
|
74
|
+
paragraph([{ text: 'a' }]),
|
|
75
|
+
paragraph([{ text: 'b' }]),
|
|
76
|
+
])
|
|
77
|
+
})
|
|
78
|
+
})
|