@sd-angular/core 19.0.0-beta.26 → 19.0.0-beta.27

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.
Files changed (46) hide show
  1. package/assets/scss/ckeditor5.scss +1 -0
  2. package/components/document-builder/src/document-builder.model.d.ts +8 -1
  3. package/components/document-builder/src/plugins/block-space/block-space.plugin.d.ts +9 -0
  4. package/components/document-builder/src/plugins/heading/heading.plugin.d.ts +1 -0
  5. package/components/document-builder/src/plugins/index.d.ts +4 -1
  6. package/components/document-builder/src/plugins/page-orientation/page-orientation.plugin.d.ts +2 -2
  7. package/components/document-builder/src/plugins/paste-handler/filters/bookmark.d.ts +14 -0
  8. package/components/document-builder/src/plugins/paste-handler/filters/br.d.ts +15 -0
  9. package/components/document-builder/src/plugins/paste-handler/filters/image.d.ts +25 -0
  10. package/components/document-builder/src/plugins/paste-handler/filters/list.d.ts +29 -0
  11. package/components/document-builder/src/plugins/paste-handler/filters/parse.d.ts +35 -0
  12. package/components/document-builder/src/plugins/paste-handler/filters/removeboldwrapper.d.ts +15 -0
  13. package/components/document-builder/src/plugins/paste-handler/filters/removegooglesheetstag.d.ts +15 -0
  14. package/components/document-builder/src/plugins/paste-handler/filters/removeinvalidtablewidth.d.ts +15 -0
  15. package/components/document-builder/src/plugins/paste-handler/filters/removemsattributes.d.ts +15 -0
  16. package/components/document-builder/src/plugins/paste-handler/filters/removestyleblock.d.ts +15 -0
  17. package/components/document-builder/src/plugins/paste-handler/filters/removexmlns.d.ts +15 -0
  18. package/components/document-builder/src/plugins/paste-handler/filters/replacemsfootnotes.d.ts +54 -0
  19. package/components/document-builder/src/plugins/paste-handler/filters/replacetabswithinprewithspaces.d.ts +24 -0
  20. package/components/document-builder/src/plugins/paste-handler/filters/space.d.ts +27 -0
  21. package/components/document-builder/src/plugins/paste-handler/filters/table.d.ts +16 -0
  22. package/components/document-builder/src/plugins/paste-handler/filters/utils.d.ts +25 -0
  23. package/components/document-builder/src/plugins/paste-handler/index.d.ts +35 -0
  24. package/components/document-builder/src/plugins/paste-handler/normalizers/googledocsnormalizer.d.ts +31 -0
  25. package/components/document-builder/src/plugins/paste-handler/normalizers/googlesheetsnormalizer.d.ts +31 -0
  26. package/components/document-builder/src/plugins/paste-handler/normalizers/mswordnormalizer.d.ts +29 -0
  27. package/components/document-builder/src/plugins/paste-handler/types.d.ts +30 -0
  28. package/components/document-builder/src/plugins/{table-fit/table-fit.plugin.d.ts → table-custom/index.d.ts} +6 -1
  29. package/fesm2022/sd-angular-core-components-document-builder.mjs +2097 -174
  30. package/fesm2022/sd-angular-core-components-document-builder.mjs.map +1 -1
  31. package/fesm2022/sd-angular-core-components-table.mjs +3 -3
  32. package/fesm2022/sd-angular-core-components-table.mjs.map +1 -1
  33. package/fesm2022/sd-angular-core-directives.mjs +29 -25
  34. package/fesm2022/sd-angular-core-directives.mjs.map +1 -1
  35. package/fesm2022/sd-angular-core-services-docx.mjs +173 -0
  36. package/fesm2022/sd-angular-core-services-docx.mjs.map +1 -0
  37. package/fesm2022/sd-angular-core-services.mjs +1 -0
  38. package/fesm2022/sd-angular-core-services.mjs.map +1 -1
  39. package/package.json +44 -36
  40. package/sd-angular-core-19.0.0-beta.27.tgz +0 -0
  41. package/services/docx/index.d.ts +1 -0
  42. package/services/docx/src/lib/docx.model.d.ts +9 -0
  43. package/services/docx/src/lib/docx.service.d.ts +13 -0
  44. package/services/docx/src/public-api.d.ts +2 -0
  45. package/services/index.d.ts +1 -0
  46. package/sd-angular-core-19.0.0-beta.26.tgz +0 -0
@@ -3,7 +3,7 @@ import { EventEmitter, Output, Input, Component } from '@angular/core';
3
3
  import { CommonModule } from '@angular/common';
4
4
  import * as i1 from '@ckeditor/ckeditor5-angular';
5
5
  import { CKEditorModule } from '@ckeditor/ckeditor5-angular';
6
- import { Plugin, ButtonView, ClassicEditor, Essentials, Paragraph, Bold, Italic, Underline, FontSize, FontColor, FontBackgroundColor, Alignment, Widget, toWidget, GeneralHtmlSupport, FontFamily, Heading, List, Table, TableToolbar, TableProperties, TableCellProperties, TableColumnResize, PasteFromOffice, PageBreak, Undo, Subscript, Superscript, Image, ImageUpload, ImageToolbar, ImageCaption, ImageResize, ImageStyle, ImageBlock, Indent, IndentBlock } from 'ckeditor5';
6
+ import { Plugin, ButtonView, ClassicEditor, Essentials, Paragraph, Bold, Italic, Underline, FontSize, FontColor, FontBackgroundColor, Alignment, Widget, toWidget, ViewUpcastWriter, Matcher, ViewDocument, ViewDomConverter, ClipboardPipeline, FontFamily, Heading, List, Table, TableToolbar, TableProperties, TableCellProperties, TableColumnResize, PageBreak, Undo, Subscript, Superscript, Image, ImageUpload, ImageToolbar, ImageCaption, ImageResize, ImageStyle, ImageBlock, Indent, IndentBlock } from 'ckeditor5';
7
7
  import { Subscription, Subject, throttleTime } from 'rxjs';
8
8
  import { SdResolveMaybeAsync, hslToHex, rgbToHex, SdUtilities } from '@sd-angular/core/utilities';
9
9
  import { v4 } from 'uuid';
@@ -130,7 +130,181 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
130
130
  type: Output
131
131
  }] } });
132
132
 
133
+ /**
134
+ * Cấu hình màu cho Document Builder
135
+ * Bảng màu tập trung và cấu hình cho việc lựa chọn màu nhất quán
136
+ */
137
+ /**
138
+ * Trả về bảng màu chung được sử dụng trong tất cả tính năng của document builder
139
+ * @returns Mảng các tùy chọn màu được định sẵn với giá trị hex và label
140
+ */
141
+ function getPresetColors() {
142
+ return [
143
+ { color: '#000000', label: 'Black' },
144
+ { color: '#4D4D4D', label: 'Dim grey' },
145
+ { color: '#999999', label: 'Grey' },
146
+ { color: '#E6E6E6', label: 'Light grey' },
147
+ { color: '#FFFFFF', label: 'White' },
148
+ { color: '#E64D4D', label: 'Red' },
149
+ { color: '#E6994D', label: 'Orange' },
150
+ { color: '#E6E64D', label: 'Yellow' },
151
+ { color: '#99E64D', label: 'Light green' },
152
+ { color: '#4DE64D', label: 'Green' },
153
+ { color: '#4DE699', label: 'Aquamarine' },
154
+ { color: '#4DE6E6', label: 'Turquoise' },
155
+ { color: '#4D99E6', label: 'Light blue' },
156
+ { color: '#4D4DE6', label: 'Blue' },
157
+ { color: '#994DE6', label: 'Purple' },
158
+ ];
159
+ }
160
+ /**
161
+ * Trả về cấu hình bộ chọn màu với định dạng hex
162
+ * @returns Đối tượng cấu hình bộ chọn màu
163
+ */
164
+ function getColorPickerConfig() {
165
+ return {
166
+ format: 'hex',
167
+ };
168
+ }
169
+ /**
170
+ * Trả về cấu hình kích thước font cho document builder
171
+ * @returns Mảng các tùy chọn kích thước font được định sẵn
172
+ */
173
+ function getFontSizeOptions() {
174
+ return [
175
+ {
176
+ title: '9',
177
+ model: '9pt',
178
+ view: {
179
+ name: 'span',
180
+ styles: { 'font-size': '9pt' },
181
+ priority: 7,
182
+ },
183
+ },
184
+ {
185
+ title: '10',
186
+ model: '10pt',
187
+ view: {
188
+ name: 'span',
189
+ styles: { 'font-size': '10pt' },
190
+ priority: 7,
191
+ },
192
+ },
193
+ {
194
+ title: '11',
195
+ model: '11pt',
196
+ view: {
197
+ name: 'span',
198
+ styles: { 'font-size': '11pt' },
199
+ priority: 7,
200
+ },
201
+ },
202
+ {
203
+ title: '12',
204
+ model: '12pt',
205
+ view: {
206
+ name: 'span',
207
+ styles: { 'font-size': '12pt' },
208
+ priority: 7,
209
+ },
210
+ },
211
+ {
212
+ title: '13',
213
+ model: '13pt',
214
+ view: {
215
+ name: 'span',
216
+ styles: { 'font-size': '13pt' },
217
+ priority: 7,
218
+ },
219
+ },
220
+ {
221
+ title: '14',
222
+ model: '14pt',
223
+ view: {
224
+ name: 'span',
225
+ styles: { 'font-size': '14pt' },
226
+ priority: 7,
227
+ },
228
+ },
229
+ {
230
+ title: '16',
231
+ model: '16pt',
232
+ view: {
233
+ name: 'span',
234
+ styles: { 'font-size': '16pt' },
235
+ priority: 7,
236
+ },
237
+ },
238
+ {
239
+ title: '18',
240
+ model: '18pt',
241
+ view: {
242
+ name: 'span',
243
+ styles: { 'font-size': '18pt' },
244
+ priority: 7,
245
+ },
246
+ },
247
+ {
248
+ title: '20',
249
+ model: '20pt',
250
+ view: {
251
+ name: 'span',
252
+ styles: { 'font-size': '20pt' },
253
+ priority: 7,
254
+ },
255
+ },
256
+ {
257
+ title: '24',
258
+ model: '24pt',
259
+ view: {
260
+ name: 'span',
261
+ styles: { 'font-size': '24pt' },
262
+ priority: 7,
263
+ },
264
+ },
265
+ ];
266
+ }
267
+ function getHeadingOptions() {
268
+ return [
269
+ {
270
+ model: 'paragraph',
271
+ title: 'Paragraph',
272
+ class: 'ck-heading_paragraph',
273
+ },
274
+ {
275
+ model: 'heading1',
276
+ view: {
277
+ name: 'h1',
278
+ styles: { 'font-size': '24pt', 'font-weight': 'bold', 'line-height': '1.15' },
279
+ },
280
+ title: 'Heading 1',
281
+ class: 'ck-heading_heading1',
282
+ },
283
+ {
284
+ model: 'heading2',
285
+ view: {
286
+ name: 'h2',
287
+ styles: { 'font-size': '20pt', 'font-weight': 'bold', 'line-height': '1.15' },
288
+ },
289
+ title: 'Heading 2',
290
+ class: 'ck-heading_heading2',
291
+ },
292
+ {
293
+ model: 'heading3',
294
+ view: {
295
+ name: 'h3',
296
+ styles: { 'font-size': '16pt', 'font-weight': 'bold', 'line-height': '1.15' },
297
+ },
298
+ title: 'Heading 3',
299
+ class: 'ck-heading_heading3',
300
+ },
301
+ ];
302
+ }
303
+
133
304
  class HeadingPlugin extends Plugin {
305
+ static get pluginName() {
306
+ return 'HeadingPlugin';
307
+ }
134
308
  init() {
135
309
  const editor = this.editor;
136
310
  editor.conversion.for('editingDowncast').markerToHighlight({
@@ -139,8 +313,55 @@ class HeadingPlugin extends Plugin {
139
313
  classes: 'ck-heading-highlight',
140
314
  },
141
315
  });
316
+ // Lấy default styles từ config
317
+ const headingOptions = getHeadingOptions();
318
+ const headingDefaults = {};
319
+ headingOptions?.forEach((opt) => {
320
+ if (opt.model?.match(/^heading[1-3]$/)) {
321
+ headingDefaults[opt.model] = {
322
+ fontSize: opt.view?.styles?.['font-size'] || 'inherit',
323
+ lineHeight: opt.view?.styles?.['line-height'] || 'inherit',
324
+ };
325
+ }
326
+ });
327
+ // Downcast: Kiểm tra heading có styled text và set style inherit
328
+ const downcastConversion = editor.conversion.for('downcast');
329
+ Object.keys(headingDefaults).forEach(modelName => {
330
+ downcastConversion.add(dispatcher => {
331
+ dispatcher.on(`insert:${modelName}`, createHeadingHandler(editor, modelName, headingDefaults), { priority: 'low' });
332
+ });
333
+ });
142
334
  }
143
335
  }
336
+ function createHeadingHandler(editor, modelName, headingDefaults) {
337
+ return (evt, data, conversionApi) => {
338
+ const element = data.item;
339
+ const viewElement = editor.editing.mapper.toViewElement(element);
340
+ if (!viewElement)
341
+ return;
342
+ const children = Array.from(element.getChildren());
343
+ const hasStyledText = children.some((child) => {
344
+ if (child.is('$text')) {
345
+ const attrs = Array.from(child.getAttributes());
346
+ return attrs.length > 0;
347
+ }
348
+ return !child.is('$text');
349
+ });
350
+ editor.editing.view.change((writer) => {
351
+ if (children.length === 1 && hasStyledText) {
352
+ // Có styled text → bỏ style mặc định
353
+ writer.setStyle('font-size', 'inherit', viewElement);
354
+ writer.setStyle('line-height', 'inherit', viewElement);
355
+ }
356
+ else {
357
+ // Không có styled text → set lại style mặc định từ config
358
+ const defaults = headingDefaults[modelName];
359
+ writer.setStyle('font-size', defaults.fontSize, viewElement);
360
+ writer.setStyle('line-height', defaults.lineHeight, viewElement);
361
+ }
362
+ });
363
+ };
364
+ }
144
365
 
145
366
  class CommentPlugin extends Plugin {
146
367
  init() {
@@ -572,7 +793,7 @@ class VariablePlugin extends Plugin {
572
793
  };
573
794
  }
574
795
 
575
- class TableFitPlugin extends Plugin {
796
+ class TableCustom extends Plugin {
576
797
  init() {
577
798
  const editor = this.editor;
578
799
  // Can thiệp vào quá trình convert từ View (HTML Paste) sang Model
@@ -589,6 +810,51 @@ class TableFitPlugin extends Plugin {
589
810
  }
590
811
  }
591
812
  }, { priority: 'low' });
813
+ // Xử lý data-column-widths từ colgroup preservation
814
+ editor.conversion.for('upcast').attributeToAttribute({
815
+ view: 'data-column-widths',
816
+ model: 'tableColumnWidth'
817
+ });
818
+ console.log('[TableCustom] Registered data-column-widths upcast converter');
819
+ // Xử lý border-style: none cho tableCell - phải explicit set 'none'
820
+ dispatcher.on('element:td', (evt, data) => {
821
+ if (!data.modelRange || !data.viewItem)
822
+ return;
823
+ const viewElement = data.viewItem;
824
+ const borderStyle = viewElement.getStyle('border-style') ||
825
+ this._parseBorderStyleFromShorthand(viewElement.getStyle('border'));
826
+ // Nếu border-style là none hoặc border shorthand là none/0, explicit set 'none'
827
+ if (borderStyle === 'none' || borderStyle === 'hidden') {
828
+ for (const item of data.modelRange.getItems()) {
829
+ if (item.is('element', 'tableCell')) {
830
+ editor.model.change(writer => {
831
+ writer.setAttribute('tableCellBorderStyle', 'none', item);
832
+ // Khi border là none, set width về 0 để không có border
833
+ writer.setAttribute('tableCellBorderWidth', '0pt', item);
834
+ });
835
+ }
836
+ }
837
+ }
838
+ }, { priority: 'high' });
839
+ dispatcher.on('element:th', (evt, data) => {
840
+ if (!data.modelRange || !data.viewItem)
841
+ return;
842
+ const viewElement = data.viewItem;
843
+ const borderStyle = viewElement.getStyle('border-style') ||
844
+ this._parseBorderStyleFromShorthand(viewElement.getStyle('border'));
845
+ // Nếu border-style là none hoặc border shorthand là none/0, explicit set 'none'
846
+ if (borderStyle === 'none' || borderStyle === 'hidden') {
847
+ for (const item of data.modelRange.getItems()) {
848
+ if (item.is('element', 'tableCell')) {
849
+ editor.model.change(writer => {
850
+ writer.setAttribute('tableCellBorderStyle', 'none', item);
851
+ // Khi border là none, set width về 0 để không có border
852
+ writer.setAttribute('tableCellBorderWidth', '0pt', item);
853
+ });
854
+ }
855
+ }
856
+ }
857
+ }, { priority: 'high' });
592
858
  });
