@contember/react-slate-editor-base 2.2.0-alpha.3 → 2.2.0-alpha.5

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.
Files changed (91) hide show
  1. package/dist/development/src/plugins/behaviour/paste/HtmlDeserializer.cjs +35 -2
  2. package/dist/development/src/plugins/behaviour/paste/HtmlDeserializer.cjs.map +1 -1
  3. package/dist/development/src/plugins/behaviour/paste/HtmlDeserializer.js +35 -2
  4. package/dist/development/src/plugins/behaviour/paste/HtmlDeserializer.js.map +1 -1
  5. package/dist/production/src/plugins/behaviour/paste/HtmlDeserializer.cjs +35 -2
  6. package/dist/production/src/plugins/behaviour/paste/HtmlDeserializer.cjs.map +1 -1
  7. package/dist/production/src/plugins/behaviour/paste/HtmlDeserializer.js +35 -2
  8. package/dist/production/src/plugins/behaviour/paste/HtmlDeserializer.js.map +1 -1
  9. package/dist/types/components/triggers/EditorElementTrigger.d.ts.map +1 -1
  10. package/dist/types/components/triggers/EditorGenericTrigger.d.ts.map +1 -1
  11. package/dist/types/components/triggers/EditorMarkTrigger.d.ts.map +1 -1
  12. package/dist/types/components/triggers/EditorWrapNodeTrigger.d.ts.map +1 -1
  13. package/dist/types/editor/createEditor.d.ts.map +1 -1
  14. package/dist/types/editor/createEditorWithEssentials.d.ts.map +1 -1
  15. package/dist/types/editor/index.d.ts +2 -2
  16. package/dist/types/editor/index.d.ts.map +1 -1
  17. package/dist/types/editor/methods/addMarks.d.ts.map +1 -1
  18. package/dist/types/editor/methods/canToggleMark.d.ts.map +1 -1
  19. package/dist/types/editor/methods/closest.d.ts.map +1 -1
  20. package/dist/types/editor/methods/closestBlockEntry.d.ts.map +1 -1
  21. package/dist/types/editor/methods/closestViableBlockContainerEntry.d.ts.map +1 -1
  22. package/dist/types/editor/methods/ejectElement.d.ts.map +1 -1
  23. package/dist/types/editor/methods/elementToSpecifics.d.ts.map +1 -1
  24. package/dist/types/editor/methods/getElementDataAttributes.d.ts.map +1 -1
  25. package/dist/types/editor/methods/getPreviousSibling.d.ts.map +1 -1
  26. package/dist/types/editor/methods/hasMarks.d.ts.map +1 -1
  27. package/dist/types/editor/methods/hasParentOfType.d.ts +1 -1
  28. package/dist/types/editor/methods/hasParentOfType.d.ts.map +1 -1
  29. package/dist/types/editor/methods/isElementType.d.ts +1 -1
  30. package/dist/types/editor/methods/isElementType.d.ts.map +1 -1
  31. package/dist/types/editor/methods/permissivelyDeserializeNodes.d.ts.map +1 -1
  32. package/dist/types/editor/methods/removeMarks.d.ts.map +1 -1
  33. package/dist/types/editor/methods/serializeNodes.d.ts.map +1 -1
  34. package/dist/types/editor/methods/strictlyDeserializeNodes.d.ts.map +1 -1
  35. package/dist/types/editor/methods/textToSpecifics.d.ts.map +1 -1
  36. package/dist/types/editor/methods/toLatestFormat.d.ts.map +1 -1
  37. package/dist/types/editor/methods/topLevelNodes.d.ts.map +1 -1
  38. package/dist/types/internal/hooks/useRichTextFieldNodes.d.ts.map +1 -1
  39. package/dist/types/internal/utils/EditorPasteUtils.d.ts.map +1 -1
  40. package/dist/types/internal/utils/parseIframeSrc.d.ts.map +1 -1
  41. package/dist/types/internal/utils/parseUrl.d.ts.map +1 -1
  42. package/dist/types/plugins/attributes/alignment/alignButton.d.ts.map +1 -1
  43. package/dist/types/plugins/behaviour/paste/HtmlDeserializer.d.ts +1 -0
  44. package/dist/types/plugins/behaviour/paste/HtmlDeserializer.d.ts.map +1 -1
  45. package/dist/types/plugins/behaviour/paste/createMarkHtmlDeserializer.d.ts.map +1 -1
  46. package/dist/types/plugins/behaviour/paste/withPaste.d.ts.map +1 -1
  47. package/dist/types/plugins/element/anchors/AnchorElement.d.ts +1 -1
  48. package/dist/types/plugins/element/anchors/AnchorElement.d.ts.map +1 -1
  49. package/dist/types/plugins/element/anchors/AnchorModifications.d.ts.map +1 -1
  50. package/dist/types/plugins/element/anchors/withAnchors.d.ts.map +1 -1
  51. package/dist/types/plugins/element/headings/HeadingElement.d.ts +1 -1
  52. package/dist/types/plugins/element/headings/HeadingElement.d.ts.map +1 -1
  53. package/dist/types/plugins/element/headings/withHeadings.d.ts.map +1 -1
  54. package/dist/types/plugins/element/horizonalRules/HorizontalRuleElement.d.ts +1 -1
  55. package/dist/types/plugins/element/horizonalRules/HorizontalRuleElement.d.ts.map +1 -1
  56. package/dist/types/plugins/element/horizonalRules/withHorizontalRules.d.ts.map +1 -1
  57. package/dist/types/plugins/element/lists/ListElement.d.ts.map +1 -1
  58. package/dist/types/plugins/element/lists/ListHtmlDeserializer.d.ts.map +1 -1
  59. package/dist/types/plugins/element/lists/ListItemElement.d.ts +1 -1
  60. package/dist/types/plugins/element/lists/ListItemElement.d.ts.map +1 -1
  61. package/dist/types/plugins/element/lists/OrderedListElement.d.ts +1 -1
  62. package/dist/types/plugins/element/lists/OrderedListElement.d.ts.map +1 -1
  63. package/dist/types/plugins/element/lists/UnorderedListElement.d.ts +1 -1
  64. package/dist/types/plugins/element/lists/UnorderedListElement.d.ts.map +1 -1
  65. package/dist/types/plugins/element/lists/transforms/dedentListItem.d.ts.map +1 -1
  66. package/dist/types/plugins/element/lists/transforms/indentListItem.d.ts.map +1 -1
  67. package/dist/types/plugins/element/lists/transforms/normalizeListElement.d.ts.map +1 -1
  68. package/dist/types/plugins/element/lists/transforms/toggleListElement.d.ts +1 -1
  69. package/dist/types/plugins/element/lists/transforms/toggleListElement.d.ts.map +1 -1
  70. package/dist/types/plugins/element/lists/withLists.d.ts.map +1 -1
  71. package/dist/types/plugins/element/paragraphs/ParagraphElement.d.ts +1 -1
  72. package/dist/types/plugins/element/paragraphs/ParagraphElement.d.ts.map +1 -1
  73. package/dist/types/plugins/element/paragraphs/withParagraphs.d.ts.map +1 -1
  74. package/dist/types/plugins/element/scrollTargets/ScrollTargetElement.d.ts +1 -1
  75. package/dist/types/plugins/element/scrollTargets/ScrollTargetElement.d.ts.map +1 -1
  76. package/dist/types/plugins/element/scrollTargets/withScrollTargets.d.ts.map +1 -1
  77. package/dist/types/plugins/element/tables/TableCellElement.d.ts +1 -1
  78. package/dist/types/plugins/element/tables/TableCellElement.d.ts.map +1 -1
  79. package/dist/types/plugins/element/tables/TableElement.d.ts +1 -1
  80. package/dist/types/plugins/element/tables/TableElement.d.ts.map +1 -1
  81. package/dist/types/plugins/element/tables/TableElementSelection.d.ts.map +1 -1
  82. package/dist/types/plugins/element/tables/TableModifications.d.ts.map +1 -1
  83. package/dist/types/plugins/element/tables/TableRowElement.d.ts +1 -1
  84. package/dist/types/plugins/element/tables/TableRowElement.d.ts.map +1 -1
  85. package/dist/types/plugins/element/tables/gaugeTableColumnCount.d.ts.map +1 -1
  86. package/dist/types/plugins/element/tables/withTables.d.ts.map +1 -1
  87. package/dist/types/tsconfig.tsbuildinfo +1 -1
  88. package/package.json +3 -3
  89. package/src/plugins/behaviour/paste/HtmlDeserializer.ts +35 -2
  90. package/tests/cases/htmlDeserializer.test.ts +79 -0
  91. 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.2.0-alpha.3",
