@ckeditor/ckeditor5-paste-from-office 48.0.1 → 48.1.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.
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * @module paste-from-office/filters/bookmark
7
7
  */
8
- import { type ViewUpcastWriter, type ViewDocumentFragment } from '@ckeditor/ckeditor5-engine';
8
+ import type { ViewUpcastWriter, ViewDocumentFragment } from '@ckeditor/ckeditor5-engine';
9
9
  /**
10
10
  * Transforms `<a>` elements which are bookmarks by moving their children after the element.
11
11
  *
package/dist/index.js CHANGED
@@ -27,8 +27,25 @@ import { ViewUpcastWriter, Matcher, ViewDocument, ViewDomConverter } from '@cked
27
27
  const index = element.parent.getChildIndex(element) + 1;
28
28
  const children = element.getChildren();
29
29
  writer.insertChild(index, children, element.parent);
30
+ if (isHiddenBookmarkAnchor(element)) {
31
+ writer.remove(element);
32
+ }
30
33
  }
31
34
  }
35
+ /**
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('_');
48
+ }
32
49
 
33
50
  /**
34
51
  * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
@@ -96,7 +113,13 @@ import { ViewUpcastWriter, Matcher, ViewDocument, ViewDomConverter } from '@cked
96
113
  if (!itemLikeElements.length) {
97
114
  return;
98
115
  }
99
- const encounteredLists = {};
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 = [];
100
123
  const stack = [];
101
124
  let topLevelListInfo = createTopLevelListInfo();
102
125
  for (const itemLikeElement of itemLikeElements){
@@ -104,26 +127,40 @@ import { ViewUpcastWriter, Matcher, ViewDocument, ViewDomConverter } from '@cked
104
127
  if (!isListContinuation(itemLikeElement)) {
105
128
  applyIndentationToTopLevelList(writer, stack, topLevelListInfo);
106
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;
107
136
  stack.length = 0;
108
137
  }
109
- // Combined list ID for addressing encounter lists counters.
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.
110
141
  const originalListId = `${itemLikeElement.id}:${itemLikeElement.indent}`;
111
142
  // Normalized list item indentation.
112
143
  const indent = Math.min(itemLikeElement.indent - 1, stack.length);
113
144
  // Trimming of the list stack on list ID change.
114
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;
115
149
  stack.length = indent;
116
150
  }
117
151
  // Trimming of the list stack on lower indent list encountered.
118
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;
119
155
  stack.length = indent + 1;
120
156
  } else {
121
157
  const listStyle = detectListStyle(itemLikeElement, stylesString);
122
158
  // Create a new OL/UL if required (greater indent or different list type).
123
159
  if (indent > stack.length - 1 || stack[indent].listElement.name != listStyle.type) {
124
- // Check if there is some start index to set from a previous list.
125
- if (indent == 0 && listStyle.type == 'ol' && itemLikeElement.id !== undefined && encounteredLists[originalListId]) {
126
- listStyle.startIndex = encounteredLists[originalListId];
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];
127
164
  }
128
165
  const listElement = createNewEmptyList(listStyle, writer, hasMultiLevelListPlugin);
129
166
  // Insert the new OL/UL.
@@ -141,9 +178,14 @@ import { ViewUpcastWriter, Matcher, ViewDocument, ViewDomConverter } from '@cked
141
178
  listElement,
142
179
  listItemElements: []
143
180
  };
144
- // Prepare list counter for start index.
145
- if (indent == 0 && itemLikeElement.id !== undefined) {
146
- encounteredLists[originalListId] = listStyle.startIndex || 1;
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;
147
189
  }
148
190
  }
149
191
  }
@@ -154,9 +196,9 @@ import { ViewUpcastWriter, Matcher, ViewDocument, ViewDomConverter } from '@cked
154
196
  // Append the LI to OL/UL.
155
197
  writer.appendChild(listItem, stack[indent].listElement);
156
198
  stack[indent].listItemElements.push(listItem);
157
- // Increment list counter.
158
- if (indent == 0 && itemLikeElement.id !== undefined) {
159
- encounteredLists[originalListId]++;
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]++;
160
202
  }
161
203
  // Append list block to LI.
162
204
  if (itemLikeElement.element != listItem) {
@@ -169,12 +211,22 @@ import { ViewUpcastWriter, Matcher, ViewDocument, ViewDomConverter } from '@cked
169
211
  } else {
170
212
  // Other blocks in a list item.
171
213
  const stackItem = stack.find((stackItem)=>stackItem.marginLeft == itemLikeElement.marginLeft);
172
- // This might be a paragraph that has known margin, but it is not a real list block.
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.
173
217
  if (stackItem) {
174
218
  const listItems = stackItem.listItemElements;
175
219
  // Append block to LI.
176
220
  writer.appendChild(itemLikeElement.element, listItems[listItems.length - 1]);
177
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;
178
230
  } else {
179
231
  stack.length = 0;
180
232
  }