@ckeditor/ckeditor5-paste-from-office 47.1.0-alpha.2 → 47.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckeditor/ckeditor5-paste-from-office",
3
- "version": "47.1.0-alpha.2",
3
+ "version": "47.2.0-alpha.0",
4
4
  "description": "Paste from Office feature for CKEditor 5.",
5
5
  "keywords": [
6
6
  "ckeditor",
@@ -13,10 +13,10 @@
13
13
  "type": "module",
14
14
  "main": "src/index.js",
15
15
  "dependencies": {
16
- "@ckeditor/ckeditor5-clipboard": "47.1.0-alpha.2",
17
- "@ckeditor/ckeditor5-core": "47.1.0-alpha.2",
18
- "@ckeditor/ckeditor5-engine": "47.1.0-alpha.2",
19
- "ckeditor5": "47.1.0-alpha.2"
16
+ "@ckeditor/ckeditor5-clipboard": "47.2.0-alpha.0",
17
+ "@ckeditor/ckeditor5-core": "47.2.0-alpha.0",
18
+ "@ckeditor/ckeditor5-engine": "47.2.0-alpha.0",
19
+ "ckeditor5": "47.2.0-alpha.0"
20
20
  },
21
21
  "author": "CKSource (http://cksource.com/)",
22
22
  "license": "SEE LICENSE IN LICENSE.md",
@@ -170,7 +170,8 @@ function findAllItemLikeElements(documentFragment, writer) {
170
170
  marginLeft = undefined;
171
171
  }
172
172
  // List item or a following list item block.
173
- if (item.hasStyle('mso-list') || marginLeft !== undefined && foundMargins.has(marginLeft)) {
173
+ if ((item.hasStyle('mso-list') && item.getStyle('mso-list') !== 'none') ||
174
+ (marginLeft !== undefined && foundMargins.has(marginLeft))) {
174
175
  const itemData = getListItemData(item);
175
176
  itemLikeElements.push({
176
177
  element: item,
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
+ */
5
+ /**
6
+ * @module paste-from-office/filters/replacemsfootnotes
7
+ */
8
+ import type { ViewDocumentFragment, ViewUpcastWriter } from 'ckeditor5/src/engine.js';
9
+ /**
10
+ * Replaces MS Word specific footnotes references and definitions with proper elements.
11
+ *
12
+ * Things to know about MS Word footnotes:
13
+ *
14
+ * * Footnote references in Word are marked with `mso-footnote-id` style.
15
+ * * Word does not support nested footnotes, so references within definitions are ignored.
16
+ * * Word appends extra spaces after footnote references within definitions, which are trimmed.
17
+ * * Footnote definitions list is marked with `mso-element: footnote-list` style it contain `mso-element: footnote` elements.
18
+ * * Footnote definition might contain tables, lists and other elements, not only text. They are placed directly within `li` element,
19
+ * without any wrapper (in opposition to text content of the definition, which is placed within `MsoFootnoteText` element).
20
+ *
21
+ * Example pseudo document showing MS Word footnote structure:
22
+ *
23
+ * ```html
24
+ * <p>Text with footnote<a style='mso-footnote-id:ftn1'>[1]</a> reference.</p>
25
+ *
26
+ * <div style='mso-element:footnote-list'>
27
+ * <div style='mso-element:footnote' id=ftn1>
28
+ * <p class=MsoFootnoteText><a style='mso-footnote-id:ftn1'>[1]</a> Footnote content</p>
29
+ * <table class="MsoTableGrid">...</table>
30
+ * </div>
31
+ * </div>
32
+ * ```
33
+ *
34
+ * Will be transformed into:
35
+ *
36
+ * ```html
37
+ * <p>Text with footnote<sup class="footnote"><a id="ref-footnote-ftn1" href="#footnote-ftn1">1</a></sup> reference.</p>
38
+ *
39
+ * <ol class="footnotes">
40
+ * <li class="footnote-definition" id="footnote-ftn1">
41
+ * <a href="#ref-footnote-ftn1" class="footnote-backlink">^</a>
42
+ * <div class="footnote-content">
43
+ * <p>Footnote content</p>
44
+ * <table>...</table>
45
+ * </div>
46
+ * </li>
47
+ * </ol>
48
+ * ```
49
+ *
50
+ * @param documentFragment `data.content` obtained from clipboard.
51
+ * @param writer The view writer instance.
52
+ * @internal
53
+ */
54
+ export declare function replaceMSFootnotes(documentFragment: ViewDocumentFragment, writer: ViewUpcastWriter): void;
@@ -0,0 +1,209 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
+ */
5
+ /**
6
+ * Replaces MS Word specific footnotes references and definitions with proper elements.
7
+ *
8
+ * Things to know about MS Word footnotes:
9
+ *
10
+ * * Footnote references in Word are marked with `mso-footnote-id` style.
11
+ * * Word does not support nested footnotes, so references within definitions are ignored.
12
+ * * Word appends extra spaces after footnote references within definitions, which are trimmed.
13
+ * * Footnote definitions list is marked with `mso-element: footnote-list` style it contain `mso-element: footnote` elements.
14
+ * * Footnote definition might contain tables, lists and other elements, not only text. They are placed directly within `li` element,
15
+ * without any wrapper (in opposition to text content of the definition, which is placed within `MsoFootnoteText` element).
16
+ *
17
+ * Example pseudo document showing MS Word footnote structure:
18
+ *
19
+ * ```html
20
+ * <p>Text with footnote<a style='mso-footnote-id:ftn1'>[1]</a> reference.</p>
21
+ *
22
+ * <div style='mso-element:footnote-list'>
23
+ * <div style='mso-element:footnote' id=ftn1>
24
+ * <p class=MsoFootnoteText><a style='mso-footnote-id:ftn1'>[1]</a> Footnote content</p>
25
+ * <table class="MsoTableGrid">...</table>
26
+ * </div>
27
+ * </div>
28
+ * ```
29
+ *
30
+ * Will be transformed into:
31
+ *
32
+ * ```html
33
+ * <p>Text with footnote<sup class="footnote"><a id="ref-footnote-ftn1" href="#footnote-ftn1">1</a></sup> reference.</p>
34
+ *
35
+ * <ol class="footnotes">
36
+ * <li class="footnote-definition" id="footnote-ftn1">
37
+ * <a href="#ref-footnote-ftn1" class="footnote-backlink">^</a>
38
+ * <div class="footnote-content">
39
+ * <p>Footnote content</p>
40
+ * <table>...</table>
41
+ * </div>
42
+ * </li>
43
+ * </ol>
44
+ * ```
45
+ *
46
+ * @param documentFragment `data.content` obtained from clipboard.
47
+ * @param writer The view writer instance.
48
+ * @internal
49
+ */
50
+ export function replaceMSFootnotes(documentFragment, writer) {
51
+ const msFootnotesRefs = new Map();
52
+ const msFootnotesDefs = new Map();
53
+ let msFootnotesDefinitionsList = null;
54
+ // Phase 1: Collect all footnotes references and definitions. Find the footnotes definitions list element.
55
+ for (const { item } of writer.createRangeIn(documentFragment)) {
56
+ if (!item.is('element')) {
57
+ continue;
58
+ }
59
+ // If spot a footnotes definitions element, let's store it. It'll be replaced later.
60
+ // There should be only one such element in the document.
61
+ if (item.getStyle('mso-element') === 'footnote-list') {
62
+ msFootnotesDefinitionsList = item;
63
+ continue;
64
+ }
65
+ // If spot a footnote reference or definition, store it in the corresponding map.
66
+ if (item.hasStyle('mso-footnote-id')) {
67
+ const msFootnoteDef = item.findAncestor('element', el => el.getStyle('mso-element') === 'footnote');
68
+ if (msFootnoteDef) {
69
+ // If it's a reference within a definition, ignore it and track only the definition.
70
+ // MS Word do not support nested footnotes, so it's safe to assume that all references within
71
+ // a definition point to the same definition.
72
+ const msFootnoteDefId = msFootnoteDef.getAttribute('id');
73
+ msFootnotesDefs.set(msFootnoteDefId, msFootnoteDef);
74
+ }
75
+ else {
76
+ // If it's a reference outside of a definition, track it as a reference.
77
+ const msFootnoteRefId = item.getStyle('mso-footnote-id');
78
+ msFootnotesRefs.set(msFootnoteRefId, item);
79
+ }
80
+ continue;
81
+ }
82
+ }
83
+ // If there are no footnotes references or definitions, or no definitions list, there's nothing to normalize.
84
+ if (!msFootnotesRefs.size || !msFootnotesDefinitionsList) {
85
+ return;
86
+ }
87
+ // Phase 2: Replace footnotes definitions list with proper element.
88
+ const footnotesDefinitionsList = createFootnotesListViewElement(writer);
89
+ writer.replace(msFootnotesDefinitionsList, footnotesDefinitionsList);
90
+ // Phase 3: Replace all footnotes references and add matching definitions to the definitions list.
91
+ for (const [footnoteId, msFootnoteRef] of msFootnotesRefs) {
92
+ const msFootnoteDef = msFootnotesDefs.get(footnoteId);
93
+ if (!msFootnoteDef) {
94
+ continue;
95
+ }
96
+ // Replace footnote reference.
97
+ writer.replace(msFootnoteRef, createFootnoteRefViewElement(writer, footnoteId));
98
+ // Append found matching definition to the definitions list.
99
+ // Order doesn't matter here, as it'll be fixed in the post-fixer.
100
+ const defElements = createFootnoteDefViewElement(writer, footnoteId);
101
+ removeMSReferences(writer, msFootnoteDef);
102
+ // Insert content within the `MsoFootnoteText` element. It's usually a definition text content.
103
+ for (const child of msFootnoteDef.getChildren()) {
104
+ let clonedChild = child;
105
+ if (child.is('element')) {
106
+ clonedChild = writer.clone(child, true);
107
+ }
108
+ writer.appendChild(clonedChild, defElements.content);
109
+ }
110
+ writer.appendChild(defElements.listItem, footnotesDefinitionsList);
111
+ }
112
+ }
113
+ /**
114
+ * Removes all MS Office specific references from the given element.
115
+ *
116
+ * It also removes leading space from text nodes following the references, as MS Word adds
117
+ * them to separate the reference from the rest of the text.
118
+ *
119
+ * @param writer The view writer.
120
+ * @param element The element to trim.
121
+ * @returns The trimmed element.
122
+ */
123
+ function removeMSReferences(writer, element) {
124
+ const elementsToRemove = [];
125
+ const textNodesToTrim = [];
126
+ for (const { item } of writer.createRangeIn(element)) {
127
+ if (item.is('element') && item.getStyle('mso-footnote-id')) {
128
+ elementsToRemove.unshift(item);
129
+ // MS Word used to add spaces after footnote references within definitions. Let's check if there's a space after
130
+ // the footnote reference and mark it for trimming.
131
+ const { nextSibling } = item;
132
+ if (nextSibling?.is('$text') && nextSibling.data.startsWith(' ')) {
133
+ textNodesToTrim.unshift(nextSibling);
134
+ }
135
+ }
136
+ }
137
+ for (const element of elementsToRemove) {
138
+ writer.remove(element);
139
+ }
140
+ // Remove only the leading space from text nodes following reference within definition, preserve the rest of the text.
141
+ for (const textNode of textNodesToTrim) {
142
+ const trimmedData = textNode.data.substring(1);
143
+ if (trimmedData.length > 0) {
144
+ // Create a new text node and replace the old one.
145
+ const parent = textNode.parent;
146
+ const index = parent.getChildIndex(textNode);
147
+ const newTextNode = writer.createText(trimmedData);
148
+ writer.remove(textNode);
149
+ writer.insertChild(index, newTextNode, parent);
150
+ }
151
+ else {
152
+ // If the text node contained only a space, remove it entirely.
153
+ writer.remove(textNode);
154
+ }
155
+ }
156
+ return element;
157
+ }
158
+ /**
159
+ * Creates a footnotes list view element.
160
+ *
161
+ * @param writer The view writer instance.
162
+ * @returns The footnotes list view element.
163
+ */
164
+ function createFootnotesListViewElement(writer) {
165
+ return writer.createElement('ol', { class: 'footnotes' });
166
+ }
167
+ /**
168
+ * Creates a footnote reference view element.
169
+ *
170
+ * @param writer The view writer instance.
171
+ * @param footnoteId The footnote ID.
172
+ * @returns The footnote reference view element.
173
+ */
174
+ function createFootnoteRefViewElement(writer, footnoteId) {
175
+ const sup = writer.createElement('sup', { class: 'footnote' });
176
+ const link = writer.createElement('a', {
177
+ id: `ref-${footnoteId}`,
178
+ href: `#${footnoteId}`
179
+ });
180
+ writer.appendChild(link, sup);
181
+ return sup;
182
+ }
183
+ /**
184
+ * Creates a footnote definition view element with a backlink and a content container.
185
+ *
186
+ * @param writer The view writer instance.
187
+ * @param footnoteId The footnote ID.
188
+ * @returns An object containing the list item element, backlink and content container.
189
+ */
190
+ function createFootnoteDefViewElement(writer, footnoteId) {
191
+ const listItem = writer.createElement('li', {
192
+ id: footnoteId,
193
+ class: 'footnote-definition'
194
+ });
195
+ const backLink = writer.createElement('a', {
196
+ href: `#ref-${footnoteId}`,
197
+ class: 'footnote-backlink'
198
+ });
199
+ const content = writer.createElement('div', {
200
+ class: 'footnote-content'
201
+ });
202
+ writer.appendChild(writer.createText('^'), backLink);
203
+ writer.appendChild(backLink, listItem);
204
+ writer.appendChild(content, listItem);
205
+ return {
206
+ listItem,
207
+ content
208
+ };
209
+ }
@@ -11,6 +11,7 @@ import { replaceImagesSourceWithBase64 } from '../filters/image.js';
11
11
  import { removeMSAttributes } from '../filters/removemsattributes.js';
12
12
  import { transformTables } from '../filters/table.js';
13
13
  import { removeInvalidTableWidth } from '../filters/removeinvalidtablewidth.js';
14
+ import { replaceMSFootnotes } from '../filters/replacemsfootnotes.js';
14
15
  import { ViewUpcastWriter } from 'ckeditor5/src/engine.js';
15
16
  const msWordMatch1 = /<meta\s*name="?generator"?\s*content="?microsoft\s*word\s*\d+"?\/?>/i;
16
17
  const msWordMatch2 = /xmlns:o="urn:schemas-microsoft-com/i;
@@ -46,6 +47,7 @@ export class PasteFromOfficeMSWordNormalizer {
46
47
  replaceImagesSourceWithBase64(documentFragment, data.dataTransfer.getData('text/rtf'));
47
48
  transformTables(documentFragment, writer);
48
49
  removeInvalidTableWidth(documentFragment, writer);
50
+ replaceMSFootnotes(documentFragment, writer);
49
51
  removeMSAttributes(documentFragment);
50
52
  data.content = documentFragment;
51
53
  }