@ckeditor/ckeditor5-paste-from-office 48.2.0 → 48.3.0-alpha.1
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/augmentation.d.ts +8 -8
- package/dist/filters/bookmark.d.ts +10 -10
- package/dist/filters/br.d.ts +11 -11
- package/dist/filters/image.d.ts +19 -19
- package/dist/filters/list.d.ts +25 -25
- package/dist/filters/parse.d.ts +28 -28
- package/dist/filters/removeboldwrapper.d.ts +11 -11
- package/dist/filters/removegooglesheetstag.d.ts +11 -11
- package/dist/filters/removeinvalidtablewidth.d.ts +11 -11
- package/dist/filters/removemsattributes.d.ts +11 -11
- package/dist/filters/removestyleblock.d.ts +11 -11
- package/dist/filters/removexmlns.d.ts +11 -11
- package/dist/filters/replacemsfootnotes.d.ts +53 -50
- package/dist/filters/replacetabswithinprewithspaces.d.ts +20 -20
- package/dist/filters/space.d.ts +23 -23
- package/dist/filters/table.d.ts +12 -12
- package/dist/filters/utils.d.ts +17 -17
- package/dist/index-content.css +1 -0
- package/dist/index-editor.css +1 -0
- package/dist/index.css +0 -2
- package/dist/index.d.ts +25 -25
- package/dist/index.js +1372 -1663
- package/dist/index.js.map +1 -1
- package/dist/normalizer.d.ts +20 -20
- package/dist/normalizers/googledocsnormalizer.d.ts +27 -27
- package/dist/normalizers/googlesheetsnormalizer.d.ts +27 -27
- package/dist/normalizers/mswordnormalizer.d.ts +28 -28
- package/dist/pastefromoffice.d.ts +54 -54
- package/package.json +5 -5
- package/dist/index.css.map +0 -1
package/dist/index.js
CHANGED
|
@@ -2,1810 +2,1519 @@
|
|
|
2
2
|
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
3
|
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
4
4
|
*/
|
|
5
|
-
import { Plugin } from
|
|
6
|
-
import {
|
|
7
|
-
import { ClipboardPipeline } from
|
|
8
|
-
import {
|
|
5
|
+
import { Plugin } from "@ckeditor/ckeditor5-core";
|
|
6
|
+
import { insertToPriorityArray, priorities } from "@ckeditor/ckeditor5-utils";
|
|
7
|
+
import { ClipboardPipeline } from "@ckeditor/ckeditor5-clipboard";
|
|
8
|
+
import { Matcher, ViewDocument, ViewDomConverter, ViewUpcastWriter } from "@ckeditor/ckeditor5-engine";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (isHiddenBookmarkAnchor(element)) {
|
|
32
|
-
writer.remove(element);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
11
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
12
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Transforms `<a>` elements which are bookmarks by moving their children after the element.
|
|
16
|
+
*
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
function transformBookmarks(documentFragment, writer) {
|
|
20
|
+
const elementsToChange = [];
|
|
21
|
+
for (const value of writer.createRangeIn(documentFragment)) {
|
|
22
|
+
const element = value.item;
|
|
23
|
+
if (element.is("element", "a") && !element.hasAttribute("href") && (element.hasAttribute("id") || element.hasAttribute("name"))) elementsToChange.push(element);
|
|
24
|
+
}
|
|
25
|
+
for (const element of elementsToChange) {
|
|
26
|
+
const index = element.parent.getChildIndex(element) + 1;
|
|
27
|
+
const children = element.getChildren();
|
|
28
|
+
writer.insertChild(index, children, element.parent);
|
|
29
|
+
if (isHiddenBookmarkAnchor(element)) writer.remove(element);
|
|
30
|
+
}
|
|
35
31
|
}
|
|
36
32
|
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
33
|
+
* Checks whether the given element is a hidden or auto-generated bookmark anchor.
|
|
34
|
+
*
|
|
35
|
+
* Editors like MS Word and Google Docs use the `name` attribute (rather than `id`)
|
|
36
|
+
* for bookmarks. Furthermore, they reserve `_`-prefixed bookmark names for
|
|
37
|
+
* auto-generated anchors (e.g., Table of Contents or internal hyperlinks) and
|
|
38
|
+
* do not allow users to manually create custom bookmarks starting with an underscore.
|
|
39
|
+
*
|
|
40
|
+
* @param element The element to check.
|
|
41
|
+
* @returns True if the element is a hidden bookmark anchor, false otherwise.
|
|
42
|
+
*/
|
|
43
|
+
function isHiddenBookmarkAnchor(element) {
|
|
44
|
+
const name = element.getAttribute("name");
|
|
45
|
+
return !!name && name.startsWith("_");
|
|
49
46
|
}
|
|
50
47
|
|
|
51
48
|
/**
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
} else if (value.endsWith('cm')) {
|
|
72
|
-
// 1cm = 96px / 2.54
|
|
73
|
-
return toPx(numericValue * 96 / 2.54);
|
|
74
|
-
} else if (value.endsWith('mm')) {
|
|
75
|
-
// 1mm = 1cm / 10
|
|
76
|
-
return toPx(numericValue / 10 * 96 / 2.54);
|
|
77
|
-
}
|
|
78
|
-
return value;
|
|
49
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
50
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
51
|
+
*/
|
|
52
|
+
/**
|
|
53
|
+
* @module paste-from-office/filters/utils
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
56
|
+
* Normalizes CSS length value to 'px'.
|
|
57
|
+
*
|
|
58
|
+
* @internal
|
|
59
|
+
*/
|
|
60
|
+
function convertCssLengthToPx(value) {
|
|
61
|
+
const numericValue = parseFloat(value);
|
|
62
|
+
if (value.endsWith("pt")) return toPx(numericValue * 96 / 72);
|
|
63
|
+
else if (value.endsWith("pc")) return toPx(numericValue * 12 * 96 / 72);
|
|
64
|
+
else if (value.endsWith("in")) return toPx(numericValue * 96);
|
|
65
|
+
else if (value.endsWith("cm")) return toPx(numericValue * 96 / 2.54);
|
|
66
|
+
else if (value.endsWith("mm")) return toPx(numericValue / 10 * 96 / 2.54);
|
|
67
|
+
return value;
|
|
79
68
|
}
|
|
80
69
|
/**
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
70
|
+
* Returns true for value with 'px' unit.
|
|
71
|
+
*
|
|
72
|
+
* @internal
|
|
73
|
+
*/
|
|
74
|
+
function isPx(value) {
|
|
75
|
+
return value !== void 0 && value.endsWith("px");
|
|
86
76
|
}
|
|
87
77
|
/**
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
78
|
+
* Returns a rounded 'px' value.
|
|
79
|
+
*
|
|
80
|
+
* @internal
|
|
81
|
+
*/
|
|
82
|
+
function toPx(value) {
|
|
83
|
+
return Math.round(value) + "px";
|
|
93
84
|
}
|
|
94
85
|
|
|
95
86
|
/**
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
stack[indent] = {
|
|
233
|
-
...itemLikeElement,
|
|
234
|
-
listElement,
|
|
235
|
-
listItemElements: []
|
|
236
|
-
};
|
|
237
|
-
// Record the starting value for this list so that if it is interrupted and resumed later,
|
|
238
|
-
// the continuation list can pick up numbering from the right value.
|
|
239
|
-
// For a fresh list `listStyle.startIndex` is undefined, so we fall back to 1.
|
|
240
|
-
if (itemLikeElement.id !== undefined) {
|
|
241
|
-
if (!encounteredLists[indent]) {
|
|
242
|
-
encounteredLists[indent] = {};
|
|
243
|
-
}
|
|
244
|
-
encounteredLists[indent][originalListId] = listStyle.startIndex || 1;
|
|
245
|
-
}
|
|
246
|
-
} else if (stack[indent].isIntermediate) {
|
|
247
|
-
// Same type as the intermediate — reuse its `<ol>`/`<ul>`. The intermediate was created
|
|
248
|
-
// without `list-style-type`, `start`, or the `legal-list` class (the fill loop only sets
|
|
249
|
-
// the tag name); apply them now from the claiming item so the reused element no longer
|
|
250
|
-
// looks like a styleless wrapper.
|
|
251
|
-
applyListStyleToElement(stack[indent].listElement, listStyle, writer, hasMultiLevelListPlugin);
|
|
252
|
-
// Update the wrapper to represent the claiming item (id, marginLeft, …) so later lookups
|
|
253
|
-
// don't see stale data from the deep item that originally seeded the intermediate.
|
|
254
|
-
stack[indent] = {
|
|
255
|
-
...itemLikeElement,
|
|
256
|
-
listElement: stack[indent].listElement,
|
|
257
|
-
listItemElements: stack[indent].listItemElements
|
|
258
|
-
};
|
|
259
|
-
// Same as the create-new path: track this list so that if it is interrupted (e.g. by a
|
|
260
|
-
// multi-block paragraph matched against an ancestor frame) and later resumed via a fresh
|
|
261
|
-
// `<ol>`, the continuation can set the correct `start` attribute instead of restarting from 1.
|
|
262
|
-
if (itemLikeElement.id !== undefined) {
|
|
263
|
-
if (!encounteredLists[indent]) {
|
|
264
|
-
encounteredLists[indent] = {};
|
|
265
|
-
}
|
|
266
|
-
encounteredLists[indent][originalListId] = listStyle.startIndex || 1;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
// Use LI if it is already it or create a new LI element.
|
|
270
|
-
// https://github.com/ckeditor/ckeditor5/issues/15964
|
|
271
|
-
const listItem = itemLikeElement.element.name == 'li' ? itemLikeElement.element : writer.createElement('li');
|
|
272
|
-
applyListItemMarginLeftAndUpdateTopLevelInfo(writer, stack, topLevelListInfo, itemLikeElement, listItem, indent);
|
|
273
|
-
// Append the LI to OL/UL.
|
|
274
|
-
writer.appendChild(listItem, stack[indent].listElement);
|
|
275
|
-
stack[indent].listItemElements.push(listItem);
|
|
276
|
-
// Count the item so that `encounteredLists` always holds the value the *next* continuation list should start at.
|
|
277
|
-
if (itemLikeElement.id !== undefined && encounteredLists[indent]) {
|
|
278
|
-
encounteredLists[indent][originalListId]++;
|
|
279
|
-
}
|
|
280
|
-
// Append list block to LI.
|
|
281
|
-
if (itemLikeElement.element != listItem) {
|
|
282
|
-
writer.appendChild(itemLikeElement.element, listItem);
|
|
283
|
-
}
|
|
284
|
-
// Clean list block.
|
|
285
|
-
removeBulletElement(itemLikeElement.element, writer);
|
|
286
|
-
writer.removeStyle('text-indent', itemLikeElement.element); // #12361
|
|
287
|
-
writer.removeStyle('margin-left', itemLikeElement.element);
|
|
288
|
-
} else {
|
|
289
|
-
// Other blocks in a list item.
|
|
290
|
-
const stackItem = stack.find((stackItem)=>stackItem.marginLeft == itemLikeElement.marginLeft);
|
|
291
|
-
// A non-list block (e.g. a plain paragraph) whose margin-left matches one of the active list items.
|
|
292
|
-
// The match is done by margin-left value — nested list items sometimes have no explicit margin-left,
|
|
293
|
-
// so the match typically resolves to an ancestor <li> rather than the deepest one.
|
|
294
|
-
if (stackItem) {
|
|
295
|
-
const listItems = stackItem.listItemElements;
|
|
296
|
-
// Append block to LI.
|
|
297
|
-
writer.appendChild(itemLikeElement.element, listItems[listItems.length - 1]);
|
|
298
|
-
writer.removeStyle('margin-left', itemLikeElement.element);
|
|
299
|
-
// Trim the stack to the matched level. Without this, the next nested list item would
|
|
300
|
-
// be appended to the existing nested <ol>/<ul> that appears *before* this paragraph
|
|
301
|
-
// in the DOM, instead of creating a new one *after* it.
|
|
302
|
-
stack.length = stack.indexOf(stackItem) + 1;
|
|
303
|
-
// Clear counters only for levels deeper than the direct children of the matched <li>.
|
|
304
|
-
// The counter at `stack.length` must survive so the next nested list can continue
|
|
305
|
-
// numbering from where it left off (e.g. <ol start="3">).
|
|
306
|
-
encounteredLists.length = stack.length + 1;
|
|
307
|
-
} else {
|
|
308
|
-
stack.length = 0;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
applyIndentationToTopLevelList(writer, stack, topLevelListInfo);
|
|
87
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
88
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
89
|
+
*/
|
|
90
|
+
/**
|
|
91
|
+
* @module paste-from-office/filters/list
|
|
92
|
+
*/
|
|
93
|
+
/**
|
|
94
|
+
* Transforms Word specific list-like elements to the semantic HTML lists.
|
|
95
|
+
*
|
|
96
|
+
* Lists in Word are represented by block elements with special attributes like:
|
|
97
|
+
*
|
|
98
|
+
* ```xml
|
|
99
|
+
* <p class=MsoListParagraphCxSpFirst style='mso-list:l1 level1 lfo1'>...</p> // Paragraph based list.
|
|
100
|
+
* <h1 style='mso-list:l0 level1 lfo1'>...</h1> // Heading 1 based list.
|
|
101
|
+
* ```
|
|
102
|
+
*
|
|
103
|
+
* @param documentFragment The view structure to be transformed.
|
|
104
|
+
* @param stylesString Styles from which list-like elements styling will be extracted.
|
|
105
|
+
* @param hasMultiLevelListPlugin Whether the editor has the multi-level list plugin enabled.
|
|
106
|
+
* @param enableSkipLevelLists Whether to enable skip-level lists.
|
|
107
|
+
* @internal
|
|
108
|
+
*/
|
|
109
|
+
function transformListItemLikeElementsIntoLists(documentFragment, stylesString, hasMultiLevelListPlugin, enableSkipLevelLists = false) {
|
|
110
|
+
if (!documentFragment.childCount) return;
|
|
111
|
+
const writer = new ViewUpcastWriter(documentFragment.document);
|
|
112
|
+
const itemLikeElements = findAllItemLikeElements(documentFragment, writer);
|
|
113
|
+
if (!itemLikeElements.length) return;
|
|
114
|
+
const encounteredLists = [];
|
|
115
|
+
const stack = [];
|
|
116
|
+
let topLevelListInfo = createTopLevelListInfo();
|
|
117
|
+
for (const itemLikeElement of itemLikeElements) if (itemLikeElement.indent !== void 0) {
|
|
118
|
+
if (!isListContinuation(itemLikeElement)) {
|
|
119
|
+
applyIndentationToTopLevelList(writer, stack, topLevelListInfo);
|
|
120
|
+
topLevelListInfo = createTopLevelListInfo();
|
|
121
|
+
encounteredLists.length = 1;
|
|
122
|
+
stack.length = 0;
|
|
123
|
+
}
|
|
124
|
+
const originalListId = `${itemLikeElement.id}:${itemLikeElement.indent}`;
|
|
125
|
+
const indent = enableSkipLevelLists ? itemLikeElement.indent - 1 : Math.min(itemLikeElement.indent - 1, stack.length);
|
|
126
|
+
if (indent < stack.length && stack[indent].id !== itemLikeElement.id) {
|
|
127
|
+
if (indent == 0 && topLevelListInfo.canApplyMarginOnList && topLevelListInfo.topLevelListItemElements.length > 0) {
|
|
128
|
+
applyIndentationToTopLevelList(writer, stack, topLevelListInfo);
|
|
129
|
+
topLevelListInfo = createTopLevelListInfo();
|
|
130
|
+
}
|
|
131
|
+
encounteredLists.length = indent;
|
|
132
|
+
stack.length = indent;
|
|
133
|
+
}
|
|
134
|
+
if (indent < stack.length - 1) {
|
|
135
|
+
encounteredLists.length = indent + 1;
|
|
136
|
+
stack.length = indent + 1;
|
|
137
|
+
}
|
|
138
|
+
const listStyle = detectListStyle(itemLikeElement, stylesString);
|
|
139
|
+
while (stack.length < indent) {
|
|
140
|
+
const intermediateList = writer.createElement(listStyle.type);
|
|
141
|
+
const intermediateListItem = writer.createElement("li");
|
|
142
|
+
writer.setStyle("list-style-type", "none", intermediateListItem);
|
|
143
|
+
if (stack.length == 0) {
|
|
144
|
+
const parent = itemLikeElement.element.parent;
|
|
145
|
+
const index = parent.getChildIndex(itemLikeElement.element) + 1;
|
|
146
|
+
writer.insertChild(index, intermediateList, parent);
|
|
147
|
+
} else {
|
|
148
|
+
const parentListItems = stack[stack.length - 1].listItemElements;
|
|
149
|
+
writer.appendChild(intermediateList, parentListItems[parentListItems.length - 1]);
|
|
150
|
+
}
|
|
151
|
+
writer.appendChild(intermediateListItem, intermediateList);
|
|
152
|
+
stack.push({
|
|
153
|
+
...itemLikeElement,
|
|
154
|
+
listElement: intermediateList,
|
|
155
|
+
listItemElements: [intermediateListItem],
|
|
156
|
+
isIntermediate: true,
|
|
157
|
+
marginLeft: void 0
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (indent > stack.length - 1 || stack[indent].listElement.name != listStyle.type) {
|
|
161
|
+
if (listStyle.type == "ol" && itemLikeElement.id !== void 0 && encounteredLists[indent] && encounteredLists[indent][originalListId]) listStyle.startIndex = encounteredLists[indent][originalListId];
|
|
162
|
+
const listElement = createNewEmptyList(listStyle, writer, hasMultiLevelListPlugin);
|
|
163
|
+
if (stack.length == 0) {
|
|
164
|
+
const parent = itemLikeElement.element.parent;
|
|
165
|
+
const index = parent.getChildIndex(itemLikeElement.element) + 1;
|
|
166
|
+
writer.insertChild(index, listElement, parent);
|
|
167
|
+
} else if (indent == 0) {
|
|
168
|
+
const existingList = stack[0].listElement;
|
|
169
|
+
const listParent = existingList.parent;
|
|
170
|
+
const insertIndex = listParent.getChildIndex(existingList) + 1;
|
|
171
|
+
writer.insertChild(insertIndex, listElement, listParent);
|
|
172
|
+
} else {
|
|
173
|
+
const parentListItems = stack[indent - 1].listItemElements;
|
|
174
|
+
writer.appendChild(listElement, parentListItems[parentListItems.length - 1]);
|
|
175
|
+
}
|
|
176
|
+
stack[indent] = {
|
|
177
|
+
...itemLikeElement,
|
|
178
|
+
listElement,
|
|
179
|
+
listItemElements: []
|
|
180
|
+
};
|
|
181
|
+
if (itemLikeElement.id !== void 0) {
|
|
182
|
+
if (!encounteredLists[indent]) encounteredLists[indent] = {};
|
|
183
|
+
encounteredLists[indent][originalListId] = listStyle.startIndex || 1;
|
|
184
|
+
}
|
|
185
|
+
} else if (stack[indent].isIntermediate) {
|
|
186
|
+
applyListStyleToElement(stack[indent].listElement, listStyle, writer, hasMultiLevelListPlugin);
|
|
187
|
+
stack[indent] = {
|
|
188
|
+
...itemLikeElement,
|
|
189
|
+
listElement: stack[indent].listElement,
|
|
190
|
+
listItemElements: stack[indent].listItemElements
|
|
191
|
+
};
|
|
192
|
+
/* v8 ignore else -- @preserve */
|
|
193
|
+
if (itemLikeElement.id !== void 0) {
|
|
194
|
+
/* v8 ignore else -- @preserve */
|
|
195
|
+
if (!encounteredLists[indent]) encounteredLists[indent] = {};
|
|
196
|
+
encounteredLists[indent][originalListId] = listStyle.startIndex || 1;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const listItem = itemLikeElement.element.name == "li" ? itemLikeElement.element : writer.createElement("li");
|
|
200
|
+
applyListItemMarginLeftAndUpdateTopLevelInfo(writer, stack, topLevelListInfo, itemLikeElement, listItem, indent);
|
|
201
|
+
writer.appendChild(listItem, stack[indent].listElement);
|
|
202
|
+
stack[indent].listItemElements.push(listItem);
|
|
203
|
+
if (itemLikeElement.id !== void 0 && encounteredLists[indent]) encounteredLists[indent][originalListId]++;
|
|
204
|
+
if (itemLikeElement.element != listItem) writer.appendChild(itemLikeElement.element, listItem);
|
|
205
|
+
removeBulletElement(itemLikeElement.element, writer);
|
|
206
|
+
writer.removeStyle("text-indent", itemLikeElement.element);
|
|
207
|
+
writer.removeStyle("margin-left", itemLikeElement.element);
|
|
208
|
+
} else {
|
|
209
|
+
const stackItem = stack.find((stackItem) => stackItem.marginLeft == itemLikeElement.marginLeft);
|
|
210
|
+
if (stackItem) {
|
|
211
|
+
const listItems = stackItem.listItemElements;
|
|
212
|
+
writer.appendChild(itemLikeElement.element, listItems[listItems.length - 1]);
|
|
213
|
+
writer.removeStyle("margin-left", itemLikeElement.element);
|
|
214
|
+
stack.length = stack.indexOf(stackItem) + 1;
|
|
215
|
+
encounteredLists.length = stack.length + 1;
|
|
216
|
+
} else {
|
|
217
|
+
applyIndentationToTopLevelList(writer, stack, topLevelListInfo);
|
|
218
|
+
topLevelListInfo = createTopLevelListInfo();
|
|
219
|
+
stack.length = 0;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
applyIndentationToTopLevelList(writer, stack, topLevelListInfo);
|
|
313
223
|
}
|
|
314
224
|
function applyListItemMarginLeftAndUpdateTopLevelInfo(writer, stack, topLevelListInfo, itemLikeElement, listItem, indent) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
// So every nested list is indented by another 40px.
|
|
338
|
-
currentListBlockIndent += stack.length * 40;
|
|
339
|
-
// Calculate relative list item indentation to the list it is in.
|
|
340
|
-
const adjustedListItemIndent = listItemBlockMarginLeft - currentListBlockIndent;
|
|
341
|
-
const listItemBlockMarginLeftPx = adjustedListItemIndent !== 0 ? toPx(adjustedListItemIndent) : undefined;
|
|
342
|
-
if (listItemBlockMarginLeftPx) {
|
|
343
|
-
writer.setStyle('margin-left', listItemBlockMarginLeftPx, listItem);
|
|
344
|
-
if (indent == 0 && topLevelListInfo.canApplyMarginOnList) {
|
|
345
|
-
if (topLevelListInfo.marginLeft === undefined) {
|
|
346
|
-
topLevelListInfo.marginLeft = listItemBlockMarginLeftPx;
|
|
347
|
-
}
|
|
348
|
-
if (listItemBlockMarginLeftPx !== topLevelListInfo.marginLeft) {
|
|
349
|
-
topLevelListInfo.canApplyMarginOnList = false;
|
|
350
|
-
}
|
|
351
|
-
topLevelListInfo.topLevelListItemElements.push(listItem);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
225
|
+
if (itemLikeElement.marginLeft === void 0) {
|
|
226
|
+
if (indent == 0) topLevelListInfo.canApplyMarginOnList = false;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const listItemBlockMarginLeft = parseFloat(itemLikeElement.marginLeft);
|
|
230
|
+
let currentListBlockIndent = 0;
|
|
231
|
+
for (let ancestorIndex = 0; ancestorIndex < stack.length - 1; ancestorIndex++) {
|
|
232
|
+
const ancestorListItems = stack[ancestorIndex].listItemElements;
|
|
233
|
+
const ancestorMargin = ancestorListItems[ancestorListItems.length - 1].getStyle("margin-left");
|
|
234
|
+
if (ancestorMargin !== void 0) currentListBlockIndent += parseFloat(ancestorMargin);
|
|
235
|
+
}
|
|
236
|
+
currentListBlockIndent += stack.length * 40;
|
|
237
|
+
const adjustedListItemIndent = listItemBlockMarginLeft - currentListBlockIndent;
|
|
238
|
+
const listItemBlockMarginLeftPx = adjustedListItemIndent !== 0 ? toPx(adjustedListItemIndent) : void 0;
|
|
239
|
+
if (listItemBlockMarginLeftPx) {
|
|
240
|
+
writer.setStyle("margin-left", listItemBlockMarginLeftPx, listItem);
|
|
241
|
+
if (indent == 0 && topLevelListInfo.canApplyMarginOnList) {
|
|
242
|
+
if (topLevelListInfo.marginLeft === void 0) topLevelListInfo.marginLeft = listItemBlockMarginLeftPx;
|
|
243
|
+
if (listItemBlockMarginLeftPx !== topLevelListInfo.marginLeft) topLevelListInfo.canApplyMarginOnList = false;
|
|
244
|
+
topLevelListInfo.topLevelListItemElements.push(listItem);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
354
247
|
}
|
|
355
248
|
function createTopLevelListInfo() {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
249
|
+
return {
|
|
250
|
+
marginLeft: void 0,
|
|
251
|
+
canApplyMarginOnList: true,
|
|
252
|
+
topLevelListItemElements: []
|
|
253
|
+
};
|
|
361
254
|
}
|
|
362
255
|
/**
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
writer.removeStyle('margin-left', topLevelListItem);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
256
|
+
* Sets margin-left style to the top-level list if all its items have the same margin-left.
|
|
257
|
+
* If margin-left is set on the list, it is removed from all its items to avoid doubling of margins.
|
|
258
|
+
*/
|
|
259
|
+
function applyIndentationToTopLevelList(writer, stack, topLevelListInfo) {
|
|
260
|
+
if (topLevelListInfo.canApplyMarginOnList && topLevelListInfo.marginLeft && topLevelListInfo.topLevelListItemElements.length > 0) {
|
|
261
|
+
writer.setStyle("margin-left", topLevelListInfo.marginLeft, stack[0].listElement);
|
|
262
|
+
for (const topLevelListItem of topLevelListInfo.topLevelListItemElements) writer.removeStyle("margin-left", topLevelListItem);
|
|
263
|
+
}
|
|
374
264
|
}
|
|
375
265
|
/**
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
}
|
|
389
|
-
}
|
|
266
|
+
* Removes paragraph wrapping content inside a list item.
|
|
267
|
+
*
|
|
268
|
+
* @internal
|
|
269
|
+
*/
|
|
270
|
+
function unwrapParagraphInListItem(documentFragment, writer) {
|
|
271
|
+
for (const value of writer.createRangeIn(documentFragment)) {
|
|
272
|
+
const element = value.item;
|
|
273
|
+
if (element.is("element", "li")) {
|
|
274
|
+
const firstChild = element.getChild(0);
|
|
275
|
+
if (firstChild && firstChild.is("element", "p")) writer.unwrapElement(firstChild);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
390
278
|
}
|
|
391
279
|
/**
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
indent: itemData.indent,
|
|
420
|
-
marginLeft
|
|
421
|
-
});
|
|
422
|
-
if (marginLeft !== undefined) {
|
|
423
|
-
foundMargins.add(marginLeft);
|
|
424
|
-
}
|
|
425
|
-
} else {
|
|
426
|
-
foundMargins.clear();
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
return itemLikeElements;
|
|
280
|
+
* Finds all list-like elements in a given document fragment.
|
|
281
|
+
*
|
|
282
|
+
* @param documentFragment Document fragment in which to look for list-like nodes.
|
|
283
|
+
* @returns Array of found list-like items. Each item is an object containing
|
|
284
|
+
* @internal
|
|
285
|
+
*/
|
|
286
|
+
function findAllItemLikeElements(documentFragment, writer) {
|
|
287
|
+
const range = writer.createRangeIn(documentFragment);
|
|
288
|
+
const itemLikeElements = [];
|
|
289
|
+
const foundMargins = /* @__PURE__ */ new Set();
|
|
290
|
+
for (const item of range.getItems()) {
|
|
291
|
+
if (!item.is("element") || !item.name.match(/^(p|h\d+|li|div)$/)) continue;
|
|
292
|
+
let marginLeft = getMarginLeftNormalized(item);
|
|
293
|
+
if (marginLeft !== void 0 && parseFloat(marginLeft) == 0 && !Array.from(item.getClassNames()).find((className) => className.startsWith("MsoList"))) marginLeft = void 0;
|
|
294
|
+
if (item.hasStyle("mso-list") && item.getStyle("mso-list") !== "none" || marginLeft !== void 0 && foundMargins.has(marginLeft)) {
|
|
295
|
+
const itemData = getListItemData(item);
|
|
296
|
+
itemLikeElements.push({
|
|
297
|
+
element: item,
|
|
298
|
+
id: itemData.id,
|
|
299
|
+
order: itemData.order,
|
|
300
|
+
indent: itemData.indent,
|
|
301
|
+
marginLeft
|
|
302
|
+
});
|
|
303
|
+
if (marginLeft !== void 0) foundMargins.add(marginLeft);
|
|
304
|
+
} else foundMargins.clear();
|
|
305
|
+
}
|
|
306
|
+
return itemLikeElements;
|
|
430
307
|
}
|
|
431
308
|
/**
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
}
|
|
444
|
-
if (!previousSibling) {
|
|
445
|
-
const parent = currentItem.element.parent;
|
|
446
|
-
// If it's a li inside ul or ol like in here: https://github.com/ckeditor/ckeditor5/issues/15964.
|
|
447
|
-
// If the parent has previous sibling, which is not a list, then it is not a continuation.
|
|
448
|
-
return isList(parent) && (!parent.previousSibling || isList(parent.previousSibling));
|
|
449
|
-
}
|
|
450
|
-
// Even with the same id the list does not have to be continuous (https://github.com/ckeditor/ckeditor5/issues/43).
|
|
451
|
-
return isList(previousSibling);
|
|
309
|
+
* Whether the given element is possibly a list continuation. Previous element was wrapped into a list
|
|
310
|
+
* or the current element already is inside a list.
|
|
311
|
+
*/
|
|
312
|
+
function isListContinuation(currentItem) {
|
|
313
|
+
let previousSibling = currentItem.element.previousSibling;
|
|
314
|
+
while (previousSibling && isStrayInlineMarker(previousSibling)) previousSibling = previousSibling.previousSibling;
|
|
315
|
+
if (!previousSibling) {
|
|
316
|
+
const parent = currentItem.element.parent;
|
|
317
|
+
return isList(parent) && (!parent.previousSibling || isList(parent.previousSibling));
|
|
318
|
+
}
|
|
319
|
+
return isList(previousSibling);
|
|
452
320
|
}
|
|
453
321
|
/**
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
322
|
+
* True for empty inline elements Word emits as residue between paragraphs (`<span>`, `<a>`, `<o:p>`).
|
|
323
|
+
* Used by `isListContinuation` to look past these when checking whether the prior block is a list —
|
|
324
|
+
* they're layout artefacts, not real content.
|
|
325
|
+
*/
|
|
326
|
+
function isStrayInlineMarker(node) {
|
|
327
|
+
return node.is("element") && node.childCount === 0 && /^(?:span|a|o:p)$/.test(node.name);
|
|
459
328
|
}
|
|
460
329
|
function isList(element) {
|
|
461
|
-
|
|
330
|
+
return element.is("element", "ol") || element.is("element", "ul");
|
|
462
331
|
}
|
|
463
332
|
/**
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
if (islegalStyleList) {
|
|
527
|
-
type = 'ol';
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
return {
|
|
531
|
-
type,
|
|
532
|
-
startIndex,
|
|
533
|
-
style: mapListStyleDefinition(listStyleType),
|
|
534
|
-
isLegalStyleList: islegalStyleList
|
|
535
|
-
};
|
|
333
|
+
* Extracts list item style from the provided CSS.
|
|
334
|
+
*
|
|
335
|
+
* List item style is extracted from the CSS stylesheet. Each list with its specific style attribute
|
|
336
|
+
* value (`mso-list:l1 level1 lfo1`) has its dedicated properties in a CSS stylesheet defined with a selector like:
|
|
337
|
+
*
|
|
338
|
+
* ```css
|
|
339
|
+
* @list l1:level1 { ... }
|
|
340
|
+
* ```
|
|
341
|
+
*
|
|
342
|
+
* It contains `mso-level-number-format` property which defines list numbering/bullet style. If this property
|
|
343
|
+
* is not defined it means default `decimal` numbering.
|
|
344
|
+
*
|
|
345
|
+
* Here CSS string representation is used as `mso-level-number-format` property is an invalid CSS property
|
|
346
|
+
* and will be removed during CSS parsing.
|
|
347
|
+
*
|
|
348
|
+
* @param listLikeItem List-like item for which list style will be searched for. Usually
|
|
349
|
+
* a result of `findAllItemLikeElements()` function.
|
|
350
|
+
* @param stylesString CSS stylesheet.
|
|
351
|
+
* @returns An object with properties:
|
|
352
|
+
*
|
|
353
|
+
* * type - List type, could be `ul` or `ol`.
|
|
354
|
+
* * startIndex - List start index, valid only for ordered lists.
|
|
355
|
+
* * style - List style, for example: `decimal`, `lower-roman`, etc. It is extracted
|
|
356
|
+
* directly from Word stylesheet and adjusted to represent proper values for the CSS `list-style-type` property.
|
|
357
|
+
* If it cannot be adjusted, the `null` value is returned.
|
|
358
|
+
*/
|
|
359
|
+
function detectListStyle(listLikeItem, stylesString) {
|
|
360
|
+
const listStyleRegexp = new RegExp(`@list l${listLikeItem.id}:level${listLikeItem.indent}\\s*({[^}]*)`, "gi");
|
|
361
|
+
const listStyleTypeRegex = /mso-level-number-format:([^;]{0,100});/gi;
|
|
362
|
+
const listStartIndexRegex = /mso-level-start-at:\s{0,100}([0-9]{0,10})\s{0,100};/gi;
|
|
363
|
+
const legalStyleListRegex = new RegExp(`@list\\s+l${listLikeItem.id}:level\\d\\s*{[^{]*mso-level-text:"%\\d\\\\.`, "gi");
|
|
364
|
+
const multiLevelNumberFormatTypeRegex = new RegExp(`@list l${listLikeItem.id}:level\\d\\s*{[^{]*mso-level-number-format:`, "gi");
|
|
365
|
+
const legalStyleListMatch = legalStyleListRegex.exec(stylesString);
|
|
366
|
+
const multiLevelNumberFormatMatch = multiLevelNumberFormatTypeRegex.exec(stylesString);
|
|
367
|
+
const islegalStyleList = legalStyleListMatch && !multiLevelNumberFormatMatch;
|
|
368
|
+
const listStyleMatch = listStyleRegexp.exec(stylesString);
|
|
369
|
+
let listStyleType = "decimal";
|
|
370
|
+
let type = "ol";
|
|
371
|
+
let startIndex = null;
|
|
372
|
+
if (listStyleMatch && listStyleMatch[1]) {
|
|
373
|
+
const listStyleTypeMatch = listStyleTypeRegex.exec(listStyleMatch[1]);
|
|
374
|
+
if (listStyleTypeMatch && listStyleTypeMatch[1]) {
|
|
375
|
+
listStyleType = listStyleTypeMatch[1].trim();
|
|
376
|
+
type = listStyleType !== "bullet" && listStyleType !== "image" ? "ol" : "ul";
|
|
377
|
+
}
|
|
378
|
+
if (listStyleType === "bullet") {
|
|
379
|
+
const bulletedStyle = findBulletedListStyle(listLikeItem.element);
|
|
380
|
+
if (bulletedStyle) listStyleType = bulletedStyle;
|
|
381
|
+
} else {
|
|
382
|
+
const listStartIndexMatch = listStartIndexRegex.exec(listStyleMatch[1]);
|
|
383
|
+
if (listStartIndexMatch && listStartIndexMatch[1]) startIndex = parseInt(listStartIndexMatch[1]);
|
|
384
|
+
}
|
|
385
|
+
if (islegalStyleList) type = "ol";
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
type,
|
|
389
|
+
startIndex,
|
|
390
|
+
style: mapListStyleDefinition(listStyleType),
|
|
391
|
+
isLegalStyleList: islegalStyleList
|
|
392
|
+
};
|
|
536
393
|
}
|
|
537
394
|
/**
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
if (listMarker === 'o') {
|
|
550
|
-
return 'circle';
|
|
551
|
-
} else if (listMarker === '·') {
|
|
552
|
-
return 'disc';
|
|
553
|
-
} else if (listMarker === '§') {
|
|
554
|
-
return 'square';
|
|
555
|
-
}
|
|
556
|
-
return null;
|
|
395
|
+
* Tries to extract the `list-style-type` value based on the marker element for bulleted list.
|
|
396
|
+
*/
|
|
397
|
+
function findBulletedListStyle(element) {
|
|
398
|
+
if (element.name == "li" && element.parent.name == "ul" && element.parent.hasAttribute("type")) return element.parent.getAttribute("type");
|
|
399
|
+
const listMarkerElement = findListMarkerNode(element);
|
|
400
|
+
if (!listMarkerElement) return null;
|
|
401
|
+
const listMarker = listMarkerElement._data;
|
|
402
|
+
if (listMarker === "o") return "circle";
|
|
403
|
+
else if (listMarker === "·") return "disc";
|
|
404
|
+
else if (listMarker === "§") return "square";
|
|
405
|
+
return null;
|
|
557
406
|
}
|
|
558
407
|
/**
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
const textNodeOrElement = childNode.getChild(0);
|
|
573
|
-
if (!textNodeOrElement) {
|
|
574
|
-
continue;
|
|
575
|
-
}
|
|
576
|
-
// If already found the marker element, use it.
|
|
577
|
-
if (textNodeOrElement.is('$text')) {
|
|
578
|
-
return textNodeOrElement;
|
|
579
|
-
}
|
|
580
|
-
return textNodeOrElement.getChild(0);
|
|
581
|
-
}
|
|
582
|
-
/* istanbul ignore next -- @preserve */ return null;
|
|
408
|
+
* Tries to find a text node that represents the marker element (list-style-type).
|
|
409
|
+
*/
|
|
410
|
+
function findListMarkerNode(element) {
|
|
411
|
+
if (element.getChild(0).is("$text")) return null;
|
|
412
|
+
for (const childNode of element.getChildren()) {
|
|
413
|
+
if (!childNode.is("element", "span")) continue;
|
|
414
|
+
const textNodeOrElement = childNode.getChild(0);
|
|
415
|
+
if (!textNodeOrElement) continue;
|
|
416
|
+
if (textNodeOrElement.is("$text")) return textNodeOrElement;
|
|
417
|
+
return textNodeOrElement.getChild(0);
|
|
418
|
+
}
|
|
419
|
+
/* v8 ignore next -- @preserve */
|
|
420
|
+
return null;
|
|
583
421
|
}
|
|
584
422
|
/**
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
case 'circle':
|
|
600
|
-
case 'disc':
|
|
601
|
-
case 'square':
|
|
602
|
-
return value;
|
|
603
|
-
default:
|
|
604
|
-
return null;
|
|
605
|
-
}
|
|
423
|
+
* Parses the `list-style-type` value extracted directly from the Word CSS stylesheet and returns proper CSS definition.
|
|
424
|
+
*/
|
|
425
|
+
function mapListStyleDefinition(value) {
|
|
426
|
+
if (value.startsWith("arabic-leading-zero")) return "decimal-leading-zero";
|
|
427
|
+
switch (value) {
|
|
428
|
+
case "alpha-upper": return "upper-alpha";
|
|
429
|
+
case "alpha-lower": return "lower-alpha";
|
|
430
|
+
case "roman-upper": return "upper-roman";
|
|
431
|
+
case "roman-lower": return "lower-roman";
|
|
432
|
+
case "circle":
|
|
433
|
+
case "disc":
|
|
434
|
+
case "square": return value;
|
|
435
|
+
default: return null;
|
|
436
|
+
}
|
|
606
437
|
}
|
|
607
438
|
/**
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
439
|
+
* Creates a new list OL/UL element.
|
|
440
|
+
*/
|
|
441
|
+
function createNewEmptyList(listStyle, writer, hasMultiLevelListPlugin) {
|
|
442
|
+
const list = writer.createElement(listStyle.type);
|
|
443
|
+
applyListStyleToElement(list, listStyle, writer, hasMultiLevelListPlugin);
|
|
444
|
+
return list;
|
|
613
445
|
}
|
|
614
446
|
/**
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
}
|
|
624
|
-
if (listStyle.startIndex && listStyle.startIndex > 1) {
|
|
625
|
-
writer.setAttribute('start', listStyle.startIndex, list);
|
|
626
|
-
}
|
|
627
|
-
if (listStyle.isLegalStyleList && hasMultiLevelListPlugin) {
|
|
628
|
-
writer.addClass('legal-list', list);
|
|
629
|
-
}
|
|
447
|
+
* Applies `list-style-type`, `start`, and the `legal-list` class to a list element based on the detected
|
|
448
|
+
* list style. Used both when creating a fresh list and when a real item claims a previously-intermediate
|
|
449
|
+
* wrapper (which was created without any of these).
|
|
450
|
+
*/
|
|
451
|
+
function applyListStyleToElement(list, listStyle, writer, hasMultiLevelListPlugin) {
|
|
452
|
+
if (listStyle.style) writer.setStyle("list-style-type", listStyle.style, list);
|
|
453
|
+
if (listStyle.startIndex && listStyle.startIndex > 1) writer.setAttribute("start", listStyle.startIndex, list);
|
|
454
|
+
if (listStyle.isLegalStyleList && hasMultiLevelListPlugin) writer.addClass("legal-list", list);
|
|
630
455
|
}
|
|
631
456
|
/**
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
return {
|
|
663
|
-
indent: 1 // Handle empty mso-list style as a marked for default list item.
|
|
664
|
-
};
|
|
457
|
+
* Extracts list item information from Word specific list-like element style:
|
|
458
|
+
*
|
|
459
|
+
* ```
|
|
460
|
+
* `style="mso-list:l1 level1 lfo1"`
|
|
461
|
+
* ```
|
|
462
|
+
*
|
|
463
|
+
* where:
|
|
464
|
+
*
|
|
465
|
+
* ```
|
|
466
|
+
* * `l1` is a list id (however it does not mean this is a continuous list - see https://github.com/ckeditor/ckeditor5/issues/43),
|
|
467
|
+
* * `level1` is a list item indentation level,
|
|
468
|
+
* * `lfo1` is a list insertion order in a document.
|
|
469
|
+
* ```
|
|
470
|
+
*
|
|
471
|
+
* @param element Element from which style data is extracted.
|
|
472
|
+
*/
|
|
473
|
+
function getListItemData(element) {
|
|
474
|
+
const listStyle = element.getStyle("mso-list");
|
|
475
|
+
if (listStyle === void 0) return {};
|
|
476
|
+
const idMatch = listStyle.match(/(^|\s{1,100})l(\d+)/i);
|
|
477
|
+
const orderMatch = listStyle.match(/\s{0,100}lfo(\d+)/i);
|
|
478
|
+
const indentMatch = listStyle.match(/\s{0,100}level(\d+)/i);
|
|
479
|
+
if (idMatch && orderMatch && indentMatch) return {
|
|
480
|
+
id: idMatch[2],
|
|
481
|
+
order: orderMatch[1],
|
|
482
|
+
indent: parseInt(indentMatch[1])
|
|
483
|
+
};
|
|
484
|
+
return { indent: 1 };
|
|
665
485
|
}
|
|
666
486
|
/**
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
const range = writer.createRangeIn(element);
|
|
677
|
-
for (const value of range){
|
|
678
|
-
if (value.type === 'elementStart' && bulletMatcher.match(value.item)) {
|
|
679
|
-
writer.remove(value.item);
|
|
680
|
-
}
|
|
681
|
-
}
|
|
487
|
+
* Removes span with a numbering/bullet from a given element.
|
|
488
|
+
*/
|
|
489
|
+
function removeBulletElement(element, writer) {
|
|
490
|
+
const bulletMatcher = new Matcher({
|
|
491
|
+
name: "span",
|
|
492
|
+
styles: { "mso-list": "Ignore" }
|
|
493
|
+
});
|
|
494
|
+
const range = writer.createRangeIn(element);
|
|
495
|
+
for (const value of range) if (value.type === "elementStart" && bulletMatcher.match(value.item)) writer.remove(value.item);
|
|
682
496
|
}
|
|
683
497
|
/**
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
return convertCssLengthToPx(value);
|
|
498
|
+
* Returns element left margin normalized to 'px' if possible.
|
|
499
|
+
*/
|
|
500
|
+
function getMarginLeftNormalized(element) {
|
|
501
|
+
const value = element.getStyle("margin-left");
|
|
502
|
+
if (value === void 0 || value.endsWith("px")) return value;
|
|
503
|
+
return convertCssLengthToPx(value);
|
|
691
504
|
}
|
|
692
505
|
|
|
693
506
|
/**
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
507
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
508
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
509
|
+
*/
|
|
510
|
+
/**
|
|
511
|
+
* @module paste-from-office/filters/image
|
|
512
|
+
*/
|
|
513
|
+
/**
|
|
514
|
+
* Replaces source attribute of all `<img>` elements representing regular
|
|
515
|
+
* images (not the Word shapes) with inlined base64 image representation extracted from RTF or Blob data.
|
|
516
|
+
*
|
|
517
|
+
* @param documentFragment Document fragment on which transform images.
|
|
518
|
+
* @param rtfData The RTF data from which images representation will be used.
|
|
519
|
+
* @internal
|
|
520
|
+
*/
|
|
521
|
+
function replaceImagesSourceWithBase64(documentFragment, rtfData) {
|
|
522
|
+
if (!documentFragment.childCount) return;
|
|
523
|
+
const upcastWriter = new ViewUpcastWriter(documentFragment.document);
|
|
524
|
+
const shapesIds = findAllShapesIds(documentFragment, upcastWriter);
|
|
525
|
+
removeAllImgElementsRepresentingShapes(shapesIds, documentFragment, upcastWriter);
|
|
526
|
+
insertMissingImgs(shapesIds, documentFragment, upcastWriter);
|
|
527
|
+
removeAllShapeElements(documentFragment, upcastWriter);
|
|
528
|
+
const images = findAllImageElementsWithLocalSource(documentFragment, upcastWriter);
|
|
529
|
+
if (images.length) replaceImagesFileSourceWithInlineRepresentation(images, extractImageDataFromRtf(rtfData), upcastWriter);
|
|
713
530
|
}
|
|
714
531
|
/**
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
532
|
+
* Converts given HEX string to base64 representation.
|
|
533
|
+
*
|
|
534
|
+
* @internal
|
|
535
|
+
* @param hexString The HEX string to be converted.
|
|
536
|
+
* @returns Base64 representation of a given HEX string.
|
|
537
|
+
*/
|
|
538
|
+
function _convertHexToBase64(hexString) {
|
|
539
|
+
return btoa(hexString.match(/\w{2}/g).map((char) => {
|
|
540
|
+
return String.fromCharCode(parseInt(char, 16));
|
|
541
|
+
}).join(""));
|
|
724
542
|
}
|
|
725
543
|
/**
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
const isElementAShape = shapeElementsMatcher.match(el);
|
|
750
|
-
const hasElementGfxdataAttribute = el.getAttribute('o:gfxdata');
|
|
751
|
-
const isPreviousSiblingAShapeType = prevSiblingName === 'v:shapetype';
|
|
752
|
-
const isElementIdInExceptionsArray = hasElementGfxdataAttribute && exceptionIds.some((item)=>el.getAttribute('id').includes(item));
|
|
753
|
-
// If shape element has 'o:gfxdata' attribute and is not directly before
|
|
754
|
-
// `<v:shapetype>` element it means that it represents a Word shape.
|
|
755
|
-
if (isElementAShape && hasElementGfxdataAttribute && !isPreviousSiblingAShapeType && !isElementIdInExceptionsArray) {
|
|
756
|
-
shapesIds.push(value.item.getAttribute('id'));
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
return shapesIds;
|
|
544
|
+
* Finds all shapes (`<v:*>...</v:*>`) ids. Shapes can represent images (canvas)
|
|
545
|
+
* or Word shapes (which does not have RTF or Blob representation).
|
|
546
|
+
*
|
|
547
|
+
* @param documentFragment Document fragment from which to extract shape ids.
|
|
548
|
+
* @returns Array of shape ids.
|
|
549
|
+
*/
|
|
550
|
+
function findAllShapesIds(documentFragment, writer) {
|
|
551
|
+
const range = writer.createRangeIn(documentFragment);
|
|
552
|
+
const shapeElementsMatcher = new Matcher({ name: /v:(.+)/ });
|
|
553
|
+
const shapesIds = [];
|
|
554
|
+
for (const value of range) {
|
|
555
|
+
if (value.type != "elementStart") continue;
|
|
556
|
+
const el = value.item;
|
|
557
|
+
const previousSibling = el.previousSibling;
|
|
558
|
+
const prevSiblingName = previousSibling && previousSibling.is("element") ? previousSibling.name : null;
|
|
559
|
+
const exceptionIds = ["Chart"];
|
|
560
|
+
const isElementAShape = shapeElementsMatcher.match(el);
|
|
561
|
+
const hasElementGfxdataAttribute = el.getAttribute("o:gfxdata");
|
|
562
|
+
const isPreviousSiblingAShapeType = prevSiblingName === "v:shapetype";
|
|
563
|
+
const isElementIdInExceptionsArray = hasElementGfxdataAttribute && exceptionIds.some((item) => el.getAttribute("id").includes(item));
|
|
564
|
+
if (isElementAShape && hasElementGfxdataAttribute && !isPreviousSiblingAShapeType && !isElementIdInExceptionsArray) shapesIds.push(value.item.getAttribute("id"));
|
|
565
|
+
}
|
|
566
|
+
return shapesIds;
|
|
760
567
|
}
|
|
761
568
|
/**
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
// Shapes may also have empty source while content is paste in some browsers (Safari).
|
|
779
|
-
} else if (!el.getAttribute('src')) {
|
|
780
|
-
imgs.push(el);
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
for (const img of imgs){
|
|
785
|
-
writer.remove(img);
|
|
786
|
-
}
|
|
569
|
+
* Removes all `<img>` elements which represents Word shapes and not regular images.
|
|
570
|
+
*
|
|
571
|
+
* @param shapesIds Shape ids which will be checked against `<img>` elements.
|
|
572
|
+
* @param documentFragment Document fragment from which to remove `<img>` elements.
|
|
573
|
+
*/
|
|
574
|
+
function removeAllImgElementsRepresentingShapes(shapesIds, documentFragment, writer) {
|
|
575
|
+
const range = writer.createRangeIn(documentFragment);
|
|
576
|
+
const imageElementsMatcher = new Matcher({ name: "img" });
|
|
577
|
+
const imgs = [];
|
|
578
|
+
for (const value of range) if (value.item.is("element") && imageElementsMatcher.match(value.item)) {
|
|
579
|
+
const el = value.item;
|
|
580
|
+
const shapes = el.getAttribute("v:shapes") ? el.getAttribute("v:shapes").split(" ") : [];
|
|
581
|
+
if (shapes.length && shapes.every((shape) => shapesIds.indexOf(shape) > -1)) imgs.push(el);
|
|
582
|
+
else if (!el.getAttribute("src")) imgs.push(el);
|
|
583
|
+
}
|
|
584
|
+
for (const img of imgs) writer.remove(img);
|
|
787
585
|
}
|
|
788
586
|
/**
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
if (value.type == 'elementStart' && shapeElementsMatcher.match(value.item)) {
|
|
800
|
-
shapes.push(value.item);
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
for (const shape of shapes){
|
|
804
|
-
writer.remove(shape);
|
|
805
|
-
}
|
|
587
|
+
* Removes all shape elements (`<v:*>...</v:*>`) so they do not pollute the output structure.
|
|
588
|
+
*
|
|
589
|
+
* @param documentFragment Document fragment from which to remove shape elements.
|
|
590
|
+
*/
|
|
591
|
+
function removeAllShapeElements(documentFragment, writer) {
|
|
592
|
+
const range = writer.createRangeIn(documentFragment);
|
|
593
|
+
const shapeElementsMatcher = new Matcher({ name: /v:(.+)/ });
|
|
594
|
+
const shapes = [];
|
|
595
|
+
for (const value of range) if (value.type == "elementStart" && shapeElementsMatcher.match(value.item)) shapes.push(value.item);
|
|
596
|
+
for (const shape of shapes) writer.remove(shape);
|
|
806
597
|
}
|
|
807
598
|
/**
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
}
|
|
839
|
-
if (containsMatchingImg(node.getChildren(), id)) {
|
|
840
|
-
return true;
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
return false;
|
|
845
|
-
}
|
|
846
|
-
function findSrc(shape) {
|
|
847
|
-
for (const child of shape.getChildren()){
|
|
848
|
-
/* istanbul ignore else -- @preserve */ if (child.is('element') && child.getAttribute('src')) {
|
|
849
|
-
return child.getAttribute('src');
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
}
|
|
599
|
+
* Inserts `img` tags if there is none after a shape.
|
|
600
|
+
*/
|
|
601
|
+
function insertMissingImgs(shapeIds, documentFragment, writer) {
|
|
602
|
+
const range = writer.createRangeIn(documentFragment);
|
|
603
|
+
const shapes = [];
|
|
604
|
+
for (const value of range) if (value.type == "elementStart" && value.item.is("element", "v:shape")) {
|
|
605
|
+
const id = value.item.getAttribute("id");
|
|
606
|
+
if (shapeIds.includes(id)) continue;
|
|
607
|
+
if (!containsMatchingImg(value.item.parent.getChildren(), id)) shapes.push(value.item);
|
|
608
|
+
}
|
|
609
|
+
for (const shape of shapes) {
|
|
610
|
+
const attrs = { src: findSrc(shape) };
|
|
611
|
+
if (shape.hasAttribute("alt")) attrs.alt = shape.getAttribute("alt");
|
|
612
|
+
const img = writer.createElement("img", attrs);
|
|
613
|
+
writer.insertChild(shape.index + 1, img, shape.parent);
|
|
614
|
+
}
|
|
615
|
+
function containsMatchingImg(nodes, id) {
|
|
616
|
+
for (const node of nodes)
|
|
617
|
+
/* v8 ignore else -- @preserve */
|
|
618
|
+
if (node.is("element")) {
|
|
619
|
+
if (node.name == "img" && node.getAttribute("v:shapes") == id) return true;
|
|
620
|
+
if (containsMatchingImg(node.getChildren(), id)) return true;
|
|
621
|
+
}
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
function findSrc(shape) {
|
|
625
|
+
for (const child of shape.getChildren())
|
|
626
|
+
/* v8 ignore else -- @preserve */
|
|
627
|
+
if (child.is("element") && child.getAttribute("src")) return child.getAttribute("src");
|
|
628
|
+
}
|
|
853
629
|
}
|
|
854
630
|
/**
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
}
|
|
876
|
-
currentImageIndex++;
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
return imgs;
|
|
631
|
+
* Finds all `<img>` elements in a given document fragment which have source pointing to local `file://` resource.
|
|
632
|
+
* This function also tracks the index position of each image in the document, which is essential for
|
|
633
|
+
* precise matching with hexadecimal representations in RTF data.
|
|
634
|
+
*
|
|
635
|
+
* @param documentFragment Document fragment in which to look for `<img>` elements.
|
|
636
|
+
* @returns Array of found images along with their position index in the document.
|
|
637
|
+
*/
|
|
638
|
+
function findAllImageElementsWithLocalSource(documentFragment, writer) {
|
|
639
|
+
const range = writer.createRangeIn(documentFragment);
|
|
640
|
+
const imageElementsMatcher = new Matcher({ name: "img" });
|
|
641
|
+
const imgs = [];
|
|
642
|
+
let currentImageIndex = 0;
|
|
643
|
+
for (const value of range) if (value.item.is("element") && imageElementsMatcher.match(value.item)) {
|
|
644
|
+
if (value.item.getAttribute("src").startsWith("file://")) imgs.push({
|
|
645
|
+
element: value.item,
|
|
646
|
+
imageIndex: currentImageIndex
|
|
647
|
+
});
|
|
648
|
+
currentImageIndex++;
|
|
649
|
+
}
|
|
650
|
+
return imgs;
|
|
880
651
|
}
|
|
881
652
|
/**
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
result.push({
|
|
907
|
-
hex: image.replace(regexPictureHeader, '').replace(/[^\da-fA-F]/g, ''),
|
|
908
|
-
type: imageType
|
|
909
|
-
});
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
return result;
|
|
653
|
+
* Extracts all images HEX representations from a given RTF data.
|
|
654
|
+
*
|
|
655
|
+
* @param rtfData The RTF data from which to extract images HEX representation.
|
|
656
|
+
* @returns Array of found HEX representations. Each array item is an object containing:
|
|
657
|
+
*
|
|
658
|
+
* * hex Image representation in HEX format.
|
|
659
|
+
* * type Type of image, `image/png` or `image/jpeg`.
|
|
660
|
+
*/
|
|
661
|
+
function extractImageDataFromRtf(rtfData) {
|
|
662
|
+
if (!rtfData) return [];
|
|
663
|
+
const regexPictureHeader = /{\\pict[\s\S]+?\\bliptag-?\d+(\\blipupi-?\d+)?({\\\*\\blipuid\s?[\da-fA-F]+)?[\s}]*?/;
|
|
664
|
+
const regexPicture = new RegExp("(?:(" + regexPictureHeader.source + "))([\\da-fA-F\\s]+)\\}", "g");
|
|
665
|
+
const images = rtfData.match(regexPicture);
|
|
666
|
+
const result = [];
|
|
667
|
+
if (images) for (const image of images) {
|
|
668
|
+
let imageType = false;
|
|
669
|
+
if (image.includes("\\pngblip")) imageType = "image/png";
|
|
670
|
+
else if (image.includes("\\jpegblip")) imageType = "image/jpeg";
|
|
671
|
+
if (imageType) result.push({
|
|
672
|
+
hex: image.replace(regexPictureHeader, "").replace(/[^\da-fA-F]/g, ""),
|
|
673
|
+
type: imageType
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
return result;
|
|
914
677
|
}
|
|
915
678
|
/**
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
679
|
+
* Replaces `src` attribute value of all given images with the corresponding base64 image representation.
|
|
680
|
+
* Uses the image index to precisely match with the correct hexadecimal representation from RTF data.
|
|
681
|
+
*
|
|
682
|
+
* @param imageElements Array of image elements along with their indices which will have their sources replaced.
|
|
683
|
+
* @param imagesHexSources Array of images hex sources (usually the result of `extractImageDataFromRtf()` function).
|
|
684
|
+
* Contains hexadecimal representations of ALL images in the document, not just those with `file://` URLs.
|
|
685
|
+
* In XML documents, the same image might be defined both as base64 in HTML and as hexadecimal in RTF data.
|
|
686
|
+
*/
|
|
687
|
+
function replaceImagesFileSourceWithInlineRepresentation(imageElements, imagesHexSources, writer) {
|
|
688
|
+
for (let i = 0; i < imageElements.length; i++) {
|
|
689
|
+
const { element, imageIndex } = imageElements[i];
|
|
690
|
+
const rtfHexSource = imagesHexSources[imageIndex];
|
|
691
|
+
if (rtfHexSource) {
|
|
692
|
+
const newSrc = `data:${rtfHexSource.type};base64,${_convertHexToBase64(rtfHexSource.hex)}`;
|
|
693
|
+
writer.setAttribute("src", newSrc, element);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
932
696
|
}
|
|
933
697
|
|
|
934
698
|
/**
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
const childIndex = itemParent.getChildIndex(item);
|
|
963
|
-
writer.insertChild(childIndex, item.getChildren(), itemParent);
|
|
964
|
-
writer.remove(item);
|
|
965
|
-
}
|
|
699
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
700
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
701
|
+
*/
|
|
702
|
+
/**
|
|
703
|
+
* @module paste-from-office/filters/removemsattributes
|
|
704
|
+
*/
|
|
705
|
+
/**
|
|
706
|
+
* Cleanup MS attributes like styles, attributes and elements.
|
|
707
|
+
*
|
|
708
|
+
* @param documentFragment element `data.content` obtained from clipboard.
|
|
709
|
+
* @internal
|
|
710
|
+
*/
|
|
711
|
+
function removeMSAttributes(documentFragment) {
|
|
712
|
+
const elementsToUnwrap = [];
|
|
713
|
+
const writer = new ViewUpcastWriter(documentFragment.document);
|
|
714
|
+
for (const { item } of writer.createRangeIn(documentFragment)) {
|
|
715
|
+
if (!item.is("element")) continue;
|
|
716
|
+
for (const className of item.getClassNames()) if (/\bmso/gi.exec(className)) writer.removeClass(className, item);
|
|
717
|
+
for (const styleName of item.getStyleNames()) if (/\bmso/gi.exec(styleName)) writer.removeStyle(styleName, item);
|
|
718
|
+
if (item.is("element", "w:sdt") || item.is("element", "w:sdtpr") && item.isEmpty || item.is("element", "o:p") && item.isEmpty) elementsToUnwrap.push(item);
|
|
719
|
+
}
|
|
720
|
+
for (const item of elementsToUnwrap) {
|
|
721
|
+
const itemParent = item.parent;
|
|
722
|
+
const childIndex = itemParent.getChildIndex(item);
|
|
723
|
+
writer.insertChild(childIndex, item.getChildren(), itemParent);
|
|
724
|
+
writer.remove(item);
|
|
725
|
+
}
|
|
966
726
|
}
|
|
967
727
|
|
|
968
728
|
/**
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
// It is enough to verify border style as border color and border width properties have default values in DOM.
|
|
1014
|
-
if (sides.every((side)=>!item.hasStyle(`border-${side}-style`))) {
|
|
1015
|
-
writer.setStyle('border-style', 'none', item);
|
|
1016
|
-
} else {
|
|
1017
|
-
for (const side of sides){
|
|
1018
|
-
if (!item.hasStyle(`border-${side}-style`)) {
|
|
1019
|
-
writer.setStyle(`border-${side}-style`, 'none', item);
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
// Translate style length units to px.
|
|
1024
|
-
const props = [
|
|
1025
|
-
'width',
|
|
1026
|
-
'height',
|
|
1027
|
-
...sides.map((side)=>`border-${side}-width`),
|
|
1028
|
-
...sides.map((side)=>`padding-${side}`)
|
|
1029
|
-
];
|
|
1030
|
-
for (const prop of props){
|
|
1031
|
-
if (item.hasStyle(prop)) {
|
|
1032
|
-
writer.setStyle(prop, convertCssLengthToPx(item.getStyle(prop)), item);
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
729
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
730
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
731
|
+
*/
|
|
732
|
+
/**
|
|
733
|
+
* Applies border none for table and cells without a border specified.
|
|
734
|
+
* Normalizes style length units to px.
|
|
735
|
+
* Handles left block table alignment.
|
|
736
|
+
*
|
|
737
|
+
* @internal
|
|
738
|
+
*/
|
|
739
|
+
function transformTables(documentFragment, writer, hasTablePropertiesPlugin = false) {
|
|
740
|
+
for (const item of writer.createRangeIn(documentFragment).getItems()) {
|
|
741
|
+
if (!item.is("element", "table") && !item.is("element", "td") && !item.is("element", "th")) continue;
|
|
742
|
+
if (hasTablePropertiesPlugin && item.is("element", "table")) {
|
|
743
|
+
const directParent = item.parent?.is("element", "div") ? item.parent : null;
|
|
744
|
+
const grandParent = item.parent?.parent?.is("element", "div") ? item.parent.parent : null;
|
|
745
|
+
const divParent = directParent ?? grandParent;
|
|
746
|
+
if (divParent && divParent.getAttribute("align") === "center" && !item.getAttribute("align")) {
|
|
747
|
+
writer.setStyle("margin-left", "auto", item);
|
|
748
|
+
writer.setStyle("margin-right", "auto", item);
|
|
749
|
+
} else if (divParent && divParent.getAttribute("align") === "right" && !item.getAttribute("align")) {
|
|
750
|
+
writer.setStyle("margin-left", "auto", item);
|
|
751
|
+
writer.setStyle("margin-right", "0", item);
|
|
752
|
+
} else if (!divParent && !item.getAttribute("align")) {
|
|
753
|
+
writer.setStyle("margin-left", "0", item);
|
|
754
|
+
writer.setStyle("margin-right", "auto", item);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
const sides = [
|
|
758
|
+
"left",
|
|
759
|
+
"top",
|
|
760
|
+
"right",
|
|
761
|
+
"bottom"
|
|
762
|
+
];
|
|
763
|
+
if (sides.every((side) => !item.hasStyle(`border-${side}-style`))) writer.setStyle("border-style", "none", item);
|
|
764
|
+
else for (const side of sides) if (!item.hasStyle(`border-${side}-style`)) writer.setStyle(`border-${side}-style`, "none", item);
|
|
765
|
+
const props = [
|
|
766
|
+
"width",
|
|
767
|
+
"height",
|
|
768
|
+
...sides.map((side) => `border-${side}-width`),
|
|
769
|
+
...sides.map((side) => `padding-${side}`)
|
|
770
|
+
];
|
|
771
|
+
for (const prop of props) if (item.hasStyle(prop)) writer.setStyle(prop, convertCssLengthToPx(item.getStyle(prop)), item);
|
|
772
|
+
}
|
|
1036
773
|
}
|
|
1037
774
|
|
|
1038
775
|
/**
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
writer.removeStyle('width', child);
|
|
1054
|
-
}
|
|
1055
|
-
// Remove invalid width attribute (Word: width="0").
|
|
1056
|
-
if (child.getAttribute('width') === '0') {
|
|
1057
|
-
writer.removeAttribute('width', child);
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
776
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
777
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
778
|
+
*/
|
|
779
|
+
/**
|
|
780
|
+
* Removes the `width:0px` style from table pasted from Google Sheets and `width="0"` attribute from Word tables.
|
|
781
|
+
*
|
|
782
|
+
* @param documentFragment element `data.content` obtained from clipboard
|
|
783
|
+
* @internal
|
|
784
|
+
*/
|
|
785
|
+
function removeInvalidTableWidth(documentFragment, writer) {
|
|
786
|
+
for (const child of writer.createRangeIn(documentFragment).getItems()) if (child.is("element", "table")) {
|
|
787
|
+
if (child.getStyle("width") === "0px") writer.removeStyle("width", child);
|
|
788
|
+
if (child.getAttribute("width") === "0") writer.removeAttribute("width", child);
|
|
789
|
+
}
|
|
1061
790
|
}
|
|
1062
791
|
|
|
1063
792
|
/**
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
*
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
if (!msFootnoteDef) {
|
|
1155
|
-
continue;
|
|
1156
|
-
}
|
|
1157
|
-
// Replace footnote reference.
|
|
1158
|
-
writer.replace(msFootnoteRef, createFootnoteRefViewElement(writer, footnoteId));
|
|
1159
|
-
// Append found matching definition to the definitions list.
|
|
1160
|
-
// Order doesn't matter here, as it'll be fixed in the post-fixer.
|
|
1161
|
-
const defElements = createFootnoteDefViewElement(writer, footnoteId);
|
|
1162
|
-
removeMSReferences(writer, msFootnoteDef);
|
|
1163
|
-
// Insert content within the `MsoFootnoteText` element. It's usually a definition text content.
|
|
1164
|
-
for (const child of msFootnoteDef.getChildren()){
|
|
1165
|
-
let clonedChild = child;
|
|
1166
|
-
if (child.is('element')) {
|
|
1167
|
-
clonedChild = writer.clone(child, true);
|
|
1168
|
-
}
|
|
1169
|
-
writer.appendChild(clonedChild, defElements.content);
|
|
1170
|
-
}
|
|
1171
|
-
writer.appendChild(defElements.listItem, footnotesDefinitionsList);
|
|
1172
|
-
}
|
|
793
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
794
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
795
|
+
*/
|
|
796
|
+
/**
|
|
797
|
+
* Replaces MS Word specific footnotes references and definitions with proper elements.
|
|
798
|
+
*
|
|
799
|
+
* Things to know about MS Word footnotes:
|
|
800
|
+
*
|
|
801
|
+
* * Footnote references in Word are marked with `mso-footnote-id` style.
|
|
802
|
+
* * Word does not support nested footnotes, so references within definitions are ignored.
|
|
803
|
+
* * Word appends extra spaces after footnote references within definitions, which are trimmed.
|
|
804
|
+
* * Footnote definitions list is marked with `mso-element: footnote-list` style it contain `mso-element: footnote` elements.
|
|
805
|
+
* * Footnote definition might contain tables, lists and other elements, not only text. They are placed directly within `li` element,
|
|
806
|
+
* without any wrapper (in opposition to text content of the definition, which is placed within `MsoFootnoteText` element).
|
|
807
|
+
*
|
|
808
|
+
* Example pseudo document showing MS Word footnote structure:
|
|
809
|
+
*
|
|
810
|
+
* ```html
|
|
811
|
+
* <p>Text with footnote<a style='mso-footnote-id:ftn1'>[1]</a> reference.</p>
|
|
812
|
+
*
|
|
813
|
+
* <div style='mso-element:footnote-list'>
|
|
814
|
+
* <div style='mso-element:footnote' id=ftn1>
|
|
815
|
+
* <p class=MsoFootnoteText><a style='mso-footnote-id:ftn1'>[1]</a> Footnote content</p>
|
|
816
|
+
* <table class="MsoTableGrid">...</table>
|
|
817
|
+
* </div>
|
|
818
|
+
* </div>
|
|
819
|
+
* ```
|
|
820
|
+
*
|
|
821
|
+
* Will be transformed into:
|
|
822
|
+
*
|
|
823
|
+
* ```html
|
|
824
|
+
* <p>Text with footnote<sup class="footnote"><a id="ref-footnote-ftn1" href="#footnote-ftn1">1</a></sup> reference.</p>
|
|
825
|
+
*
|
|
826
|
+
* <div class="footnotes">
|
|
827
|
+
* <hr class="footnotes-divider">
|
|
828
|
+
* <ol class="footnotes-list">
|
|
829
|
+
* <li class="footnote-definition" id="footnote-ftn1">
|
|
830
|
+
* <a href="#ref-footnote-ftn1" class="footnote-backlink">^</a>
|
|
831
|
+
* <div class="footnote-content">
|
|
832
|
+
* <p>Footnote content</p>
|
|
833
|
+
* <table>...</table>
|
|
834
|
+
* </div>
|
|
835
|
+
* </li>
|
|
836
|
+
* </ol>
|
|
837
|
+
* </div>
|
|
838
|
+
* ```
|
|
839
|
+
*
|
|
840
|
+
* @param documentFragment `data.content` obtained from clipboard.
|
|
841
|
+
* @param writer The view writer instance.
|
|
842
|
+
* @internal
|
|
843
|
+
*/
|
|
844
|
+
function replaceMSFootnotes(documentFragment, writer) {
|
|
845
|
+
const msFootnotesRefs = /* @__PURE__ */ new Map();
|
|
846
|
+
const msFootnotesDefs = /* @__PURE__ */ new Map();
|
|
847
|
+
let msFootnotesDefinitionsList = null;
|
|
848
|
+
for (const { item } of writer.createRangeIn(documentFragment)) {
|
|
849
|
+
if (!item.is("element")) continue;
|
|
850
|
+
if (item.getStyle("mso-element") === "footnote-list") {
|
|
851
|
+
msFootnotesDefinitionsList = item;
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
if (item.hasStyle("mso-footnote-id")) {
|
|
855
|
+
const msFootnoteDef = item.findAncestor("element", (el) => el.getStyle("mso-element") === "footnote");
|
|
856
|
+
if (msFootnoteDef) {
|
|
857
|
+
const msFootnoteDefId = msFootnoteDef.getAttribute("id");
|
|
858
|
+
msFootnotesDefs.set(msFootnoteDefId, msFootnoteDef);
|
|
859
|
+
} else {
|
|
860
|
+
const msFootnoteRefId = item.getStyle("mso-footnote-id");
|
|
861
|
+
msFootnotesRefs.set(msFootnoteRefId, item);
|
|
862
|
+
}
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
if (!msFootnotesRefs.size || !msFootnotesDefinitionsList) return;
|
|
867
|
+
const footnotesList = createFootnotesListContainerElement(writer);
|
|
868
|
+
writer.replace(msFootnotesDefinitionsList, footnotesList.wrapper);
|
|
869
|
+
for (const [footnoteId, msFootnoteRef] of msFootnotesRefs) {
|
|
870
|
+
const msFootnoteDef = msFootnotesDefs.get(footnoteId);
|
|
871
|
+
if (!msFootnoteDef) continue;
|
|
872
|
+
writer.replace(msFootnoteRef, createFootnoteRefViewElement(writer, footnoteId));
|
|
873
|
+
const defElements = createFootnoteDefViewElement(writer, footnoteId);
|
|
874
|
+
removeMSReferences(writer, msFootnoteDef);
|
|
875
|
+
for (const child of msFootnoteDef.getChildren()) {
|
|
876
|
+
let clonedChild = child;
|
|
877
|
+
/* v8 ignore else -- @preserve */
|
|
878
|
+
if (child.is("element")) clonedChild = writer.clone(child, true);
|
|
879
|
+
writer.appendChild(clonedChild, defElements.content);
|
|
880
|
+
}
|
|
881
|
+
writer.appendChild(defElements.listItem, footnotesList.list);
|
|
882
|
+
}
|
|
1173
883
|
}
|
|
1174
884
|
/**
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
// Create a new text node and replace the old one.
|
|
1205
|
-
const parent = textNode.parent;
|
|
1206
|
-
const index = parent.getChildIndex(textNode);
|
|
1207
|
-
const newTextNode = writer.createText(trimmedData);
|
|
1208
|
-
writer.remove(textNode);
|
|
1209
|
-
writer.insertChild(index, newTextNode, parent);
|
|
1210
|
-
} else {
|
|
1211
|
-
// If the text node contained only a space, remove it entirely.
|
|
1212
|
-
writer.remove(textNode);
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
return element;
|
|
885
|
+
* Removes all MS Office specific references from the given element.
|
|
886
|
+
*
|
|
887
|
+
* It also removes leading space from text nodes following the references, as MS Word adds
|
|
888
|
+
* them to separate the reference from the rest of the text.
|
|
889
|
+
*
|
|
890
|
+
* @param writer The view writer.
|
|
891
|
+
* @param element The element to trim.
|
|
892
|
+
* @returns The trimmed element.
|
|
893
|
+
*/
|
|
894
|
+
function removeMSReferences(writer, element) {
|
|
895
|
+
const elementsToRemove = [];
|
|
896
|
+
const textNodesToTrim = [];
|
|
897
|
+
for (const { item } of writer.createRangeIn(element)) if (item.is("element") && item.getStyle("mso-footnote-id")) {
|
|
898
|
+
elementsToRemove.unshift(item);
|
|
899
|
+
const { nextSibling } = item;
|
|
900
|
+
if (nextSibling?.is("$text") && nextSibling.data.startsWith(" ")) textNodesToTrim.unshift(nextSibling);
|
|
901
|
+
}
|
|
902
|
+
for (const element of elementsToRemove) writer.remove(element);
|
|
903
|
+
for (const textNode of textNodesToTrim) {
|
|
904
|
+
const trimmedData = textNode.data.substring(1);
|
|
905
|
+
if (trimmedData.length > 0) {
|
|
906
|
+
const parent = textNode.parent;
|
|
907
|
+
const index = parent.getChildIndex(textNode);
|
|
908
|
+
const newTextNode = writer.createText(trimmedData);
|
|
909
|
+
writer.remove(textNode);
|
|
910
|
+
writer.insertChild(index, newTextNode, parent);
|
|
911
|
+
} else writer.remove(textNode);
|
|
912
|
+
}
|
|
913
|
+
return element;
|
|
1216
914
|
}
|
|
1217
915
|
/**
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
916
|
+
* Creates a footnotes list container element.
|
|
917
|
+
*
|
|
918
|
+
* @param writer The view writer instance.
|
|
919
|
+
* @returns The footnotes list container element and list itself.
|
|
920
|
+
*/
|
|
921
|
+
function createFootnotesListContainerElement(writer) {
|
|
922
|
+
const divider = writer.createElement("hr", { class: "footnotes-divider" });
|
|
923
|
+
const list = writer.createElement("ol", { class: "footnotes-list" });
|
|
924
|
+
return {
|
|
925
|
+
list,
|
|
926
|
+
wrapper: writer.createElement("div", { class: "footnotes" }, [divider, list])
|
|
927
|
+
};
|
|
1226
928
|
}
|
|
1227
929
|
/**
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
return sup;
|
|
930
|
+
* Creates a footnote reference view element.
|
|
931
|
+
*
|
|
932
|
+
* @param writer The view writer instance.
|
|
933
|
+
* @param footnoteId The footnote ID.
|
|
934
|
+
* @returns The footnote reference view element.
|
|
935
|
+
*/
|
|
936
|
+
function createFootnoteRefViewElement(writer, footnoteId) {
|
|
937
|
+
const sup = writer.createElement("sup", { class: "footnote" });
|
|
938
|
+
const link = writer.createElement("a", {
|
|
939
|
+
id: `ref-${footnoteId}`,
|
|
940
|
+
href: `#${footnoteId}`
|
|
941
|
+
});
|
|
942
|
+
writer.appendChild(link, sup);
|
|
943
|
+
return sup;
|
|
1243
944
|
}
|
|
1244
945
|
/**
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
};
|
|
946
|
+
* Creates a footnote definition view element with a backlink and a content container.
|
|
947
|
+
*
|
|
948
|
+
* @param writer The view writer instance.
|
|
949
|
+
* @param footnoteId The footnote ID.
|
|
950
|
+
* @returns An object containing the list item element, backlink and content container.
|
|
951
|
+
*/
|
|
952
|
+
function createFootnoteDefViewElement(writer, footnoteId) {
|
|
953
|
+
const listItem = writer.createElement("li", {
|
|
954
|
+
id: footnoteId,
|
|
955
|
+
class: "footnote-definition"
|
|
956
|
+
});
|
|
957
|
+
const backLink = writer.createElement("a", {
|
|
958
|
+
href: `#ref-${footnoteId}`,
|
|
959
|
+
class: "footnote-backlink"
|
|
960
|
+
});
|
|
961
|
+
const content = writer.createElement("div", { class: "footnote-content" });
|
|
962
|
+
writer.appendChild(writer.createText("^"), backLink);
|
|
963
|
+
writer.appendChild(backLink, listItem);
|
|
964
|
+
writer.appendChild(content, listItem);
|
|
965
|
+
return {
|
|
966
|
+
listItem,
|
|
967
|
+
content
|
|
968
|
+
};
|
|
1269
969
|
}
|
|
1270
970
|
|
|
971
|
+
/**
|
|
972
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
973
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
974
|
+
*/
|
|
1271
975
|
const msWordMatch1 = /<meta\s*name="?generator"?\s*content="?microsoft\s*word\s*\d+"?\/?>/i;
|
|
1272
976
|
const msWordMatch2 = /xmlns:o="urn:schemas-microsoft-com/i;
|
|
977
|
+
const msExcelMatch = /<meta\s*name="?generator"?\s*content="?microsoft\s*excel\s*\d+"?\/?>/i;
|
|
1273
978
|
/**
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
979
|
+
* Normalizer for the content pasted from Microsoft Word.
|
|
980
|
+
*/
|
|
981
|
+
var PasteFromOfficeMSWordNormalizer = class {
|
|
982
|
+
document;
|
|
983
|
+
hasMultiLevelListPlugin;
|
|
984
|
+
hasTablePropertiesPlugin;
|
|
985
|
+
enableSkipLevelLists;
|
|
986
|
+
/**
|
|
987
|
+
* Creates a new `PasteFromOfficeMSWordNormalizer` instance.
|
|
988
|
+
*
|
|
989
|
+
* @param document View document.
|
|
990
|
+
*/
|
|
991
|
+
constructor(document, hasMultiLevelListPlugin = false, hasTablePropertiesPlugin = false, enableSkipLevelLists = false) {
|
|
992
|
+
this.document = document;
|
|
993
|
+
this.hasMultiLevelListPlugin = hasMultiLevelListPlugin;
|
|
994
|
+
this.hasTablePropertiesPlugin = hasTablePropertiesPlugin;
|
|
995
|
+
this.enableSkipLevelLists = enableSkipLevelLists;
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* @inheritDoc
|
|
999
|
+
*/
|
|
1000
|
+
isActive(htmlString) {
|
|
1001
|
+
return msWordMatch1.test(htmlString) || msWordMatch2.test(htmlString) || msExcelMatch.test(htmlString);
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* @inheritDoc
|
|
1005
|
+
*/
|
|
1006
|
+
execute(data) {
|
|
1007
|
+
const writer = new ViewUpcastWriter(this.document);
|
|
1008
|
+
const stylesString = data.extraContent.stylesString;
|
|
1009
|
+
transformBookmarks(data.content, writer);
|
|
1010
|
+
transformListItemLikeElementsIntoLists(data.content, stylesString, this.hasMultiLevelListPlugin, this.enableSkipLevelLists);
|
|
1011
|
+
replaceImagesSourceWithBase64(data.content, data.dataTransfer.getData("text/rtf"));
|
|
1012
|
+
transformTables(data.content, writer, this.hasTablePropertiesPlugin);
|
|
1013
|
+
removeInvalidTableWidth(data.content, writer);
|
|
1014
|
+
replaceMSFootnotes(data.content, writer);
|
|
1015
|
+
removeMSAttributes(data.content);
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1309
1018
|
|
|
1310
1019
|
/**
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1020
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
1021
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
1022
|
+
*/
|
|
1023
|
+
/**
|
|
1024
|
+
* Removes the `<b>` tag wrapper added by Google Docs to a copied content.
|
|
1025
|
+
*
|
|
1026
|
+
* @param documentFragment element `data.content` obtained from clipboard
|
|
1027
|
+
* @internal
|
|
1028
|
+
*/
|
|
1029
|
+
function removeBoldWrapper(documentFragment, writer) {
|
|
1030
|
+
for (const child of documentFragment.getChildren()) if (child.is("element", "b") && child.getStyle("font-weight") === "normal") {
|
|
1031
|
+
const childIndex = documentFragment.getChildIndex(child);
|
|
1032
|
+
writer.remove(child);
|
|
1033
|
+
writer.insertChild(childIndex, child.getChildren(), documentFragment);
|
|
1034
|
+
}
|
|
1328
1035
|
}
|
|
1329
1036
|
|
|
1330
1037
|
/**
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
for (const element of elementsToReplace){
|
|
1366
|
-
if (element.hasClass('Apple-interchange-newline')) {
|
|
1367
|
-
writer.remove(element);
|
|
1368
|
-
} else {
|
|
1369
|
-
writer.replace(element, writer.createElement('p'));
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1038
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
1039
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
1040
|
+
*/
|
|
1041
|
+
/**
|
|
1042
|
+
* @module paste-from-office/filters/br
|
|
1043
|
+
*/
|
|
1044
|
+
/**
|
|
1045
|
+
* Transforms `<br>` elements that are siblings to some block element into a paragraphs.
|
|
1046
|
+
*
|
|
1047
|
+
* @param documentFragment The view structure to be transformed.
|
|
1048
|
+
* @internal
|
|
1049
|
+
*/
|
|
1050
|
+
function transformBlockBrsToParagraphs(documentFragment, writer) {
|
|
1051
|
+
const domConverter = new ViewDomConverter(new ViewDocument(writer.document.stylesProcessor), { renderingMode: "data" });
|
|
1052
|
+
const blockElements = domConverter.blockElements;
|
|
1053
|
+
const inlineObjectElements = domConverter.inlineObjectElements;
|
|
1054
|
+
const elementsToReplace = [];
|
|
1055
|
+
for (const value of writer.createRangeIn(documentFragment)) {
|
|
1056
|
+
const element = value.item;
|
|
1057
|
+
if (element.is("element", "br")) {
|
|
1058
|
+
const nextSibling = findSibling(element, "forward", writer, {
|
|
1059
|
+
blockElements,
|
|
1060
|
+
inlineObjectElements
|
|
1061
|
+
});
|
|
1062
|
+
const previousSibling = findSibling(element, "backward", writer, {
|
|
1063
|
+
blockElements,
|
|
1064
|
+
inlineObjectElements
|
|
1065
|
+
});
|
|
1066
|
+
const nextSiblingIsBlock = isBlockViewElement(nextSibling, blockElements);
|
|
1067
|
+
if (isBlockViewElement(previousSibling, blockElements) || nextSiblingIsBlock) elementsToReplace.push(element);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
for (const element of elementsToReplace) if (element.hasClass("Apple-interchange-newline")) writer.remove(element);
|
|
1071
|
+
else writer.replace(element, writer.createElement("p"));
|
|
1372
1072
|
}
|
|
1373
1073
|
/**
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
// * inline object element.
|
|
1381
|
-
// It's ignoring any inline (non-object) elements like span, strong, etc.
|
|
1382
|
-
position = position.getLastMatchingPosition(({ item })=>item.is('element') && !blockElements.includes(item.name) && !inlineObjectElements.includes(item.name), {
|
|
1383
|
-
direction
|
|
1384
|
-
});
|
|
1385
|
-
return direction == 'forward' ? position.nodeAfter : position.nodeBefore;
|
|
1074
|
+
* Returns sibling node, threats inline elements as transparent (but should stop on an inline objects).
|
|
1075
|
+
*/
|
|
1076
|
+
function findSibling(viewElement, direction, writer, { blockElements, inlineObjectElements }) {
|
|
1077
|
+
let position = writer.createPositionAt(viewElement, direction == "forward" ? "after" : "before");
|
|
1078
|
+
position = position.getLastMatchingPosition(({ item }) => item.is("element") && !blockElements.includes(item.name) && !inlineObjectElements.includes(item.name), { direction });
|
|
1079
|
+
return direction == "forward" ? position.nodeAfter : position.nodeBefore;
|
|
1386
1080
|
}
|
|
1387
1081
|
/**
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1082
|
+
* Returns true for view elements that are listed as block view elements.
|
|
1083
|
+
*/
|
|
1084
|
+
function isBlockViewElement(node, blockElements) {
|
|
1085
|
+
return !!node && node.is("element") && blockElements.includes(node.name);
|
|
1391
1086
|
}
|
|
1392
1087
|
|
|
1393
1088
|
/**
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
// Check if any parent has `white-space: pre-wrap`.
|
|
1420
|
-
if (hasPreWrapParent(child.parent)) {
|
|
1421
|
-
textNodesToReplace.add(child.textNode);
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
// Replace tabs in each collected text node.
|
|
1425
|
-
for (const textNode of textNodesToReplace){
|
|
1426
|
-
replaceTabsInTextNode(textNode, writer, tabWidth);
|
|
1427
|
-
}
|
|
1089
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
1090
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
1091
|
+
*/
|
|
1092
|
+
/**
|
|
1093
|
+
* Replaces tab characters with spaces in text nodes that are inside elements styled with `white-space: pre-wrap`.
|
|
1094
|
+
*
|
|
1095
|
+
* This is a workaround for incorrect detection of pre-like formatting in the DOM converter for pasted Google Docs documents.
|
|
1096
|
+
* When an element uses `white-space: pre-wrap`, the editor reduces tab characters to a single space, causing
|
|
1097
|
+
* inconsistent spacing in pasted content. This function replaces tabs with spaces to ensure visual consistency.
|
|
1098
|
+
* This is intended as a temporary solution.
|
|
1099
|
+
*
|
|
1100
|
+
* See: https://github.com/ckeditor/ckeditor5/issues/18995
|
|
1101
|
+
*
|
|
1102
|
+
* @param documentFragment The `data.content` element obtained from the clipboard.
|
|
1103
|
+
* @param writer The upcast writer used to manipulate the view structure.
|
|
1104
|
+
* @param tabWidth The number of spaces to replace each tab with. Defaults to 8.
|
|
1105
|
+
* @internal
|
|
1106
|
+
*/
|
|
1107
|
+
function replaceTabsWithinPreWithSpaces(documentFragment, writer, tabWidth) {
|
|
1108
|
+
const textNodesToReplace = /* @__PURE__ */ new Set();
|
|
1109
|
+
for (const child of writer.createRangeIn(documentFragment).getItems()) {
|
|
1110
|
+
if (!child.is("view:$textProxy") || !child.data.includes(" ")) continue;
|
|
1111
|
+
if (hasPreWrapParent(child.parent)) textNodesToReplace.add(child.textNode);
|
|
1112
|
+
}
|
|
1113
|
+
for (const textNode of textNodesToReplace) replaceTabsInTextNode(textNode, writer, tabWidth);
|
|
1428
1114
|
}
|
|
1429
1115
|
/**
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
}
|
|
1442
|
-
return false;
|
|
1116
|
+
* Checks if element or any of its parents has `white-space: pre-wrap` style.
|
|
1117
|
+
*/
|
|
1118
|
+
function hasPreWrapParent(element) {
|
|
1119
|
+
let parent = element;
|
|
1120
|
+
while (parent) {
|
|
1121
|
+
if (parent.is("element")) {
|
|
1122
|
+
if (parent.getStyle?.("white-space") === "pre-wrap") return true;
|
|
1123
|
+
}
|
|
1124
|
+
parent = parent.parent;
|
|
1125
|
+
}
|
|
1126
|
+
return false;
|
|
1443
1127
|
}
|
|
1444
1128
|
/**
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1129
|
+
* Replaces all tabs with spaces in the given text node.
|
|
1130
|
+
*/
|
|
1131
|
+
function replaceTabsInTextNode(textNode, writer, tabWidth) {
|
|
1132
|
+
const { parent, data } = textNode;
|
|
1133
|
+
const replacedData = data.replaceAll(" ", " ".repeat(tabWidth));
|
|
1134
|
+
const index = parent.getChildIndex(textNode);
|
|
1135
|
+
writer.remove(textNode);
|
|
1136
|
+
writer.insertChild(index, writer.createText(replacedData), parent);
|
|
1453
1137
|
}
|
|
1454
1138
|
|
|
1139
|
+
/**
|
|
1140
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
1141
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
1142
|
+
*/
|
|
1143
|
+
/**
|
|
1144
|
+
* @module paste-from-office/normalizers/googledocsnormalizer
|
|
1145
|
+
*/
|
|
1455
1146
|
const googleDocsMatch = /id=("|')docs-internal-guid-[-0-9a-f]+("|')/i;
|
|
1456
1147
|
/**
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1148
|
+
* Normalizer for the content pasted from Google Docs.
|
|
1149
|
+
*
|
|
1150
|
+
* @internal
|
|
1151
|
+
*/
|
|
1152
|
+
var GoogleDocsNormalizer = class {
|
|
1153
|
+
document;
|
|
1154
|
+
/**
|
|
1155
|
+
* Creates a new `GoogleDocsNormalizer` instance.
|
|
1156
|
+
*
|
|
1157
|
+
* @param document View document.
|
|
1158
|
+
*/
|
|
1159
|
+
constructor(document) {
|
|
1160
|
+
this.document = document;
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* @inheritDoc
|
|
1164
|
+
*/
|
|
1165
|
+
isActive(htmlString) {
|
|
1166
|
+
return googleDocsMatch.test(htmlString);
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* @inheritDoc
|
|
1170
|
+
*/
|
|
1171
|
+
execute(data) {
|
|
1172
|
+
const writer = new ViewUpcastWriter(this.document);
|
|
1173
|
+
removeBoldWrapper(data.content, writer);
|
|
1174
|
+
unwrapParagraphInListItem(data.content, writer);
|
|
1175
|
+
transformBlockBrsToParagraphs(data.content, writer);
|
|
1176
|
+
replaceTabsWithinPreWithSpaces(data.content, writer, 8);
|
|
1177
|
+
}
|
|
1178
|
+
};
|
|
1484
1179
|
|
|
1485
1180
|
/**
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
if (child.is('element', 'table') && child.hasAttribute('xmlns')) {
|
|
1498
|
-
writer.removeAttribute('xmlns', child);
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1181
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
1182
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
1183
|
+
*/
|
|
1184
|
+
/**
|
|
1185
|
+
* Removes the `xmlns` attribute from table pasted from Google Sheets.
|
|
1186
|
+
*
|
|
1187
|
+
* @param documentFragment element `data.content` obtained from clipboard
|
|
1188
|
+
* @internal
|
|
1189
|
+
*/
|
|
1190
|
+
function removeXmlns(documentFragment, writer) {
|
|
1191
|
+
for (const child of documentFragment.getChildren()) if (child.is("element", "table") && child.hasAttribute("xmlns")) writer.removeAttribute("xmlns", child);
|
|
1501
1192
|
}
|
|
1502
1193
|
|
|
1503
1194
|
/**
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1195
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
1196
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
1197
|
+
*/
|
|
1198
|
+
/**
|
|
1199
|
+
* Removes the `<google-sheets-html-origin>` tag wrapper added by Google Sheets to a copied content.
|
|
1200
|
+
*
|
|
1201
|
+
* @param documentFragment element `data.content` obtained from clipboard
|
|
1202
|
+
* @internal
|
|
1203
|
+
*/
|
|
1204
|
+
function removeGoogleSheetsTag(documentFragment, writer) {
|
|
1205
|
+
for (const child of documentFragment.getChildren())
|
|
1206
|
+
/* v8 ignore else -- @preserve */
|
|
1207
|
+
if (child.is("element", "google-sheets-html-origin")) {
|
|
1208
|
+
const childIndex = documentFragment.getChildIndex(child);
|
|
1209
|
+
writer.remove(child);
|
|
1210
|
+
writer.insertChild(childIndex, child.getChildren(), documentFragment);
|
|
1211
|
+
}
|
|
1521
1212
|
}
|
|
1522
1213
|
|
|
1523
1214
|
/**
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
if (child.is('element', 'style')) {
|
|
1536
|
-
writer.remove(child);
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1215
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
1216
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
1217
|
+
*/
|
|
1218
|
+
/**
|
|
1219
|
+
* Removes `<style>` block added by Google Sheets to a copied content.
|
|
1220
|
+
*
|
|
1221
|
+
* @param documentFragment element `data.content` obtained from clipboard
|
|
1222
|
+
* @internal
|
|
1223
|
+
*/
|
|
1224
|
+
function removeStyleBlock(documentFragment, writer) {
|
|
1225
|
+
for (const child of Array.from(documentFragment.getChildren())) if (child.is("element", "style")) writer.remove(child);
|
|
1539
1226
|
}
|
|
1540
1227
|
|
|
1228
|
+
/**
|
|
1229
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
1230
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
1231
|
+
*/
|
|
1232
|
+
/**
|
|
1233
|
+
* @module paste-from-office/normalizers/googlesheetsnormalizer
|
|
1234
|
+
*/
|
|
1541
1235
|
const googleSheetsMatch = /<google-sheets-html-origin/i;
|
|
1542
1236
|
/**
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1237
|
+
* Normalizer for the content pasted from Google Sheets.
|
|
1238
|
+
*
|
|
1239
|
+
* @internal
|
|
1240
|
+
*/
|
|
1241
|
+
var GoogleSheetsNormalizer = class {
|
|
1242
|
+
document;
|
|
1243
|
+
/**
|
|
1244
|
+
* Creates a new `GoogleSheetsNormalizer` instance.
|
|
1245
|
+
*
|
|
1246
|
+
* @param document View document.
|
|
1247
|
+
*/
|
|
1248
|
+
constructor(document) {
|
|
1249
|
+
this.document = document;
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* @inheritDoc
|
|
1253
|
+
*/
|
|
1254
|
+
isActive(htmlString) {
|
|
1255
|
+
return googleSheetsMatch.test(htmlString);
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* @inheritDoc
|
|
1259
|
+
*/
|
|
1260
|
+
execute(data) {
|
|
1261
|
+
const writer = new ViewUpcastWriter(this.document);
|
|
1262
|
+
removeGoogleSheetsTag(data.content, writer);
|
|
1263
|
+
removeXmlns(data.content, writer);
|
|
1264
|
+
removeInvalidTableWidth(data.content, writer);
|
|
1265
|
+
removeStyleBlock(data.content, writer);
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1570
1268
|
|
|
1571
1269
|
/**
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
.replace(/>([^\S\r\n]*[\r\n]\s*)</g, '><');
|
|
1270
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
1271
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
1272
|
+
*/
|
|
1273
|
+
/**
|
|
1274
|
+
* @module paste-from-office/filters/space
|
|
1275
|
+
*/
|
|
1276
|
+
/**
|
|
1277
|
+
* Replaces last space preceding elements closing tag with ` `. Such operation prevents spaces from being removed
|
|
1278
|
+
* during further DOM/View processing (see especially {@link module:engine/view/domconverter~ViewDomConverter#_processDomInlineNodes}).
|
|
1279
|
+
* This method also takes into account Word specific `<o:p></o:p>` empty tags.
|
|
1280
|
+
* Additionally multiline sequences of spaces and new lines between tags are removed (see
|
|
1281
|
+
* https://github.com/ckeditor/ckeditor5-paste-from-office/issues/39
|
|
1282
|
+
* and https://github.com/ckeditor/ckeditor5-paste-from-office/issues/40).
|
|
1283
|
+
*
|
|
1284
|
+
* @param htmlString HTML string in which spacing should be normalized.
|
|
1285
|
+
* @returns Input HTML with spaces normalized.
|
|
1286
|
+
* @internal
|
|
1287
|
+
*/
|
|
1288
|
+
function normalizeSpacing(htmlString) {
|
|
1289
|
+
return normalizeSafariSpaceSpans(normalizeSafariSpaceSpans(htmlString)).replace(/(<span\s+style=['"]mso-spacerun:yes['"]>[^\S\r\n]*?)[\r\n]+([^\S\r\n]*<\/span>)/g, "$1$2").replace(/<span\s+style=['"]mso-spacerun:yes['"]><\/span>/g, "").replace(/(<span\s+style=['"]letter-spacing:[^'"]+?['"]>)[\r\n]+(<\/span>)/g, "$1 $2").replace(/ <\//g, "\xA0</").replace(/ <o:p><\/o:p>/g, "\xA0<o:p></o:p>").replace(/<o:p>( |\u00A0)<\/o:p>/g, "").replace(/>([^\S\r\n]*[\r\n]\s*)</g, "><");
|
|
1593
1290
|
}
|
|
1594
1291
|
/**
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1292
|
+
* Normalizes spacing in special Word `spacerun spans` (`<span style='mso-spacerun:yes'>\s+</span>`) by replacing
|
|
1293
|
+
* all spaces with ` ` pairs. This prevents spaces from being removed during further DOM/View processing
|
|
1294
|
+
* (see especially {@link module:engine/view/domconverter~ViewDomConverter#_processDomInlineNodes}).
|
|
1295
|
+
*
|
|
1296
|
+
* @param htmlDocument Native `Document` object in which spacing should be normalized.
|
|
1297
|
+
* @internal
|
|
1298
|
+
*/
|
|
1299
|
+
function normalizeSpacerunSpans(htmlDocument) {
|
|
1300
|
+
htmlDocument.querySelectorAll("span[style*=spacerun]").forEach((el) => {
|
|
1301
|
+
const htmlElement = el;
|
|
1302
|
+
const innerTextLength = htmlElement.innerText.length || 0;
|
|
1303
|
+
htmlElement.innerText = Array(innerTextLength + 1).join("\xA0 ").substr(0, innerTextLength);
|
|
1304
|
+
});
|
|
1607
1305
|
}
|
|
1608
1306
|
/**
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1307
|
+
* Normalizes specific spacing generated by Safari when content pasted from Word (`<span class="Apple-converted-space"> </span>`)
|
|
1308
|
+
* by replacing all spaces sequences longer than 1 space with ` ` pairs. This prevents spaces from being removed during
|
|
1309
|
+
* further DOM/View processing (see especially {@link module:engine/view/domconverter~ViewDomConverter#_processDataFromDomText}).
|
|
1310
|
+
*
|
|
1311
|
+
* This function is similar to {@link module:clipboard/utils/normalizeclipboarddata normalizeClipboardData util} but uses
|
|
1312
|
+
* regular spaces / sequence for replacement.
|
|
1313
|
+
*
|
|
1314
|
+
* @param htmlString HTML string in which spacing should be normalized
|
|
1315
|
+
* @returns Input HTML with spaces normalized.
|
|
1316
|
+
* @internal
|
|
1317
|
+
*/
|
|
1318
|
+
function normalizeSafariSpaceSpans(htmlString) {
|
|
1319
|
+
return htmlString.replace(/<span(?: class="Apple-converted-space"|)>(\s+)<\/span>/g, (fullMatch, spaces) => {
|
|
1320
|
+
return spaces.length === 1 ? " " : Array(spaces.length + 1).join("\xA0 ").substr(0, spaces.length);
|
|
1321
|
+
});
|
|
1623
1322
|
}
|
|
1624
1323
|
|
|
1625
1324
|
/**
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1325
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
1326
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
1327
|
+
*/
|
|
1328
|
+
/**
|
|
1329
|
+
* @module paste-from-office/filters/parse
|
|
1330
|
+
*/
|
|
1331
|
+
/**
|
|
1332
|
+
* Parses the provided HTML extracting contents of `<body>` and `<style>` tags.
|
|
1333
|
+
*
|
|
1334
|
+
* @param htmlString HTML string to be parsed.
|
|
1335
|
+
*/
|
|
1336
|
+
function parsePasteOfficeHtml(htmlString, stylesProcessor) {
|
|
1337
|
+
const domParser = new DOMParser();
|
|
1338
|
+
htmlString = htmlString.replace(/<!--\[if gte vml 1]>/g, "");
|
|
1339
|
+
htmlString = htmlString.replace(/<o:SmartTagType(?:\s+[^\s>=]+(?:="[^"]*")?)*\s*\/?>/gi, "");
|
|
1340
|
+
const normalizedHtml = normalizeSpacing(cleanContentAfterBody(htmlString));
|
|
1341
|
+
const htmlDocument = domParser.parseFromString(normalizedHtml, "text/html");
|
|
1342
|
+
normalizeSpacerunSpans(htmlDocument);
|
|
1343
|
+
removeInBodyStyleBlocks(htmlDocument);
|
|
1344
|
+
const bodyString = htmlDocument.body.innerHTML;
|
|
1345
|
+
const bodyView = documentToView(htmlDocument, stylesProcessor);
|
|
1346
|
+
const stylesObject = extractStyles(htmlDocument);
|
|
1347
|
+
return {
|
|
1348
|
+
body: bodyView,
|
|
1349
|
+
bodyString,
|
|
1350
|
+
styles: stylesObject.styles,
|
|
1351
|
+
stylesString: stylesObject.stylesString
|
|
1352
|
+
};
|
|
1652
1353
|
}
|
|
1653
1354
|
/**
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
while(nodes.length > 0){
|
|
1665
|
-
fragment.appendChild(nodes[0]);
|
|
1666
|
-
}
|
|
1667
|
-
return domConverter.domToView(fragment, {
|
|
1668
|
-
skipComments: true
|
|
1669
|
-
});
|
|
1355
|
+
* Transforms native `Document` object into {@link module:engine/view/documentfragment~ViewDocumentFragment}. Comments are skipped.
|
|
1356
|
+
*
|
|
1357
|
+
* @param htmlDocument Native `Document` object to be transformed.
|
|
1358
|
+
*/
|
|
1359
|
+
function documentToView(htmlDocument, stylesProcessor) {
|
|
1360
|
+
const domConverter = new ViewDomConverter(new ViewDocument(stylesProcessor), { renderingMode: "data" });
|
|
1361
|
+
const fragment = htmlDocument.createDocumentFragment();
|
|
1362
|
+
const nodes = htmlDocument.body.childNodes;
|
|
1363
|
+
while (nodes.length > 0) fragment.appendChild(nodes[0]);
|
|
1364
|
+
return domConverter.domToView(fragment, { skipComments: true });
|
|
1670
1365
|
}
|
|
1671
1366
|
/**
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
styles.push(style.sheet);
|
|
1682
|
-
stylesString.push(style.innerHTML);
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
return {
|
|
1686
|
-
styles,
|
|
1687
|
-
stylesString: stylesString.join(' ')
|
|
1688
|
-
};
|
|
1367
|
+
* Removes all `<style>` elements found inside the `<body>` of the provided `htmlDocument`.
|
|
1368
|
+
*
|
|
1369
|
+
* This guards against sources that place (or flatten) a `<style>` block into the body, where its CSS text
|
|
1370
|
+
* would otherwise be converted to visible text. Styles located in the `<head>` are not affected.
|
|
1371
|
+
*
|
|
1372
|
+
* @param htmlDocument Native `Document` object to be cleaned.
|
|
1373
|
+
*/
|
|
1374
|
+
function removeInBodyStyleBlocks(htmlDocument) {
|
|
1375
|
+
for (const style of Array.from(htmlDocument.body.querySelectorAll("style"))) style.remove();
|
|
1689
1376
|
}
|
|
1690
1377
|
/**
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
const htmlCloseIndex = htmlString.indexOf(htmlCloseTag, bodyCloseIndex + bodyCloseTag.length);
|
|
1708
|
-
return htmlString.substring(0, bodyCloseIndex + bodyCloseTag.length) + (htmlCloseIndex >= 0 ? htmlString.substring(htmlCloseIndex) : '');
|
|
1378
|
+
* Extracts both `CSSStyleSheet` and string representation from all `style` elements available in a provided `htmlDocument`.
|
|
1379
|
+
*
|
|
1380
|
+
* @param htmlDocument Native `Document` object from which styles will be extracted.
|
|
1381
|
+
*/
|
|
1382
|
+
function extractStyles(htmlDocument) {
|
|
1383
|
+
const styles = [];
|
|
1384
|
+
const stylesString = [];
|
|
1385
|
+
const styleTags = Array.from(htmlDocument.getElementsByTagName("style"));
|
|
1386
|
+
for (const style of styleTags) if (style.sheet && style.sheet.cssRules && style.sheet.cssRules.length) {
|
|
1387
|
+
styles.push(style.sheet);
|
|
1388
|
+
stylesString.push(style.innerHTML);
|
|
1389
|
+
}
|
|
1390
|
+
return {
|
|
1391
|
+
styles,
|
|
1392
|
+
stylesString: stylesString.join(" ")
|
|
1393
|
+
};
|
|
1709
1394
|
}
|
|
1710
|
-
|
|
1711
1395
|
/**
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
*/ static get pluginName() {
|
|
1730
|
-
return 'PasteFromOffice';
|
|
1731
|
-
}
|
|
1732
|
-
/**
|
|
1733
|
-
* @inheritDoc
|
|
1734
|
-
* @internal
|
|
1735
|
-
*/ static get licenseFeatureCode() {
|
|
1736
|
-
return 'PFO';
|
|
1737
|
-
}
|
|
1738
|
-
/**
|
|
1739
|
-
* @inheritDoc
|
|
1740
|
-
*/ static get isOfficialPlugin() {
|
|
1741
|
-
return true;
|
|
1742
|
-
}
|
|
1743
|
-
/**
|
|
1744
|
-
* @inheritDoc
|
|
1745
|
-
*/ static get isPremiumPlugin() {
|
|
1746
|
-
return true;
|
|
1747
|
-
}
|
|
1748
|
-
/**
|
|
1749
|
-
* @inheritDoc
|
|
1750
|
-
*/ static get requires() {
|
|
1751
|
-
return [
|
|
1752
|
-
ClipboardPipeline
|
|
1753
|
-
];
|
|
1754
|
-
}
|
|
1755
|
-
/**
|
|
1756
|
-
* @inheritDoc
|
|
1757
|
-
*/ init() {
|
|
1758
|
-
const editor = this.editor;
|
|
1759
|
-
const clipboardPipeline = editor.plugins.get('ClipboardPipeline');
|
|
1760
|
-
const viewDocument = editor.editing.view.document;
|
|
1761
|
-
const hasMultiLevelListPlugin = this.editor.plugins.has('MultiLevelListEditing');
|
|
1762
|
-
const hasTablePropertiesPlugin = this.editor.plugins.has('TablePropertiesEditing');
|
|
1763
|
-
const enableSkipLevelLists = !!this.editor.config.get('list.enableSkipLevelLists');
|
|
1764
|
-
this.registerNormalizer(new PasteFromOfficeMSWordNormalizer(viewDocument, hasMultiLevelListPlugin, hasTablePropertiesPlugin, enableSkipLevelLists));
|
|
1765
|
-
this.registerNormalizer(new GoogleDocsNormalizer(viewDocument));
|
|
1766
|
-
this.registerNormalizer(new GoogleSheetsNormalizer(viewDocument));
|
|
1767
|
-
viewDocument.on('clipboardInput', (evt, data)=>{
|
|
1768
|
-
if (typeof data.content != 'string') {
|
|
1769
|
-
return;
|
|
1770
|
-
}
|
|
1771
|
-
// The `htmlString` is used only to detect (match) the active normalizer.
|
|
1772
|
-
// The actual content processing is happening on `data.content` below.
|
|
1773
|
-
const htmlString = data.dataTransfer.getData('text/html');
|
|
1774
|
-
const activeNormalizer = this._normalizers.find(({ normalizer })=>normalizer.isActive(htmlString));
|
|
1775
|
-
if (activeNormalizer) {
|
|
1776
|
-
const parsedData = parsePasteOfficeHtml(data.content, viewDocument.stylesProcessor);
|
|
1777
|
-
data.content = parsedData.body;
|
|
1778
|
-
data.extraContent = {
|
|
1779
|
-
...parsedData,
|
|
1780
|
-
isTransformedWithPasteFromOffice: true
|
|
1781
|
-
};
|
|
1782
|
-
}
|
|
1783
|
-
}, {
|
|
1784
|
-
priority: priorities.low + 10
|
|
1785
|
-
});
|
|
1786
|
-
clipboardPipeline.on('inputTransformation', (evt, data)=>{
|
|
1787
|
-
if (!data.extraContent || !data.extraContent.isTransformedWithPasteFromOffice) {
|
|
1788
|
-
return;
|
|
1789
|
-
}
|
|
1790
|
-
// The `htmlString` is used only to detect (match) the active normalizers, not for processing.
|
|
1791
|
-
const htmlString = data.dataTransfer.getData('text/html');
|
|
1792
|
-
const normalizers = this._normalizers.filter(({ normalizer })=>normalizer.isActive(htmlString));
|
|
1793
|
-
for (const { normalizer } of normalizers){
|
|
1794
|
-
normalizer.execute(data);
|
|
1795
|
-
}
|
|
1796
|
-
}, {
|
|
1797
|
-
priority: 'high'
|
|
1798
|
-
});
|
|
1799
|
-
}
|
|
1800
|
-
/**
|
|
1801
|
-
* Registers a normalizer with the given priority.
|
|
1802
|
-
*/ registerNormalizer(normalizer, priority) {
|
|
1803
|
-
insertToPriorityArray(this._normalizers, {
|
|
1804
|
-
normalizer,
|
|
1805
|
-
priority: priorities.get(priority)
|
|
1806
|
-
});
|
|
1807
|
-
}
|
|
1396
|
+
* Removes leftover content from between closing </body> and closing </html> tag:
|
|
1397
|
+
*
|
|
1398
|
+
* ```html
|
|
1399
|
+
* <html><body><p>Foo Bar</p></body><span>Fo</span></html> -> <html><body><p>Foo Bar</p></body></html>
|
|
1400
|
+
* ```
|
|
1401
|
+
*
|
|
1402
|
+
* This function is used as specific browsers (Edge) add some random content after `body` tag when pasting from Word.
|
|
1403
|
+
* @param htmlString The HTML string to be cleaned.
|
|
1404
|
+
* @returns The HTML string with leftover content removed.
|
|
1405
|
+
*/
|
|
1406
|
+
function cleanContentAfterBody(htmlString) {
|
|
1407
|
+
const bodyCloseTag = "</body>";
|
|
1408
|
+
const htmlCloseTag = "</html>";
|
|
1409
|
+
const bodyCloseIndex = htmlString.indexOf(bodyCloseTag);
|
|
1410
|
+
if (bodyCloseIndex < 0) return htmlString;
|
|
1411
|
+
const htmlCloseIndex = htmlString.indexOf(htmlCloseTag, bodyCloseIndex + 7);
|
|
1412
|
+
return htmlString.substring(0, bodyCloseIndex + 7) + (htmlCloseIndex >= 0 ? htmlString.substring(htmlCloseIndex) : "");
|
|
1808
1413
|
}
|
|
1809
1414
|
|
|
1415
|
+
/**
|
|
1416
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
1417
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
1418
|
+
*/
|
|
1419
|
+
/**
|
|
1420
|
+
* @module paste-from-office/pastefromoffice
|
|
1421
|
+
*/
|
|
1422
|
+
/**
|
|
1423
|
+
* The Paste from Office plugin.
|
|
1424
|
+
*
|
|
1425
|
+
* This plugin handles content pasted from Office apps and transforms it (if necessary)
|
|
1426
|
+
* to a valid structure which can then be understood by the editor features.
|
|
1427
|
+
*
|
|
1428
|
+
* Transformation is made by a set of predefined {@link module:paste-from-office/normalizer~PasteFromOfficeNormalizer normalizers}.
|
|
1429
|
+
* This plugin includes following normalizers:
|
|
1430
|
+
* * {@link module:paste-from-office/normalizers/mswordnormalizer~PasteFromOfficeMSWordNormalizer Microsoft Word normalizer}
|
|
1431
|
+
* * {@link module:paste-from-office/normalizers/googledocsnormalizer~GoogleDocsNormalizer Google Docs normalizer}
|
|
1432
|
+
*
|
|
1433
|
+
* For more information about this feature check the {@glink api/paste-from-office package page}.
|
|
1434
|
+
*/
|
|
1435
|
+
var PasteFromOffice = class extends Plugin {
|
|
1436
|
+
/**
|
|
1437
|
+
* The priority array of registered normalizers.
|
|
1438
|
+
*/
|
|
1439
|
+
_normalizers = [];
|
|
1440
|
+
/**
|
|
1441
|
+
* @inheritDoc
|
|
1442
|
+
*/
|
|
1443
|
+
static get pluginName() {
|
|
1444
|
+
return "PasteFromOffice";
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* @inheritDoc
|
|
1448
|
+
* @internal
|
|
1449
|
+
*/
|
|
1450
|
+
static get licenseFeatureCode() {
|
|
1451
|
+
return "PFO";
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* @inheritDoc
|
|
1455
|
+
*/
|
|
1456
|
+
static get isOfficialPlugin() {
|
|
1457
|
+
return true;
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* @inheritDoc
|
|
1461
|
+
*/
|
|
1462
|
+
static get isPremiumPlugin() {
|
|
1463
|
+
return true;
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* @inheritDoc
|
|
1467
|
+
*/
|
|
1468
|
+
static get requires() {
|
|
1469
|
+
return [ClipboardPipeline];
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* @inheritDoc
|
|
1473
|
+
*/
|
|
1474
|
+
init() {
|
|
1475
|
+
const editor = this.editor;
|
|
1476
|
+
const clipboardPipeline = editor.plugins.get("ClipboardPipeline");
|
|
1477
|
+
const viewDocument = editor.editing.view.document;
|
|
1478
|
+
const hasMultiLevelListPlugin = this.editor.plugins.has("MultiLevelListEditing");
|
|
1479
|
+
const hasTablePropertiesPlugin = this.editor.plugins.has("TablePropertiesEditing");
|
|
1480
|
+
const enableSkipLevelLists = !!this.editor.config.get("list.enableSkipLevelLists");
|
|
1481
|
+
this.registerNormalizer(new PasteFromOfficeMSWordNormalizer(viewDocument, hasMultiLevelListPlugin, hasTablePropertiesPlugin, enableSkipLevelLists));
|
|
1482
|
+
this.registerNormalizer(new GoogleDocsNormalizer(viewDocument));
|
|
1483
|
+
this.registerNormalizer(new GoogleSheetsNormalizer(viewDocument));
|
|
1484
|
+
viewDocument.on("clipboardInput", (evt, data) => {
|
|
1485
|
+
if (typeof data.content != "string") return;
|
|
1486
|
+
const htmlString = data.dataTransfer.getData("text/html");
|
|
1487
|
+
if (this._normalizers.find(({ normalizer }) => normalizer.isActive(htmlString))) {
|
|
1488
|
+
const parsedData = parsePasteOfficeHtml(data.content, viewDocument.stylesProcessor);
|
|
1489
|
+
data.content = parsedData.body;
|
|
1490
|
+
data.extraContent = {
|
|
1491
|
+
...parsedData,
|
|
1492
|
+
isTransformedWithPasteFromOffice: true
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
}, { priority: priorities.low + 10 });
|
|
1496
|
+
clipboardPipeline.on("inputTransformation", (evt, data) => {
|
|
1497
|
+
if (!data.extraContent || !data.extraContent.isTransformedWithPasteFromOffice) return;
|
|
1498
|
+
const htmlString = data.dataTransfer.getData("text/html");
|
|
1499
|
+
const normalizers = this._normalizers.filter(({ normalizer }) => normalizer.isActive(htmlString));
|
|
1500
|
+
for (const { normalizer } of normalizers) normalizer.execute(data);
|
|
1501
|
+
}, { priority: "high" });
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Registers a normalizer with the given priority.
|
|
1505
|
+
*/
|
|
1506
|
+
registerNormalizer(normalizer, priority) {
|
|
1507
|
+
insertToPriorityArray(this._normalizers, {
|
|
1508
|
+
normalizer,
|
|
1509
|
+
priority: priorities.get(priority)
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
};
|
|
1513
|
+
|
|
1514
|
+
/**
|
|
1515
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
1516
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
1517
|
+
*/
|
|
1518
|
+
|
|
1810
1519
|
export { PasteFromOffice, GoogleDocsNormalizer as PasteFromOfficeGoogleDocsNormalizer, GoogleSheetsNormalizer as PasteFromOfficeGoogleSheetsNormalizer, PasteFromOfficeMSWordNormalizer, _convertHexToBase64, convertCssLengthToPx as _convertPasteOfficeCssLengthToPx, isPx as _isPasteOfficePxValue, normalizeSpacerunSpans as _normalizePasteOfficeSpaceRunSpans, normalizeSpacing as _normalizePasteOfficeSpacing, removeGoogleSheetsTag as _removePasteGoogleOfficeSheetsTag, removeMSAttributes as _removePasteMSOfficeAttributes, removeBoldWrapper as _removePasteOfficeBoldWrapper, removeInvalidTableWidth as _removePasteOfficeInvalidTableWidths, removeStyleBlock as _removePasteOfficeStyleBlock, removeXmlns as _removePasteOfficeXmlnsAttributes, replaceImagesSourceWithBase64 as _replacePasteOfficeImagesSourceWithBase64, toPx as _toPasteOfficePxValue, transformBlockBrsToParagraphs as _transformPasteOfficeBlockBrsToParagraphs, transformBookmarks as _transformPasteOfficeBookmarks, transformListItemLikeElementsIntoLists as _transformPasteOfficeListItemLikeElementsIntoLists, transformTables as _transformPasteOfficeTables, unwrapParagraphInListItem as _unwrapPasteOfficeParagraphInListItem, parsePasteOfficeHtml };
|
|
1811
|
-
//# sourceMappingURL=index.js.map
|
|
1520
|
+
//# sourceMappingURL=index.js.map
|