593
859
  const findInnerTable = (viewElement) => {
594
860
  if (!viewElement)
@@ -631,6 +897,37 @@ class TableFitPlugin extends Plugin {
631
897
  viewWriter.setStyle('width', '100%', innerTable);
632
898
  viewWriter.setStyle('width', '100%', viewElement);
633
899
  });
900
+ // Downcast tableColumnWidth to colgroup
901
+ dispatcher.on('attribute:tableColumnWidth:table', (evt, data, conversionApi) => {
902
+ const viewWriter = conversionApi.writer;
903
+ const viewElement = conversionApi.mapper.toViewElement(data.item);
904
+ if (!viewElement)
905
+ return;
906
+ const innerTable = findInnerTable(viewElement);
907
+ if (!innerTable)
908
+ return;
909
+ const columnWidths = data.item.getAttribute('tableColumnWidth');
910
+ if (!columnWidths)
911
+ return;
912
+ // Remove existing colgroup if any
913
+ for (const child of innerTable.getChildren()) {
914
+ if (child.is('element', 'colgroup')) {
915
+ viewWriter.remove(child);
916
+ break;
917
+ }
918
+ }
919
+ // Create new colgroup with col elements
920
+ const colgroup = viewWriter.createContainerElement('colgroup');
921
+ const widths = columnWidths.split(',');
922
+ for (const width of widths) {
923
+ const col = viewWriter.createEmptyElement('col');
924
+ viewWriter.setAttribute('width', width.trim(), col);
925
+ viewWriter.setStyle('width', width.trim(), col);
926
+ viewWriter.insertChild(0, col, colgroup);
927
+ }
928
+ // Insert colgroup as first child of table
929
+ viewWriter.insertChild(0, colgroup, innerTable);
930
+ });
634
931
  });
635
932
  // Lắng nghe lệnh insertTable
636
933
  const insertTableCommand = editor.commands.get('insertTable');
@@ -682,6 +979,24 @@ class TableFitPlugin extends Plugin {
682
979
  // Setup style preservation on model change
683
980
  this._setupStylePreservationOnModelChange();
684
981
  }
982
+ /**
983
+ * Parse border style from CSS shorthand (e.g., "1px solid red" or "none")
984
+ */
985
+ _parseBorderStyleFromShorthand(borderValue) {
986
+ if (!borderValue)
987
+ return null;
988
+ const val = borderValue.toLowerCase().trim();
989
+ if (val === 'none' || val === '0')
990
+ return 'none';
991
+ // Parse shorthand: width style color (e.g., "1px solid red")
992
+ const parts = val.split(/\s+/);
993
+ for (const part of parts) {
994
+ if (['none', 'hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset'].includes(part)) {
995
+ return part;
996
+ }
997
+ }
998
+ return null;
999
+ }
685
1000
  /**
686
1001
  * Apply default table width
687
1002
  */
@@ -692,22 +1007,31 @@ class TableFitPlugin extends Plugin {
692
1007
  }
693
1008
  /**
694
1009
  * Apply default borders to all cells in a table
1010
+ * Nếu cell đã có border rồi thì bỏ qua
695
1011
  */