4
+ "version": "2.2.0-alpha.5",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "main": "./dist/production/index.js",
@@ -34,8 +34,8 @@
34
34
  "directory": "packages/react-slate-editor-base"
35
35
  },
36
36
  "dependencies": {
37
- "@contember/react-binding": "2.2.0-alpha.3",
38
- "@contember/utilities": "2.2.0-alpha.3",
37
+ "@contember/react-binding": "2.2.0-alpha.5",
38
+ "@contember/utilities": "2.2.0-alpha.5",
39
39
  "@radix-ui/primitive": "^1.1.3",
40
40
  "@radix-ui/react-slot": "^1.2.4",
41
41
  "is-hotkey": "^0.2.0",
@@ -82,7 +82,7 @@ export class HtmlDeserializer {
82
82
  return {
83
83
  elements: processed.flatMap(item => {
84
84
  if (item.text !== undefined) {
85
- return item.isWhiteSpace ? [] : [this.createDefaultElement([item.text])]
85
+ return item.isWhiteSpace ? [] : [this.createDefaultElement(this.trimBlockBoundaryWhitespace([item.text]))]
86
86
  } else if (item.element !== undefined) {
87
87
  return [item.element]
88
88
  }
@@ -108,7 +108,40 @@ export class HtmlDeserializer {
108
108
 
109
109
  deserializeBlocks(list: Node[], cumulativeTextAttrs: TextAttrs): Descendant[] {
110
110
  const result = this.processNodeListPaste(list, cumulativeTextAttrs)
111
- return result?.texts ?? result?.elements ?? []
111
+ if (result?.texts !== undefined) {
112
+ return this.trimBlockBoundaryWhitespace(result.texts)
113
+ }
114
+ return result?.elements ?? []
115
+ }
116
+
117
+ // Collapsible whitespace at the start/end of a block renders as nothing in HTML,
118
+ // so strip it from the edges of a block's inline content. Non-breaking spaces are kept.
119
+ private trimBlockBoundaryWhitespace(texts: Descendant[]): Descendant[] {
120
+ const trimmed = [...texts]
121
+ const trimEdge = (direction: 'start' | 'end') => {
122
+ for (let i = direction === 'start' ? 0 : trimmed.length - 1; i >= 0 && i < trimmed.length;) {
123
+ const node = trimmed[i]
124
+ if (!SlateText.isText(node)) {
125
+ return
126
+ }
127
+ const text = direction === 'start' ? node.text.replace(/^[ \t\r\n]+/, '') : node.text.replace(/[ \t\r\n]+$/, '')
128
+ if (text !== '') {
129
+ trimmed[i] = { ...node, text }
130
+ return
131
+ }
132
+ if (trimmed.length === 1) {
133
+ trimmed[i] = { ...node, text }
134
+ return
135
+ }
136
+ trimmed.splice(i, 1)
137
+ if (direction === 'end') {
138
+ i--
139
+ }
140
+ }
141
+ }
142
+ trimEdge('start')
143
+ trimEdge('end')
144
+ return trimmed
112
145
  }
113
146
 
114
147
  private deserializeTextNode(node: Node, cumulativeTextAttrs: TextAttrs): Descendant[] | null {
@@ -0,0 +1,79 @@
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.js'
4
+ import { paragraphHtmlDeserializer } from '../../src/plugins/element/paragraphs/ParagraphHtmlDeserializer.js'
5
+
6
+ const paragraph = (children: Descendant[]): SlateElement => ({ type: 'paragraph', children } as SlateElement)
7
+
8
+ const createDeserializer = () =>
9
+ new HtmlDeserializer(
10
+ children => paragraph(children),
11
+ [paragraphHtmlDeserializer],
12
+ )
13
+
14
+ const deserialize = (html: string) => {
15
+ const document = new DOMParser().parseFromString(html, 'text/html')
16
+ return createDeserializer().deserializeBlocks(Array.from(document.body.childNodes), {})
17
+ }
18
+
19
+ const childrenText = (node: unknown): string => {
20
+ if (SlateText.isText(node)) {
21
+ return node.text
22
+ }
23
+ return (node as SlateElement).children.map(childrenText).join('')
24
+ }
25
+
26
+ describe('html deserializer', () => {
27
+ it('trims block boundary whitespace in LibreOffice paste', () => {
28
+ const html = '<p style="line-height: 100%; margin-bottom: 0cm">\nFirst.</p>\n'
29
+ + '<p style="line-height: 100%; margin-bottom: 0cm"><br>\n\n</p>\n'
30
+ + '<p style="line-height: 100%; margin-bottom: 0cm">Second.</p>'
31
+ expect(deserialize(html)).toEqual([
32
+ paragraph([{ text: 'First.' }]),
33
+ paragraph([{ text: '' }]),
34
+ paragraph([{ text: 'Second.' }]),
35
+ ])
36
+ })
37
+
38
+ it('trims leading and trailing whitespace inside a paragraph', () => {
39
+ expect(deserialize('<p>\n\tfoo\n</p>')).toEqual([
40
+ paragraph([{ text: 'foo' }]),
41
+ ])
42
+ })
43
+
44
+ it('preserves inner whitespace around inline elements', () => {
45
+ const result = deserialize('<p>foo <span>bar</span> baz</p>')
46
+ expect(result).toHaveLength(1)
47
+ expect(childrenText(result[0])).toBe('foo bar baz')
48
+ expect((result[0] as SlateElement).children).toEqual([
49
+ { text: 'foo ' },
50
+ { text: 'bar' },
51
+ { text: ' baz' },
52
+ ])
53
+ })
54
+
55
+ it('maps <br> to a single space and keeps a lone <br> paragraph', () => {
56
+ const result = deserialize('<p>foo<br>bar</p>')
57
+ expect(result).toHaveLength(1)
58
+ expect(childrenText(result[0])).toBe('foo bar')
59
+
60
+ expect(deserialize('<p><br></p>')).toEqual([
61
+ paragraph([{ text: '' }]),
62
+ ])
63
+ })
64
+
65
+ it('keeps non-breaking space at block edge', () => {
66
+ const result = deserialize('<p>&nbsp;foo</p>')
67
+ expect(result).toHaveLength(1)
68
+ expect(childrenText(result[0])).toBe(' foo')
69
+ })
70
+
71
+ it('does not produce extra elements from whitespace between paragraphs', () => {
72
+ const result = deserialize('<p>a</p>\n<p>b</p>')
73
+ expect(result).toHaveLength(2)
74
+ expect(result).toEqual([
75
+ paragraph([{ text: 'a' }]),
76
+ paragraph([{ text: 'b' }]),
77
+ ])
78
+ })
79
+ })
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../../tsconfig.settings.json",
3
+ "compilerOptions": {
4
+ "noEmit": true,
5
+ "emitDeclarationOnly": false
6
+ },
7
+ "references": [
8
+ { "path": "../src" },
9
+ ]
10
+ }