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