@ckeditor/ckeditor5-paste-from-office 48.1.1 → 48.2.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.
@@ -18,9 +18,11 @@ import { ViewUpcastWriter, type ViewDocumentFragment } from '@ckeditor/ckeditor5
18
18
  *
19
19
  * @param documentFragment The view structure to be transformed.
20
20
  * @param stylesString Styles from which list-like elements styling will be extracted.
21
+ * @param hasMultiLevelListPlugin Whether the editor has the multi-level list plugin enabled.
22
+ * @param enableSkipLevelLists Whether to enable skip-level lists.
21
23
  * @internal
22
24
  */
23
- export declare function transformListItemLikeElementsIntoLists(documentFragment: ViewDocumentFragment, stylesString: string, hasMultiLevelListPlugin: boolean): void;
25
+ export declare function transformListItemLikeElementsIntoLists(documentFragment: ViewDocumentFragment, stylesString: string, hasMultiLevelListPlugin: boolean, enableSkipLevelLists?: boolean): void;
24
26
  /**
25
27
  * Removes paragraph wrapping content inside a list item.
26
28
  *
@@ -9,7 +9,9 @@
9
9
  * Replaces last space preceding elements closing tag with ` `. Such operation prevents spaces from being removed
10
10
  * during further DOM/View processing (see especially {@link module:engine/view/domconverter~ViewDomConverter#_processDomInlineNodes}).
11
11
  * This method also takes into account Word specific `<o:p></o:p>` empty tags.
12
- * Additionally multiline sequences of spaces and new lines between tags are removed (see #39 and #40).
12
+ * Additionally multiline sequences of spaces and new lines between tags are removed (see
13
+ * https://github.com/ckeditor/ckeditor5-paste-from-office/issues/39
14
+ * and https://github.com/ckeditor/ckeditor5-paste-from-office/issues/40).
13
15
  *
14
16
  * @param htmlString HTML string in which spacing should be normalized.
15
17
  * @returns Input HTML with spaces normalized.
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * @module paste-from-office
7
7
  */
8
8
  export { PasteFromOffice } from './pastefromoffice.js';
9
- export type { PasteFromOfficeNormalizer, PasteFromOfficeNormalizerData } from './normalizer.js';
9
+ export type { PasteFromOfficeNormalizer } from './normalizer.js';
10
10
  export { PasteFromOfficeMSWordNormalizer } from './normalizers/mswordnormalizer.js';
11
11
  export { parsePasteOfficeHtml, type PasteOfficeHtmlParseResult } from './filters/parse.js';
12
12
  export { transformBookmarks as _transformPasteOfficeBookmarks } from './filters/bookmark.js';
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
4
  */
5
5
  import { Plugin } from '@ckeditor/ckeditor5-core/dist/index.js';
6
+ import { priorities, insertToPriorityArray } from '@ckeditor/ckeditor5-utils/dist/index.js';
6
7
  import { ClipboardPipeline } from '@ckeditor/ckeditor5-clipboard/dist/index.js';
7
8
  import { ViewUpcastWriter, Matcher, ViewDocument, ViewDomConverter } from '@ckeditor/ckeditor5-engine/dist/index.js';
8
9
 
@@ -103,8 +104,10 @@ import { ViewUpcastWriter, Matcher, ViewDocument, ViewDomConverter } from '@cked
103
104
  *
104
105
  * @param documentFragment The view structure to be transformed.
105
106
  * @param stylesString Styles from which list-like elements styling will be extracted.
107
+ * @param hasMultiLevelListPlugin Whether the editor has the multi-level list plugin enabled.
108
+ * @param enableSkipLevelLists Whether to enable skip-level lists.
106
109
  * @internal
