@ckeditor/ckeditor5-paste-from-office 48.2.0 → 48.3.0-alpha.0

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/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 '@ckeditor/ckeditor5-core/dist/index.js';
6
- import { priorities, insertToPriorityArray } from '@ckeditor/ckeditor5-utils/dist/index.js';
7
- import { ClipboardPipeline } from '@ckeditor/ckeditor5-clipboard/dist/index.js';
8
- import { ViewUpcastWriter, Matcher, ViewDocument, ViewDomConverter } from '@ckeditor/ckeditor5-engine/dist/index.js';
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
- * @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
- * @module paste-from-office/filters/bookmark
15
- */ /**
16
- * Transforms `<a>` elements which are bookmarks by moving their children after the element.
17
- *
18
- * @internal
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'))) {
24
- elementsToChange.push(element);
25
- }
26
- }
27
- for (const element of elementsToChange){
28
- const index = element.parent.getChildIndex(element) + 1;
29
- const children = element.getChildren();
30
- writer.insertChild(index, children, element.parent);
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
- * Checks whether the given element is a hidden or auto-generated bookmark anchor.
38
- *
39
- * Editors like MS Word and Google Docs use the `name` attribute (rather than `id`)
40
- * for bookmarks. Furthermore, they reserve `_`-prefixed bookmark names for
41
- * auto-generated anchors (e.g., Table of Contents or internal hyperlinks) and
42
- * do not allow users to manually create custom bookmarks starting with an underscore.
43
- *
44
- * @param element The element to check.
45
- * @returns True if the element is a hidden bookmark anchor, false otherwise.
46
- */ function isHiddenBookmarkAnchor(element) {
47
- const name = element.getAttribute('name');
48
- return !!name && name.startsWith('_');
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
- * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
53
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
54
- */ /**
55
- * @module paste-from-office/filters/utils
56
- */ /**
57
- * Normalizes CSS length value to 'px'.
58
- *
59
- * @internal
60
- */ function convertCssLengthToPx(value) {
61
- const numericValue = parseFloat(value);
62
- if (value.endsWith('pt')) {
63
- // 1pt = 1in / 72
64
- return toPx(numericValue * 96 / 72);
65
- } else if (value.endsWith('pc')) {
66
- // 1pc = 12pt = 1in / 6.
67
- return toPx(numericValue * 12 * 96 / 72);
68
- } else if (value.endsWith('in')) {
69
- // 1in = 2.54cm = 96px
70
- return toPx(numericValue * 96);
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
- * Returns true for value with 'px' unit.
82
- *
83
- * @internal
84
- */ function isPx(value) {
85
- return value !== undefined && value.endsWith('px');
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
- * Returns a rounded 'px' value.
89
- *
90
- * @internal
91
- */ function toPx(value) {
92
- return Math.round(value) + 'px';
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
- * Transforms Word specific list-like elements to the semantic HTML lists.
97
- *
98
- * Lists in Word are represented by block elements with special attributes like:
99
- *
100
- * ```xml
101
- * <p class=MsoListParagraphCxSpFirst style='mso-list:l1 level1 lfo1'>...</p> // Paragraph based list.
102
- * <h1 style='mso-list:l0 level1 lfo1'>...</h1> // Heading 1 based list.
103
- * ```
104
- *
105
- * @param documentFragment The view structure to be transformed.
106
- * @param stylesString Styles from which list-like elements styling will be extracted.
107
- * @param hasMultiLevelListPlugin Whether the editor has the multi-level list plugin enabled.
108
- * @param enableSkipLevelLists Whether to enable skip-level lists.
109
- * @internal
110
- */ function transformListItemLikeElementsIntoLists(documentFragment, stylesString, hasMultiLevelListPlugin, enableSkipLevelLists = false) {
111
- if (!documentFragment.childCount) {
112
- return;
113
- }
114
- const writer = new ViewUpcastWriter(documentFragment.document);
115
- const itemLikeElements = findAllItemLikeElements(documentFragment, writer);
116
- if (!itemLikeElements.length) {
117
- return;
118
- }
119
- // Tracks how many items have been added to each encountered list, keyed by indent level and list ID.
120
- // Used to set the `start` attribute on a new <ol> when a list at a given indent is interrupted by
121
- // a non-list block (e.g. a paragraph) and then resumed.
122
- // Structure: [ { [listId:level]: itemCount } ] (array index is the indent level)
123
- // Example: [ { '1:1': 3 }, { '0:2': 2 } ] means the top-level list (id=1) has 3 items,
124
- // and the nested list (id=0) has 2 items so the next continuation should start at 3.
125
- const encounteredLists = [];
126
- const stack = [];
127
- let topLevelListInfo = createTopLevelListInfo();
128
- for (const itemLikeElement of itemLikeElements){
129
- if (itemLikeElement.indent !== undefined) {
130
- if (!isListContinuation(itemLikeElement)) {
131
- applyIndentationToTopLevelList(writer, stack, topLevelListInfo);
132
- topLevelListInfo = createTopLevelListInfo();
133
- // Clear counters for nested levels only. The top-level counter (index 0) must survive
134
- // so that a resumed top-level list (same id, interrupted by a paragraph) can still
135
- // receive the correct `start` attribute. Nested counters must be cleared because
136
- // a sibling top-level list item should not inherit the nested list counts from
137
- // a previous top-level list item.
138
- encounteredLists.length = 1;
139
- stack.length = 0;
140
- }
141
- // Key used to look up this list inside `encounteredLists[indent]`.
142
- // Combines the list id and level so that two different lists at the same indent
143
- // level (e.g. first an <ol>, then a <ul> after a paragraph break) don't share a counter.
144
- const originalListId = `${itemLikeElement.id}:${itemLikeElement.indent}`;
145
- // When the editor opts into skip-level lists, preserve Word indent gaps (the fill loop below
146
- // inserts `<li style="list-style-type:none">` wrappers for them). Otherwise clamp to one
147
- // level below the current stack top — the original pre-skip-level behavior — so the editor's
148
- // list post-fixer doesn't have to bridge the gap with empty filler paragraphs.
149
- const indent = enableSkipLevelLists ? itemLikeElement.indent - 1 : Math.min(itemLikeElement.indent - 1, stack.length);
150
- // Trimming of the list stack on list ID change.
151
- if (indent < stack.length && stack[indent].id !== itemLikeElement.id) {
152
- // A different list at the top level starts here. `isListContinuation` returned true
153
- // (the previous sibling in the DOM is the prior list's `<ol>/<ul>`), so the outer reset
154
- // path didn't run. Flush the prior list's accumulated margin onto its own `<ol>/<ul>`
155
- // BEFORE the stack reference is replaced — otherwise a later flush would hoist that
156
- // margin onto the wrong (interrupting) list and strip it from the original list's
157
- // `<li>`s. The flush is a no-op unless the prior list actually had uniform-margin
158
- // items pushed, so single-item / mixed-margin lists keep their pre-existing
159
- // per-`<li>` margin semantics.
160
- if (indent == 0 && topLevelListInfo.canApplyMarginOnList && topLevelListInfo.topLevelListItemElements.length > 0) {
161
- applyIndentationToTopLevelList(writer, stack, topLevelListInfo);
162
- topLevelListInfo = createTopLevelListInfo();
163
- }
164
- // A different list started at this indent level — counters for this level and deeper
165
- // belong to the previous list context and must not carry over.
166
- encounteredLists.length = indent;
167
- stack.length = indent;
168
- }
169
- // Trimming of the list stack on lower indent list encountered.
170
- if (indent < stack.length - 1) {
171
- // We jumped back to a shallower indent — any counters deeper than the new top are stale.
172
- encounteredLists.length = indent + 1;
173
- stack.length = indent + 1;
174
- }
175
- const listStyle = detectListStyle(itemLikeElement, stylesString);
176
- // Word can jump indent levels (e.g. from level 1 directly to level 3) without producing
177
- // items for the in-between levels. Fill the missing levels with intermediate wrappers —
178
- // an `<ol>`/`<ul>` of the deepest item's type containing a single `<li style="list-style-type:none">`
179
- // so the resulting view matches the shape the skip-level upcast (`listItemSkipLevelConsumer`) expects.
180
- while(stack.length < indent){
181
- const intermediateList = writer.createElement(listStyle.type);
182
- const intermediateListItem = writer.createElement('li');
183
- writer.setStyle('list-style-type', 'none', intermediateListItem);
184
- if (stack.length == 0) {
185
- const parent = itemLikeElement.element.parent;
186
- const index = parent.getChildIndex(itemLikeElement.element) + 1;
187
- writer.insertChild(index, intermediateList, parent);
188
- } else {
189
- const parentListItems = stack[stack.length - 1].listItemElements;
190
- writer.appendChild(intermediateList, parentListItems[parentListItems.length - 1]);
191
- }
192
- writer.appendChild(intermediateListItem, intermediateList);
193
- stack.push({
194
- ...itemLikeElement,
195
- listElement: intermediateList,
196
- listItemElements: [
197
- intermediateListItem
198
- ],
199
- isIntermediate: true,
200
- // Intermediate wrappers hold no real list item, so they must not pretend to "own" the
201
- // deep item's `margin-left` otherwise `stack.find` (matching non-list multi-block
202
- // continuations by margin) returns the shallower intermediate before the real item.
203
- marginLeft: undefined
204
- });
205
- }
206
- // Create a new OL/UL if required (greater indent or different list type).
207
- if (indent > stack.length - 1 || stack[indent].listElement.name != listStyle.type) {
208
- // If this list was seen before at this indent (i.e. it was interrupted by a non-list block
209
- // and is now resuming), set `start` so the numbering continues from where it left off.
210
- if (listStyle.type == 'ol' && itemLikeElement.id !== undefined && encounteredLists[indent] && encounteredLists[indent][originalListId]) {
211
- listStyle.startIndex = encounteredLists[indent][originalListId];
212
- }
213
- const listElement = createNewEmptyList(listStyle, writer, hasMultiLevelListPlugin);
214
- // Insert the new OL/UL.
215
- if (stack.length == 0) {
216
- const parent = itemLikeElement.element.parent;
217
- const index = parent.getChildIndex(itemLikeElement.element) + 1;
218
- writer.insertChild(index, listElement, parent);
219
- } else if (indent == 0) {
220
- // A real list at root indent while a skip-level intermediate of a different type
221
- // already sits there — insert the new list as a sibling of the intermediate in the
222
- // same parent (can't merge two lists of different types).
223
- const existingList = stack[0].listElement;
224
- const listParent = existingList.parent;
225
- const insertIndex = listParent.getChildIndex(existingList) + 1;
226
- writer.insertChild(insertIndex, listElement, listParent);
227
- } else {
228
- const parentListItems = stack[indent - 1].listItemElements;
229
- writer.appendChild(listElement, parentListItems[parentListItems.length - 1]);
230
- }
231
- // Update the list stack for other items to reference.
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
- if (itemLikeElement.marginLeft === undefined) {
316
- // If at least one of the list items at indent = 0 does not have margin-left style, we cannot set margin-left on the list.
317
- if (indent == 0) {
318
- topLevelListInfo.canApplyMarginOnList = false;
319
- }
320
- return;
321
- }
322
- const listItemBlockMarginLeft = parseFloat(itemLikeElement.marginLeft);
323
- let currentListBlockIndent = 0;
324
- // Sum the relative `margin-left` of the last `<li>` in every ancestor stack frame.
325
- // Browser nesting cumulates: each ancestor `<li>`'s margin pushes its descendants further right,
326
- // so to convert Word's absolute `margin-left` into the editor's relative value we have to subtract
327
- // every ancestor's contribution — not only the immediate parent. Skip-level intermediate wrappers
328
- // contribute 0 (no margin set) and so naturally drop out of the sum.
329
- for(let ancestorIndex = 0; ancestorIndex < stack.length - 1; ancestorIndex++){
330
- const ancestorListItems = stack[ancestorIndex].listItemElements;
331
- const ancestorMargin = ancestorListItems[ancestorListItems.length - 1].getStyle('margin-left');
332
- if (ancestorMargin !== undefined) {
333
- currentListBlockIndent += parseFloat(ancestorMargin);
334
- }
335
- }
336
- // Add 40px for each indent level because by default HTML lists have 40px indentation (padding-inline-start: 40px).
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
- return {
357
- marginLeft: undefined,
358
- canApplyMarginOnList: true,
359
- topLevelListItemElements: []
360
- };
249
+ return {
250
+ marginLeft: void 0,
251
+ canApplyMarginOnList: true,
252
+ topLevelListItemElements: []
253
+ };
361
254
  }
362
255
  /**
363
- * Sets margin-left style to the top-level list if all its items have the same margin-left.
364
- * If margin-left is set on the list, it is removed from all its items to avoid doubling of margins.
365
- */ function applyIndentationToTopLevelList(writer, stack, topLevelListInfo) {
366
- if (topLevelListInfo.canApplyMarginOnList && topLevelListInfo.marginLeft && topLevelListInfo.topLevelListItemElements.length > 0) {
367
- // Apply margin-left to the top-level list if all its items have the same margin-left.
368
- writer.setStyle('margin-left', topLevelListInfo.marginLeft, stack[0].listElement);
369
- // Remove margin-left from all top-level list items.
370
- for (const topLevelListItem of topLevelListInfo.topLevelListItemElements){
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
- * Removes paragraph wrapping content inside a list item.
377
- *
378
- * @internal
379
- */ function unwrapParagraphInListItem(documentFragment, writer) {
380
- for (const value of writer.createRangeIn(documentFragment)){
381
- const element = value.item;
382
- if (element.is('element', 'li')) {
383
- // Google Docs allows for single paragraph inside LI.
384
- const firstChild = element.getChild(0);
385
- if (firstChild && firstChild.is('element', 'p')) {
386
- writer.unwrapElement(firstChild);
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
- * Finds all list-like elements in a given document fragment.
393
- *
394
- * @param documentFragment Document fragment in which to look for list-like nodes.
395
- * @returns Array of found list-like items. Each item is an object containing
396
- * @internal
397
- */ function findAllItemLikeElements(documentFragment, writer) {
398
- const range = writer.createRangeIn(documentFragment);
399
- const itemLikeElements = [];
400
- const foundMargins = new Set();
401
- for (const item of range.getItems()){
402
- // https://github.com/ckeditor/ckeditor5/issues/15964
403
- if (!item.is('element') || !item.name.match(/^(p|h\d+|li|div)$/)) {
404
- continue;
405
- }
406
- // Try to rely on margin-left style to find paragraphs visually aligned with previously encountered list item.
407
- let marginLeft = getMarginLeftNormalized(item);
408
- // Ignore margin-left 0 style if there is no MsoList... class.
409
- if (marginLeft !== undefined && parseFloat(marginLeft) == 0 && !Array.from(item.getClassNames()).find((className)=>className.startsWith('MsoList'))) {
410
- marginLeft = undefined;
411
- }
412
- // List item or a following list item block.
413
- if (item.hasStyle('mso-list') && item.getStyle('mso-list') !== 'none' || marginLeft !== undefined && foundMargins.has(marginLeft)) {
414
- const itemData = getListItemData(item);
415
- itemLikeElements.push({
416
- element: item,
417
- id: itemData.id,
418
- order: itemData.order,
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
- * Whether the given element is possibly a list continuation. Previous element was wrapped into a list
433
- * or the current element already is inside a list.
434
- */ function isListContinuation(currentItem) {
435
- let previousSibling = currentItem.element.previousSibling;
436
- // Skip past stray inline markers that Word leaves between list paragraphs (e.g. empty
437
- // `<span style='mso-bookmark:...'></span>` or empty `<o:p></o:p>`). They have no visual effect
438
- // but would otherwise break list continuity here — which matters most for nested items pasted
439
- // right after their parent, where breaking the chain causes PFO to clamp the deeper item to
440
- // indent 0 instead of nesting it.
441
- while(previousSibling && isStrayInlineMarker(previousSibling)){
442
- previousSibling = previousSibling.previousSibling;
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
- * True for empty inline elements Word emits as residue between paragraphs (`<span>`, `<a>`, `<o:p>`).
455
- * Used by `isListContinuation` to look past these when checking whether the prior block is a list —
456
- * they're layout artefacts, not real content.
457
- */ function isStrayInlineMarker(node) {
458
- return node.is('element') && node.childCount === 0 && /^(?:span|a|o:p)$/.test(node.name);
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
- return element.is('element', 'ol') || element.is('element', 'ul');
330
+ return element.is("element", "ol") || element.is("element", "ul");
462
331
  }
463
332
  /**
464
- * Extracts list item style from the provided CSS.
465
- *
466
- * List item style is extracted from the CSS stylesheet. Each list with its specific style attribute
467
- * value (`mso-list:l1 level1 lfo1`) has its dedicated properties in a CSS stylesheet defined with a selector like:
468
- *
469
- * ```css
470
- * @list l1:level1 { ... }
471
- * ```
472
- *
473
- * It contains `mso-level-number-format` property which defines list numbering/bullet style. If this property
474
- * is not defined it means default `decimal` numbering.
475
- *
476
- * Here CSS string representation is used as `mso-level-number-format` property is an invalid CSS property
477
- * and will be removed during CSS parsing.
478
- *
479
- * @param listLikeItem List-like item for which list style will be searched for. Usually
480
- * a result of `findAllItemLikeElements()` function.
481
- * @param stylesString CSS stylesheet.
482
- * @returns An object with properties:
483
- *
484
- * * type - List type, could be `ul` or `ol`.
485
- * * startIndex - List start index, valid only for ordered lists.
486
- * * style - List style, for example: `decimal`, `lower-roman`, etc. It is extracted
487
- * directly from Word stylesheet and adjusted to represent proper values for the CSS `list-style-type` property.
488
- * If it cannot be adjusted, the `null` value is returned.
489
- */ function detectListStyle(listLikeItem, stylesString) {
490
- const listStyleRegexp = new RegExp(`@list l${listLikeItem.id}:level${listLikeItem.indent}\\s*({[^}]*)`, 'gi');
491
- const listStyleTypeRegex = /mso-level-number-format:([^;]{0,100});/gi;
492
- const listStartIndexRegex = /mso-level-start-at:\s{0,100}([0-9]{0,10})\s{0,100};/gi;
493
- const legalStyleListRegex = new RegExp(`@list\\s+l${listLikeItem.id}:level\\d\\s*{[^{]*mso-level-text:"%\\d\\\\.`, 'gi');
494
- const multiLevelNumberFormatTypeRegex = new RegExp(`@list l${listLikeItem.id}:level\\d\\s*{[^{]*mso-level-number-format:`, 'gi');
495
- const legalStyleListMatch = legalStyleListRegex.exec(stylesString);
496
- const multiLevelNumberFormatMatch = multiLevelNumberFormatTypeRegex.exec(stylesString);
497
- // Multi level lists in Word have mso-level-number-format attribute except legal lists,
498
- // so we used that. If list has legal list match and doesn't has mso-level-number-format
499
- // then this is legal-list.
500
- const islegalStyleList = legalStyleListMatch && !multiLevelNumberFormatMatch;
501
- const listStyleMatch = listStyleRegexp.exec(stylesString);
502
- let listStyleType = 'decimal'; // Decimal is default one.
503
- let type = 'ol'; // <ol> is default list.
504
- let startIndex = null;
505
- if (listStyleMatch && listStyleMatch[1]) {
506
- const listStyleTypeMatch = listStyleTypeRegex.exec(listStyleMatch[1]);
507
- if (listStyleTypeMatch && listStyleTypeMatch[1]) {
508
- listStyleType = listStyleTypeMatch[1].trim();
509
- type = listStyleType !== 'bullet' && listStyleType !== 'image' ? 'ol' : 'ul';
510
- }
511
- // Styles for the numbered lists are always defined in the Word CSS stylesheet.
512
- // Unordered lists MAY contain a value for the Word CSS definition `mso-level-text` but sometimes
513
- // this tag is missing. And because of that, we cannot depend on that. We need to predict the list style value
514
- // based on the list style marker element.
515
- if (listStyleType === 'bullet') {
516
- const bulletedStyle = findBulletedListStyle(listLikeItem.element);
517
- if (bulletedStyle) {
518
- listStyleType = bulletedStyle;
519
- }
520
- } else {
521
- const listStartIndexMatch = listStartIndexRegex.exec(listStyleMatch[1]);
522
- if (listStartIndexMatch && listStartIndexMatch[1]) {
523
- startIndex = parseInt(listStartIndexMatch[1]);
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
- * Tries to extract the `list-style-type` value based on the marker element for bulleted list.
539
- */ function findBulletedListStyle(element) {
540
- // https://github.com/ckeditor/ckeditor5/issues/15964
541
- if (element.name == 'li' && element.parent.name == 'ul' && element.parent.hasAttribute('type')) {
542
- return element.parent.getAttribute('type');
543
- }
544
- const listMarkerElement = findListMarkerNode(element);
545
- if (!listMarkerElement) {
546
- return null;
547
- }
548
- const listMarker = listMarkerElement._data;
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
- * Tries to find a text node that represents the marker element (list-style-type).
560
- */ function findListMarkerNode(element) {
561
- // If the first child is a text node, it is the data for the element.
562
- // The list-style marker is not present here.
563
- if (element.getChild(0).is('$text')) {
564
- return null;
565
- }
566
- for (const childNode of element.getChildren()){
567
- // The list-style marker will be inside the `<span>` element. Let's ignore all non-span elements.
568
- // It may happen that the `<a>` element is added as the first child. Most probably, it's an anchor element.
569
- if (!childNode.is('element', 'span')) {
570
- continue;
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
- * Parses the `list-style-type` value extracted directly from the Word CSS stylesheet and returns proper CSS definition.
586
- */ function mapListStyleDefinition(value) {
587
- if (value.startsWith('arabic-leading-zero')) {
588
- return 'decimal-leading-zero';
589
- }
590
- switch(value){
591
- case 'alpha-upper':
592
- return 'upper-alpha';
593
- case 'alpha-lower':
594
- return 'lower-alpha';
595
- case 'roman-upper':
596
- return 'upper-roman';
597
- case 'roman-lower':
598
- return 'lower-roman';
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
- * Creates a new list OL/UL element.
609
- */ function createNewEmptyList(listStyle, writer, hasMultiLevelListPlugin) {
610
- const list = writer.createElement(listStyle.type);
611
- applyListStyleToElement(list, listStyle, writer, hasMultiLevelListPlugin);
612
- return list;
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
- * Applies `list-style-type`, `start`, and the `legal-list` class to a list element based on the detected
616
- * list style. Used both when creating a fresh list and when a real item claims a previously-intermediate
617
- * wrapper (which was created without any of these).
618
- */ function applyListStyleToElement(list, listStyle, writer, hasMultiLevelListPlugin) {
619
- // We do not support modifying the marker for a particular list item.
620
- // Set the value for the `list-style-type` property directly to the list container.
621
- if (listStyle.style) {
622
- writer.setStyle('list-style-type', listStyle.style, list);
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
- * Extracts list item information from Word specific list-like element style:
633
- *
634
- * ```
635
- * `style="mso-list:l1 level1 lfo1"`
636
- * ```
637
- *
638
- * where:
639
- *
640
- * ```
641
- * * `l1` is a list id (however it does not mean this is a continuous list - see https://github.com/ckeditor/ckeditor5/issues/43),
642
- * * `level1` is a list item indentation level,
643
- * * `lfo1` is a list insertion order in a document.
644
- * ```
645
- *
646
- * @param element Element from which style data is extracted.
647
- */ function getListItemData(element) {
648
- const listStyle = element.getStyle('mso-list');
649
- if (listStyle === undefined) {
650
- return {};
651
- }
652
- const idMatch = listStyle.match(/(^|\s{1,100})l(\d+)/i);
653
- const orderMatch = listStyle.match(/\s{0,100}lfo(\d+)/i);
654
- const indentMatch = listStyle.match(/\s{0,100}level(\d+)/i);
655
- if (idMatch && orderMatch && indentMatch) {
656
- return {
657
- id: idMatch[2],
658
- order: orderMatch[1],
659
- indent: parseInt(indentMatch[1])
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
- * Removes span with a numbering/bullet from a given element.
668
- */ function removeBulletElement(element, writer) {
669
- // Matcher for finding `span` elements holding lists numbering/bullets.
670
- const bulletMatcher = new Matcher({
671
- name: 'span',
672
- styles: {
673
- 'mso-list': 'Ignore'
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
- * Returns element left margin normalized to 'px' if possible.
685
- */ function getMarginLeftNormalized(element) {
686
- const value = element.getStyle('margin-left');
687
- if (value === undefined || value.endsWith('px')) {
688
- return value;
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
- * Replaces source attribute of all `<img>` elements representing regular
695
- * images (not the Word shapes) with inlined base64 image representation extracted from RTF or Blob data.
696
- *
697
- * @param documentFragment Document fragment on which transform images.
698
- * @param rtfData The RTF data from which images representation will be used.
699
- * @internal
700
- */ function replaceImagesSourceWithBase64(documentFragment, rtfData) {
701
- if (!documentFragment.childCount) {
702
- return;
703
- }
704
- const upcastWriter = new ViewUpcastWriter(documentFragment.document);
705
- const shapesIds = findAllShapesIds(documentFragment, upcastWriter);
706
- removeAllImgElementsRepresentingShapes(shapesIds, documentFragment, upcastWriter);
707
- insertMissingImgs(shapesIds, documentFragment, upcastWriter);
708
- removeAllShapeElements(documentFragment, upcastWriter);
709
- const images = findAllImageElementsWithLocalSource(documentFragment, upcastWriter);
710
- if (images.length) {
711
- replaceImagesFileSourceWithInlineRepresentation(images, extractImageDataFromRtf(rtfData), upcastWriter);
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
- * Converts given HEX string to base64 representation.
716
- *
717
- * @internal
718
- * @param hexString The HEX string to be converted.
719
- * @returns Base64 representation of a given HEX string.
720
- */ function _convertHexToBase64(hexString) {
721
- return btoa(hexString.match(/\w{2}/g).map((char)=>{
722
- return String.fromCharCode(parseInt(char, 16));
723
- }).join(''));
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
- * Finds all shapes (`<v:*>...</v:*>`) ids. Shapes can represent images (canvas)
727
- * or Word shapes (which does not have RTF or Blob representation).
728
- *
729
- * @param documentFragment Document fragment from which to extract shape ids.
730
- * @returns Array of shape ids.
731
- */ function findAllShapesIds(documentFragment, writer) {
732
- const range = writer.createRangeIn(documentFragment);
733
- const shapeElementsMatcher = new Matcher({
734
- name: /v:(.+)/
735
- });
736
- const shapesIds = [];
737
- for (const value of range){
738
- if (value.type != 'elementStart') {
739
- continue;
740
- }
741
- const el = value.item;
742
- const previousSibling = el.previousSibling;
743
- const prevSiblingName = previousSibling && previousSibling.is('element') ? previousSibling.name : null;
744
- // List of ids which should not be considered as shapes.
745
- // https://github.com/ckeditor/ckeditor5/pull/15847#issuecomment-1941543983
746
- const exceptionIds = [
747
- 'Chart'
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
- * Removes all `<img>` elements which represents Word shapes and not regular images.
763
- *
764
- * @param shapesIds Shape ids which will be checked against `<img>` elements.
765
- * @param documentFragment Document fragment from which to remove `<img>` elements.
766
- */ function removeAllImgElementsRepresentingShapes(shapesIds, documentFragment, writer) {
767
- const range = writer.createRangeIn(documentFragment);
768
- const imageElementsMatcher = new Matcher({
769
- name: 'img'
770
- });
771
- const imgs = [];
772
- for (const value of range){
773
- if (value.item.is('element') && imageElementsMatcher.match(value.item)) {
774
- const el = value.item;
775
- const shapes = el.getAttribute('v:shapes') ? el.getAttribute('v:shapes').split(' ') : [];
776
- if (shapes.length && shapes.every((shape)=>shapesIds.indexOf(shape) > -1)) {
777
- imgs.push(el);
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
- * Removes all shape elements (`<v:*>...</v:*>`) so they do not pollute the output structure.
790
- *
791
- * @param documentFragment Document fragment from which to remove shape elements.
792
- */ function removeAllShapeElements(documentFragment, writer) {
793
- const range = writer.createRangeIn(documentFragment);
794
- const shapeElementsMatcher = new Matcher({
795
- name: /v:(.+)/
796
- });
797
- const shapes = [];
798
- for (const value of range){
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
- * Inserts `img` tags if there is none after a shape.
809
- */ function insertMissingImgs(shapeIds, documentFragment, writer) {
810
- const range = writer.createRangeIn(documentFragment);
811
- const shapes = [];
812
- for (const value of range){
813
- if (value.type == 'elementStart' && value.item.is('element', 'v:shape')) {
814
- const id = value.item.getAttribute('id');
815
- if (shapeIds.includes(id)) {
816
- continue;
817
- }
818
- if (!containsMatchingImg(value.item.parent.getChildren(), id)) {
819
- shapes.push(value.item);
820
- }
821
- }
822
- }
823
- for (const shape of shapes){
824
- const attrs = {
825
- src: findSrc(shape)
826
- };
827
- if (shape.hasAttribute('alt')) {
828
- attrs.alt = shape.getAttribute('alt');
829
- }
830
- const img = writer.createElement('img', attrs);
831
- writer.insertChild(shape.index + 1, img, shape.parent);
832
- }
833
- function containsMatchingImg(nodes, id) {
834
- for (const node of nodes){
835
- /* istanbul ignore else -- @preserve */ if (node.is('element')) {
836
- if (node.name == 'img' && node.getAttribute('v:shapes') == id) {
837
- return true;
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
- * Finds all `<img>` elements in a given document fragment which have source pointing to local `file://` resource.
856
- * This function also tracks the index position of each image in the document, which is essential for
857
- * precise matching with hexadecimal representations in RTF data.
858
- *
859
- * @param documentFragment Document fragment in which to look for `<img>` elements.
860
- * @returns Array of found images along with their position index in the document.
861
- */ function findAllImageElementsWithLocalSource(documentFragment, writer) {
862
- const range = writer.createRangeIn(documentFragment);
863
- const imageElementsMatcher = new Matcher({
864
- name: 'img'
865
- });
866
- const imgs = [];
867
- let currentImageIndex = 0;
868
- for (const value of range){
869
- if (value.item.is('element') && imageElementsMatcher.match(value.item)) {
870
- if (value.item.getAttribute('src').startsWith('file://')) {
871
- imgs.push({
872
- element: value.item,
873
- imageIndex: currentImageIndex
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
- * Extracts all images HEX representations from a given RTF data.
883
- *
884
- * @param rtfData The RTF data from which to extract images HEX representation.
885
- * @returns Array of found HEX representations. Each array item is an object containing:
886
- *
887
- * * hex Image representation in HEX format.
888
- * * type Type of image, `image/png` or `image/jpeg`.
889
- */ function extractImageDataFromRtf(rtfData) {
890
- if (!rtfData) {
891
- return [];
892
- }
893
- const regexPictureHeader = /{\\pict[\s\S]+?\\bliptag-?\d+(\\blipupi-?\d+)?({\\\*\\blipuid\s?[\da-fA-F]+)?[\s}]*?/;
894
- const regexPicture = new RegExp('(?:(' + regexPictureHeader.source + '))([\\da-fA-F\\s]+)\\}', 'g');
895
- const images = rtfData.match(regexPicture);
896
- const result = [];
897
- if (images) {
898
- for (const image of images){
899
- let imageType = false;
900
- if (image.includes('\\pngblip')) {
901
- imageType = 'image/png';
902
- } else if (image.includes('\\jpegblip')) {
903
- imageType = 'image/jpeg';
904
- }
905
- if (imageType) {
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
- * Replaces `src` attribute value of all given images with the corresponding base64 image representation.
917
- * Uses the image index to precisely match with the correct hexadecimal representation from RTF data.
918
- *
919
- * @param imageElements Array of image elements along with their indices which will have their sources replaced.
920
- * @param imagesHexSources Array of images hex sources (usually the result of `extractImageDataFromRtf()` function).
921
- * Contains hexadecimal representations of ALL images in the document, not just those with `file://` URLs.
922
- * In XML documents, the same image might be defined both as base64 in HTML and as hexadecimal in RTF data.
923
- */ function replaceImagesFileSourceWithInlineRepresentation(imageElements, imagesHexSources, writer) {
924
- for(let i = 0; i < imageElements.length; i++){
925
- const { element, imageIndex } = imageElements[i];
926
- const rtfHexSource = imagesHexSources[imageIndex];
927
- if (rtfHexSource) {
928
- const newSrc = `data:${rtfHexSource.type};base64,${_convertHexToBase64(rtfHexSource.hex)}`;
929
- writer.setAttribute('src', newSrc, element);
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
- * Cleanup MS attributes like styles, attributes and elements.
936
- *
937
- * @param documentFragment element `data.content` obtained from clipboard.
938
- * @internal
939
- */ function removeMSAttributes(documentFragment) {
940
- const elementsToUnwrap = [];
941
- const writer = new ViewUpcastWriter(documentFragment.document);
942
- for (const { item } of writer.createRangeIn(documentFragment)){
943
- if (!item.is('element')) {
944
- continue;
945
- }
946
- for (const className of item.getClassNames()){
947
- if (/\bmso/gi.exec(className)) {
948
- writer.removeClass(className, item);
949
- }
950
- }
951
- for (const styleName of item.getStyleNames()){
952
- if (/\bmso/gi.exec(styleName)) {
953
- writer.removeStyle(styleName, item);
954
- }
955
- }
956
- if (item.is('element', 'w:sdt') || item.is('element', 'w:sdtpr') && item.isEmpty || item.is('element', 'o:p') && item.isEmpty) {
957
- elementsToUnwrap.push(item);
958
- }
959
- }
960
- for (const item of elementsToUnwrap){
961
- const itemParent = item.parent;
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
- * Applies border none for table and cells without a border specified.
970
- * Normalizes style length units to px.
971
- * Handles left block table alignment.
972
- *
973
- * @internal
974
- */ function transformTables(documentFragment, writer, hasTablePropertiesPlugin = false) {
975
- for (const item of writer.createRangeIn(documentFragment).getItems()){
976
- if (!item.is('element', 'table') && !item.is('element', 'td') && !item.is('element', 'th')) {
977
- continue;
978
- }
979
- // In MS Word, left-aligned tables (default) have no align attribute on the `<table>` and are not wrapped in a `<div>`.
980
- // In such cases, we need to set `margin-left: 0` and `margin-right: auto` to indicate to the editor that
981
- // the table is block-aligned to the left.
982
- //
983
- // Center- and right-aligned tables in MS Word are wrapped in a `<div>` with the `align` attribute set to
984
- // `center` or `right`, respectively with no align attribute on the `<table>` itself.
985
- //
986
- // Additionally, the structure may change when pasting content from MS Word.
987
- // Some browsers (e.g., Safari) may insert extra elements around the table (e.g., a <span>),
988
- // so the surrounding `<div>` with the `align` attribute may end up being the table's grandparent.
989
- if (hasTablePropertiesPlugin && item.is('element', 'table')) {
990
- const directParent = item.parent?.is('element', 'div') ? item.parent : null;
991
- const grandParent = item.parent?.parent?.is('element', 'div') ? item.parent.parent : null;
992
- const divParent = directParent ?? grandParent;
993
- // Center block table alignment.
994
- if (divParent && divParent.getAttribute('align') === 'center' && !item.getAttribute('align')) {
995
- writer.setStyle('margin-left', 'auto', item);
996
- writer.setStyle('margin-right', 'auto', item);
997
- } else if (divParent && divParent.getAttribute('align') === 'right' && !item.getAttribute('align')) {
998
- writer.setStyle('margin-left', 'auto', item);
999
- writer.setStyle('margin-right', '0', item);
1000
- } else if (!divParent && !item.getAttribute('align')) {
1001
- writer.setStyle('margin-left', '0', item);
1002
- writer.setStyle('margin-right', 'auto', item);
1003
- }
1004
- }
1005
- const sides = [
1006
- 'left',
1007
- 'top',
1008
- 'right',
1009
- 'bottom'
1010
- ];
1011
- // As this is a pasted table, we do not want default table styles to apply here
1012
- // so we set border node for sides that does not have any border style.
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
- * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1040
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1041
- */ /**
1042
- * @module paste-from-office/filters/removeinvalidtablewidth
1043
- */ /**
1044
- * Removes the `width:0px` style from table pasted from Google Sheets and `width="0"` attribute from Word tables.
1045
- *
1046
- * @param documentFragment element `data.content` obtained from clipboard
1047
- * @internal
1048
- */ function removeInvalidTableWidth(documentFragment, writer) {
1049
- for (const child of writer.createRangeIn(documentFragment).getItems()){
1050
- if (child.is('element', 'table')) {
1051
- // Remove invalid width style (Google Sheets: width:0px).
1052
- if (child.getStyle('width') === '0px') {
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
- * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1065
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1066
- */ /**
1067
- * @module paste-from-office/filters/replacemsfootnotes
1068
- */ /**
1069
- * Replaces MS Word specific footnotes references and definitions with proper elements.
1070
- *
1071
- * Things to know about MS Word footnotes:
1072
- *
1073
- * * Footnote references in Word are marked with `mso-footnote-id` style.
1074
- * * Word does not support nested footnotes, so references within definitions are ignored.
1075
- * * Word appends extra spaces after footnote references within definitions, which are trimmed.
1076
- * * Footnote definitions list is marked with `mso-element: footnote-list` style it contain `mso-element: footnote` elements.
1077
- * * Footnote definition might contain tables, lists and other elements, not only text. They are placed directly within `li` element,
1078
- * without any wrapper (in opposition to text content of the definition, which is placed within `MsoFootnoteText` element).
1079
- *
1080
- * Example pseudo document showing MS Word footnote structure:
1081
- *
1082
- * ```html
1083
- * <p>Text with footnote<a style='mso-footnote-id:ftn1'>[1]</a> reference.</p>
1084
- *
1085
- * <div style='mso-element:footnote-list'>
1086
- * <div style='mso-element:footnote' id=ftn1>
1087
- * <p class=MsoFootnoteText><a style='mso-footnote-id:ftn1'>[1]</a> Footnote content</p>
1088
- * <table class="MsoTableGrid">...</table>
1089
- * </div>
1090
- * </div>
1091
- * ```
1092
- *
1093
- * Will be transformed into:
1094
- *
1095
- * ```html
1096
- * <p>Text with footnote<sup class="footnote"><a id="ref-footnote-ftn1" href="#footnote-ftn1">1</a></sup> reference.</p>
1097
- *
1098
- * <ol class="footnotes">
1099
- * <li class="footnote-definition" id="footnote-ftn1">
1100
- * <a href="#ref-footnote-ftn1" class="footnote-backlink">^</a>
1101
- * <div class="footnote-content">
1102
- * <p>Footnote content</p>
1103
- * <table>...</table>
1104
- * </div>
1105
- * </li>
1106
- * </ol>
1107
- * ```
1108
- *
1109
- * @param documentFragment `data.content` obtained from clipboard.
1110
- * @param writer The view writer instance.
1111
- * @internal
1112
- */ function replaceMSFootnotes(documentFragment, writer) {
1113
- const msFootnotesRefs = new Map();
1114
- const msFootnotesDefs = new Map();
1115
- let msFootnotesDefinitionsList = null;
1116
- // Phase 1: Collect all footnotes references and definitions. Find the footnotes definitions list element.
1117
- for (const { item } of writer.createRangeIn(documentFragment)){
1118
- if (!item.is('element')) {
1119
- continue;
1120
- }
1121
- // If spot a footnotes definitions element, let's store it. It'll be replaced later.
1122
- // There should be only one such element in the document.
1123
- if (item.getStyle('mso-element') === 'footnote-list') {
1124
- msFootnotesDefinitionsList = item;
1125
- continue;
1126
- }
1127
- // If spot a footnote reference or definition, store it in the corresponding map.
1128
- if (item.hasStyle('mso-footnote-id')) {
1129
- const msFootnoteDef = item.findAncestor('element', (el)=>el.getStyle('mso-element') === 'footnote');
1130
- if (msFootnoteDef) {
1131
- // If it's a reference within a definition, ignore it and track only the definition.
1132
- // MS Word do not support nested footnotes, so it's safe to assume that all references within
1133
- // a definition point to the same definition.
1134
- const msFootnoteDefId = msFootnoteDef.getAttribute('id');
1135
- msFootnotesDefs.set(msFootnoteDefId, msFootnoteDef);
1136
- } else {
1137
- // If it's a reference outside of a definition, track it as a reference.
1138
- const msFootnoteRefId = item.getStyle('mso-footnote-id');
1139
- msFootnotesRefs.set(msFootnoteRefId, item);
1140
- }
1141
- continue;
1142
- }
1143
- }
1144
- // If there are no footnotes references or definitions, or no definitions list, there's nothing to normalize.
1145
- if (!msFootnotesRefs.size || !msFootnotesDefinitionsList) {
1146
- return;
1147
- }
1148
- // Phase 2: Replace footnotes definitions list with proper element.
1149
- const footnotesDefinitionsList = createFootnotesListViewElement(writer);
1150
- writer.replace(msFootnotesDefinitionsList, footnotesDefinitionsList);
1151
- // Phase 3: Replace all footnotes references and add matching definitions to the definitions list.
1152
- for (const [footnoteId, msFootnoteRef] of msFootnotesRefs){
1153
- const msFootnoteDef = msFootnotesDefs.get(footnoteId);
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
- * Removes all MS Office specific references from the given element.
1176
- *
1177
- * It also removes leading space from text nodes following the references, as MS Word adds
1178
- * them to separate the reference from the rest of the text.
1179
- *
1180
- * @param writer The view writer.
1181
- * @param element The element to trim.
1182
- * @returns The trimmed element.
1183
- */ function removeMSReferences(writer, element) {
1184
- const elementsToRemove = [];
1185
- const textNodesToTrim = [];
1186
- for (const { item } of writer.createRangeIn(element)){
1187
- if (item.is('element') && item.getStyle('mso-footnote-id')) {
1188
- elementsToRemove.unshift(item);
1189
- // MS Word used to add spaces after footnote references within definitions. Let's check if there's a space after
1190
- // the footnote reference and mark it for trimming.
1191
- const { nextSibling } = item;
1192
- if (nextSibling?.is('$text') && nextSibling.data.startsWith(' ')) {
1193
- textNodesToTrim.unshift(nextSibling);
1194
- }
1195
- }
1196
- }
1197
- for (const element of elementsToRemove){
1198
- writer.remove(element);
1199
- }
1200
- // Remove only the leading space from text nodes following reference within definition, preserve the rest of the text.
1201
- for (const textNode of textNodesToTrim){
1202
- const trimmedData = textNode.data.substring(1);
1203
- if (trimmedData.length > 0) {
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
- * Creates a footnotes list view element.
1219
- *
1220
- * @param writer The view writer instance.
1221
- * @returns The footnotes list view element.
1222
- */ function createFootnotesListViewElement(writer) {
1223
- return writer.createElement('ol', {
1224
- class: 'footnotes'
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
- * Creates a footnote reference view element.
1229
- *
1230
- * @param writer The view writer instance.
1231
- * @param footnoteId The footnote ID.
1232
- * @returns The footnote reference view element.
1233
- */ function createFootnoteRefViewElement(writer, footnoteId) {
1234
- const sup = writer.createElement('sup', {
1235
- class: 'footnote'
1236
- });
1237
- const link = writer.createElement('a', {
1238
- id: `ref-${footnoteId}`,
1239
- href: `#${footnoteId}`
1240
- });
1241
- writer.appendChild(link, sup);
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
- * Creates a footnote definition view element with a backlink and a content container.
1246
- *
1247
- * @param writer The view writer instance.
1248
- * @param footnoteId The footnote ID.
1249
- * @returns An object containing the list item element, backlink and content container.
1250
- */ function createFootnoteDefViewElement(writer, footnoteId) {
1251
- const listItem = writer.createElement('li', {
1252
- id: footnoteId,
1253
- class: 'footnote-definition'
1254
- });
1255
- const backLink = writer.createElement('a', {
1256
- href: `#ref-${footnoteId}`,
1257
- class: 'footnote-backlink'
1258
- });
1259
- const content = writer.createElement('div', {
1260
- class: 'footnote-content'
1261
- });
1262
- writer.appendChild(writer.createText('^'), backLink);
1263
- writer.appendChild(backLink, listItem);
1264
- writer.appendChild(content, listItem);
1265
- return {
1266
- listItem,
1267
- content
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
- * Normalizer for the content pasted from Microsoft Word.
1275
- */ class PasteFromOfficeMSWordNormalizer {
1276
- document;
1277
- hasMultiLevelListPlugin;
1278
- hasTablePropertiesPlugin;
1279
- enableSkipLevelLists;
1280
- /**
1281
- * Creates a new `PasteFromOfficeMSWordNormalizer` instance.
1282
- *
1283
- * @param document View document.
1284
- */ constructor(document, hasMultiLevelListPlugin = false, hasTablePropertiesPlugin = false, enableSkipLevelLists = false){
1285
- this.document = document;
1286
- this.hasMultiLevelListPlugin = hasMultiLevelListPlugin;
1287
- this.hasTablePropertiesPlugin = hasTablePropertiesPlugin;
1288
- this.enableSkipLevelLists = enableSkipLevelLists;
1289
- }
1290
- /**
1291
- * @inheritDoc
1292
- */ isActive(htmlString) {
1293
- return msWordMatch1.test(htmlString) || msWordMatch2.test(htmlString);
1294
- }
1295
- /**
1296
- * @inheritDoc
1297
- */ execute(data) {
1298
- const writer = new ViewUpcastWriter(this.document);
1299
- const stylesString = data.extraContent.stylesString;
1300
- transformBookmarks(data.content, writer);
1301
- transformListItemLikeElementsIntoLists(data.content, stylesString, this.hasMultiLevelListPlugin, this.enableSkipLevelLists);
1302
- replaceImagesSourceWithBase64(data.content, data.dataTransfer.getData('text/rtf'));
1303
- transformTables(data.content, writer, this.hasTablePropertiesPlugin);
1304
- removeInvalidTableWidth(data.content, writer);
1305
- replaceMSFootnotes(data.content, writer);
1306
- removeMSAttributes(data.content);
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
- * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1312
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1313
- */ /**
1314
- * @module paste-from-office/filters/removeboldwrapper
1315
- */ /**
1316
- * Removes the `<b>` tag wrapper added by Google Docs to a copied content.
1317
- *
1318
- * @param documentFragment element `data.content` obtained from clipboard
1319
- * @internal
1320
- */ function removeBoldWrapper(documentFragment, writer) {
1321
- for (const child of documentFragment.getChildren()){
1322
- if (child.is('element', 'b') && child.getStyle('font-weight') === 'normal') {
1323
- const childIndex = documentFragment.getChildIndex(child);
1324
- writer.remove(child);
1325
- writer.insertChild(childIndex, child.getChildren(), documentFragment);
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
- * Transforms `<br>` elements that are siblings to some block element into a paragraphs.
1332
- *
1333
- * @param documentFragment The view structure to be transformed.
1334
- * @internal
1335
- */ function transformBlockBrsToParagraphs(documentFragment, writer) {
1336
- const viewDocument = new ViewDocument(writer.document.stylesProcessor);
1337
- const domConverter = new ViewDomConverter(viewDocument, {
1338
- renderingMode: 'data'
1339
- });
1340
- const blockElements = domConverter.blockElements;
1341
- const inlineObjectElements = domConverter.inlineObjectElements;
1342
- const elementsToReplace = [];
1343
- for (const value of writer.createRangeIn(documentFragment)){
1344
- const element = value.item;
1345
- if (element.is('element', 'br')) {
1346
- const nextSibling = findSibling(element, 'forward', writer, {
1347
- blockElements,
1348
- inlineObjectElements
1349
- });
1350
- const previousSibling = findSibling(element, 'backward', writer, {
1351
- blockElements,
1352
- inlineObjectElements
1353
- });
1354
- const nextSiblingIsBlock = isBlockViewElement(nextSibling, blockElements);
1355
- const previousSiblingIsBlock = isBlockViewElement(previousSibling, blockElements);
1356
- // If the <br> is surrounded by blocks then convert it to a paragraph:
1357
- // * <p>foo</p>[<br>]<p>bar</p> -> <p>foo</p>[<p></p>]<p>bar</p>
1358
- // * <p>foo</p>[<br>] -> <p>foo</p>[<p></p>]
1359
- // * [<br>]<p>foo</p> -> [<p></p>]<p>foo</p>
1360
- if (previousSiblingIsBlock || nextSiblingIsBlock) {
1361
- elementsToReplace.push(element);
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
- * Returns sibling node, threats inline elements as transparent (but should stop on an inline objects).
1375
- */ function findSibling(viewElement, direction, writer, { blockElements, inlineObjectElements }) {
1376
- let position = writer.createPositionAt(viewElement, direction == 'forward' ? 'after' : 'before');
1377
- // Find first position that is just before a first:
1378
- // * text node,
1379
- // * block element,
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
- * Returns true for view elements that are listed as block view elements.
1389
- */ function isBlockViewElement(node, blockElements) {
1390
- return !!node && node.is('element') && blockElements.includes(node.name);
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
- * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1395
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1396
- */ /**
1397
- * @module paste-from-office/filters/replacetabswithinprewithspaces
1398
- */ /**
1399
- * Replaces tab characters with spaces in text nodes that are inside elements styled with `white-space: pre-wrap`.
1400
- *
1401
- * This is a workaround for incorrect detection of pre-like formatting in the DOM converter for pasted Google Docs documents.
1402
- * When an element uses `white-space: pre-wrap`, the editor reduces tab characters to a single space, causing
1403
- * inconsistent spacing in pasted content. This function replaces tabs with spaces to ensure visual consistency.
1404
- * This is intended as a temporary solution.
1405
- *
1406
- * See: https://github.com/ckeditor/ckeditor5/issues/18995
1407
- *
1408
- * @param documentFragment The `data.content` element obtained from the clipboard.
1409
- * @param writer The upcast writer used to manipulate the view structure.
1410
- * @param tabWidth The number of spaces to replace each tab with. Defaults to 8.
1411
- * @internal
1412
- */ function replaceTabsWithinPreWithSpaces(documentFragment, writer, tabWidth) {
1413
- // Collect all text nodes with tabs that are inside pre-wrap elements.
1414
- const textNodesToReplace = new Set();
1415
- for (const child of writer.createRangeIn(documentFragment).getItems()){
1416
- if (!child.is('view:$textProxy') || !child.data.includes('\t')) {
1417
- continue;
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
- * Checks if element or any of its parents has `white-space: pre-wrap` style.
1431
- */ function hasPreWrapParent(element) {
1432
- let parent = element;
1433
- while(parent){
1434
- if (parent.is('element')) {
1435
- const whiteSpace = parent.getStyle?.('white-space');
1436
- if (whiteSpace === 'pre-wrap') {
1437
- return true;
1438
- }
1439
- }
1440
- parent = parent.parent;
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
- * Replaces all tabs with spaces in the given text node.
1446
- */ function replaceTabsInTextNode(textNode, writer, tabWidth) {
1447
- const { parent, data } = textNode;
1448
- const replacedData = data.replaceAll('\t', ' '.repeat(tabWidth));
1449
- const index = parent.getChildIndex(textNode);
1450
- // Remove old node and insert new one with replaced tabs.
1451
- writer.remove(textNode);
1452
- writer.insertChild(index, writer.createText(replacedData), parent);
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
- * Normalizer for the content pasted from Google Docs.
1458
- *
1459
- * @internal
1460
- */ class GoogleDocsNormalizer {
1461
- document;
1462
- /**
1463
- * Creates a new `GoogleDocsNormalizer` instance.
1464
- *
1465
- * @param document View document.
1466
- */ constructor(document){
1467
- this.document = document;
1468
- }
1469
- /**
1470
- * @inheritDoc
1471
- */ isActive(htmlString) {
1472
- return googleDocsMatch.test(htmlString);
1473
- }
1474
- /**
1475
- * @inheritDoc
1476
- */ execute(data) {
1477
- const writer = new ViewUpcastWriter(this.document);
1478
- removeBoldWrapper(data.content, writer);
1479
- unwrapParagraphInListItem(data.content, writer);
1480
- transformBlockBrsToParagraphs(data.content, writer);
1481
- replaceTabsWithinPreWithSpaces(data.content, writer, 8);
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
- * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1487
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1488
- */ /**
1489
- * @module paste-from-office/filters/removexmlns
1490
- */ /**
1491
- * Removes the `xmlns` attribute from table pasted from Google Sheets.
1492
- *
1493
- * @param documentFragment element `data.content` obtained from clipboard
1494
- * @internal
1495
- */ function removeXmlns(documentFragment, writer) {
1496
- for (const child of documentFragment.getChildren()){
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
- * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1505
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1506
- */ /**
1507
- * @module paste-from-office/filters/removegooglesheetstag
1508
- */ /**
1509
- * Removes the `<google-sheets-html-origin>` tag wrapper added by Google Sheets to a copied content.
1510
- *
1511
- * @param documentFragment element `data.content` obtained from clipboard
1512
- * @internal
1513
- */ function removeGoogleSheetsTag(documentFragment, writer) {
1514
- for (const child of documentFragment.getChildren()){
1515
- if (child.is('element', 'google-sheets-html-origin')) {
1516
- const childIndex = documentFragment.getChildIndex(child);
1517
- writer.remove(child);
1518
- writer.insertChild(childIndex, child.getChildren(), documentFragment);
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
- * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1525
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1526
- */ /**
1527
- * @module paste-from-office/filters/removestyleblock
1528
- */ /**
1529
- * Removes `<style>` block added by Google Sheets to a copied content.
1530
- *
1531
- * @param documentFragment element `data.content` obtained from clipboard
1532
- * @internal
1533
- */ function removeStyleBlock(documentFragment, writer) {
1534
- for (const child of Array.from(documentFragment.getChildren())){
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
- * Normalizer for the content pasted from Google Sheets.
1544
- *
1545
- * @internal
1546
- */ class GoogleSheetsNormalizer {
1547
- document;
1548
- /**
1549
- * Creates a new `GoogleSheetsNormalizer` instance.
1550
- *
1551
- * @param document View document.
1552
- */ constructor(document){
1553
- this.document = document;
1554
- }
1555
- /**
1556
- * @inheritDoc
1557
- */ isActive(htmlString) {
1558
- return googleSheetsMatch.test(htmlString);
1559
- }
1560
- /**
1561
- * @inheritDoc
1562
- */ execute(data) {
1563
- const writer = new ViewUpcastWriter(this.document);
1564
- removeGoogleSheetsTag(data.content, writer);
1565
- removeXmlns(data.content, writer);
1566
- removeInvalidTableWidth(data.content, writer);
1567
- removeStyleBlock(data.content, writer);
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
- * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1573
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1574
- */ /**
1575
- * @module paste-from-office/filters/space
1576
- */ /**
1577
- * Replaces last space preceding elements closing tag with `&nbsp;`. Such operation prevents spaces from being removed
1578
- * during further DOM/View processing (see especially {@link module:engine/view/domconverter~ViewDomConverter#_processDomInlineNodes}).
1579
- * This method also takes into account Word specific `<o:p></o:p>` empty tags.
1580
- * Additionally multiline sequences of spaces and new lines between tags are removed (see
1581
- * https://github.com/ckeditor/ckeditor5-paste-from-office/issues/39
1582
- * and https://github.com/ckeditor/ckeditor5-paste-from-office/issues/40).
1583
- *
1584
- * @param htmlString HTML string in which spacing should be normalized.
1585
- * @returns Input HTML with spaces normalized.
1586
- * @internal
1587
- */ function normalizeSpacing(htmlString) {
1588
- // Run normalizeSafariSpaceSpans() two times to cover nested spans.
1589
- return normalizeSafariSpaceSpans(normalizeSafariSpaceSpans(htmlString))// Remove all \r\n from "spacerun spans" so the last replace line doesn't strip all whitespaces.
1590
- .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, '\u00A0</').replace(/ <o:p><\/o:p>/g, '\u00A0<o:p></o:p>')// Remove <o:p> block filler from empty paragraph. Safari uses \u00A0 instead of &nbsp;.
1591
- .replace(/<o:p>(&nbsp;|\u00A0)<\/o:p>/g, '')// Remove all whitespaces when they contain any \r or \n.
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 `&nbsp;`. 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>(&nbsp;|\u00A0)<\/o:p>/g, "").replace(/>([^\S\r\n]*[\r\n]\s*)</g, "><");
1593
1290
  }
1594
1291
  /**
1595
- * Normalizes spacing in special Word `spacerun spans` (`<span style='mso-spacerun:yes'>\s+</span>`) by replacing
1596
- * all spaces with `&nbsp; ` pairs. This prevents spaces from being removed during further DOM/View processing
1597
- * (see especially {@link module:engine/view/domconverter~ViewDomConverter#_processDomInlineNodes}).
1598
- *
1599
- * @param htmlDocument Native `Document` object in which spacing should be normalized.
1600
- * @internal
1601
- */ function normalizeSpacerunSpans(htmlDocument) {
1602
- htmlDocument.querySelectorAll('span[style*=spacerun]').forEach((el)=>{
1603
- const htmlElement = el;
1604
- const innerTextLength = htmlElement.innerText.length || 0;
1605
- htmlElement.innerText = Array(innerTextLength + 1).join('\u00A0 ').substr(0, innerTextLength);
1606
- });
1292
+ * Normalizes spacing in special Word `spacerun spans` (`<span style='mso-spacerun:yes'>\s+</span>`) by replacing
1293
+ * all spaces with `&nbsp; ` 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
- * Normalizes specific spacing generated by Safari when content pasted from Word (`<span class="Apple-converted-space"> </span>`)
1610
- * by replacing all spaces sequences longer than 1 space with `&nbsp; ` pairs. This prevents spaces from being removed during
1611
- * further DOM/View processing (see especially {@link module:engine/view/domconverter~ViewDomConverter#_processDataFromDomText}).
1612
- *
1613
- * This function is similar to {@link module:clipboard/utils/normalizeclipboarddata normalizeClipboardData util} but uses
1614
- * regular spaces / &nbsp; sequence for replacement.
1615
- *
1616
- * @param htmlString HTML string in which spacing should be normalized
1617
- * @returns Input HTML with spaces normalized.
1618
- * @internal
1619
- */ function normalizeSafariSpaceSpans(htmlString) {
1620
- return htmlString.replace(/<span(?: class="Apple-converted-space"|)>(\s+)<\/span>/g, (fullMatch, spaces)=>{
1621
- return spaces.length === 1 ? ' ' : Array(spaces.length + 1).join('\u00A0 ').substr(0, spaces.length);
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 `&nbsp; ` 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 / &nbsp; 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
- * Parses the provided HTML extracting contents of `<body>` and `<style>` tags.
1627
- *
1628
- * @param htmlString HTML string to be parsed.
1629
- */ function parsePasteOfficeHtml(htmlString, stylesProcessor) {
1630
- const domParser = new DOMParser();
1631
- // Remove Word specific "if comments" so content inside is not omitted by the parser.
1632
- htmlString = htmlString.replace(/<!--\[if gte vml 1]>/g, '');
1633
- // Clean the <head> section of MS Windows specific tags. See https://github.com/ckeditor/ckeditor5/issues/15333.
1634
- // The regular expression matches the <o:SmartTagType> tag with optional attributes (with or without values).
1635
- htmlString = htmlString.replace(/<o:SmartTagType(?:\s+[^\s>=]+(?:="[^"]*")?)*\s*\/?>/gi, '');
1636
- const normalizedHtml = normalizeSpacing(cleanContentAfterBody(htmlString));
1637
- // Parse htmlString as native Document object.
1638
- const htmlDocument = domParser.parseFromString(normalizedHtml, 'text/html');
1639
- normalizeSpacerunSpans(htmlDocument);
1640
- // Get `innerHTML` first as transforming to View modifies the source document.
1641
- const bodyString = htmlDocument.body.innerHTML;
1642
- // Transform document.body to View.
1643
- const bodyView = documentToView(htmlDocument, stylesProcessor);
1644
- // Extract stylesheets.
1645
- const stylesObject = extractStyles(htmlDocument);
1646
- return {
1647
- body: bodyView,
1648
- bodyString,
1649
- styles: stylesObject.styles,
1650
- stylesString: stylesObject.stylesString
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
- * Transforms native `Document` object into {@link module:engine/view/documentfragment~ViewDocumentFragment}. Comments are skipped.
1655
- *
1656
- * @param htmlDocument Native `Document` object to be transformed.
1657
- */ function documentToView(htmlDocument, stylesProcessor) {
1658
- const viewDocument = new ViewDocument(stylesProcessor);
1659
- const domConverter = new ViewDomConverter(viewDocument, {
1660
- renderingMode: 'data'
1661
- });
1662
- const fragment = htmlDocument.createDocumentFragment();
1663
- const nodes = htmlDocument.body.childNodes;
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
- * Extracts both `CSSStyleSheet` and string representation from all `style` elements available in a provided `htmlDocument`.
1673
- *
1674
- * @param htmlDocument Native `Document` object from which styles will be extracted.
1675
- */ function extractStyles(htmlDocument) {
1676
- const styles = [];
1677
- const stylesString = [];
1678
- const styleTags = Array.from(htmlDocument.getElementsByTagName('style'));
1679
- for (const style of styleTags){
1680
- if (style.sheet && style.sheet.cssRules && style.sheet.cssRules.length) {
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
- * Removes leftover content from between closing </body> and closing </html> tag:
1692
- *
1693
- * ```html
1694
- * <html><body><p>Foo Bar</p></body><span>Fo</span></html> -> <html><body><p>Foo Bar</p></body></html>
1695
- * ```
1696
- *
1697
- * This function is used as specific browsers (Edge) add some random content after `body` tag when pasting from Word.
1698
- * @param htmlString The HTML string to be cleaned.
1699
- * @returns The HTML string with leftover content removed.
1700
- */ function cleanContentAfterBody(htmlString) {
1701
- const bodyCloseTag = '</body>';
1702
- const htmlCloseTag = '</html>';
1703
- const bodyCloseIndex = htmlString.indexOf(bodyCloseTag);
1704
- if (bodyCloseIndex < 0) {
1705
- return htmlString;
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
- * The Paste from Office plugin.
1713
- *
1714
- * This plugin handles content pasted from Office apps and transforms it (if necessary)
1715
- * to a valid structure which can then be understood by the editor features.
1716
- *
1717
- * Transformation is made by a set of predefined {@link module:paste-from-office/normalizer~PasteFromOfficeNormalizer normalizers}.
1718
- * This plugin includes following normalizers:
1719
- * * {@link module:paste-from-office/normalizers/mswordnormalizer~PasteFromOfficeMSWordNormalizer Microsoft Word normalizer}
1720
- * * {@link module:paste-from-office/normalizers/googledocsnormalizer~GoogleDocsNormalizer Google Docs normalizer}
1721
- *
1722
- * For more information about this feature check the {@glink api/paste-from-office package page}.
1723
- */ class PasteFromOffice extends Plugin {
1724
- /**
1725
- * The priority array of registered normalizers.
1726
- */ _normalizers = [];
1727
- /**
1728
- * @inheritDoc
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