696
1012
  _applyCellBorders(writer, tableElement) {
697
1013
  if (!tableElement)
698
1014
  return;
1015
+ // Lấy border info từ table để cell kế thừa
1016
+ const tableBorderColor = tableElement.getAttribute('tableBorderColor');
1017
+ const tableBorderStyle = tableElement.getAttribute('tableBorderStyle');
1018
+ const tableBorderWidth = tableElement.getAttribute('tableBorderWidth');
699
1019
  for (const row of tableElement.getChildren()) {
700
1020
  for (const cell of row.getChildren()) {
701
- if (!cell.getAttribute('tableCellBorderColor')) {
702
- writer.setAttribute('tableCellBorderColor', '#000000', cell);
1021
+ // Nếu cell đã có border attribute nào rồi thì bỏ qua
1022
+ if (cell.hasAttribute('tableCellBorderStyle') ||
1023
+ cell.hasAttribute('tableCellBorderColor') ||
1024
+ cell.hasAttribute('tableCellBorderWidth')) {
1025
+ continue;
703
1026
  }
704
- if (!cell.getAttribute('tableCellBorderStyle')) {
705
- writer.setAttribute('tableCellBorderStyle', 'solid', cell);
706
- }
707
- if (!cell.getAttribute('tableCellBorderWidth')) {
708
- writer.setAttribute('tableCellBorderWidth', '1pt', cell);
709
- }
710
- if (!cell.getAttribute('tableCellPadding')) {
1027
+ // Áp dụng border từ table hoặc default
1028
+ const inheritedStyle = tableBorderStyle || 'solid';
1029
+ const inheritedColor = tableBorderColor || '#000000';
1030
+ const inheritedWidth = tableBorderWidth || '1pt';
1031
+ writer.setAttribute('tableCellBorderStyle', inheritedStyle, cell);
1032
+ writer.setAttribute('tableCellBorderColor', inheritedColor, cell);
1033
+ writer.setAttribute('tableCellBorderWidth', inheritedWidth, cell);
1034
+ if (!cell.hasAttribute('tableCellPadding')) {
711
1035
  writer.setAttribute('tableCellPadding', '0.4em', cell);
712
1036
  }
713
1037
  }
@@ -1068,8 +1392,8 @@ class ImageCustomPlugin extends Plugin {
1068
1392
  const ICON_PORTRAIT = '<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M14 2H6c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6V4h8v12z"/></svg>';
1069
1393
  // Icon khổ ngang (Mới)
1070
1394
  const ICON_LANDSCAPE = '<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M18 4H2C.9 4 0 4.9 0 6v8c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 10H2V6h16v8z"/></svg>';
1071
- class PageOrientationPlugin extends Plugin {
1072
- static pluginName = 'PageOrientationPlugin';
1395
+ class PageOrientation extends Plugin {
1396
+ static pluginName = 'PageOrientation';
1073
1397
  _currentOrientation = 'PORTRAIT';
1074
1398
  orientationChangeEmitter;
1075
1399
  buttonView;
@@ -1138,146 +1462,1758 @@ class PageOrientationPlugin extends Plugin {
1138
1462
  }
1139
1463
 
1140
1464
  /**
1141
- * Cấu hình màu cho Document Builder
1142
- * Bảng màu tập trung cấu hình cho việc lựa chọn màu nhất quán
1465
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1466
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1143
1467
  */
1144
1468
  /**
1145
- * Trả về bảng màu chung được sử dụng trong tất cả tính năng của document builder
1146
- * @returns Mảng các tùy chọn màu được định sẵn với giá trị hex và label
1469
+ * Transforms `<a>` elements which are bookmarks by moving their children after the element.
1470
+ *
1471
+ * @internal
1147
1472
  */
1148
- function getPresetColors() {
1149
- return [
1150
- { color: '#000000', label: 'Black' },
1151
- { color: '#4D4D4D', label: 'Dim grey' },
1152
- { color: '#999999', label: 'Grey' },
1153
- { color: '#E6E6E6', label: 'Light grey' },
1154
- { color: '#FFFFFF', label: 'White' },
1155
- { color: '#E64D4D', label: 'Red' },
1156
- { color: '#E6994D', label: 'Orange' },
1157
- { color: '#E6E64D', label: 'Yellow' },
1158
- { color: '#99E64D', label: 'Light green' },
1159
- { color: '#4DE64D', label: 'Green' },
1160
- { color: '#4DE699', label: 'Aquamarine' },
1161
- { color: '#4DE6E6', label: 'Turquoise' },
1162
- { color: '#4D99E6', label: 'Light blue' },
1163
- { color: '#4D4DE6', label: 'Blue' },
1164
- { color: '#994DE6', label: 'Purple' },
1165
- ];
1473
+ function transformBookmarks(documentFragment, writer) {
1474
+ const elementsToChange = [];
1475
+ for (const value of writer.createRangeIn(documentFragment)) {
1476
+ const element = value.item;
1477
+ if (element.is('element', 'a') &&
1478
+ !element.hasAttribute('href') &&
1479
+ (element.hasAttribute('id') || element.hasAttribute('name'))) {
1480
+ elementsToChange.push(element);
1481
+ }
1482
+ }
1483
+ for (const element of elementsToChange) {
1484
+ const index = element.parent.getChildIndex(element) + 1;
1485
+ const children = element.getChildren();
1486
+ writer.insertChild(index, children, element.parent);
1487
+ }
1166
1488
  }
1489
+
1167
1490
  /**
1168
- * Trả về cấu hình bộ chọn màu với định dạng hex
1169
- * @returns Đối tượng cấu hình bộ chọn màu
1491
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1492
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1170
1493
  */
1171
- function getColorPickerConfig() {
1172
- return {
1173
- format: 'hex',
1174
- };
1494
+ /**
1495
+ * @module paste-from-office/filters/image
1496
+ */
1497
+ /**
1498
+ * Replaces source attribute of all `<img>` elements representing regular
1499
+ * images (not the Word shapes) with inlined base64 image representation extracted from RTF or Blob data.
1500
+ *
1501
+ * @param documentFragment Document fragment on which transform images.
1502
+ * @param rtfData The RTF data from which images representation will be used.
1503
+ * @internal
1504
+ */
1505
+ function replaceImagesSourceWithBase64(documentFragment, rtfData) {
1506
+ if (!documentFragment.childCount) {
1507
+ return;
1508
+ }
1509
+ const upcastWriter = new ViewUpcastWriter(documentFragment.document);
1510
+ const shapesIds = findAllShapesIds(documentFragment, upcastWriter);
1511
+ removeAllImgElementsRepresentingShapes(shapesIds, documentFragment, upcastWriter);
1512
+ insertMissingImgs(shapesIds, documentFragment, upcastWriter);
1513
+ removeAllShapeElements(documentFragment, upcastWriter);
1514
+ const images = findAllImageElementsWithLocalSource(documentFragment, upcastWriter);
1515
+ if (images.length) {
1516
+ replaceImagesFileSourceWithInlineRepresentation(images, extractImageDataFromRtf(rtfData), upcastWriter);
1517
+ }
1175
1518
  }
1176
1519
  /**
1177
- * Trả về cấu hình kích thước font cho document builder
1178
- * @returns Mảng các tùy chọn kích thước font được định sẵn
1520
+ * Converts given HEX string to base64 representation.
1521
+ *
1522
+ * @internal
1523
+ * @param hexString The HEX string to be converted.
1524
+ * @returns Base64 representation of a given HEX string.
1179
1525
  */
1180
- function getFontSizeOptions() {
1181
- return [
1182
- {
1183
- title: '9',
1184
- model: '9pt',
1185
- view: {
1186
- name: 'span',
1187
- styles: { 'font-size': '9pt' },
1188
- priority: 7,
1189
- },
1190
- },
1191
- {
1192
- title: '10',
1193
- model: '10pt',
1194
- view: {
1195
- name: 'span',
1196
- styles: { 'font-size': '10pt' },
1197
- priority: 7,
1198
- },
1199
- },
1200
- {
1201
- title: '11',
1202
- model: '11pt',
1203
- view: {
1204
- name: 'span',
1205
- styles: { 'font-size': '11pt' },
1206
- priority: 7,
1207
- },
1208
- },
1209
- {
1210
- title: '12',
1211
- model: '12pt',
1212
- view: {
1213
- name: 'span',
1214
- styles: { 'font-size': '12pt' },
1215
- priority: 7,
1216
- },
1217
- },
1218
- {
1219
- title: '13',
1220
- model: '13pt',
1221
- view: {
1222
- name: 'span',
1223
- styles: { 'font-size': '13pt' },
1224
- priority: 7,
1225
- },
1226
- },
1227
- {
1228
- title: '14',
1229
- model: '14pt',
1230
- view: {
1231
- name: 'span',
1232
- styles: { 'font-size': '14pt' },
1233
- priority: 7,
1234
- },
1235
- },
1236
- {
1237
- title: '16',
1238
- model: '16pt',
1239
- view: {
1240
- name: 'span',
1241
- styles: { 'font-size': '16pt' },
1242
- priority: 7,
1243
- },
1244
- },
1245
- {
1246
- title: '18',
1247
- model: '18pt',
1248
- view: {
1249
- name: 'span',
1250
- styles: { 'font-size': '18pt' },
1251
- priority: 7,
1252
- },
1253
- },
1254
- {
1255
- title: '20',
1256
- model: '20pt',
1257
- view: {
1258
- name: 'span',
1259
- styles: { 'font-size': '20pt' },
1260
- priority: 7,
1261
- },
1526
+ function _convertHexToBase64(hexString) {
1527
+ return btoa(hexString
1528
+ .match(/\w{2}/g)
1529
+ .map(char => {
1530
+ return String.fromCharCode(parseInt(char, 16));
1531
+ })
1532
+ .join(''));
1533
+ }
1534
+ /**
1535
+ * Finds all shapes (`<v:*>...</v:*>`) ids. Shapes can represent images (canvas)
1536
+ * or Word shapes (which does not have RTF or Blob representation).
1537
+ *
1538
+ * @param documentFragment Document fragment from which to extract shape ids.
1539
+ * @returns Array of shape ids.
1540
+ */
1541
+ function findAllShapesIds(documentFragment, writer) {
1542
+ const range = writer.createRangeIn(documentFragment);
1543
+ const shapeElementsMatcher = new Matcher({
1544
+ name: /v:(.+)/,
1545
+ });
1546
+ const shapesIds = [];
1547
+ for (const value of range) {
1548
+ if (value.type != 'elementStart') {
1549
+ continue;
1550
+ }
1551
+ const el = value.item;
1552
+ const previousSibling = el.previousSibling;
1553
+ const prevSiblingName = previousSibling && previousSibling.is('element') ? previousSibling.name : null;
1554
+ // List of ids which should not be considered as shapes.
1555
+ // https://github.com/ckeditor/ckeditor5/pull/15847#issuecomment-1941543983
1556
+ const exceptionIds = ['Chart'];
1557
+ const isElementAShape = shapeElementsMatcher.match(el);
1558
+ const hasElementGfxdataAttribute = el.getAttribute('o:gfxdata');
1559
+ const isPreviousSiblingAShapeType = prevSiblingName === 'v:shapetype';
1560
+ const isElementIdInExceptionsArray = hasElementGfxdataAttribute && exceptionIds.some(item => el.getAttribute('id').includes(item));
1561
+ // If shape element has 'o:gfxdata' attribute and is not directly before
1562
+ // `<v:shapetype>` element it means that it represents a Word shape.
1563
+ if (isElementAShape && hasElementGfxdataAttribute && !isPreviousSiblingAShapeType && !isElementIdInExceptionsArray) {
1564
+ shapesIds.push(value.item.getAttribute('id'));
1565
+ }
1566
+ }
1567
+ return shapesIds;
1568
+ }
1569
+ /**
1570
+ * Removes all `<img>` elements which represents Word shapes and not regular images.
1571
+ *
1572
+ * @param shapesIds Shape ids which will be checked against `<img>` elements.
1573
+ * @param documentFragment Document fragment from which to remove `<img>` elements.
1574
+ */
1575
+ function removeAllImgElementsRepresentingShapes(shapesIds, documentFragment, writer) {
1576
+ const range = writer.createRangeIn(documentFragment);
1577
+ const imageElementsMatcher = new Matcher({
1578
+ name: 'img',
1579
+ });
1580
+ const imgs = [];
1581
+ for (const value of range) {
1582
+ if (value.item.is('element') && imageElementsMatcher.match(value.item)) {
1583
+ const el = value.item;
1584
+ const shapes = el.getAttribute('v:shapes') ? el.getAttribute('v:shapes').split(' ') : [];
1585
+ if (shapes.length && shapes.every(shape => shapesIds.indexOf(shape) > -1)) {
1586
+ imgs.push(el);
1587
+ // Shapes may also have empty source while content is paste in some browsers (Safari).
1588
+ }
1589
+ else if (!el.getAttribute('src')) {
1590
+ imgs.push(el);
1591
+ }
1592
+ }
1593
+ }
1594
+ for (const img of imgs) {
1595
+ writer.remove(img);
1596
+ }
1597
+ }
1598
+ /**
1599
+ * Removes all shape elements (`<v:*>...</v:*>`) so they do not pollute the output structure.
1600
+ *
1601
+ * @param documentFragment Document fragment from which to remove shape elements.
1602
+ */
1603
+ function removeAllShapeElements(documentFragment, writer) {
1604
+ const range = writer.createRangeIn(documentFragment);
1605
+ const shapeElementsMatcher = new Matcher({
1606
+ name: /v:(.+)/,
1607
+ });
1608
+ const shapes = [];
1609
+ for (const value of range) {
1610
+ if (value.type == 'elementStart' && shapeElementsMatcher.match(value.item)) {
1611
+ shapes.push(value.item);
1612
+ }
1613
+ }
1614
+ for (const shape of shapes) {
1615
+ writer.remove(shape);
1616
+ }
1617
+ }
1618
+ /**
1619
+ * Inserts `img` tags if there is none after a shape.
1620
+ */
1621
+ function insertMissingImgs(shapeIds, documentFragment, writer) {
1622
+ const range = writer.createRangeIn(documentFragment);
1623
+ const shapes = [];
1624
+ for (const value of range) {
1625
+ if (value.type == 'elementStart' && value.item.is('element', 'v:shape')) {
1626
+ const id = value.item.getAttribute('id');
1627
+ if (shapeIds.includes(id)) {
1628
+ continue;
1629
+ }
1630
+ if (!containsMatchingImg(value.item.parent.getChildren(), id)) {
1631
+ shapes.push(value.item);
1632
+ }
1633
+ }
1634
+ }
1635
+ for (const shape of shapes) {
1636
+ const attrs = {
1637
+ src: findSrc(shape),
1638
+ };
1639
+ if (shape.hasAttribute('alt')) {
1640
+ attrs['alt'] = shape.getAttribute('alt');
1641
+ }
1642
+ const img = writer.createElement('img', attrs);
1643
+ writer.insertChild(shape.index + 1, img, shape.parent);
1644
+ }
1645
+ function containsMatchingImg(nodes, id) {
1646
+ for (const node of nodes) {
1647
+ /* istanbul ignore else -- @preserve */
1648
+ if (node.is('element')) {
1649
+ if (node.name == 'img' && node.getAttribute('v:shapes') == id) {
1650
+ return true;
1651
+ }
1652
+ if (containsMatchingImg(node.getChildren(), id)) {
1653
+ return true;
1654
+ }
1655
+ }
1656
+ }
1657
+ return false;
1658
+ }
1659
+ function findSrc(shape) {
1660
+ for (const child of shape.getChildren()) {
1661
+ /* istanbul ignore else -- @preserve */
1662
+ if (child.is('element') && child.getAttribute('src')) {
1663
+ return child.getAttribute('src');
1664
+ }
1665
+ }
1666
+ return '';
1667
+ }
1668
+ }
1669
+ /**
1670
+ * Finds all `<img>` elements in a given document fragment which have source pointing to local `file://` resource.
1671
+ * This function also tracks the index position of each image in the document, which is essential for
1672
+ * precise matching with hexadecimal representations in RTF data.
1673
+ *
1674
+ * @param documentFragment Document fragment in which to look for `<img>` elements.
1675
+ * @returns Array of found images along with their position index in the document.
1676
+ */
1677
+ function findAllImageElementsWithLocalSource(documentFragment, writer) {
1678
+ const range = writer.createRangeIn(documentFragment);
1679
+ const imageElementsMatcher = new Matcher({
1680
+ name: 'img',
1681
+ });
1682
+ const imgs = [];
1683
+ let currentImageIndex = 0;
1684
+ for (const value of range) {
1685
+ if (value.item.is('element') && imageElementsMatcher.match(value.item)) {
1686
+ if (value.item.getAttribute('src').startsWith('file://')) {
1687
+ imgs.push({
1688
+ element: value.item,
1689
+ imageIndex: currentImageIndex,
1690
+ });
1691
+ }
1692
+ currentImageIndex++;
1693
+ }
1694
+ }
1695
+ return imgs;
1696
+ }
1697
+ /**
1698
+ * Extracts all images HEX representations from a given RTF data.
1699
+ *
1700
+ * @param rtfData The RTF data from which to extract images HEX representation.
1701
+ * @returns Array of found HEX representations. Each array item is an object containing:
1702
+ *
1703
+ * * hex Image representation in HEX format.
1704
+ * * type Type of image, `image/png` or `image/jpeg`.
1705
+ */
1706
+ function extractImageDataFromRtf(rtfData) {
1707
+ if (!rtfData) {
1708
+ return [];
1709
+ }
1710
+ const regexPictureHeader = /{\\pict[\s\S]+?\\bliptag-?\d+(\\blipupi-?\d+)?({\\\*\\blipuid\s?[\da-fA-F]+)?[\s}]*?/;
1711
+ const regexPicture = new RegExp('(?:(' + regexPictureHeader.source + '))([\\da-fA-F\\s]+)\\}', 'g');
1712
+ const images = rtfData.match(regexPicture);
1713
+ const result = [];
1714
+ if (images) {
1715
+ for (const image of images) {
1716
+ let imageType = false;
1717
+ if (image.includes('\\pngblip')) {
1718
+ imageType = 'image/png';
1719
+ }
1720
+ else if (image.includes('\\jpegblip')) {
1721
+ imageType = 'image/jpeg';
1722
+ }
1723
+ if (imageType) {
1724
+ result.push({
1725
+ hex: image.replace(regexPictureHeader, '').replace(/[^\da-fA-F]/g, ''),
1726
+ type: imageType,
1727
+ });
1728
+ }
1729
+ }
1730
+ }
1731
+ return result;
1732
+ }
1733
+ /**
1734
+ * Replaces `src` attribute value of all given images with the corresponding base64 image representation.
1735
+ * Uses the image index to precisely match with the correct hexadecimal representation from RTF data.
1736
+ *
1737
+ * @param imageElements Array of image elements along with their indices which will have their sources replaced.
1738
+ * @param imagesHexSources Array of images hex sources (usually the result of `extractImageDataFromRtf()` function).
1739
+ * Contains hexadecimal representations of ALL images in the document, not just those with `file://` URLs.
1740
+ * In XML documents, the same image might be defined both as base64 in HTML and as hexadecimal in RTF data.
1741
+ */
1742
+ function replaceImagesFileSourceWithInlineRepresentation(imageElements, imagesHexSources, writer) {
1743
+ for (let i = 0; i < imageElements.length; i++) {
1744
+ const { element, imageIndex } = imageElements[i];
1745
+ const rtfHexSource = imagesHexSources[imageIndex];
1746
+ if (rtfHexSource) {
1747
+ const newSrc = `data:${rtfHexSource.type};base64,${_convertHexToBase64(rtfHexSource.hex)}`;
1748
+ writer.setAttribute('src', newSrc, element);
1749
+ }
1750
+ }
1751
+ }
1752
+
1753
+ /**
1754
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1755
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1756
+ */
1757
+ /**
1758
+ * @module paste-from-office/filters/removemsattributes
1759
+ */
1760
+ /**
1761
+ * Cleanup MS attributes like styles, attributes and elements.
1762
+ *
1763
+ * @param documentFragment element `data.content` obtained from clipboard.
1764
+ * @internal
1765
+ */
1766
+ function removeMSAttributes(documentFragment) {
1767
+ const elementsToUnwrap = [];
1768
+ const writer = new ViewUpcastWriter(documentFragment.document);
1769
+ for (const { item } of writer.createRangeIn(documentFragment)) {
1770
+ if (!item.is('element')) {
1771
+ continue;
1772
+ }
1773
+ for (const className of item.getClassNames()) {
1774
+ if (/\bmso/gi.exec(className)) {
1775
+ writer.removeClass(className, item);
1776
+ }
1777
+ }
1778
+ for (const styleName of item.getStyleNames()) {
1779
+ if (/\bmso/gi.exec(styleName)) {
1780
+ writer.removeStyle(styleName, item);
1781
+ }
1782
+ }
1783
+ if (item.is('element', 'w:sdt') ||
1784
+ item.is('element', 'w:sdtpr') && item.isEmpty ||
1785
+ item.is('element', 'o:p') && item.isEmpty) {
1786
+ elementsToUnwrap.push(item);
1787
+ }
1788
+ }
1789
+ for (const item of elementsToUnwrap) {
1790
+ const itemParent = item.parent;
1791
+ const childIndex = itemParent.getChildIndex(item);
1792
+ writer.insertChild(childIndex, item.getChildren(), itemParent);
1793
+ writer.remove(item);
1794
+ }
1795
+ }
1796
+
1797
+ /**
1798
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1799
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1800
+ */
1801
+ /**
1802
+ * @module paste-from-office/filters/utils
1803
+ */
1804
+ /**
1805
+ * Normalizes CSS length value to 'px'.
1806
+ *
1807
+ * @internal
1808
+ */
1809
+ function convertCssLengthToPx(value) {
1810
+ const numericValue = parseFloat(value);
1811
+ if (value.endsWith('pt')) {
1812
+ // 1pt = 1in / 72
1813
+ return toPx(numericValue * 96 / 72);
1814
+ }
1815
+ else if (value.endsWith('pc')) {
1816
+ // 1pc = 12pt = 1in / 6.
1817
+ return toPx(numericValue * 12 * 96 / 72);
1818
+ }
1819
+ else if (value.endsWith('in')) {
1820
+ // 1in = 2.54cm = 96px
1821
+ return toPx(numericValue * 96);
1822
+ }
1823
+ else if (value.endsWith('cm')) {
1824
+ // 1cm = 96px / 2.54
1825
+ return toPx(numericValue * 96 / 2.54);
1826
+ }
1827
+ else if (value.endsWith('mm')) {
1828
+ // 1mm = 1cm / 10
1829
+ return toPx(numericValue / 10 * 96 / 2.54);
1830
+ }
1831
+ return value;
1832
+ }
1833
+ /**
1834
+ * Returns true for value with 'px' unit.
1835
+ *
1836
+ * @internal
1837
+ */
1838
+ function isPx(value) {
1839
+ return value !== undefined && value.endsWith('px');
1840
+ }
1841
+ /**
1842
+ * Returns a rounded 'px' value.
1843
+ *
1844
+ * @internal
1845
+ */
1846
+ function toPx(value) {
1847
+ return Math.round(value) + 'px';
1848
+ }
1849
+
1850
+ /**
1851
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1852
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1853
+ */
1854
+ /**
1855
+ * Applies border none for table and cells without a border specified.
1856
+ * Normalizes style length units to px.
1857
+ * Handles left block table alignment.
1858
+ *
1859
+ * @internal
1860
+ */
1861
+ function transformTables(documentFragment, writer, hasTablePropertiesPlugin = false, hasExtendedTableBlockAlignment = false) {
1862
+ for (const item of writer.createRangeIn(documentFragment).getItems()) {
1863
+ if (!item.is('element', 'table') && !item.is('element', 'td') && !item.is('element', 'th')) {
1864
+ continue;
1865
+ }
1866
+ // In MS Word, left-aligned tables (default) have no align attribute on the `<table>` and are not wrapped in a `<div>`.
1867
+ // In such cases, we need to set `margin-left: 0` and `margin-right: auto` to indicate to the editor that
1868
+ // the table is block-aligned to the left.
1869
+ //
1870
+ // Center- and right-aligned tables in MS Word are wrapped in a `<div>` with the `align` attribute set to
1871
+ // `center` or `right`, respectively with no align attribute on the `<table>` itself.
1872
+ //
1873
+ // Additionally, the structure may change when pasting content from MS Word.
1874
+ // Some browsers (e.g., Safari) may insert extra elements around the table (e.g., a <span>),
1875
+ // so the surrounding `<div>` with the `align` attribute may end up being the table's grandparent.
1876
+ if (hasTablePropertiesPlugin && hasExtendedTableBlockAlignment && item.is('element', 'table')) {
1877
+ const directParent = item.parent?.is('element', 'div') ? item.parent : null;
1878
+ const grandParent = item.parent?.parent?.is('element', 'div') ? item.parent.parent : null;
1879
+ const divParent = directParent ?? grandParent;
1880
+ // Center block table alignment.
1881
+ if (divParent && divParent.getAttribute('align') === 'center' && !item.getAttribute('align')) {
1882
+ writer.setStyle('margin-left', 'auto', item);
1883
+ writer.setStyle('margin-right', 'auto', item);
1884
+ }
1885
+ // Right block table alignment.
1886
+ else if (divParent && divParent.getAttribute('align') === 'right' && !item.getAttribute('align')) {
1887
+ writer.setStyle('margin-left', 'auto', item);
1888
+ writer.setStyle('margin-right', '0', item);
1889
+ }
1890
+ // Left block table alignment.
1891
+ else if (!divParent && !item.getAttribute('align')) {
1892
+ writer.setStyle('margin-left', '0', item);
1893
+ writer.setStyle('margin-right', 'auto', item);
1894
+ }
1895
+ }
1896
+ const sides = ['left', 'top', 'right', 'bottom'];
1897
+ // As this is a pasted table, we do not want default table styles to apply here
1898
+ // so we set border node for sides that does not have any border style.
1899
+ // It is enough to verify border style as border color and border width properties have default values in DOM.
1900
+ if (sides.every(side => !item.hasStyle(`border-${side}-style`))) {
1901
+ writer.setStyle('border-style', 'none', item);
1902
+ }
1903
+ else {
1904
+ for (const side of sides) {
1905
+ if (!item.hasStyle(`border-${side}-style`)) {
1906
+ writer.setStyle(`border-${side}-style`, 'none', item);
1907
+ }
1908
+ }
1909
+ }
1910
+ // Translate style length units to px.
1911
+ const props = ['width', 'height', ...sides.map(side => `border-${side}-width`), ...sides.map(side => `padding-${side}`)];
1912
+ for (const prop of props) {
1913
+ if (item.hasStyle(prop)) {
1914
+ writer.setStyle(prop, convertCssLengthToPx(item.getStyle(prop)), item);
1915
+ }
1916
+ }
1917
+ }
1918
+ }
1919
+
1920
+ /**
1921
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1922
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1923
+ */
1924
+ /**
1925
+ * Removes the `width:0px` style from table pasted from Google Sheets and `width="0"` attribute from Word tables.
1926
+ *
1927
+ * @param documentFragment element `data.content` obtained from clipboard
1928
+ * @internal
1929
+ */
1930
+ function removeInvalidTableWidth(documentFragment, writer) {
1931
+ for (const child of writer.createRangeIn(documentFragment).getItems()) {
1932
+ if (child.is('element', 'table')) {
1933
+ // Remove invalid width style (Google Sheets: width:0px).
1934
+ if (child.getStyle('width') === '0px') {
1935
+ writer.removeStyle('width', child);
1936
+ }
1937
+ // Remove invalid width attribute (Word: width="0").
1938
+ if (child.getAttribute('width') === '0') {
1939
+ writer.removeAttribute('width', child);
1940
+ }
1941
+ }
1942
+ }
1943
+ }
1944
+
1945
+ /**
1946
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1947
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1948
+ */
1949
+ /**
1950
+ * Replaces MS Word specific footnotes references and definitions with proper elements.
1951
+ *
1952
+ * Things to know about MS Word footnotes:
1953
+ *
1954
+ * * Footnote references in Word are marked with `mso-footnote-id` style.
1955
+ * * Word does not support nested footnotes, so references within definitions are ignored.
1956
+ * * Word appends extra spaces after footnote references within definitions, which are trimmed.
1957
+ * * Footnote definitions list is marked with `mso-element: footnote-list` style it contain `mso-element: footnote` elements.
1958
+ * * Footnote definition might contain tables, lists and other elements, not only text. They are placed directly within `li` element,
1959
+ * without any wrapper (in opposition to text content of the definition, which is placed within `MsoFootnoteText` element).
1960
+ *
1961
+ * Example pseudo document showing MS Word footnote structure:
1962
+ *
1963
+ * ```html
1964
+ * <p>Text with footnote<a style='mso-footnote-id:ftn1'>[1]</a> reference.</p>
1965
+ *
1966
+ * <div style='mso-element:footnote-list'>
1967
+ * <div style='mso-element:footnote' id=ftn1>
1968
+ * <p class=MsoFootnoteText><a style='mso-footnote-id:ftn1'>[1]</a> Footnote content</p>
1969
+ * <table class="MsoTableGrid">...</table>
1970
+ * </div>
1971
+ * </div>
1972
+ * ```
1973
+ *
1974
+ * Will be transformed into:
1975
+ *
1976
+ * ```html
1977
+ * <p>Text with footnote<sup class="footnote"><a id="ref-footnote-ftn1" href="#footnote-ftn1">1</a></sup> reference.</p>
1978
+ *
1979
+ * <ol class="footnotes">
1980
+ * <li class="footnote-definition" id="footnote-ftn1">
1981
+ * <a href="#ref-footnote-ftn1" class="footnote-backlink">^</a>
1982
+ * <div class="footnote-content">
1983
+ * <p>Footnote content</p>
1984
+ * <table>...</table>
1985
+ * </div>
1986
+ * </li>
1987
+ * </ol>
1988
+ * ```
1989
+ *
1990
+ * @param documentFragment `data.content` obtained from clipboard.
1991
+ * @param writer The view writer instance.
1992
+ * @internal
1993
+ */
1994
+ function replaceMSFootnotes(documentFragment, writer) {
1995
+ const msFootnotesRefs = new Map();
1996
+ const msFootnotesDefs = new Map();
1997
+ let msFootnotesDefinitionsList = null;
1998
+ // Phase 1: Collect all footnotes references and definitions. Find the footnotes definitions list element.
1999
+ for (const { item } of writer.createRangeIn(documentFragment)) {
2000
+ if (!item.is('element')) {
2001
+ continue;
2002
+ }
2003
+ // If spot a footnotes definitions element, let's store it. It'll be replaced later.
2004
+ // There should be only one such element in the document.
2005
+ if (item.getStyle('mso-element') === 'footnote-list') {
2006
+ msFootnotesDefinitionsList = item;
2007
+ continue;
2008
+ }
2009
+ // If spot a footnote reference or definition, store it in the corresponding map.
2010
+ if (item.hasStyle('mso-footnote-id')) {
2011
+ const msFootnoteDef = item.findAncestor('element', el => el.getStyle('mso-element') === 'footnote');
2012
+ if (msFootnoteDef) {
2013
+ // If it's a reference within a definition, ignore it and track only the definition.
2014
+ // MS Word do not support nested footnotes, so it's safe to assume that all references within
2015
+ // a definition point to the same definition.
2016
+ const msFootnoteDefId = msFootnoteDef.getAttribute('id');
2017
+ msFootnotesDefs.set(msFootnoteDefId, msFootnoteDef);
2018
+ }
2019
+ else {
2020
+ // If it's a reference outside of a definition, track it as a reference.
2021
+ const msFootnoteRefId = item.getStyle('mso-footnote-id');
2022
+ msFootnotesRefs.set(msFootnoteRefId, item);
2023
+ }
2024
+ continue;
2025
+ }
2026
+ }
2027
+ // If there are no footnotes references or definitions, or no definitions list, there's nothing to normalize.
2028
+ if (!msFootnotesRefs.size || !msFootnotesDefinitionsList) {
2029
+ return;
2030
+ }
2031
+ // Phase 2: Replace footnotes definitions list with proper element.
2032
+ const footnotesDefinitionsList = createFootnotesListViewElement(writer);
2033
+ writer.replace(msFootnotesDefinitionsList, footnotesDefinitionsList);
2034
+ // Phase 3: Replace all footnotes references and add matching definitions to the definitions list.
2035
+ for (const [footnoteId, msFootnoteRef] of msFootnotesRefs) {
2036
+ const msFootnoteDef = msFootnotesDefs.get(footnoteId);
2037
+ if (!msFootnoteDef) {
2038
+ continue;
2039
+ }
2040
+ // Replace footnote reference.
2041
+ writer.replace(msFootnoteRef, createFootnoteRefViewElement(writer, footnoteId));
2042
+ // Append found matching definition to the definitions list.
2043
+ // Order doesn't matter here, as it'll be fixed in the post-fixer.
2044
+ const defElements = createFootnoteDefViewElement(writer, footnoteId);
2045
+ removeMSReferences(writer, msFootnoteDef);
2046
+ // Insert content within the `MsoFootnoteText` element. It's usually a definition text content.
2047
+ for (const child of msFootnoteDef.getChildren()) {
2048
+ let clonedChild = child;
2049
+ if (child.is('element')) {
2050
+ clonedChild = writer.clone(child, true);
2051
+ }
2052
+ writer.appendChild(clonedChild, defElements.content);
2053
+ }
2054
+ writer.appendChild(defElements.listItem, footnotesDefinitionsList);
2055
+ }
2056
+ }
2057
+ /**
2058
+ * Removes all MS Office specific references from the given element.
2059
+ *
2060
+ * It also removes leading space from text nodes following the references, as MS Word adds
2061
+ * them to separate the reference from the rest of the text.
2062
+ *
2063
+ * @param writer The view writer.
2064
+ * @param element The element to trim.
2065
+ * @returns The trimmed element.
2066
+ */
2067
+ function removeMSReferences(writer, element) {
2068
+ const elementsToRemove = [];
2069
+ const textNodesToTrim = [];
2070
+ for (const { item } of writer.createRangeIn(element)) {
2071
+ if (item.is('element') && item.getStyle('mso-footnote-id')) {
2072
+ elementsToRemove.unshift(item);
2073
+ // MS Word used to add spaces after footnote references within definitions. Let's check if there's a space after
2074
+ // the footnote reference and mark it for trimming.
2075
+ const { nextSibling } = item;
2076
+ if (nextSibling?.is('$text') && nextSibling.data.startsWith(' ')) {
2077
+ textNodesToTrim.unshift(nextSibling);
2078
+ }
2079
+ }
2080
+ }
2081
+ for (const element of elementsToRemove) {
2082
+ writer.remove(element);
2083
+ }
2084
+ // Remove only the leading space from text nodes following reference within definition, preserve the rest of the text.
2085
+ for (const textNode of textNodesToTrim) {
2086
+ const trimmedData = textNode.data.substring(1);
2087
+ if (trimmedData.length > 0) {
2088
+ // Create a new text node and replace the old one.
2089
+ const parent = textNode.parent;
2090
+ const index = parent.getChildIndex(textNode);
2091
+ const newTextNode = writer.createText(trimmedData);
2092
+ writer.remove(textNode);
2093
+ writer.insertChild(index, newTextNode, parent);
2094
+ }
2095
+ else {
2096
+ // If the text node contained only a space, remove it entirely.
2097
+ writer.remove(textNode);
2098
+ }
2099
+ }
2100
+ return element;
2101
+ }
2102
+ /**
2103
+ * Creates a footnotes list view element.
2104
+ *
2105
+ * @param writer The view writer instance.
2106
+ * @returns The footnotes list view element.
2107
+ */
2108
+ function createFootnotesListViewElement(writer) {
2109
+ return writer.createElement('ol', { class: 'footnotes' });
2110
+ }
2111
+ /**
2112
+ * Creates a footnote reference view element.
2113
+ *
2114
+ * @param writer The view writer instance.
2115
+ * @param footnoteId The footnote ID.
2116
+ * @returns The footnote reference view element.
2117
+ */
2118
+ function createFootnoteRefViewElement(writer, footnoteId) {
2119
+ const sup = writer.createElement('sup', { class: 'footnote' });
2120
+ const link = writer.createElement('a', {
2121
+ id: `ref-${footnoteId}`,
2122
+ href: `#${footnoteId}`,
2123
+ });
2124
+ writer.appendChild(link, sup);
2125
+ return sup;
2126
+ }
2127
+ /**
2128
+ * Creates a footnote definition view element with a backlink and a content container.
2129
+ *
2130
+ * @param writer The view writer instance.
2131
+ * @param footnoteId The footnote ID.
2132
+ * @returns An object containing the list item element, backlink and content container.
2133
+ */
2134
+ function createFootnoteDefViewElement(writer, footnoteId) {
2135
+ const listItem = writer.createElement('li', {
2136
+ id: footnoteId,
2137
+ class: 'footnote-definition',
2138
+ });
2139
+ const backLink = writer.createElement('a', {
2140
+ href: `#ref-${footnoteId}`,
2141
+ class: 'footnote-backlink',
2142
+ });
2143
+ const content = writer.createElement('div', {
2144
+ class: 'footnote-content',
2145
+ });
2146
+ writer.appendChild(writer.createText('^'), backLink);
2147
+ writer.appendChild(backLink, listItem);
2148
+ writer.appendChild(content, listItem);
2149
+ return {
2150
+ listItem,
2151
+ content,
2152
+ };
2153
+ }
2154
+
2155
+ /**
2156
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
2157
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
2158
+ */
2159
+ /**
2160
+ * @module paste-from-office/normalizers/mswordnormalizer
2161
+ */
2162
+ const msWordMatch1 = /<meta\s*name="?generator"?\s*content="?microsoft\s*word\s*\d+"?\/?>/i;
2163
+ const msWordMatch2 = /xmlns:o="urn:schemas-microsoft-com/i;
2164
+ /**
2165
+ * Normalizer for the content pasted from Microsoft Word.
2166
+ */
2167
+ class PasteFromOfficeMSWordNormalizer {
2168
+ document;
2169
+ hasMultiLevelListPlugin;
2170
+ hasTablePropertiesPlugin;
2171
+ hasExtendedTableBlockAlignment;
2172
+ /**
2173
+ * Creates a new `PasteFromOfficeMSWordNormalizer` instance.
2174
+ *
2175
+ * @param document View document.
2176
+ */
2177
+ constructor(document, hasMultiLevelListPlugin = false, hasTablePropertiesPlugin = false, hasExtendedTableBlockAlignment = false) {
2178
+ this.document = document;
2179
+ this.hasMultiLevelListPlugin = hasMultiLevelListPlugin;
2180
+ this.hasTablePropertiesPlugin = hasTablePropertiesPlugin;
2181
+ this.hasExtendedTableBlockAlignment = hasExtendedTableBlockAlignment;
2182
+ }
2183
+ /**
2184
+ * @inheritDoc
2185
+ */
2186
+ isActive(htmlString) {
2187
+ return msWordMatch1.test(htmlString) || msWordMatch2.test(htmlString);
2188
+ }
2189
+ /**
2190
+ * @inheritDoc
2191
+ */
2192
+ execute(data) {
2193
+ const writer = new ViewUpcastWriter(this.document);
2194
+ const { body: documentFragment, stylesString } = data._parsedData;
2195
+ transformBookmarks(documentFragment, writer);
2196
+ // transformListItemLikeElementsIntoLists(documentFragment, stylesString, this.hasMultiLevelListPlugin);
2197
+ replaceImagesSourceWithBase64(documentFragment, data.dataTransfer.getData('text/rtf'));
2198
+ transformTables(documentFragment, writer, this.hasTablePropertiesPlugin, this.hasExtendedTableBlockAlignment);
2199
+ removeInvalidTableWidth(documentFragment, writer);
2200
+ replaceMSFootnotes(documentFragment, writer);
2201
+ removeMSAttributes(documentFragment);
2202
+ data.content = documentFragment;
2203
+ }
2204
+ }
2205
+
2206
+ /**
2207
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
2208
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
2209
+ */
2210
+ /**
2211
+ * Removes the `<b>` tag wrapper added by Google Docs to a copied content.
2212
+ *
2213
+ * @param documentFragment element `data.content` obtained from clipboard
2214
+ * @internal
2215
+ */
2216
+ function removeBoldWrapper(documentFragment, writer) {
2217
+ for (const child of documentFragment.getChildren()) {
2218
+ if (child.is('element', 'b') && child.getStyle('font-weight') === 'normal') {
2219
+ const childIndex = documentFragment.getChildIndex(child);
2220
+ writer.remove(child);
2221
+ writer.insertChild(childIndex, child.getChildren(), documentFragment);
2222
+ }
2223
+ }
2224
+ }
2225
+
2226
+ /**
2227
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
2228
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
2229
+ */
2230
+ /**
2231
+ * @module paste-from-office/filters/br
2232
+ */
2233
+ /**
2234
+ * Transforms `<br>` elements that are siblings to some block element into a paragraphs.
2235
+ *
2236
+ * @param documentFragment The view structure to be transformed.
2237
+ * @internal
2238
+ */
2239
+ function transformBlockBrsToParagraphs(documentFragment, writer) {
2240
+ const viewDocument = new ViewDocument(writer.document.stylesProcessor);
2241
+ const domConverter = new ViewDomConverter(viewDocument, { renderingMode: 'data' });
2242
+ const blockElements = domConverter.blockElements;
2243
+ const inlineObjectElements = domConverter.inlineObjectElements;
2244
+ const elementsToReplace = [];
2245
+ for (const value of writer.createRangeIn(documentFragment)) {
2246
+ const element = value.item;
2247
+ if (element.is('element', 'br')) {
2248
+ const nextSibling = findSibling(element, 'forward', writer, { blockElements, inlineObjectElements });
2249
+ const previousSibling = findSibling(element, 'backward', writer, { blockElements, inlineObjectElements });
2250
+ const nextSiblingIsBlock = isBlockViewElement(nextSibling, blockElements);
2251
+ const previousSiblingIsBlock = isBlockViewElement(previousSibling, blockElements);
2252
+ // If the <br> is surrounded by blocks then convert it to a paragraph:
2253
+ // * <p>foo</p>[<br>]<p>bar</p> -> <p>foo</p>[<p></p>]<p>bar</p>
2254
+ // * <p>foo</p>[<br>] -> <p>foo</p>[<p></p>]
2255
+ // * [<br>]<p>foo</p> -> [<p></p>]<p>foo</p>
2256
+ if (previousSiblingIsBlock || nextSiblingIsBlock) {
2257
+ elementsToReplace.push(element);
2258
+ }
2259
+ }
2260
+ }
2261
+ for (const element of elementsToReplace) {
2262
+ if (element.hasClass('Apple-interchange-newline')) {
2263
+ writer.remove(element);
2264
+ }
2265
+ else {
2266
+ writer.replace(element, writer.createElement('p'));
2267
+ }
2268
+ }
2269
+ }
2270
+ /**
2271
+ * Returns sibling node, threats inline elements as transparent (but should stop on an inline objects).
2272
+ */
2273
+ function findSibling(viewElement, direction, writer, { blockElements, inlineObjectElements }) {
2274
+ let position = writer.createPositionAt(viewElement, direction == 'forward' ? 'after' : 'before');
2275
+ // Find first position that is just before a first:
2276
+ // * text node,
2277
+ // * block element,
2278
+ // * inline object element.
2279
+ // It's ignoring any inline (non-object) elements like span, strong, etc.
2280
+ position = position.getLastMatchingPosition(({ item }) => (item.is('element') &&
2281
+ !blockElements.includes(item.name) &&
2282
+ !inlineObjectElements.includes(item.name)), { direction });
2283
+ return direction == 'forward' ? position.nodeAfter : position.nodeBefore;
2284
+ }
2285
+ /**
2286
+ * Returns true for view elements that are listed as block view elements.
2287
+ */
2288
+ function isBlockViewElement(node, blockElements) {
2289
+ return !!node && node.is('element') && blockElements.includes(node.name);
2290
+ }
2291
+
2292
+ /**
2293
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
2294
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
2295
+ */
2296
+ /**
2297
+ * @module paste-from-office/filters/list
2298
+ */
2299
+ /**
2300
+ * Transforms Word specific list-like elements to the semantic HTML lists.
2301
+ *
2302
+ * Lists in Word are represented by block elements with special attributes like:
2303
+ *
2304
+ * ```xml
2305
+ * <p class=MsoListParagraphCxSpFirst style='mso-list:l1 level1 lfo1'>...</p> // Paragraph based list.
2306
+ * <h1 style='mso-list:l0 level1 lfo1'>...</h1> // Heading 1 based list.
2307
+ * ```
2308
+ *
2309
+ * @param documentFragment The view structure to be transformed.
2310
+ * @param stylesString Styles from which list-like elements styling will be extracted.
2311
+ * @internal
2312
+ */
2313
+ function transformListItemLikeElementsIntoLists(documentFragment, stylesString, hasMultiLevelListPlugin) {
2314
+ if (!documentFragment.childCount) {
2315
+ return;
2316
+ }
2317
+ const writer = new ViewUpcastWriter(documentFragment.document);
2318
+ const itemLikeElements = findAllItemLikeElements(documentFragment, writer);
2319
+ if (!itemLikeElements.length) {
2320
+ return;
2321
+ }
2322
+ const encounteredLists = {};
2323
+ const stack = [];
2324
+ let topLevelListInfo = createTopLevelListInfo();
2325
+ for (const itemLikeElement of itemLikeElements) {
2326
+ if (itemLikeElement.indent !== undefined) {
2327
+ if (!isListContinuation(itemLikeElement)) {
2328
+ applyIndentationToTopLevelList(writer, stack, topLevelListInfo);
2329
+ topLevelListInfo = createTopLevelListInfo();
2330
+ stack.length = 0;
2331
+ }
2332
+ // Combined list ID for addressing encounter lists counters.
2333
+ const originalListId = `${itemLikeElement.id}:${itemLikeElement.indent}`;
2334
+ // Normalized list item indentation.
2335
+ const indent = Math.min(itemLikeElement.indent - 1, stack.length);
2336
+ // Trimming of the list stack on list ID change.
2337
+ if (indent < stack.length && stack[indent].id !== itemLikeElement.id) {
2338
+ stack.length = indent;
2339
+ }
2340
+ // Trimming of the list stack on lower indent list encountered.
2341
+ if (indent < stack.length - 1) {
2342
+ stack.length = indent + 1;
2343
+ }
2344
+ else {
2345
+ const listStyle = detectListStyle(itemLikeElement, stylesString);
2346
+ // Create a new OL/UL if required (greater indent or different list type).
2347
+ if (indent > stack.length - 1 || stack[indent].listElement.name != listStyle.type) {
2348
+ // Check if there is some start index to set from a previous list.
2349
+ if (indent == 0 && listStyle.type == 'ol' && itemLikeElement.id !== undefined && encounteredLists[originalListId]) {
2350
+ listStyle.startIndex = encounteredLists[originalListId];
2351
+ }
2352
+ const listElement = createNewEmptyList(listStyle, writer, hasMultiLevelListPlugin);
2353
+ // Insert the new OL/UL.
2354
+ if (stack.length == 0) {
2355
+ const parent = itemLikeElement.element.parent;
2356
+ const index = parent.getChildIndex(itemLikeElement.element) + 1;
2357
+ writer.insertChild(index, listElement, parent);
2358
+ }
2359
+ else {
2360
+ const parentListItems = stack[indent - 1].listItemElements;
2361
+ writer.appendChild(listElement, parentListItems[parentListItems.length - 1]);
2362
+ }
2363
+ // Update the list stack for other items to reference.
2364
+ stack[indent] = {
2365
+ ...itemLikeElement,
2366
+ listElement,
2367
+ listItemElements: [],
2368
+ };
2369
+ // Prepare list counter for start index.
2370
+ if (indent == 0 && itemLikeElement.id !== undefined) {
2371
+ encounteredLists[originalListId] = listStyle.startIndex || 1;
2372
+ }
2373
+ }
2374
+ }
2375
+ // Use LI if it is already it or create a new LI element.
2376
+ // https://github.com/ckeditor/ckeditor5/issues/15964
2377
+ const listItem = itemLikeElement.element.name == 'li' ? itemLikeElement.element : writer.createElement('li');
2378
+ applyListItemMarginLeftAndUpdateTopLevelInfo(writer, stack, topLevelListInfo, itemLikeElement, listItem, indent);
2379
+ // Append the LI to OL/UL.
2380
+ writer.appendChild(listItem, stack[indent].listElement);
2381
+ stack[indent].listItemElements.push(listItem);
2382
+ // Increment list counter.
2383
+ if (indent == 0 && itemLikeElement.id !== undefined) {
2384
+ encounteredLists[originalListId]++;
2385
+ }
2386
+ // Append list block to LI.
2387
+ if (itemLikeElement.element != listItem) {
2388
+ writer.appendChild(itemLikeElement.element, listItem);
2389
+ }
2390
+ // Clean list block.
2391
+ removeBulletElement(itemLikeElement.element, writer);
2392
+ writer.removeStyle('text-indent', itemLikeElement.element); // #12361
2393
+ writer.removeStyle('margin-left', itemLikeElement.element);
2394
+ }
2395
+ else {
2396
+ // Other blocks in a list item.
2397
+ const stackItem = stack.find(stackItem => stackItem.marginLeft == itemLikeElement.marginLeft);
2398
+ // This might be a paragraph that has known margin, but it is not a real list block.
2399
+ if (stackItem) {
2400
+ const listItems = stackItem.listItemElements;
2401
+ // Append block to LI.
2402
+ writer.appendChild(itemLikeElement.element, listItems[listItems.length - 1]);
2403
+ writer.removeStyle('margin-left', itemLikeElement.element);
2404
+ }
2405
+ else {
2406
+ stack.length = 0;
2407
+ }
2408
+ }
2409
+ }
2410
+ applyIndentationToTopLevelList(writer, stack, topLevelListInfo);
2411
+ }
2412
+ function applyListItemMarginLeftAndUpdateTopLevelInfo(writer, stack, topLevelListInfo, itemLikeElement, listItem, indent) {
2413
+ if (itemLikeElement.marginLeft === undefined) {
2414
+ // 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.
2415
+ if (indent == 0) {
2416
+ topLevelListInfo.canApplyMarginOnList = false;
2417
+ }
2418
+ return;
2419
+ }
2420
+ const listItemBlockMarginLeft = parseFloat(itemLikeElement.marginLeft);
2421
+ let currentListBlockIndent = 0;
2422
+ if (stack.length > 1) {
2423
+ const prevStackLevelItems = stack[stack.length - 2].listItemElements;
2424
+ if (prevStackLevelItems.length > 0) {
2425
+ // The margin-left style of the previous indent level last item is already a relative value applied in the previous iteration.
2426
+ const lastItemMargin = prevStackLevelItems[prevStackLevelItems.length - 1].getStyle('margin-left');
2427
+ if (lastItemMargin !== undefined) {
2428
+ currentListBlockIndent += parseFloat(lastItemMargin);
2429
+ }
2430
+ }
2431
+ }
2432
+ // Add 40px for each indent level because by default HTML lists have 40px indentation (padding-inline-start: 40px).
2433
+ // So every nested list is indented by another 40px.
2434
+ // Additionally, the nested list itself may be placed in a list item with margin-left style.
2435
+ currentListBlockIndent += stack.length * 40;
2436
+ // Calculate relative list item indentation to the list it is in.
2437
+ const adjustedListItemIndent = listItemBlockMarginLeft - currentListBlockIndent;
2438
+ const listItemBlockMarginLeftPx = adjustedListItemIndent !== 0 ? toPx(adjustedListItemIndent) : undefined;
2439
+ if (listItemBlockMarginLeftPx) {
2440
+ writer.setStyle('margin-left', listItemBlockMarginLeftPx, listItem);
2441
+ if (indent == 0 && topLevelListInfo.canApplyMarginOnList) {
2442
+ if (topLevelListInfo.marginLeft === undefined) {
2443
+ topLevelListInfo.marginLeft = listItemBlockMarginLeftPx;
2444
+ }
2445
+ if (listItemBlockMarginLeftPx !== topLevelListInfo.marginLeft) {
2446
+ topLevelListInfo.canApplyMarginOnList = false;
2447
+ }
2448
+ topLevelListInfo.topLevelListItemElements.push(listItem);
2449
+ }
2450
+ }
2451
+ }
2452
+ function createTopLevelListInfo() {
2453
+ return {
2454
+ marginLeft: undefined,
2455
+ canApplyMarginOnList: true,
2456
+ topLevelListItemElements: [],
2457
+ };
2458
+ }
2459
+ /**
2460
+ * Sets margin-left style to the top-level list if all its items have the same margin-left.
2461
+ * If margin-left is set on the list, it is removed from all its items to avoid doubling of margins.
2462
+ */
2463
+ function applyIndentationToTopLevelList(writer, stack, topLevelListInfo) {
2464
+ if (topLevelListInfo.canApplyMarginOnList && topLevelListInfo.marginLeft && topLevelListInfo.topLevelListItemElements.length > 0) {
2465
+ // Apply margin-left to the top-level list if all its items have the same margin-left.
2466
+ writer.setStyle('margin-left', topLevelListInfo.marginLeft, stack[0].listElement);
2467
+ // Remove margin-left from all top-level list items.
2468
+ for (const topLevelListItem of topLevelListInfo.topLevelListItemElements) {
2469
+ writer.removeStyle('margin-left', topLevelListItem);
2470
+ }
2471
+ }
2472
+ }
2473
+ /**
2474
+ * Removes paragraph wrapping content inside a list item.
2475
+ *
2476
+ * @internal
2477
+ */
2478
+ function unwrapParagraphInListItem(documentFragment, writer) {
2479
+ for (const value of writer.createRangeIn(documentFragment)) {
2480
+ const element = value.item;
2481
+ if (element.is('element', 'li')) {
2482
+ // Google Docs allows for single paragraph inside LI.
2483
+ const firstChild = element.getChild(0);
2484
+ if (firstChild && firstChild.is('element', 'p')) {
2485
+ writer.unwrapElement(firstChild);
2486
+ }
2487
+ }
2488
+ }
2489
+ }
2490
+ /**
2491
+ * Finds all list-like elements in a given document fragment.
2492
+ *
2493
+ * @param documentFragment Document fragment in which to look for list-like nodes.
2494
+ * @returns Array of found list-like items. Each item is an object containing
2495
+ * @internal
2496
+ */
2497
+ function findAllItemLikeElements(documentFragment, writer) {
2498
+ const range = writer.createRangeIn(documentFragment);
2499
+ const itemLikeElements = [];
2500
+ const foundMargins = new Set();
2501
+ for (const item of range.getItems()) {
2502
+ // https://github.com/ckeditor/ckeditor5/issues/15964
2503
+ if (!item.is('element') || !item.name.match(/^(p|h\d+|li|div)$/)) {
2504
+ continue;
2505
+ }
2506
+ // Try to rely on margin-left style to find paragraphs visually aligned with previously encountered list item.
2507
+ let marginLeft = getMarginLeftNormalized(item);
2508
+ // Ignore margin-left 0 style if there is no MsoList... class.
2509
+ if (marginLeft !== undefined &&
2510
+ parseFloat(marginLeft) == 0 &&
2511
+ !Array.from(item.getClassNames()).find(className => className.startsWith('MsoList'))) {
2512
+ marginLeft = undefined;
2513
+ }
2514
+ // List item or a following list item block.
2515
+ if ((item.hasStyle('mso-list') && item.getStyle('mso-list') !== 'none') || (marginLeft !== undefined && foundMargins.has(marginLeft))) {
2516
+ const itemData = getListItemData(item);
2517
+ itemLikeElements.push({
2518
+ element: item,
2519
+ id: itemData.id,
2520
+ order: itemData.order,
2521
+ indent: itemData.indent,
2522
+ marginLeft,
2523
+ });
2524
+ if (marginLeft !== undefined) {
2525
+ foundMargins.add(marginLeft);
2526
+ }
2527
+ }
2528
+ // Clear found margins as we found block after a list.
2529
+ else {
2530
+ foundMargins.clear();
2531
+ }
2532
+ }
2533
+ return itemLikeElements;
2534
+ }
2535
+ /**
2536
+ * Whether the given element is possibly a list continuation. Previous element was wrapped into a list
2537
+ * or the current element already is inside a list.
2538
+ */
2539
+ function isListContinuation(currentItem) {
2540
+ const previousSibling = currentItem.element.previousSibling;
2541
+ if (!previousSibling) {
2542
+ const parent = currentItem.element.parent;
2543
+ // If it's a li inside ul or ol like in here: https://github.com/ckeditor/ckeditor5/issues/15964.
2544
+ // If the parent has previous sibling, which is not a list, then it is not a continuation.
2545
+ return isList(parent) && (!parent.previousSibling || isList(parent.previousSibling));
2546
+ }
2547
+ // Even with the same id the list does not have to be continuous (#43).
2548
+ return isList(previousSibling);
2549
+ }
2550
+ function isList(element) {
2551
+ return element.is('element', 'ol') || element.is('element', 'ul');
2552
+ }
2553
+ /**
2554
+ * Extracts list item style from the provided CSS.
2555
+ *
2556
+ * List item style is extracted from the CSS stylesheet. Each list with its specific style attribute
2557
+ * value (`mso-list:l1 level1 lfo1`) has its dedicated properties in a CSS stylesheet defined with a selector like:
2558
+ *
2559
+ * ```css
2560
+ * @list l1:level1 { ... }
2561
+ * ```
2562
+ *
2563
+ * It contains `mso-level-number-format` property which defines list numbering/bullet style. If this property
2564
+ * is not defined it means default `decimal` numbering.
2565
+ *
2566
+ * Here CSS string representation is used as `mso-level-number-format` property is an invalid CSS property
2567
+ * and will be removed during CSS parsing.
2568
+ *
2569
+ * @param listLikeItem List-like item for which list style will be searched for. Usually
2570
+ * a result of `findAllItemLikeElements()` function.
2571
+ * @param stylesString CSS stylesheet.
2572
+ * @returns An object with properties:
2573
+ *
2574
+ * * type - List type, could be `ul` or `ol`.
2575
+ * * startIndex - List start index, valid only for ordered lists.
2576
+ * * style - List style, for example: `decimal`, `lower-roman`, etc. It is extracted
2577
+ * directly from Word stylesheet and adjusted to represent proper values for the CSS `list-style-type` property.
2578
+ * If it cannot be adjusted, the `null` value is returned.
2579
+ */
2580
+ function detectListStyle(listLikeItem, stylesString) {
2581
+ const listStyleRegexp = new RegExp(`@list l${listLikeItem.id}:level${listLikeItem.indent}\\s*({[^}]*)`, 'gi');
2582
+ const listStyleTypeRegex = /mso-level-number-format:([^;]{0,100});/gi;
2583
+ const listStartIndexRegex = /mso-level-start-at:\s{0,100}([0-9]{0,10})\s{0,100};/gi;
2584
+ const legalStyleListRegex = new RegExp(`@list\\s+l${listLikeItem.id}:level\\d\\s*{[^{]*mso-level-text:"%\\d\\\\.`, 'gi');
2585
+ const multiLevelNumberFormatTypeRegex = new RegExp(`@list l${listLikeItem.id}:level\\d\\s*{[^{]*mso-level-number-format:`, 'gi');
2586
+ const legalStyleListMatch = legalStyleListRegex.exec(stylesString);
2587
+ const multiLevelNumberFormatMatch = multiLevelNumberFormatTypeRegex.exec(stylesString);
2588
+ // Multi level lists in Word have mso-level-number-format attribute except legal lists,
2589
+ // so we used that. If list has legal list match and doesn't has mso-level-number-format
2590
+ // then this is legal-list.
2591
+ const islegalStyleList = legalStyleListMatch && !multiLevelNumberFormatMatch;
2592
+ const listStyleMatch = listStyleRegexp.exec(stylesString);
2593
+ let listStyleType = 'decimal'; // Decimal is default one.
2594
+ let type = 'ol'; // <ol> is default list.
2595
+ let startIndex = null;
2596
+ if (listStyleMatch && listStyleMatch[1]) {
2597
+ const listStyleTypeMatch = listStyleTypeRegex.exec(listStyleMatch[1]);
2598
+ if (listStyleTypeMatch && listStyleTypeMatch[1]) {
2599
+ listStyleType = listStyleTypeMatch[1].trim();
2600
+ type = listStyleType !== 'bullet' && listStyleType !== 'image' ? 'ol' : 'ul';
2601
+ }
2602
+ // Styles for the numbered lists are always defined in the Word CSS stylesheet.
2603
+ // Unordered lists MAY contain a value for the Word CSS definition `mso-level-text` but sometimes
2604
+ // this tag is missing. And because of that, we cannot depend on that. We need to predict the list style value
2605
+ // based on the list style marker element.
2606
+ if (listStyleType === 'bullet') {
2607
+ const bulletedStyle = findBulletedListStyle(listLikeItem.element);
2608
+ if (bulletedStyle) {
2609
+ listStyleType = bulletedStyle;
2610
+ }
2611
+ }
2612
+ else {
2613
+ const listStartIndexMatch = listStartIndexRegex.exec(listStyleMatch[1]);
2614
+ if (listStartIndexMatch && listStartIndexMatch[1]) {
2615
+ startIndex = parseInt(listStartIndexMatch[1]);
2616
+ }
2617
+ }
2618
+ if (islegalStyleList) {
2619
+ type = 'ol';
2620
+ }
2621
+ }
2622
+ return {
2623
+ type,
2624
+ startIndex,
2625
+ style: mapListStyleDefinition(listStyleType),
2626
+ isLegalStyleList: islegalStyleList,
2627
+ };
2628
+ }
2629
+ /**
2630
+ * Tries to extract the `list-style-type` value based on the marker element for bulleted list.
2631
+ */
2632
+ function findBulletedListStyle(element) {
2633
+ // https://github.com/ckeditor/ckeditor5/issues/15964
2634
+ if (element.name == 'li' && element.parent.name == 'ul' && element.parent.hasAttribute('type')) {
2635
+ return element.parent.getAttribute('type');
2636
+ }
2637
+ const listMarkerElement = findListMarkerNode(element);
2638
+ if (!listMarkerElement) {
2639
+ return null;
2640
+ }
2641
+ const listMarker = listMarkerElement._data;
2642
+ if (listMarker === 'o') {
2643
+ return 'circle';
2644
+ }
2645
+ else if (listMarker === '·') {
2646
+ return 'disc';
2647
+ }
2648
+ // Word returns '§' instead of '■' for the square list style.
2649
+ else if (listMarker === '§') {
2650
+ return 'square';
2651
+ }
2652
+ return null;
2653
+ }
2654
+ /**
2655
+ * Tries to find a text node that represents the marker element (list-style-type).
2656
+ */
2657
+ function findListMarkerNode(element) {
2658
+ // If the first child is a text node, it is the data for the element.
2659
+ // The list-style marker is not present here.
2660
+ if (element.getChild(0).is('$text')) {
2661
+ return null;
2662
+ }
2663
+ for (const childNode of element.getChildren()) {
2664
+ // The list-style marker will be inside the `<span>` element. Let's ignore all non-span elements.
2665
+ // It may happen that the `<a>` element is added as the first child. Most probably, it's an anchor element.
2666
+ if (!childNode.is('element', 'span')) {
2667
+ continue;
2668
+ }
2669
+ const textNodeOrElement = childNode.getChild(0);
2670
+ if (!textNodeOrElement) {
2671
+ continue;
2672
+ }
2673
+ // If already found the marker element, use it.
2674
+ if (textNodeOrElement.is('$text')) {
2675
+ return textNodeOrElement;
2676
+ }
2677
+ return textNodeOrElement.getChild(0);
2678
+ }
2679
+ /* istanbul ignore next -- @preserve */
2680
+ return null;
2681
+ }
2682
+ /**
2683
+ * Parses the `list-style-type` value extracted directly from the Word CSS stylesheet and returns proper CSS definition.
2684
+ */
2685
+ function mapListStyleDefinition(value) {
2686
+ if (value.startsWith('arabic-leading-zero')) {
2687
+ return 'decimal-leading-zero';
2688
+ }
2689
+ switch (value) {
2690
+ case 'alpha-upper':
2691
+ return 'upper-alpha';
2692
+ case 'alpha-lower':
2693
+ return 'lower-alpha';
2694
+ case 'roman-upper':
2695
+ return 'upper-roman';
2696
+ case 'roman-lower':
2697
+ return 'lower-roman';
2698
+ case 'circle':
2699
+ case 'disc':
2700
+ case 'square':
2701
+ return value;
2702
+ default:
2703
+ return null;
2704
+ }
2705
+ }
2706
+ /**
2707
+ * Creates a new list OL/UL element.
2708
+ */
2709
+ function createNewEmptyList(listStyle, writer, hasMultiLevelListPlugin) {
2710
+ const list = writer.createElement(listStyle.type);
2711
+ // We do not support modifying the marker for a particular list item.
2712
+ // Set the value for the `list-style-type` property directly to the list container.
2713
+ if (listStyle.style) {
2714
+ writer.setStyle('list-style-type', listStyle.style, list);
2715
+ }
2716
+ if (listStyle.startIndex && listStyle.startIndex > 1) {
2717
+ writer.setAttribute('start', listStyle.startIndex, list);
2718
+ }
2719
+ if (listStyle.isLegalStyleList && hasMultiLevelListPlugin) {
2720
+ writer.addClass('legal-list', list);
2721
+ }
2722
+ return list;
2723
+ }
2724
+ /**
2725
+ * Extracts list item information from Word specific list-like element style:
2726
+ *
2727
+ * ```
2728
+ * `style="mso-list:l1 level1 lfo1"`
2729
+ * ```
2730
+ *
2731
+ * where:
2732
+ *
2733
+ * ```
2734
+ * * `l1` is a list id (however it does not mean this is a continuous list - see #43),
2735
+ * * `level1` is a list item indentation level,
2736
+ * * `lfo1` is a list insertion order in a document.
2737
+ * ```
2738
+ *
2739
+ * @param element Element from which style data is extracted.
2740
+ */
2741
+ function getListItemData(element) {
2742
+ const listStyle = element.getStyle('mso-list');
2743
+ if (listStyle === undefined) {
2744
+ return {};
2745
+ }
2746
+ const idMatch = listStyle.match(/(^|\s{1,100})l(\d+)/i);
2747
+ const orderMatch = listStyle.match(/\s{0,100}lfo(\d+)/i);
2748
+ const indentMatch = listStyle.match(/\s{0,100}level(\d+)/i);
2749
+ if (idMatch && orderMatch && indentMatch) {
2750
+ return {
2751
+ id: idMatch[2],
2752
+ order: orderMatch[1],
2753
+ indent: parseInt(indentMatch[1]),
2754
+ };
2755
+ }
2756
+ return {
2757
+ indent: 1, // Handle empty mso-list style as a marked for default list item.
2758
+ };
2759
+ }
2760
+ /**
2761
+ * Removes span with a numbering/bullet from a given element.
2762
+ */
2763
+ function removeBulletElement(element, writer) {
2764
+ // Matcher for finding `span` elements holding lists numbering/bullets.
2765
+ const bulletMatcher = new Matcher({
2766
+ name: 'span',
2767
+ styles: {
2768
+ 'mso-list': 'Ignore',
1262
2769
  },
1263
- {
1264
- title: '24',
1265
- model: '24pt',
2770
+ });
2771
+ const range = writer.createRangeIn(element);
2772
+ for (const value of range) {
2773
+ if (value.type === 'elementStart' && bulletMatcher.match(value.item)) {
2774
+ writer.remove(value.item);
2775
+ }
2776
+ }
2777
+ }
2778
+ /**
2779
+ * Returns element left margin normalized to 'px' if possible.
2780
+ */
2781
+ function getMarginLeftNormalized(element) {
2782
+ const value = element.getStyle('margin-left');
2783
+ if (value === undefined || value.endsWith('px')) {
2784
+ return value;
2785
+ }
2786
+ return convertCssLengthToPx(value);
2787
+ }
2788
+
2789
+ /**
2790
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
2791
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
2792
+ */
2793
+ /**
2794
+ * @module paste-from-office/normalizers/googledocsnormalizer
2795
+ */
2796
+ const googleDocsMatch = /id=("|')docs-internal-guid-[-0-9a-f]+("|')/i;
2797
+ /**
2798
+ * Normalizer for the content pasted from Google Docs.
2799
+ *
2800
+ * @internal
2801
+ */
2802
+ class GoogleDocsNormalizer {
2803
+ document;
2804
+ /**
2805
+ * Creates a new `GoogleDocsNormalizer` instance.
2806
+ *
2807
+ * @param document View document.
2808
+ */
2809
+ constructor(document) {
2810
+ this.document = document;
2811
+ }
2812
+ /**
2813
+ * @inheritDoc
2814
+ */
2815
+ isActive(htmlString) {
2816
+ return googleDocsMatch.test(htmlString);
2817
+ }
2818
+ /**
2819
+ * @inheritDoc
2820
+ */
2821
+ execute(data) {
2822
+ const writer = new ViewUpcastWriter(this.document);
2823
+ const { body: documentFragment } = data._parsedData;
2824
+ removeBoldWrapper(documentFragment, writer);
2825
+ unwrapParagraphInListItem(documentFragment, writer);
2826
+ transformBlockBrsToParagraphs(documentFragment, writer);
2827
+ // replaceTabsWithinPreWithSpaces( documentFragment, writer, 8 );
2828
+ data.content = documentFragment;
2829
+ }
2830
+ }
2831
+
2832
+ /**
2833
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
2834
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
2835
+ */
2836
+ /**
2837
+ * Removes the `xmlns` attribute from table pasted from Google Sheets.
2838
+ *
2839
+ * @param documentFragment element `data.content` obtained from clipboard
2840
+ * @internal
2841
+ */
2842
+ function removeXmlns(documentFragment, writer) {
2843
+ for (const child of documentFragment.getChildren()) {
2844
+ if (child.is('element', 'table') && child.hasAttribute('xmlns')) {
2845
+ writer.removeAttribute('xmlns', child);
2846
+ }
2847
+ }
2848
+ }
2849
+
2850
+ /**
2851
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
2852
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
2853
+ */
2854
+ /**
2855
+ * Removes the `<google-sheets-html-origin>` tag wrapper added by Google Sheets to a copied content.
2856
+ *
2857
+ * @param documentFragment element `data.content` obtained from clipboard
2858
+ * @internal
2859
+ */
2860
+ function removeGoogleSheetsTag(documentFragment, writer) {
2861
+ for (const child of documentFragment.getChildren()) {
2862
+ if (child.is('element', 'google-sheets-html-origin')) {
2863
+ const childIndex = documentFragment.getChildIndex(child);
2864
+ writer.remove(child);
2865
+ writer.insertChild(childIndex, child.getChildren(), documentFragment);
2866
+ }
2867
+ }
2868
+ }
2869
+
2870
+ /**
2871
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
2872
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
2873
+ */
2874
+ /**
2875
+ * Removes `<style>` block added by Google Sheets to a copied content.
2876
+ *
2877
+ * @param documentFragment element `data.content` obtained from clipboard
2878
+ * @internal
2879
+ */
2880
+ function removeStyleBlock(documentFragment, writer) {
2881
+ for (const child of Array.from(documentFragment.getChildren())) {
2882
+ if (child.is('element', 'style')) {
2883
+ writer.remove(child);
2884
+ }
2885
+ }
2886
+ }
2887
+
2888
+ /**
2889
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
2890
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
2891
+ */
2892
+ /**
2893
+ * @module paste-from-office/normalizers/googlesheetsnormalizer
2894
+ */
2895
+ const googleSheetsMatch = /<google-sheets-html-origin/i;
2896
+ /**
2897
+ * Normalizer for the content pasted from Google Sheets.
2898
+ *
2899
+ * @internal
2900
+ */
2901
+ class GoogleSheetsNormalizer {
2902
+ document;
2903
+ /**
2904
+ * Creates a new `GoogleSheetsNormalizer` instance.
2905
+ *
2906
+ * @param document View document.
2907
+ */
2908
+ constructor(document) {
2909
+ this.document = document;
2910
+ }
2911
+ /**
2912
+ * @inheritDoc
2913
+ */
2914
+ isActive(htmlString) {
2915
+ return googleSheetsMatch.test(htmlString);
2916
+ }
2917
+ /**
2918
+ * @inheritDoc
2919
+ */
2920
+ execute(data) {
2921
+ const writer = new ViewUpcastWriter(this.document);
2922
+ const { body: documentFragment } = data._parsedData;
2923
+ removeGoogleSheetsTag(documentFragment, writer);
2924
+ removeXmlns(documentFragment, writer);
2925
+ removeInvalidTableWidth(documentFragment, writer);
2926
+ removeStyleBlock(documentFragment, writer);
2927
+ data.content = documentFragment;
2928
+ }
2929
+ }
2930
+
2931
+ /**
2932
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
2933
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
2934
+ */
2935
+ /**
2936
+ * @module paste-from-office/filters/space
2937
+ */
2938
+ /**
2939
+ * Replaces last space preceding elements closing tag with `&nbsp;`. Such operation prevents spaces from being removed
2940
+ * during further DOM/View processing (see especially {@link module:engine/view/domconverter~ViewDomConverter#_processDomInlineNodes}).
2941
+ * This method also takes into account Word specific `<o:p></o:p>` empty tags.
2942
+ * Additionally multiline sequences of spaces and new lines between tags are removed (see #39 and #40).
2943
+ *
2944
+ * @param htmlString HTML string in which spacing should be normalized.
2945
+ * @returns Input HTML with spaces normalized.
2946
+ * @internal
2947
+ */
2948
+ function normalizeSpacing(htmlString) {
2949
+ // Run normalizeSafariSpaceSpans() two times to cover nested spans.
2950
+ return (normalizeSafariSpaceSpans(normalizeSafariSpaceSpans(htmlString))
2951
+ // Remove all \r\n from "spacerun spans" so the last replace line doesn't strip all whitespaces.
2952
+ .replace(/(<span\s+style=['"]mso-spacerun:yes['"]>[^\S\r\n]*?)[\r\n]+([^\S\r\n]*<\/span>)/g, '$1$2')
2953
+ .replace(/<span\s+style=['"]mso-spacerun:yes['"]><\/span>/g, '')
2954
+ .replace(/(<span\s+style=['"]letter-spacing:[^'"]+?['"]>)[\r\n]+(<\/span>)/g, '$1 $2')
2955
+ .replace(/ <\//g, '\u00A0</')
2956
+ .replace(/ <o:p><\/o:p>/g, '\u00A0<o:p></o:p>')
2957
+ // Remove <o:p> block filler from empty paragraph. Safari uses \u00A0 instead of &nbsp;.
2958
+ .replace(/<o:p>(&nbsp;|\u00A0)<\/o:p>/g, '')
2959
+ // Remove all whitespaces when they contain any \r or \n.
2960
+ .replace(/>([^\S\r\n]*[\r\n]\s*)</g, '><'));
2961
+ }
2962
+ /**
2963
+ * Normalizes spacing in special Word `spacerun spans` (`<span style='mso-spacerun:yes'>\s+</span>`) by replacing
2964
+ * all spaces with `&nbsp; ` pairs. This prevents spaces from being removed during further DOM/View processing
2965
+ * (see especially {@link module:engine/view/domconverter~ViewDomConverter#_processDomInlineNodes}).
2966
+ *
2967
+ * @param htmlDocument Native `Document` object in which spacing should be normalized.
2968
+ * @internal
2969
+ */
2970
+ function normalizeSpacerunSpans(htmlDocument) {
2971
+ htmlDocument.querySelectorAll('span[style*=spacerun]').forEach(el => {
2972
+ const htmlElement = el;
2973
+ const innerTextLength = htmlElement.innerText.length || 0;
2974
+ htmlElement.innerText = Array(innerTextLength + 1)
2975
+ .join('\u00A0 ')
2976
+ .substr(0, innerTextLength);
2977
+ });
2978
+ }
2979
+ /**
2980
+ * Normalizes specific spacing generated by Safari when content pasted from Word (`<span class="Apple-converted-space"> </span>`)
2981
+ * by replacing all spaces sequences longer than 1 space with `&nbsp; ` pairs. This prevents spaces from being removed during
2982
+ * further DOM/View processing (see especially {@link module:engine/view/domconverter~ViewDomConverter#_processDataFromDomText}).
2983
+ *
2984
+ * This function is similar to {@link module:clipboard/utils/normalizeclipboarddata normalizeClipboardData util} but uses
2985
+ * regular spaces / &nbsp; sequence for replacement.
2986
+ *
2987
+ * @param htmlString HTML string in which spacing should be normalized
2988
+ * @returns Input HTML with spaces normalized.
2989
+ * @internal
2990
+ */
2991
+ function normalizeSafariSpaceSpans(htmlString) {
2992
+ return htmlString.replace(/<span(?: class="Apple-converted-space"|)>(\s+)<\/span>/g, (fullMatch, spaces) => {
2993
+ return spaces.length === 1
2994
+ ? ' '
2995
+ : Array(spaces.length + 1)
2996
+ .join('\u00A0 ')
2997
+ .substr(0, spaces.length);
2998
+ });
2999
+ }
3000
+
3001
+ /**
3002
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
3003
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
3004
+ */
3005
+ /**
3006
+ * @module paste-from-office/filters/parse
3007
+ */
3008
+ /**
3009
+ * Parses the provided HTML extracting contents of `<body>` and `<style>` tags.
3010
+ *
3011
+ * @param htmlString HTML string to be parsed.
3012
+ */
3013
+ function parsePasteOfficeHtml(htmlString, stylesProcessor) {
3014
+ const domParser = new DOMParser();
3015
+ // Remove Word specific "if comments" so content inside is not omitted by the parser.
3016
+ htmlString = htmlString.replace(/<!--\[if gte vml 1]>/g, '');
3017
+ // Clean the <head> section of MS Windows specific tags. See https://github.com/ckeditor/ckeditor5/issues/15333.
3018
+ // The regular expression matches the <o:SmartTagType> tag with optional attributes (with or without values).
3019
+ htmlString = htmlString.replace(/<o:SmartTagType(?:\s+[^\s>=]+(?:="[^"]*")?)*\s*\/?>/gi, '');
3020
+ const normalizedHtml = normalizeSpacing(cleanContentAfterBody(htmlString));
3021
+ // Parse htmlString as native Document object.
3022
+ const htmlDocument = domParser.parseFromString(normalizedHtml, 'text/html');
3023
+ normalizeSpacerunSpans(htmlDocument);
3024
+ // Get `innerHTML` first as transforming to View modifies the source document.
3025
+ const bodyString = htmlDocument.body.innerHTML;
3026
+ // Transform document.body to View.
3027
+ const bodyView = documentToView(htmlDocument, stylesProcessor);
3028
+ // Extract stylesheets.
3029
+ const stylesObject = extractStyles(htmlDocument);
3030
+ return {
3031
+ body: bodyView,
3032
+ bodyString,
3033
+ styles: stylesObject.styles,
3034
+ stylesString: stylesObject.stylesString,
3035
+ };
3036
+ }
3037
+ /**
3038
+ * Transforms native `Document` object into {@link module:engine/view/documentfragment~ViewDocumentFragment}. Comments are skipped.
3039
+ *
3040
+ * @param htmlDocument Native `Document` object to be transformed.
3041
+ */
3042
+ function documentToView(htmlDocument, stylesProcessor) {
3043
+ const viewDocument = new ViewDocument(stylesProcessor);
3044
+ const domConverter = new ViewDomConverter(viewDocument, { renderingMode: 'data' });
3045
+ const fragment = htmlDocument.createDocumentFragment();
3046
+ const nodes = htmlDocument.body.childNodes;
3047
+ while (nodes.length > 0) {
3048
+ fragment.appendChild(nodes[0]);
3049
+ }
3050
+ return domConverter.domToView(fragment, { skipComments: true });
3051
+ }
3052
+ /**
3053
+ * Extracts both `CSSStyleSheet` and string representation from all `style` elements available in a provided `htmlDocument`.
3054
+ *
3055
+ * @param htmlDocument Native `Document` object from which styles will be extracted.
3056
+ */
3057
+ function extractStyles(htmlDocument) {
3058
+ const styles = [];
3059
+ const stylesString = [];
3060
+ const styleTags = Array.from(htmlDocument.getElementsByTagName('style'));
3061
+ for (const style of styleTags) {
3062
+ if (style.sheet && style.sheet.cssRules && style.sheet.cssRules.length) {
3063
+ styles.push(style.sheet);
3064
+ stylesString.push(style.innerHTML);
3065
+ }
3066
+ }
3067
+ return {
3068
+ styles,
3069
+ stylesString: stylesString.join(' '),
3070
+ };
3071
+ }
3072
+ /**
3073
+ * Removes leftover content from between closing </body> and closing </html> tag:
3074
+ *
3075
+ * ```html
3076
+ * <html><body><p>Foo Bar</p></body><span>Fo</span></html> -> <html><body><p>Foo Bar</p></body></html>
3077
+ * ```
3078
+ *
3079
+ * This function is used as specific browsers (Edge) add some random content after `body` tag when pasting from Word.
3080
+ * @param htmlString The HTML string to be cleaned.
3081
+ * @returns The HTML string with leftover content removed.
3082
+ */
3083
+ function cleanContentAfterBody(htmlString) {
3084
+ const bodyCloseTag = '</body>';
3085
+ const htmlCloseTag = '</html>';
3086
+ const bodyCloseIndex = htmlString.indexOf(bodyCloseTag);
3087
+ if (bodyCloseIndex < 0) {
3088
+ return htmlString;
3089
+ }
3090
+ const htmlCloseIndex = htmlString.indexOf(htmlCloseTag, bodyCloseIndex + bodyCloseTag.length);
3091
+ return htmlString.substring(0, bodyCloseIndex + bodyCloseTag.length) + (htmlCloseIndex >= 0 ? htmlString.substring(htmlCloseIndex) : '');
3092
+ }
3093
+
3094
+ /**
3095
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
3096
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
3097
+ */
3098
+ /**
3099
+ * @module paste-from-office/pastefromoffice
3100
+ */
3101
+ /**
3102
+ * The Paste from Office plugin.
3103
+ *
3104
+ * This plugin handles content pasted from Office apps and transforms it (if necessary)
3105
+ * to a valid structure which can then be understood by the editor features.
3106
+ *
3107
+ * Transformation is made by a set of predefined {@link module:paste-from-office/normalizer~PasteFromOfficeNormalizer normalizers}.
3108
+ * This plugin includes following normalizers:
3109
+ * * {@link module:paste-from-office/normalizers/mswordnormalizer~PasteFromOfficeMSWordNormalizer Microsoft Word normalizer}
3110
+ * * {@link module:paste-from-office/normalizers/googledocsnormalizer~GoogleDocsNormalizer Google Docs normalizer}
3111
+ *
3112
+ * For more information about this feature check the {@glink api/paste-from-office package page}.
3113
+ */
3114
+ class PasteHandler extends Plugin {
3115
+ /**
3116
+ * @inheritDoc
3117
+ */
3118
+ static get pluginName() {
3119
+ return 'PasteHandler';
3120
+ }
3121
+ /**
3122
+ * @inheritDoc
3123
+ */
3124
+ static get requires() {
3125
+ return [ClipboardPipeline];
3126
+ }
3127
+ /**
3128
+ * @inheritDoc
3129
+ */
3130
+ init() {
3131
+ const editor = this.editor;
3132
+ const clipboardPipeline = editor.plugins.get('ClipboardPipeline');
3133
+ const viewDocument = editor.editing.view.document;
3134
+ const normalizers = [];
3135
+ const hasMultiLevelListPlugin = this.editor.plugins.has('MultiLevelListEditing');
3136
+ const hasTablePropertiesPlugin = this.editor.plugins.has('TablePropertiesEditing');
3137
+ const hasExtendedTableBlockAlignment = !!this.editor.config.get('experimentalFlags.useExtendedTableBlockAlignment');
3138
+ normalizers.push(new PasteFromOfficeMSWordNormalizer(viewDocument, hasMultiLevelListPlugin, hasTablePropertiesPlugin, hasExtendedTableBlockAlignment));
3139
+ normalizers.push(new GoogleDocsNormalizer(viewDocument));
3140
+ normalizers.push(new GoogleSheetsNormalizer(viewDocument));
3141
+ clipboardPipeline.on('inputTransformation', (evt, data) => {
3142
+ if (data._isTransformedWithPasteFromOffice) {
3143
+ return;
3144
+ }
3145
+ const codeBlock = editor.model.document.selection.getFirstPosition().parent;
3146
+ if (codeBlock.is('element', 'codeBlock')) {
3147
+ return;
3148
+ }
3149
+ const htmlString = data.dataTransfer.getData('text/html');
3150
+ const activeNormalizer = normalizers.find(normalizer => normalizer.isActive(htmlString));
3151
+ if (activeNormalizer) {
3152
+ if (!data._parsedData) {
3153
+ data._parsedData = parsePasteOfficeHtml(htmlString, viewDocument.stylesProcessor);
3154
+ }
3155
+ activeNormalizer.execute(data);
3156
+ data._isTransformedWithPasteFromOffice = true;
3157
+ }
3158
+ }, { priority: 'high' });
3159
+ }
3160
+ }
3161
+
3162
+ class HighlightRangePlugin extends Plugin {
3163
+ init() {
3164
+ const editor = this.editor;
3165
+ editor.conversion.for('editingDowncast').markerToHighlight({
3166
+ model: 'highlightRange',
1266
3167
  view: {
1267
- name: 'span',
1268
- styles: { 'font-size': '24pt' },
1269
- priority: 7,
3168
+ classes: 'highlight-range',
1270
3169
  },
1271
- },
1272
- ];
3170
+ });
3171
+ }
1273
3172
  }
1274
- function getHeadingOptions() {
1275
- return [
1276
- { model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
1277
- { model: 'heading1', view: 'h1', title: 'Heading 1', class: 'ck-heading_heading1' },
1278
- { model: 'heading2', view: 'h2', title: 'Heading 2', class: 'ck-heading_heading2' },
1279
- { model: 'heading3', view: 'h3', title: 'Heading 3', class: 'ck-heading_heading3' },
1280
- ];
3173
+
3174
+ /**
3175
+ * Plugin để thêm margin-bottom: 4px cho các block elements
3176
+ * (paragraph, heading, list, table) thông qua downcast conversion
3177
+ */
3178
+ class BlockSpace extends Plugin {
3179
+ static get pluginName() {
3180
+ return 'BlockSpace';
3181
+ }
3182
+ init() {
3183
+ const editor = this.editor;
3184
+ const conversion = editor.conversion;
3185
+ // Handler factory để áp dụng margin-bottom: 4px
3186
+ const makeHandler = (elementType) => {
3187
+ return (evt, data, conversionApi) => {
3188
+ const viewElement = conversionApi.mapper.toViewElement(data.item);
3189
+ if (viewElement) {
3190
+ conversionApi.writer.setStyle('margin-top', '0', viewElement);
3191
+ conversionApi.writer.setStyle('margin-bottom', '6pt', viewElement);
3192
+ conversionApi.writer.setStyle('padding-top', '0', viewElement);
3193
+ conversionApi.writer.setStyle('padding-bottom', '0', viewElement);
3194
+ conversionApi.writer.setStyle('line-height', '1.15', viewElement);
3195
+ }
3196
+ };
3197
+ };
3198
+ // Áp dụng margin-bottom cho tất cả block elements
3199
+ conversion.for('downcast').add(dispatcher => {
3200
+ // Paragraph
3201
+ dispatcher.on('insert:paragraph', makeHandler('paragraph'), { priority: 'low' });
3202
+ // Heading (h1-h6)
3203
+ ['heading1', 'heading2', 'heading3', 'heading4', 'heading5', 'heading6'].forEach(heading => {
3204
+ dispatcher.on(`insert:${heading}`, makeHandler(heading), { priority: 'low' });
3205
+ });
3206
+ // Table
3207
+ dispatcher.on('insert:table', makeHandler('table'), { priority: 'low' });
3208
+ // PageBreak - add page-break-before style
3209
+ dispatcher.on('insert:pageBreak', (evt, data, conversionApi) => {
3210
+ const viewElement = conversionApi.mapper.toViewElement(data.item);
3211
+ if (viewElement) {
3212
+ conversionApi.writer.setStyle('page-break-before', 'always', viewElement);
3213
+ }
3214
+ }, { priority: 'low' });
3215
+ });
3216
+ }
1281
3217
  }
1282
3218
 
1283
3219
  /**
@@ -1327,18 +3263,6 @@ function normalize(content) {
1327
3263
  return normalized;
1328
3264
  }
1329
3265
 
1330
- class HighlightRangePlugin extends Plugin {
1331
- init() {
1332
- const editor = this.editor;
1333
- editor.conversion.for('editingDowncast').markerToHighlight({
1334
- model: 'highlightRange',
1335
- view: {
1336
- classes: 'highlight-range',
1337
- },
1338
- });
1339
- }
1340
- }
1341
-
1342
3266
  class SdDocumentBuilder {
1343
3267
  option;
1344
3268
  disabled = false;
@@ -1363,7 +3287,7 @@ class SdDocumentBuilder {
1363
3287
  getOption: () => this.option,
1364
3288
  licenseKey: 'GPL', // Hoặc key thương mại nếu có
1365
3289
  plugins: [
1366
- GeneralHtmlSupport,
3290
+ // GeneralHtmlSupport,
1367
3291
  FontSize,
1368
3292
  FontColor,
1369
3293
  FontFamily,
@@ -1380,7 +3304,6 @@ class SdDocumentBuilder {
1380
3304
  TableProperties,
1381
3305
  TableCellProperties,
1382
3306
  TableColumnResize,
1383
- PasteFromOffice,
1384
3307
  PageBreak,
1385
3308
  Undo,
1386
3309
  Alignment, // Canh lề
@@ -1394,17 +3317,20 @@ class SdDocumentBuilder {
1394
3317
  ImageResize,
1395
3318
  ImageStyle,
1396
3319
  ImageBlock,
3320
+ // Indent
3321
+ Indent,
3322
+ IndentBlock,
1397
3323
  // Custom Plugin
1398
3324
  HeadingPlugin,
1399
3325
  CommentPlugin,
1400
3326
  VariablePlugin,
1401
- TableFitPlugin,
1402
- Indent,
1403
- IndentBlock,
1404
- PageOrientationPlugin,
3327
+ TableCustom,
3328
+ PageOrientation,
1405
3329
  ImageUploadPlugin,
1406
3330
  ImageCustomPlugin,
1407
3331
  HighlightRangePlugin,
3332
+ PasteHandler,
3333
+ BlockSpace,
1408
3334
  ],
1409
3335
  toolbar: {
1410
3336
  items: [
@@ -1454,7 +3380,7 @@ class SdDocumentBuilder {
1454
3380
  },
1455
3381
  fontSize: {
1456
3382
  options: this.#fontSizeOptions,
1457
- supportAllValues: false, // Khuyên dùng false để ép user chọn đúng size chuẩn
3383
+ supportAllValues: true,
1458
3384
  },
1459
3385
  heading: {
1460
3386
  options: this.#headingOptions,
@@ -1503,13 +3429,10 @@ class SdDocumentBuilder {
1503
3429
  htmlSupport: {
1504
3430
  allow: [
1505
3431
  {
1506
- name: /.*/,
1507
- attributes: /.*/, // attributes chấp nhận boolean
1508
- classes: /.*/, // Cho phép mọi class
1509
- styles: {
1510
- // Cho phép tất cả styles trừ margin và padding
1511
- match: /^(?!margin|padding).*/,
1512
- },
3432
+ name: /.*/, // Cho phép tất cả tên thẻ HTML
3433
+ attributes: true, // Cho phép tất cả attributes
3434
+ classes: true, // Cho phép tất cả classes
3435
+ styles: true, // Cho phép tất cả styles
1513
3436
  },
1514
3437
  ],
1515
3438
  },
@@ -1527,7 +3450,7 @@ class SdDocumentBuilder {
1527
3450
  this.#editor = editor;
1528
3451
  // Setup orientation plugin callback
1529
3452
  try {
1530
- const orientationPlugin = editor.plugins.get('PageOrientationPlugin');
3453
+ const orientationPlugin = editor.plugins.get('PageOrientation');
1531
3454
  if (orientationPlugin && typeof orientationPlugin.onOrientationChange === 'function') {
1532
3455
  orientationPlugin.onOrientationChange(orientation => {
1533
3456
  this.option.onOrientation?.(orientation);
@@ -1539,7 +3462,7 @@ class SdDocumentBuilder {
1539
3462
  }
1540
3463
  }
1541
3464
  catch (error) {
1542
- console.warn('PageOrientationPlugin not available:', error);
3465
+ console.warn('PageOrientation not available:', error);
1543
3466
  }
1544
3467
  // Lắng nghe selection
1545
3468
  editor.model.document.selection.on('change', $event => {
@@ -1586,7 +3509,7 @@ class SdDocumentBuilder {
1586
3509
  if (!this.#editor)
1587
3510
  return;
1588
3511
  try {
1589
- const orientationPlugin = this.#editor.plugins.get('PageOrientationPlugin');
3512
+ const orientationPlugin = this.#editor.plugins.get('PageOrientation');
1590
3513
  if (orientationPlugin && typeof orientationPlugin.setOrientation === 'function') {
1591
3514
  orientationPlugin.setOrientation(orientation);
1592
3515
  }
@@ -1599,7 +3522,7 @@ class SdDocumentBuilder {
1599
3522
  if (!this.#editor)
1600
3523
  return 'PORTRAIT';
1601
3524
  try {
1602
- const orientationPlugin = this.#editor.plugins.get('PageOrientationPlugin');
3525
+ const orientationPlugin = this.#editor.plugins.get('PageOrientation');
1603
3526
  if (orientationPlugin && typeof orientationPlugin.getOrientation === 'function') {
1604
3527
  return orientationPlugin.getOrientation();
1605
3528
  }
@@ -1636,7 +3559,7 @@ class SdDocumentBuilder {
1636
3559
  this.#editor.enableReadOnlyMode(this.#id);
1637
3560
  // Disable page orientation button
1638
3561
  try {
1639
- const orientationPlugin = this.#editor.plugins.get('PageOrientationPlugin');
3562
+ const orientationPlugin = this.#editor.plugins.get('PageOrientation');
1640
3563
  if (orientationPlugin && orientationPlugin.buttonView) {
1641
3564
  orientationPlugin.buttonView.isEnabled = false;
1642
3565
  }
@@ -1650,7 +3573,7 @@ class SdDocumentBuilder {
1650
3573
  this.#editor.disableReadOnlyMode(this.#id);
1651
3574
  // Enable page orientation button
1652
3575
  try {
1653
- const orientationPlugin = this.#editor.plugins.get('PageOrientationPlugin');
3576
+ const orientationPlugin = this.#editor.plugins.get('PageOrientation');
1654
3577
  if (orientationPlugin && orientationPlugin.buttonView) {
1655
3578
  orientationPlugin.buttonView.isEnabled = true;
1656
3579
  }
@@ -2135,11 +4058,11 @@ class SdDocumentBuilder {
2135
4058
  });
2136
4059
  };
2137
4060
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: SdDocumentBuilder, deps: [], target: i0.ɵɵFactoryTarget.Component });
2138
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.17", type: SdDocumentBuilder, isStandalone: true, selector: "sd-document-builder", inputs: { option: "option", _disabled: ["disabled", "_disabled"] }, outputs: { contentChange: "contentChange" }, ngImport: i0, template: "<div class=\"builder-container\">\n <ckeditor\n style=\"width: 100%\"\n [editor]=\"Editor\" \n [config]=\"config\" \n (ready)=\"onReady($event)\"\n [disabled]=\"disabled\">\n </ckeditor>\n</div>", styles: ["@charset \"UTF-8\";.builder-container{background-color:#f3f4f6;height:100%;overflow-y:auto;width:100%;display:flex;flex-direction:column;align-items:center}:host{display:inline-block}:host ::ng-deep .ck-editor{display:flex;flex-direction:column;align-items:center;width:100%;--ck-content-font-family: \"Times New Roman\", serif !important;--ck-content-font-size: 13pt;--ck-content-line-height: 1.5}:host ::ng-deep .ck-editor__top,:host ::ng-deep .ck-editor__main{border:none!important;box-shadow:none!important}:host ::ng-deep .ck-editor__top{position:sticky;top:0;z-index:100;width:100%;min-width:600px;margin-bottom:10px}:host ::ng-deep .ck-editor__top .ck-sticky-panel__content{border:none!important}:host ::ng-deep .ck-editor__top .ck-toolbar{background:#fff!important;box-shadow:0 4px 6px -1px #0000001a!important;padding:8px!important}:host ::ng-deep .ck-editor__top .ck-toolbar .ck-toolbar__items{display:flex;justify-content:center;flex-wrap:wrap;align-items:center}:host ::ng-deep .ck-content{background-color:#fff;width:210mm;min-height:1123px;padding:20mm!important;box-sizing:border-box!important;box-shadow:0 10px 15px -3px #0000001a}:host ::ng-deep .ck-content h1,:host ::ng-deep .ck-content h2,:host ::ng-deep .ck-content h3,:host ::ng-deep .ck-content h4,:host ::ng-deep .ck-content h5,:host ::ng-deep .ck-content h6{font-weight:400}:host ::ng-deep .ck-content.landscape{width:297mm}:host ::ng-deep .ck-content>*{max-width:100%!important;box-sizing:border-box!important}:host ::ng-deep .ck-content p{margin-bottom:var(--ck-spacing-large);text-indent:0}:host ::ng-deep .ck-content ul,:host ::ng-deep .ck-content ol{padding-left:20px!important;margin-left:0!important}\n", ":host ::ng-deep .ck-heading-highlight{background-color:#fef08a}\n", ":host ::ng-deep .ck-comment-marker{background-color:#ffeb3b80;border-bottom:2px solid #fbc02d;transition:background-color .2s;cursor:pointer}:host ::ng-deep .ck-comment-marker:hover{background-color:#ffeb3bcc}:host ::ng-deep .ck-comment-marker.active-highlight{background-color:#ffeb3b;outline:2px dashed #f57f17}\n", "@charset \"UTF-8\";::ng-deep .ck-clipboard-drop-target-line{display:none!important}:host ::ng-deep .variable-widget{background-color:#e3f2fd;color:#1976d2;border:1px solid #90caf9!important;border-radius:4px;padding:2px 6px;font-weight:600;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif;font-size:10px;-webkit-user-select:none;user-select:none;display:inline-block;margin:0 4px;vertical-align:middle;font-size:0;cursor:default}:host ::ng-deep .variable-widget:before{content:attr(data-display);font-size:10px}:host ::ng-deep .variable-widget:hover{background-color:#bbdefb;box-shadow:0 1px 2px #0000001a}:host ::ng-deep .variable-widget.ck-widget_selected{outline:2px solid #2196f3;background-color:#bbdefb}:host ::ng-deep .ck.ck-content .ck-widget,:host ::ng-deep .ck.ck-content .ck-widget:hover,:host ::ng-deep .ck.ck-content .ck-widget:focus,:host ::ng-deep .ck.ck-content .ck-widget.ck-widget_selected,:host ::ng-deep .ck.ck-content .ck-widget.ck-widget_selected:hover{outline:none!important;box-shadow:none!important}\n", "", ":host ::ng-deep .highlight-range{background-color:#ffeb3b80;border-bottom:2px solid #fbc02d;transition:background-color .2s;cursor:pointer}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: CKEditorModule }, { kind: "component", type: i1.CKEditorComponent, selector: "ckeditor", inputs: ["editor", "config", "data", "tagName", "watchdog", "editorWatchdogConfig", "disableWatchdog", "disableTwoWayDataBinding", "disabled"], outputs: ["ready", "change", "blur", "focus", "error"] }] });
4061
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.17", type: SdDocumentBuilder, isStandalone: true, selector: "sd-document-builder", inputs: { option: "option", _disabled: ["disabled", "_disabled"] }, outputs: { contentChange: "contentChange" }, ngImport: i0, template: "<div class=\"builder-container\">\n <ckeditor\n style=\"width: 100%\"\n [editor]=\"Editor\" \n [config]=\"config\" \n (ready)=\"onReady($event)\"\n [disabled]=\"disabled\">\n </ckeditor>\n</div>", styles: ["@charset \"UTF-8\";.builder-container{background-color:#f3f4f6;height:100%;overflow-y:auto;width:100%;display:flex;flex-direction:column;align-items:center}:host{display:inline-block}:host ::ng-deep .ck-editor{display:flex;flex-direction:column;align-items:center;width:100%;--ck-content-font-family: \"Times New Roman\", serif !important;--ck-content-font-size: 13pt;--ck-content-line-height: 1.15}:host ::ng-deep .ck-editor__top,:host ::ng-deep .ck-editor__main{border:none!important;box-shadow:none!important}:host ::ng-deep .ck-editor__top{position:sticky;top:0;z-index:100;width:100%;min-width:600px;margin-bottom:10px}:host ::ng-deep .ck-editor__top .ck-sticky-panel__content{border:none!important}:host ::ng-deep .ck-editor__top .ck-toolbar{background:#fff!important;box-shadow:0 4px 6px -1px #0000001a!important;padding:8px!important}:host ::ng-deep .ck-editor__top .ck-toolbar .ck-toolbar__items{display:flex;justify-content:center;flex-wrap:wrap;align-items:center}:host ::ng-deep .ck-content{background-color:#fff;width:210mm;min-height:1123px;padding:20mm!important;box-sizing:border-box!important;box-shadow:0 10px 15px -3px #0000001a}:host ::ng-deep .ck-content h1,:host ::ng-deep .ck-content h2,:host ::ng-deep .ck-content h3,:host ::ng-deep .ck-content h4,:host ::ng-deep .ck-content h5,:host ::ng-deep .ck-content h6{font-weight:400;font-size:inherit;line-height:inherit;margin-bottom:4px}:host ::ng-deep .ck-content.landscape{width:297mm}:host ::ng-deep .ck-content>*{max-width:100%!important;box-sizing:border-box!important}:host ::ng-deep .ck-content p{margin-bottom:4px;text-indent:0}:host ::ng-deep .ck-content ul,:host ::ng-deep .ck-content ol{padding-left:20px!important;margin-left:0!important;margin-bottom:4px}:host ::ng-deep .ck-content li{margin-bottom:0}:host ::ng-deep .ck-content table{margin-bottom:4px}\n", ":host ::ng-deep .ck-heading-highlight{background-color:#fef08a}\n", ":host ::ng-deep .ck-comment-marker{background-color:#ffeb3b80;border-bottom:2px solid #fbc02d;transition:background-color .2s;cursor:pointer}:host ::ng-deep .ck-comment-marker:hover{background-color:#ffeb3bcc}:host ::ng-deep .ck-comment-marker.active-highlight{background-color:#ffeb3b;outline:2px dashed #f57f17}\n", "@charset \"UTF-8\";::ng-deep .ck-clipboard-drop-target-line{display:none!important}:host ::ng-deep .variable-widget{background-color:#e3f2fd;color:#1976d2;border:1px solid #90caf9!important;border-radius:4px;padding:2px 6px;font-weight:600;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif;font-size:10px;-webkit-user-select:none;user-select:none;display:inline-block;margin:0 4px;vertical-align:middle;font-size:0;cursor:default}:host ::ng-deep .variable-widget:before{content:attr(data-display);font-size:10px}:host ::ng-deep .variable-widget:hover{background-color:#bbdefb;box-shadow:0 1px 2px #0000001a}:host ::ng-deep .variable-widget.ck-widget_selected{outline:2px solid #2196f3;background-color:#bbdefb}:host ::ng-deep .ck.ck-content .ck-widget,:host ::ng-deep .ck.ck-content .ck-widget:hover,:host ::ng-deep .ck.ck-content .ck-widget:focus,:host ::ng-deep .ck.ck-content .ck-widget.ck-widget_selected,:host ::ng-deep .ck.ck-content .ck-widget.ck-widget_selected:hover{outline:none!important;box-shadow:none!important}\n", ":host ::ng-deep .highlight-range{background-color:#ffeb3b80;border-bottom:2px solid #fbc02d;transition:background-color .2s;cursor:pointer}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: CKEditorModule }, { kind: "component", type: i1.CKEditorComponent, selector: "ckeditor", inputs: ["editor", "config", "data", "tagName", "watchdog", "editorWatchdogConfig", "disableWatchdog", "disableTwoWayDataBinding", "disabled"], outputs: ["ready", "change", "blur", "focus", "error"] }] });
2139
4062
  }
2140
4063
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: SdDocumentBuilder, decorators: [{
2141
4064
  type: Component,
2142
- args: [{ selector: 'sd-document-builder', standalone: true, imports: [CommonModule, CKEditorModule], template: "<div class=\"builder-container\">\n <ckeditor\n style=\"width: 100%\"\n [editor]=\"Editor\" \n [config]=\"config\" \n (ready)=\"onReady($event)\"\n [disabled]=\"disabled\">\n </ckeditor>\n</div>", styles: ["@charset \"UTF-8\";.builder-container{background-color:#f3f4f6;height:100%;overflow-y:auto;width:100%;display:flex;flex-direction:column;align-items:center}:host{display:inline-block}:host ::ng-deep .ck-editor{display:flex;flex-direction:column;align-items:center;width:100%;--ck-content-font-family: \"Times New Roman\", serif !important;--ck-content-font-size: 13pt;--ck-content-line-height: 1.5}:host ::ng-deep .ck-editor__top,:host ::ng-deep .ck-editor__main{border:none!important;box-shadow:none!important}:host ::ng-deep .ck-editor__top{position:sticky;top:0;z-index:100;width:100%;min-width:600px;margin-bottom:10px}:host ::ng-deep .ck-editor__top .ck-sticky-panel__content{border:none!important}:host ::ng-deep .ck-editor__top .ck-toolbar{background:#fff!important;box-shadow:0 4px 6px -1px #0000001a!important;padding:8px!important}:host ::ng-deep .ck-editor__top .ck-toolbar .ck-toolbar__items{display:flex;justify-content:center;flex-wrap:wrap;align-items:center}:host ::ng-deep .ck-content{background-color:#fff;width:210mm;min-height:1123px;padding:20mm!important;box-sizing:border-box!important;box-shadow:0 10px 15px -3px #0000001a}:host ::ng-deep .ck-content h1,:host ::ng-deep .ck-content h2,:host ::ng-deep .ck-content h3,:host ::ng-deep .ck-content h4,:host ::ng-deep .ck-content h5,:host ::ng-deep .ck-content h6{font-weight:400}:host ::ng-deep .ck-content.landscape{width:297mm}:host ::ng-deep .ck-content>*{max-width:100%!important;box-sizing:border-box!important}:host ::ng-deep .ck-content p{margin-bottom:var(--ck-spacing-large);text-indent:0}:host ::ng-deep .ck-content ul,:host ::ng-deep .ck-content ol{padding-left:20px!important;margin-left:0!important}\n", ":host ::ng-deep .ck-heading-highlight{background-color:#fef08a}\n", ":host ::ng-deep .ck-comment-marker{background-color:#ffeb3b80;border-bottom:2px solid #fbc02d;transition:background-color .2s;cursor:pointer}:host ::ng-deep .ck-comment-marker:hover{background-color:#ffeb3bcc}:host ::ng-deep .ck-comment-marker.active-highlight{background-color:#ffeb3b;outline:2px dashed #f57f17}\n", "@charset \"UTF-8\";::ng-deep .ck-clipboard-drop-target-line{display:none!important}:host ::ng-deep .variable-widget{background-color:#e3f2fd;color:#1976d2;border:1px solid #90caf9!important;border-radius:4px;padding:2px 6px;font-weight:600;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif;font-size:10px;-webkit-user-select:none;user-select:none;display:inline-block;margin:0 4px;vertical-align:middle;font-size:0;cursor:default}:host ::ng-deep .variable-widget:before{content:attr(data-display);font-size:10px}:host ::ng-deep .variable-widget:hover{background-color:#bbdefb;box-shadow:0 1px 2px #0000001a}:host ::ng-deep .variable-widget.ck-widget_selected{outline:2px solid #2196f3;background-color:#bbdefb}:host ::ng-deep .ck.ck-content .ck-widget,:host ::ng-deep .ck.ck-content .ck-widget:hover,:host ::ng-deep .ck.ck-content .ck-widget:focus,:host ::ng-deep .ck.ck-content .ck-widget.ck-widget_selected,:host ::ng-deep .ck.ck-content .ck-widget.ck-widget_selected:hover{outline:none!important;box-shadow:none!important}\n", ":host ::ng-deep .highlight-range{background-color:#ffeb3b80;border-bottom:2px solid #fbc02d;transition:background-color .2s;cursor:pointer}\n"] }]
4065
+ args: [{ selector: 'sd-document-builder', standalone: true, imports: [CommonModule, CKEditorModule], template: "<div class=\"builder-container\">\n <ckeditor\n style=\"width: 100%\"\n [editor]=\"Editor\" \n [config]=\"config\" \n (ready)=\"onReady($event)\"\n [disabled]=\"disabled\">\n </ckeditor>\n</div>", styles: ["@charset \"UTF-8\";.builder-container{background-color:#f3f4f6;height:100%;overflow-y:auto;width:100%;display:flex;flex-direction:column;align-items:center}:host{display:inline-block}:host ::ng-deep .ck-editor{display:flex;flex-direction:column;align-items:center;width:100%;--ck-content-font-family: \"Times New Roman\", serif !important;--ck-content-font-size: 13pt;--ck-content-line-height: 1.15}:host ::ng-deep .ck-editor__top,:host ::ng-deep .ck-editor__main{border:none!important;box-shadow:none!important}:host ::ng-deep .ck-editor__top{position:sticky;top:0;z-index:100;width:100%;min-width:600px;margin-bottom:10px}:host ::ng-deep .ck-editor__top .ck-sticky-panel__content{border:none!important}:host ::ng-deep .ck-editor__top .ck-toolbar{background:#fff!important;box-shadow:0 4px 6px -1px #0000001a!important;padding:8px!important}:host ::ng-deep .ck-editor__top .ck-toolbar .ck-toolbar__items{display:flex;justify-content:center;flex-wrap:wrap;align-items:center}:host ::ng-deep .ck-content{background-color:#fff;width:210mm;min-height:1123px;padding:20mm!important;box-sizing:border-box!important;box-shadow:0 10px 15px -3px #0000001a}:host ::ng-deep .ck-content h1,:host ::ng-deep .ck-content h2,:host ::ng-deep .ck-content h3,:host ::ng-deep .ck-content h4,:host ::ng-deep .ck-content h5,:host ::ng-deep .ck-content h6{font-weight:400;font-size:inherit;line-height:inherit;margin-bottom:4px}:host ::ng-deep .ck-content.landscape{width:297mm}:host ::ng-deep .ck-content>*{max-width:100%!important;box-sizing:border-box!important}:host ::ng-deep .ck-content p{margin-bottom:4px;text-indent:0}:host ::ng-deep .ck-content ul,:host ::ng-deep .ck-content ol{padding-left:20px!important;margin-left:0!important;margin-bottom:4px}:host ::ng-deep .ck-content li{margin-bottom:0}:host ::ng-deep .ck-content table{margin-bottom:4px}\n", ":host ::ng-deep .ck-heading-highlight{background-color:#fef08a}\n", ":host ::ng-deep .ck-comment-marker{background-color:#ffeb3b80;border-bottom:2px solid #fbc02d;transition:background-color .2s;cursor:pointer}:host ::ng-deep .ck-comment-marker:hover{background-color:#ffeb3bcc}:host ::ng-deep .ck-comment-marker.active-highlight{background-color:#ffeb3b;outline:2px dashed #f57f17}\n", "@charset \"UTF-8\";::ng-deep .ck-clipboard-drop-target-line{display:none!important}:host ::ng-deep .variable-widget{background-color:#e3f2fd;color:#1976d2;border:1px solid #90caf9!important;border-radius:4px;padding:2px 6px;font-weight:600;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif;font-size:10px;-webkit-user-select:none;user-select:none;display:inline-block;margin:0 4px;vertical-align:middle;font-size:0;cursor:default}:host ::ng-deep .variable-widget:before{content:attr(data-display);font-size:10px}:host ::ng-deep .variable-widget:hover{background-color:#bbdefb;box-shadow:0 1px 2px #0000001a}:host ::ng-deep .variable-widget.ck-widget_selected{outline:2px solid #2196f3;background-color:#bbdefb}:host ::ng-deep .ck.ck-content .ck-widget,:host ::ng-deep .ck.ck-content .ck-widget:hover,:host ::ng-deep .ck.ck-content .ck-widget:focus,:host ::ng-deep .ck.ck-content .ck-widget.ck-widget_selected,:host ::ng-deep .ck.ck-content .ck-widget.ck-widget_selected:hover{outline:none!important;box-shadow:none!important}\n", ":host ::ng-deep .highlight-range{background-color:#ffeb3b80;border-bottom:2px solid #fbc02d;transition:background-color .2s;cursor:pointer}\n"] }]
2143
4066
  }], propDecorators: { option: [{
2144
4067
  type: Input,
2145
4068
  args: [{ required: true }]