107
- */ function transformListItemLikeElementsIntoLists(documentFragment, stylesString, hasMultiLevelListPlugin) {
110
+ */ function transformListItemLikeElementsIntoLists(documentFragment, stylesString, hasMultiLevelListPlugin, enableSkipLevelLists = false) {
108
111
  if (!documentFragment.childCount) {
109
112
  return;
110
113
  }
@@ -139,10 +142,25 @@ import { ViewUpcastWriter, Matcher, ViewDocument, ViewDomConverter } from '@cked
139
142
  // Combines the list id and level so that two different lists at the same indent
140
143
  // level (e.g. first an <ol>, then a <ul> after a paragraph break) don't share a counter.
141
144
  const originalListId = `${itemLikeElement.id}:${itemLikeElement.indent}`;
142
- // Normalized list item indentation.
143
- const indent = Math.min(itemLikeElement.indent - 1, stack.length);
145
+ // When the editor opts into skip-level lists, preserve Word indent gaps (the fill loop below
146
+ // inserts `<li style="list-style-type:none">` wrappers for them). Otherwise clamp to one
147
+ // level below the current stack top — the original pre-skip-level behavior — so the editor's
148
+ // list post-fixer doesn't have to bridge the gap with empty filler paragraphs.
149
+ const indent = enableSkipLevelLists ? itemLikeElement.indent - 1 : Math.min(itemLikeElement.indent - 1, stack.length);
144
150
  // Trimming of the list stack on list ID change.
145
151
  if (indent < stack.length && stack[indent].id !== itemLikeElement.id) {
152
+ // A different list at the top level starts here. `isListContinuation` returned true
153
+ // (the previous sibling in the DOM is the prior list's `<ol>/<ul>`), so the outer reset
154
+ // path didn't run. Flush the prior list's accumulated margin onto its own `<ol>/<ul>`
155
+ // BEFORE the stack reference is replaced — otherwise a later flush would hoist that
156
+ // margin onto the wrong (interrupting) list and strip it from the original list's
157
+ // `<li>`s. The flush is a no-op unless the prior list actually had uniform-margin
158
+ // items pushed, so single-item / mixed-margin lists keep their pre-existing
159
+ // per-`<li>` margin semantics.
160
+ if (indent == 0 && topLevelListInfo.canApplyMarginOnList && topLevelListInfo.topLevelListItemElements.length > 0) {
161
+ applyIndentationToTopLevelList(writer, stack, topLevelListInfo);
162
+ topLevelListInfo = createTopLevelListInfo();
163
+ }
146
164
  // A different list started at this indent level — counters for this level and deeper
147
165
  // belong to the previous list context and must not carry over.
148
166
  encounteredLists.length = indent;
@@ -153,40 +171,99 @@ import { ViewUpcastWriter, Matcher, ViewDocument, ViewDomConverter } from '@cked
153
171
  // We jumped back to a shallower indent — any counters deeper than the new top are stale.
154
172
  encounteredLists.length = indent + 1;
155
173
  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
+ const listStyle = detectListStyle(itemLikeElement, stylesString);
176
+ // Word can jump indent levels (e.g. from level 1 directly to level 3) without producing
177
+ // items for the in-between levels. Fill the missing levels with intermediate wrappers —
178
+ // an `<ol>`/`<ul>` of the deepest item's type containing a single `<li style="list-style-type:none">`
179
+ // so the resulting view matches the shape the skip-level upcast (`listItemSkipLevelConsumer`) expects.
180
+ while(stack.length < indent){
181
+ const intermediateList = writer.createElement(listStyle.type);
182
+ const intermediateListItem = writer.createElement('li');
183
+ writer.setStyle('list-style-type', 'none', intermediateListItem);
184
+ if (stack.length == 0) {
185
+ const parent = itemLikeElement.element.parent;
186
+ const index = parent.getChildIndex(itemLikeElement.element) + 1;
187
+ writer.insertChild(index, intermediateList, parent);
188
+ } else {
189
+ const parentListItems = stack[stack.length - 1].listItemElements;
190
+ writer.appendChild(intermediateList, parentListItems[parentListItems.length - 1]);
191
+ }
192
+ writer.appendChild(intermediateListItem, intermediateList);
193
+ stack.push({
194
+ ...itemLikeElement,
195
+ listElement: intermediateList,
196
+ listItemElements: [
197
+ intermediateListItem
198
+ ],
199
+ isIntermediate: true,
200
+ // Intermediate wrappers hold no real list item, so they must not pretend to "own" the
201
+ // deep item's `margin-left` — otherwise `stack.find` (matching non-list multi-block
202
+ // continuations by margin) returns the shallower intermediate before the real item.
203
+ marginLeft: undefined
204
+ });
205
+ }
206
+ // Create a new OL/UL if required (greater indent or different list type).
207
+ if (indent > stack.length - 1 || stack[indent].listElement.name != listStyle.type) {
208
+ // If this list was seen before at this indent (i.e. it was interrupted by a non-list block
209
+ // and is now resuming), set `start` so the numbering continues from where it left off.
210
+ if (listStyle.type == 'ol' && itemLikeElement.id !== undefined && encounteredLists[indent] && encounteredLists[indent][originalListId]) {
211
+ listStyle.startIndex = encounteredLists[indent][originalListId];
212
+ }
213
+ const listElement = createNewEmptyList(listStyle, writer, hasMultiLevelListPlugin);
214
+ // Insert the new OL/UL.
215
+ if (stack.length == 0) {
216
+ const parent = itemLikeElement.element.parent;
217
+ const index = parent.getChildIndex(itemLikeElement.element) + 1;
218
+ writer.insertChild(index, listElement, parent);
219
+ } else if (indent == 0) {
220
+ // A real list at root indent while a skip-level intermediate of a different type
221
+ // already sits there — insert the new list as a sibling of the intermediate in the
222
+ // same parent (can't merge two lists of different types).
223
+ const existingList = stack[0].listElement;
224
+ const listParent = existingList.parent;
225
+ const insertIndex = listParent.getChildIndex(existingList) + 1;
226
+ writer.insertChild(insertIndex, listElement, listParent);
227
+ } else {
228
+ const parentListItems = stack[indent - 1].listItemElements;
229
+ writer.appendChild(listElement, parentListItems[parentListItems.length - 1]);
230
+ }
231
+ // Update the list stack for other items to reference.
232
+ stack[indent] = {
233
+ ...itemLikeElement,
234
+ listElement,
235
+ listItemElements: []
236
+ };
237
+ // Record the starting value for this list so that if it is interrupted and resumed later,
238
+ // the continuation list can pick up numbering from the right value.
239
+ // For a fresh list `listStyle.startIndex` is undefined, so we fall back to 1.
240
+ if (itemLikeElement.id !== undefined) {
241
+ if (!encounteredLists[indent]) {
242
+ encounteredLists[indent] = {};
174
243
  }
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;
244
+ encounteredLists[indent][originalListId] = listStyle.startIndex || 1;
245
+ }
246
+ } else if (stack[indent].isIntermediate) {
247
+ // Same type as the intermediate — reuse its `<ol>`/`<ul>`. The intermediate was created
248
+ // without `list-style-type`, `start`, or the `legal-list` class (the fill loop only sets
249
+ // the tag name); apply them now from the claiming item so the reused element no longer
250
+ // looks like a styleless wrapper.
251
+ applyListStyleToElement(stack[indent].listElement, listStyle, writer, hasMultiLevelListPlugin);
252
+ // Update the wrapper to represent the claiming item (id, marginLeft, …) so later lookups
253
+ // don't see stale data from the deep item that originally seeded the intermediate.
254
+ stack[indent] = {
255
+ ...itemLikeElement,
256
+ listElement: stack[indent].listElement,
257
+ listItemElements: stack[indent].listItemElements
258
+ };
259
+ // Same as the create-new path: track this list so that if it is interrupted (e.g. by a
260
+ // multi-block paragraph matched against an ancestor frame) and later resumed via a fresh
261
+ // `<ol>`, the continuation can set the correct `start` attribute instead of restarting from 1.
262
+ if (itemLikeElement.id !== undefined) {
263
+ if (!encounteredLists[indent]) {
264
+ encounteredLists[indent] = {};
189
265
  }
266
+ encounteredLists[indent][originalListId] = listStyle.startIndex || 1;
190
267
  }
191
268
  }
192
269
  // Use LI if it is already it or create a new LI element.
@@ -244,19 +321,20 @@ function applyListItemMarginLeftAndUpdateTopLevelInfo(writer, stack, topLevelLis
244
321
  }
245
322
  const listItemBlockMarginLeft = parseFloat(itemLikeElement.marginLeft);
246
323
  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
- }
324
+ // Sum the relative `margin-left` of the last `<li>` in every ancestor stack frame.
325
+ // Browser nesting cumulates: each ancestor `<li>`'s margin pushes its descendants further right,
326
+ // so to convert Word's absolute `margin-left` into the editor's relative value we have to subtract
327
+ // every ancestor's contribution not only the immediate parent. Skip-level intermediate wrappers
328
+ // contribute 0 (no margin set) and so naturally drop out of the sum.
329
+ for(let ancestorIndex = 0; ancestorIndex < stack.length - 1; ancestorIndex++){
330
+ const ancestorListItems = stack[ancestorIndex].listItemElements;
331
+ const ancestorMargin = ancestorListItems[ancestorListItems.length - 1].getStyle('margin-left');
332
+ if (ancestorMargin !== undefined) {
333
+ currentListBlockIndent += parseFloat(ancestorMargin);
255
334
  }
256
335
  }
257
336
  // Add 40px for each indent level because by default HTML lists have 40px indentation (padding-inline-start: 40px).
258
337
  // 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
338
  currentListBlockIndent += stack.length * 40;
261
339
  // Calculate relative list item indentation to the list it is in.
262
340
  const adjustedListItemIndent = listItemBlockMarginLeft - currentListBlockIndent;
@@ -354,16 +432,31 @@ function createTopLevelListInfo() {
354
432
  * Whether the given element is possibly a list continuation. Previous element was wrapped into a list
355
433
  * or the current element already is inside a list.
356
434
  */ function isListContinuation(currentItem) {
357
- const previousSibling = currentItem.element.previousSibling;
435
+ let previousSibling = currentItem.element.previousSibling;
436
+ // Skip past stray inline markers that Word leaves between list paragraphs (e.g. empty
437
+ // `<span style='mso-bookmark:...'></span>` or empty `<o:p></o:p>`). They have no visual effect
438
+ // but would otherwise break list continuity here — which matters most for nested items pasted
439
+ // right after their parent, where breaking the chain causes PFO to clamp the deeper item to
440
+ // indent 0 instead of nesting it.
441
+ while(previousSibling && isStrayInlineMarker(previousSibling)){
442
+ previousSibling = previousSibling.previousSibling;
443
+ }
358
444
  if (!previousSibling) {
359
445
  const parent = currentItem.element.parent;
360
446
  // If it's a li inside ul or ol like in here: https://github.com/ckeditor/ckeditor5/issues/15964.
361
447
  // If the parent has previous sibling, which is not a list, then it is not a continuation.
362
448
  return isList(parent) && (!parent.previousSibling || isList(parent.previousSibling));
363
449
  }
364
- // Even with the same id the list does not have to be continuous (#43).
450
+ // Even with the same id the list does not have to be continuous (https://github.com/ckeditor/ckeditor5/issues/43).
365
451
  return isList(previousSibling);
366
452
  }
453
+ /**
454
+ * True for empty inline elements Word emits as residue between paragraphs (`<span>`, `<a>`, `<o:p>`).
455
+ * Used by `isListContinuation` to look past these when checking whether the prior block is a list —
456
+ * they're layout artefacts, not real content.
457
+ */ function isStrayInlineMarker(node) {
458
+ return node.is('element') && node.childCount === 0 && /^(?:span|a|o:p)$/.test(node.name);
459
+ }
367
460
  function isList(element) {
368
461
  return element.is('element', 'ol') || element.is('element', 'ul');
369
462
  }
@@ -515,6 +608,14 @@ function isList(element) {
515
608
  * Creates a new list OL/UL element.
516
609
  */ function createNewEmptyList(listStyle, writer, hasMultiLevelListPlugin) {
517
610
  const list = writer.createElement(listStyle.type);
611
+ applyListStyleToElement(list, listStyle, writer, hasMultiLevelListPlugin);
612
+ return list;
613
+ }
614
+ /**
615
+ * Applies `list-style-type`, `start`, and the `legal-list` class to a list element based on the detected
616
+ * list style. Used both when creating a fresh list and when a real item claims a previously-intermediate
617
+ * wrapper (which was created without any of these).
618
+ */ function applyListStyleToElement(list, listStyle, writer, hasMultiLevelListPlugin) {
518
619
  // We do not support modifying the marker for a particular list item.
519
620
  // Set the value for the `list-style-type` property directly to the list container.
520
621
  if (listStyle.style) {
@@ -526,7 +627,6 @@ function isList(element) {
526
627
  if (listStyle.isLegalStyleList && hasMultiLevelListPlugin) {
527
628
  writer.addClass('legal-list', list);
528
629
  }
529
- return list;
530
630
  }
531
631
  /**
532
632
  * Extracts list item information from Word specific list-like element style:
@@ -538,7 +638,7 @@ function isList(element) {
538
638
  * where:
539
639
  *
540
640
  * ```
541
- * * `l1` is a list id (however it does not mean this is a continuous list - see #43),
641
+ * * `l1` is a list id (however it does not mean this is a continuous list - see https://github.com/ckeditor/ckeditor5/issues/43),
542
642
  * * `level1` is a list item indentation level,
543
643
  * * `lfo1` is a list insertion order in a document.
544
644
  * ```
@@ -1176,14 +1276,16 @@ const msWordMatch2 = /xmlns:o="urn:schemas-microsoft-com/i;
1176
1276
  document;
1177
1277
  hasMultiLevelListPlugin;
1178
1278
  hasTablePropertiesPlugin;
1279
+ enableSkipLevelLists;
1179
1280
  /**
1180
1281
  * Creates a new `PasteFromOfficeMSWordNormalizer` instance.
1181
1282
  *
1182
1283
  * @param document View document.
1183
- */ constructor(document, hasMultiLevelListPlugin = false, hasTablePropertiesPlugin = false){
1284
+ */ constructor(document, hasMultiLevelListPlugin = false, hasTablePropertiesPlugin = false, enableSkipLevelLists = false){
1184
1285
  this.document = document;
1185
1286
  this.hasMultiLevelListPlugin = hasMultiLevelListPlugin;
1186
1287
  this.hasTablePropertiesPlugin = hasTablePropertiesPlugin;
1288
+ this.enableSkipLevelLists = enableSkipLevelLists;
1187
1289
  }
1188
1290
  /**
1189
1291
  * @inheritDoc
@@ -1194,15 +1296,14 @@ const msWordMatch2 = /xmlns:o="urn:schemas-microsoft-com/i;
1194
1296
  * @inheritDoc
1195
1297
  */ execute(data) {
1196
1298
  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;
1299
+ const stylesString = data.extraContent.stylesString;
1300
+ transformBookmarks(data.content, writer);
1301
+ transformListItemLikeElementsIntoLists(data.content, stylesString, this.hasMultiLevelListPlugin, this.enableSkipLevelLists);
1302
+ replaceImagesSourceWithBase64(data.content, data.dataTransfer.getData('text/rtf'));
1303
+ transformTables(data.content, writer, this.hasTablePropertiesPlugin);
1304
+ removeInvalidTableWidth(data.content, writer);
1305
+ replaceMSFootnotes(data.content, writer);
1306
+ removeMSAttributes(data.content);
1206
1307
  }
1207
1308
  }
1208
1309
 
@@ -1374,12 +1475,10 @@ const googleDocsMatch = /id=("|')docs-internal-guid-[-0-9a-f]+("|')/i;
1374
1475
  * @inheritDoc
1375
1476
  */ execute(data) {
1376
1477
  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;
1478
+ removeBoldWrapper(data.content, writer);
1479
+ unwrapParagraphInListItem(data.content, writer);
1480
+ transformBlockBrsToParagraphs(data.content, writer);
1481
+ replaceTabsWithinPreWithSpaces(data.content, writer, 8);
1383
1482
  }
1384
1483
  }
1385
1484
 
@@ -1462,12 +1561,10 @@ const googleSheetsMatch = /<google-sheets-html-origin/i;
1462
1561
  * @inheritDoc
1463
1562
  */ execute(data) {
1464
1563
  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;
1564
+ removeGoogleSheetsTag(data.content, writer);
1565
+ removeXmlns(data.content, writer);
1566
+ removeInvalidTableWidth(data.content, writer);
1567
+ removeStyleBlock(data.content, writer);
1471
1568
  }
1472
1569
  }
1473
1570
 
@@ -1480,7 +1577,9 @@ const googleSheetsMatch = /<google-sheets-html-origin/i;
1480
1577
  * Replaces last space preceding elements closing tag with `&nbsp;`. Such operation prevents spaces from being removed
1481
1578
  * during further DOM/View processing (see especially {@link module:engine/view/domconverter~ViewDomConverter#_processDomInlineNodes}).
1482
1579
  * 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).
1580
+ * Additionally multiline sequences of spaces and new lines between tags are removed (see
1581
+ * https://github.com/ckeditor/ckeditor5-paste-from-office/issues/39
1582
+ * and https://github.com/ckeditor/ckeditor5-paste-from-office/issues/40).
1484
1583
  *
1485
1584
  * @param htmlString HTML string in which spacing should be normalized.
1486
1585
  * @returns Input HTML with spaces normalized.
@@ -1622,6 +1721,9 @@ const googleSheetsMatch = /<google-sheets-html-origin/i;
1622
1721
  *
1623
1722
  * For more information about this feature check the {@glink api/paste-from-office package page}.
1624
1723
  */ class PasteFromOffice extends Plugin {
1724
+ /**
1725
+ * The priority array of registered normalizers.
1726
+ */ _normalizers = [];
1625
1727
  /**
1626
1728
  * @inheritDoc
1627
1729
  */ static get pluginName() {
@@ -1656,33 +1758,53 @@ const googleSheetsMatch = /<google-sheets-html-origin/i;
1656
1758
  const editor = this.editor;
1657
1759
  const clipboardPipeline = editor.plugins.get('ClipboardPipeline');
1658
1760
  const viewDocument = editor.editing.view.document;
1659
- const normalizers = [];
1660
1761
  const hasMultiLevelListPlugin = this.editor.plugins.has('MultiLevelListEditing');
1661
1762
  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) {
1763
+ const enableSkipLevelLists = !!this.editor.config.get('list.enableSkipLevelLists');
1764
+ this.registerNormalizer(new PasteFromOfficeMSWordNormalizer(viewDocument, hasMultiLevelListPlugin, hasTablePropertiesPlugin, enableSkipLevelLists));
1765
+ this.registerNormalizer(new GoogleDocsNormalizer(viewDocument));
1766
+ this.registerNormalizer(new GoogleSheetsNormalizer(viewDocument));
1767
+ viewDocument.on('clipboardInput', (evt, data)=>{
1768
+ if (typeof data.content != 'string') {
1667
1769
  return;
1668
1770
  }
1669
- const codeBlock = editor.model.document.selection.getFirstPosition().parent;
1670
- if (codeBlock.is('element', 'codeBlock')) {
1771
+ // The `htmlString` is used only to detect (match) the active normalizer.
1772
+ // The actual content processing is happening on `data.content` below.
1773
+ const htmlString = data.dataTransfer.getData('text/html');
1774
+ const activeNormalizer = this._normalizers.find(({ normalizer })=>normalizer.isActive(htmlString));
1775
+ if (activeNormalizer) {
1776
+ const parsedData = parsePasteOfficeHtml(data.content, viewDocument.stylesProcessor);
1777
+ data.content = parsedData.body;
1778
+ data.extraContent = {
1779
+ ...parsedData,
1780
+ isTransformedWithPasteFromOffice: true
1781
+ };
1782
+ }
1783
+ }, {
1784
+ priority: priorities.low + 10
1785
+ });
1786
+ clipboardPipeline.on('inputTransformation', (evt, data)=>{
1787
+ if (!data.extraContent || !data.extraContent.isTransformedWithPasteFromOffice) {
1671
1788
  return;
1672
1789
  }
1790
+ // The `htmlString` is used only to detect (match) the active normalizers, not for processing.
1673
1791
  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;
1792
+ const normalizers = this._normalizers.filter(({ normalizer })=>normalizer.isActive(htmlString));
1793
+ for (const { normalizer } of normalizers){
1794
+ normalizer.execute(data);
1681
1795
  }
1682
1796
  }, {
1683
1797
  priority: 'high'
1684
1798
  });
1685
1799
  }
1800
+ /**
1801
+ * Registers a normalizer with the given priority.
1802
+ */ registerNormalizer(normalizer, priority) {
1803
+ insertToPriorityArray(this._normalizers, {
1804
+ normalizer,
1805
+ priority: priorities.get(priority)
1806
+ });
1807
+ }
1686
1808
  }
1687
1809
 
1688
1810
  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 };