@ckeditor/ckeditor5-engine 29.2.0 → 32.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/LICENSE.md +2 -2
  2. package/package.json +25 -23
  3. package/src/controller/datacontroller.js +60 -12
  4. package/src/controller/editingcontroller.js +1 -1
  5. package/src/conversion/conversion.js +1 -1
  6. package/src/conversion/conversionhelpers.js +1 -1
  7. package/src/conversion/downcastdispatcher.js +1 -1
  8. package/src/conversion/downcasthelpers.js +1 -1
  9. package/src/conversion/mapper.js +1 -1
  10. package/src/conversion/modelconsumable.js +1 -1
  11. package/src/conversion/upcastdispatcher.js +1 -1
  12. package/src/conversion/upcasthelpers.js +1 -1
  13. package/src/conversion/viewconsumable.js +1 -1
  14. package/src/dataprocessor/basichtmlwriter.js +1 -1
  15. package/src/dataprocessor/dataprocessor.jsdoc +1 -1
  16. package/src/dataprocessor/htmldataprocessor.js +18 -43
  17. package/src/dataprocessor/htmlwriter.js +1 -1
  18. package/src/dataprocessor/xmldataprocessor.js +13 -17
  19. package/src/dev-utils/model.js +3 -3
  20. package/src/dev-utils/operationreplayer.js +1 -1
  21. package/src/dev-utils/utils.js +1 -1
  22. package/src/dev-utils/view.js +22 -4
  23. package/src/index.js +1 -1
  24. package/src/model/batch.js +77 -10
  25. package/src/model/differ.js +1 -1
  26. package/src/model/document.js +1 -1
  27. package/src/model/documentfragment.js +1 -1
  28. package/src/model/documentselection.js +1 -1
  29. package/src/model/element.js +1 -1
  30. package/src/model/history.js +1 -1
  31. package/src/model/item.jsdoc +1 -1
  32. package/src/model/liveposition.js +1 -1
  33. package/src/model/liverange.js +1 -1
  34. package/src/model/markercollection.js +6 -5
  35. package/src/model/model.js +17 -9
  36. package/src/model/node.js +1 -1
  37. package/src/model/nodelist.js +1 -1
  38. package/src/model/operation/attributeoperation.js +1 -1
  39. package/src/model/operation/detachoperation.js +1 -1
  40. package/src/model/operation/insertoperation.js +1 -1
  41. package/src/model/operation/markeroperation.js +1 -1
  42. package/src/model/operation/mergeoperation.js +1 -1
  43. package/src/model/operation/moveoperation.js +1 -1
  44. package/src/model/operation/nooperation.js +1 -1
  45. package/src/model/operation/operation.js +1 -1
  46. package/src/model/operation/operationfactory.js +1 -1
  47. package/src/model/operation/renameoperation.js +1 -1
  48. package/src/model/operation/rootattributeoperation.js +1 -1
  49. package/src/model/operation/splitoperation.js +1 -1
  50. package/src/model/operation/transform.js +1 -1
  51. package/src/model/operation/utils.js +1 -1
  52. package/src/model/position.js +1 -1
  53. package/src/model/range.js +2 -2
  54. package/src/model/rootelement.js +1 -1
  55. package/src/model/schema.js +1 -1
  56. package/src/model/selection.js +1 -1
  57. package/src/model/text.js +1 -1
  58. package/src/model/textproxy.js +1 -1
  59. package/src/model/treewalker.js +1 -1
  60. package/src/model/utils/autoparagraphing.js +1 -1
  61. package/src/model/utils/deletecontent.js +1 -1
  62. package/src/model/utils/getselectedcontent.js +1 -1
  63. package/src/model/utils/insertcontent.js +1 -1
  64. package/src/model/utils/modifyselection.js +1 -1
  65. package/src/model/utils/selection-post-fixer.js +44 -29
  66. package/src/model/writer.js +1 -1
  67. package/src/view/attributeelement.js +1 -1
  68. package/src/view/containerelement.js +1 -1
  69. package/src/view/document.js +13 -1
  70. package/src/view/documentfragment.js +1 -1
  71. package/src/view/documentselection.js +1 -1
  72. package/src/view/domconverter.js +268 -24
  73. package/src/view/downcastwriter.js +33 -2
  74. package/src/view/editableelement.js +1 -1
  75. package/src/view/element.js +29 -1
  76. package/src/view/elementdefinition.jsdoc +1 -1
  77. package/src/view/emptyelement.js +1 -1
  78. package/src/view/filler.js +1 -1
  79. package/src/view/item.jsdoc +1 -1
  80. package/src/view/matcher.js +22 -17
  81. package/src/view/node.js +1 -1
  82. package/src/view/observer/arrowkeysobserver.js +1 -1
  83. package/src/view/observer/bubblingemittermixin.js +1 -1
  84. package/src/view/observer/bubblingeventinfo.js +1 -1
  85. package/src/view/observer/clickobserver.js +1 -1
  86. package/src/view/observer/compositionobserver.js +1 -1
  87. package/src/view/observer/domeventdata.js +1 -1
  88. package/src/view/observer/domeventobserver.js +1 -1
  89. package/src/view/observer/fakeselectionobserver.js +1 -1
  90. package/src/view/observer/focusobserver.js +1 -1
  91. package/src/view/observer/inputobserver.js +1 -1
  92. package/src/view/observer/keyobserver.js +1 -1
  93. package/src/view/observer/mouseobserver.js +1 -1
  94. package/src/view/observer/mutationobserver.js +1 -1
  95. package/src/view/observer/observer.js +1 -1
  96. package/src/view/observer/selectionobserver.js +49 -2
  97. package/src/view/placeholder.js +1 -1
  98. package/src/view/position.js +1 -1
  99. package/src/view/range.js +1 -1
  100. package/src/view/rawelement.js +4 -3
  101. package/src/view/renderer.js +94 -40
  102. package/src/view/rooteditableelement.js +1 -1
  103. package/src/view/selection.js +1 -1
  104. package/src/view/styles/background.js +1 -1
  105. package/src/view/styles/border.js +1 -1
  106. package/src/view/styles/margin.js +1 -1
  107. package/src/view/styles/padding.js +1 -1
  108. package/src/view/styles/utils.js +6 -1
  109. package/src/view/stylesmap.js +1 -1
  110. package/src/view/text.js +1 -1
  111. package/src/view/textproxy.js +1 -1
  112. package/src/view/treewalker.js +1 -1
  113. package/src/view/uielement.js +6 -3
  114. package/src/view/upcastwriter.js +1 -1
  115. package/src/view/view.js +2 -2
  116. package/theme/placeholder.css +1 -1
  117. package/theme/renderer.css +9 -0
  118. package/CHANGELOG.md +0 -823
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
2
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
5
 
@@ -128,9 +128,17 @@ function tryFixingCollapsedRange( range, schema ) {
128
128
 
129
129
  const nearestSelectionRange = schema.getNearestSelectionRange( originalPosition );
130
130
 
131
- // This might be null ie when editor data is empty.
132
- // In such cases there is no need to fix the selection range.
131
+ // This might be null, i.e. when the editor data is empty or the selection is inside a limit element
132
+ // that doesn't allow text inside.
133
+ // In the first case, there is no need to fix the selection range.
134
+ // In the second, let's go up to the outer selectable element
133
135
  if ( !nearestSelectionRange ) {
136
+ const ancestorObject = originalPosition.getAncestors().reverse().find( item => schema.isObject( item ) );
137
+
138
+ if ( ancestorObject ) {
139
+ return Range._createOn( ancestorObject );
140
+ }
141
+
134
142
  return null;
135
143
  }
136
144
 
@@ -255,35 +263,42 @@ function checkSelectionOnNonLimitElements( start, end, schema ) {
255
263
  return startIsOnBlock || endIsOnBlock;
256
264
  }
257
265
 
258
- // Returns a minimal non-intersecting array of ranges.
259
- //
260
- // @param {Array.<module:engine/model/range~Range>} ranges
261
- // @returns {Array.<module:engine/model/range~Range>}
262
- function mergeIntersectingRanges( ranges ) {
263
- const nonIntersectingRanges = [];
264
-
265
- // First range will always be fine.
266
- nonIntersectingRanges.push( ranges.shift() );
267
-
268
- for ( const range of ranges ) {
269
- const previousRange = nonIntersectingRanges.pop();
270
-
271
- if ( range.isEqual( previousRange ) ) {
272
- // Use only one of two identical ranges.
273
- nonIntersectingRanges.push( previousRange );
274
- } else if ( range.isIntersecting( previousRange ) ) {
275
- // Get the sum of two ranges.
276
- const start = previousRange.start.isAfter( range.start ) ? range.start : previousRange.start;
277
- const end = previousRange.end.isAfter( range.end ) ? previousRange.end : range.end;
278
-
279
- const merged = new Range( start, end );
280
- nonIntersectingRanges.push( merged );
281
- } else {
282
- nonIntersectingRanges.push( previousRange );
283
- nonIntersectingRanges.push( range );
266
+ /**
267
+ * Returns a minimal non-intersecting array of ranges without duplicates.
268
+ *
269
+ * @param {Array.<module:engine/model/range~Range>} Ranges to merge.
270
+ * @returns {Array.<module:engine/model/range~Range>} Array of unique and nonIntersecting ranges.
271
+ */
272
+ export function mergeIntersectingRanges( ranges ) {
273
+ const rangesToMerge = [ ...ranges ];
274
+ const rangeIndexesToRemove = new Set();
275
+ let currentRangeIndex = 1;
276
+
277
+ while ( currentRangeIndex < rangesToMerge.length ) {
278
+ const currentRange = rangesToMerge[ currentRangeIndex ];
279
+ const previousRanges = rangesToMerge.slice( 0, currentRangeIndex );
280
+
281
+ for ( const [ previousRangeIndex, previousRange ] of previousRanges.entries() ) {
282
+ if ( rangeIndexesToRemove.has( previousRangeIndex ) ) {
283
+ continue;
284
+ }
285
+
286
+ if ( currentRange.isEqual( previousRange ) ) {
287
+ rangeIndexesToRemove.add( previousRangeIndex );
288
+ } else if ( currentRange.isIntersecting( previousRange ) ) {
289
+ rangeIndexesToRemove.add( previousRangeIndex );
290
+ rangeIndexesToRemove.add( currentRangeIndex );
291
+
292
+ const mergedRange = currentRange.getJoined( previousRange );
293
+ rangesToMerge.push( mergedRange );
294
+ }
284
295
  }
296
+
297
+ currentRangeIndex++;
285
298
  }
286
299
 
300
+ const nonIntersectingRanges = rangesToMerge.filter( ( _, index ) => !rangeIndexesToRemove.has( index ) );
301
+
287
302
  return nonIntersectingRanges;
288
303
  }
289
304
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
2
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
2
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
2
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
2
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
5
 
@@ -80,6 +80,18 @@ export default class Document {
80
80
  */
81
81
  this.set( 'isFocused', false );
82
82
 
83
+ /**
84
+ * `true` while the user is making a selection in the document (e.g. holding the mouse button and moving the cursor).
85
+ * When they stop selecting, the property goes back to `false`.
86
+ *
87
+ * This property is updated by the {@link module:engine/view/observer/selectionobserver~SelectionObserver}.
88
+ *
89
+ * @readonly
90
+ * @observable
91
+ * @member {Boolean} module:engine/view/document~Document#isSelecting
92
+ */
93
+ this.set( 'isSelecting', false );
94
+
83
95
  /**
84
96
  * True if composition is in progress inside the document.
85
97
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
2
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
2
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
2
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
5
 
@@ -7,7 +7,7 @@
7
7
  * @module engine/view/domconverter
8
8
  */
9
9
 
10
- /* globals document, Node, Text */
10
+ /* globals document, Node, NodeFilter, DOMParser, Text */
11
11
 
12
12
  import ViewText from './text';
13
13
  import ViewElement from './element';
@@ -24,13 +24,17 @@ import {
24
24
  } from './filler';
25
25
 
26
26
  import global from '@ckeditor/ckeditor5-utils/src/dom/global';
27
+ import { logWarning } from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
27
28
  import indexOf from '@ckeditor/ckeditor5-utils/src/dom/indexof';
28
29
  import getAncestors from '@ckeditor/ckeditor5-utils/src/dom/getancestors';
29
30
  import isText from '@ckeditor/ckeditor5-utils/src/dom/istext';
31
+ import isComment from '@ckeditor/ckeditor5-utils/src/dom/iscomment';
30
32
 
31
33
  const BR_FILLER_REF = BR_FILLER( document ); // eslint-disable-line new-cap
32
34
  const NBSP_FILLER_REF = NBSP_FILLER( document ); // eslint-disable-line new-cap
33
35
  const MARKED_NBSP_FILLER_REF = MARKED_NBSP_FILLER( document ); // eslint-disable-line new-cap
36
+ const UNSAFE_ATTRIBUTE_NAME_PREFIX = 'data-ck-unsafe-attribute-';
37
+ const UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE = 'data-ck-unsafe-element';
34
38
 
35
39
  /**
36
40
  * `DomConverter` is a set of tools to do transformations between DOM nodes and view nodes. It also handles
@@ -51,7 +55,12 @@ export default class DomConverter {
51
55
  *
52
56
  * @param {module:engine/view/document~Document} document The view document instance.
53
57
  * @param {Object} options An object with configuration options.
54
- * @param {module:engine/view/filler~BlockFillerMode} [options.blockFillerMode='br'] The type of the block filler to use.
58
+ * @param {module:engine/view/filler~BlockFillerMode} [options.blockFillerMode] The type of the block filler to use.
59
+ * Default value depends on the options.renderingMode:
60
+ * 'nbsp' when options.renderingMode == 'data',
61
+ * 'br' when options.renderingMode == 'editing'.
62
+ * @param {'data'|'editing'} [options.renderingMode='editing'] Whether to leave the View-to-DOM conversion result unchanged
63
+ * or improve editing experience by filtering out interactive data.
55
64
  */
56
65
  constructor( document, options = {} ) {
57
66
  /**
@@ -60,12 +69,19 @@ export default class DomConverter {
60
69
  */
61
70
  this.document = document;
62
71
 
72
+ /**
73
+ * Whether to leave the View-to-DOM conversion result unchanged or improve editing experience by filtering out interactive data.
74
+ *
75
+ * @member {'data'|'editing'} module:engine/view/domconverter~DomConverter#renderingMode
76
+ */
77
+ this.renderingMode = options.renderingMode || 'editing';
78
+
63
79
  /**
64
80
  * The mode of a block filler used by the DOM converter.
65
81
  *
66
82
  * @member {'br'|'nbsp'|'markedNbsp'} module:engine/view/domconverter~DomConverter#blockFillerMode
67
83
  */
68
- this.blockFillerMode = options.blockFillerMode || 'br';
84
+ this.blockFillerMode = options.blockFillerMode || ( this.renderingMode === 'editing' ? 'br' : 'nbsp' );
69
85
 
70
86
  /**
71
87
  * Elements which are considered pre-formatted elements.
@@ -221,6 +237,106 @@ export default class DomConverter {
221
237
  this._viewToDomMapping.set( viewFragment, domFragment );
222
238
  }
223
239
 
240
+ /**
241
+ * Decides whether a given pair of attribute key and value should be passed further down the pipeline.
242
+ *
243
+ * @param {String} attributeKey
244
+ * @param {String} attributeValue
245
+ * @param {String} elementName Element name in lower case.
246
+ * @returns {Boolean}
247
+ */
248
+ shouldRenderAttribute( attributeKey, attributeValue, elementName ) {
249
+ if ( this.renderingMode === 'data' ) {
250
+ return true;
251
+ }
252
+
253
+ attributeKey = attributeKey.toLowerCase();
254
+
255
+ if ( attributeKey.startsWith( 'on' ) ) {
256
+ return false;
257
+ }
258
+
259
+ if (
260
+ attributeKey === 'srcdoc' &&
261
+ attributeValue.match( /\bon\S+\s*=|javascript:|<\s*\/*script/i )
262
+ ) {
263
+ return false;
264
+ }
265
+
266
+ if (
267
+ elementName === 'img' &&
268
+ ( attributeKey === 'src' || attributeKey === 'srcset' )
269
+ ) {
270
+ return true;
271
+ }
272
+
273
+ if ( elementName === 'source' && attributeKey === 'srcset' ) {
274
+ return true;
275
+ }
276
+
277
+ if ( attributeValue.match( /^\s*(javascript:|data:(image\/svg|text\/x?html))/i ) ) {
278
+ return false;
279
+ }
280
+
281
+ return true;
282
+ }
283
+
284
+ /**
285
+ * Set `domElement`'s content using provided `html` argument. Apply necessary filtering for the editing pipeline.
286
+ *
287
+ * @param {Element} domElement DOM element that should have `html` set as its content.
288
+ * @param {String} html Textual representation of the HTML that will be set on `domElement`.
289
+ */
290
+ setContentOf( domElement, html ) {
291
+ // For data pipeline we pass the HTML as-is.
292
+ if ( this.renderingMode === 'data' ) {
293
+ domElement.innerHTML = html;
294
+
295
+ return;
296
+ }
297
+
298
+ const document = new DOMParser().parseFromString( html, 'text/html' );
299
+ const fragment = document.createDocumentFragment();
300
+ const bodyChildNodes = document.body.childNodes;
301
+
302
+ while ( bodyChildNodes.length > 0 ) {
303
+ fragment.appendChild( bodyChildNodes[ 0 ] );
304
+ }
305
+
306
+ const treeWalker = document.createTreeWalker( fragment, NodeFilter.SHOW_ELEMENT );
307
+ const nodes = [];
308
+
309
+ let currentNode;
310
+
311
+ // eslint-disable-next-line no-cond-assign
312
+ while ( currentNode = treeWalker.nextNode() ) {
313
+ nodes.push( currentNode );
314
+ }
315
+
316
+ for ( const currentNode of nodes ) {
317
+ // Go through nodes to remove those that are prohibited in editing pipeline.
318
+ for ( const attributeName of currentNode.getAttributeNames() ) {
319
+ this.setDomElementAttribute( currentNode, attributeName, currentNode.getAttribute( attributeName ) );
320
+ }
321
+
322
+ const elementName = currentNode.tagName.toLowerCase();
323
+
324
+ // There are certain nodes, that should be renamed to <span> in editing pipeline.
325
+ if ( this._shouldRenameElement( elementName ) ) {
326
+ logWarning( 'domconverter-unsafe-element-detected', { unsafeElement: currentNode } );
327
+
328
+ currentNode.replaceWith( this._createReplacementDomElement( elementName, currentNode ) );
329
+ }
330
+ }
331
+
332
+ // Empty the target element.
333
+ while ( domElement.firstChild ) {
334
+ domElement.firstChild.remove();
335
+ }
336
+
337
+ domElement.append( fragment );
338
+ }
339
+
224
340
  /**
225
341
  * Converts the view to the DOM. For all text nodes, not bound elements and document fragments new items will
226
342
  * be created. For bound elements and document fragments the method will return corresponding items.
@@ -257,7 +373,7 @@ export default class DomConverter {
257
373
  domElement = domDocument.createComment( viewNode.getCustomProperty( '$rawContent' ) );
258
374
  } else {
259
375
  // UIElement has its own render() method (see #799).
260
- domElement = viewNode.render( domDocument );
376
+ domElement = viewNode.render( domDocument, this );
261
377
  }
262
378
 
263
379
  if ( options.bind ) {
@@ -267,7 +383,11 @@ export default class DomConverter {
267
383
  return domElement;
268
384
  } else {
269
385
  // Create DOM element.
270
- if ( viewNode.hasAttribute( 'xmlns' ) ) {
386
+ if ( this._shouldRenameElement( viewNode.name ) ) {
387
+ logWarning( 'domconverter-unsafe-element-detected', { unsafeElement: viewNode } );
388
+
389
+ domElement = this._createReplacementDomElement( viewNode.name );
390
+ } else if ( viewNode.hasAttribute( 'xmlns' ) ) {
271
391
  domElement = domDocument.createElementNS( viewNode.getAttribute( 'xmlns' ), viewNode.name );
272
392
  } else {
273
393
  domElement = domDocument.createElement( viewNode.name );
@@ -276,7 +396,7 @@ export default class DomConverter {
276
396
  // RawElement take care of their children in RawElement#render() method which can be customized
277
397
  // (see https://github.com/ckeditor/ckeditor5/issues/4469).
278
398
  if ( viewNode.is( 'rawElement' ) ) {
279
- viewNode.render( domElement );
399
+ viewNode.render( domElement, this );
280
400
  }
281
401
 
282
402
  if ( options.bind ) {
@@ -285,7 +405,7 @@ export default class DomConverter {
285
405
 
286
406
  // Copy element's attributes.
287
407
  for ( const key of viewNode.getAttributeKeys() ) {
288
- domElement.setAttribute( key, viewNode.getAttribute( key ) );
408
+ this.setDomElementAttribute( domElement, key, viewNode.getAttribute( key ), viewNode );
289
409
  }
290
410
  }
291
411
 
@@ -299,6 +419,60 @@ export default class DomConverter {
299
419
  }
300
420
  }
301
421
 
422
+ /**
423
+ * Sets the attribute on a DOM element.
424
+ *
425
+ * **Note**: To remove the attribute, use {@link #removeDomElementAttribute}.
426
+ *
427
+ * @param {HTMLElement} domElement The DOM element the attribute should be set on.
428
+ * @param {String} key The name of the attribute.
429
+ * @param {String} value The value of the attribute.
430
+ * @param {module:engine/view/element~Element} [relatedViewElement] The view element related to the `domElement` (if there is any).
431
+ * It helps decide whether the attribute set is unsafe. For instance, view elements created via
432
+ * {@link module:engine/view/downcastwriter~DowncastWriter} methods can allow certain attributes that would normally be filtered out.
433
+ */
434
+ setDomElementAttribute( domElement, key, value, relatedViewElement = null ) {
435
+ const shouldRenderAttribute = this.shouldRenderAttribute( key, value, domElement.tagName.toLowerCase() ) ||
436
+ relatedViewElement && relatedViewElement.shouldRenderUnsafeAttribute( key );
437
+
438
+ if ( !shouldRenderAttribute ) {
439
+ logWarning( 'domconverter-unsafe-attribute-detected', { domElement, key, value } );
440
+ }
441
+
442
+ // The old value was safe but the new value is unsafe.
443
+ if ( domElement.hasAttribute( key ) && !shouldRenderAttribute ) {
444
+ domElement.removeAttribute( key );
445
+ }
446
+ // The old value was unsafe (but prefixed) but the new value will be safe (will be unprefixed).
447
+ else if ( domElement.hasAttribute( UNSAFE_ATTRIBUTE_NAME_PREFIX + key ) && shouldRenderAttribute ) {
448
+ domElement.removeAttribute( UNSAFE_ATTRIBUTE_NAME_PREFIX + key );
449
+ }
450
+
451
+ // If the attribute should not be rendered, rename it (instead of removing) to give developers some idea of what
452
+ // is going on (https://github.com/ckeditor/ckeditor5/issues/10801).
453
+ domElement.setAttribute( shouldRenderAttribute ? key : UNSAFE_ATTRIBUTE_NAME_PREFIX + key, value );
454
+ }
455
+
456
+ /**
457
+ * Removes an attribute from a DOM element.
458
+ *
459
+ * **Note**: To set the attribute, use {@link #setDomElementAttribute}.
460
+ *
461
+ * @param {HTMLElement} domElement The DOM element the attribute should be removed from.
462
+ * @param {String} key The name of the attribute.
463
+ */
464
+ removeDomElementAttribute( domElement, key ) {
465
+ // See #_createReplacementDomElement() to learn what this is.
466
+ if ( key == UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE ) {
467
+ return;
468
+ }
469
+
470
+ domElement.removeAttribute( key );
471
+
472
+ // See setDomElementAttribute() to learn what this is.
473
+ domElement.removeAttribute( UNSAFE_ATTRIBUTE_NAME_PREFIX + key );
474
+ }
475
+
302
476
  /**
303
477
  * Converts children of the view element to DOM using the
304
478
  * {@link module:engine/view/domconverter~DomConverter#viewToDom} method.
@@ -444,7 +618,7 @@ export default class DomConverter {
444
618
  return hostElement;
445
619
  }
446
620
 
447
- if ( this.isComment( domNode ) && options.skipComments ) {
621
+ if ( isComment( domNode ) && options.skipComments ) {
448
622
  return null;
449
623
  }
450
624
 
@@ -489,8 +663,8 @@ export default class DomConverter {
489
663
 
490
664
  // Treat this element's content as a raw data if it was registered as such.
491
665
  // Comment node is also treated as an element with raw data.
492
- if ( this._isViewElementWithRawContent( viewElement, options ) || this.isComment( domNode ) ) {
493
- const rawContent = this.isComment( domNode ) ? domNode.data : domNode.innerHTML;
666
+ if ( this._isViewElementWithRawContent( viewElement, options ) || isComment( domNode ) ) {
667
+ const rawContent = isComment( domNode ) ? domNode.data : domNode.innerHTML;
494
668
 
495
669
  viewElement._setCustomProperty( '$rawContent', rawContent );
496
670
 
@@ -603,10 +777,10 @@ export default class DomConverter {
603
777
  * If structures are too different and it is not possible to find corresponding position then `null` will be returned.
604
778
  *
605
779
  * @param {Node} domParent DOM position parent.
606
- * @param {Number} domOffset DOM position offset.
780
+ * @param {Number} [domOffset=0] DOM position offset. You can skip it when converting the inline filler node.
607
781
  * @returns {module:engine/view/position~Position} viewPosition View position.
608
782
  */
609
- domPositionToView( domParent, domOffset ) {
783
+ domPositionToView( domParent, domOffset = 0 ) {
610
784
  if ( this.isBlockFiller( domParent ) ) {
611
785
  return this.domPositionToView( domParent.parentNode, indexOf( domParent ) );
612
786
  }
@@ -859,16 +1033,6 @@ export default class DomConverter {
859
1033
  return node && node.nodeType == Node.DOCUMENT_FRAGMENT_NODE;
860
1034
  }
861
1035
 
862
- /**
863
- * Returns `true` when `node.nodeType` equals `Node.COMMENT_NODE`.
864
- *
865
- * @param {Node} node Node to check.
866
- * @returns {Boolean}
867
- */
868
- isComment( node ) {
869
- return node && node.nodeType == Node.COMMENT_NODE;
870
- }
871
-
872
1036
  /**
873
1037
  * Checks if the node is an instance of the block filler for this DOM converter.
874
1038
  *
@@ -1353,7 +1517,7 @@ export default class DomConverter {
1353
1517
  * @returns {Element}
1354
1518
  */
1355
1519
  _createViewElement( node, options ) {
1356
- if ( this.isComment( node ) ) {
1520
+ if ( isComment( node ) ) {
1357
1521
  return new ViewUIElement( this.document, '$comment' );
1358
1522
  }
1359
1523
 
@@ -1373,6 +1537,45 @@ export default class DomConverter {
1373
1537
  _isViewElementWithRawContent( viewElement, options ) {
1374
1538
  return options.withChildren !== false && this._rawContentElementMatcher.match( viewElement );
1375
1539
  }
1540
+
1541
+ /**
1542
+ * Checks whether a given element name should be renamed in a current rendering mode.
1543
+ *
1544
+ * @private
1545
+ * @param {String} elementName The name of view element.
1546
+ * @returns {Boolean}
1547
+ */
1548
+ _shouldRenameElement( elementName ) {
1549
+ return this.renderingMode == 'editing' && elementName.toLowerCase() == 'script';
1550
+ }
1551
+
1552
+ /**
1553
+ * Return a <span> element with a special attribute holding the name of the original element.
1554
+ * Optionally, copy all the attributes of the original element if that element is provided.
1555
+ *
1556
+ * @private
1557
+ * @param {String} elementName The name of view element.
1558
+ * @param {Element} [originalDomElement] The original DOM element to copy attributes and content from.
1559
+ * @returns {Element}
1560
+ */
1561
+ _createReplacementDomElement( elementName, originalDomElement = null ) {
1562
+ const newDomElement = document.createElement( 'span' );
1563
+
1564
+ // Mark the span replacing a script as hidden.
1565
+ newDomElement.setAttribute( UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE, elementName );
1566
+
1567
+ if ( originalDomElement ) {
1568
+ while ( originalDomElement.firstChild ) {
1569
+ newDomElement.appendChild( originalDomElement.firstChild );
1570
+ }
1571
+
1572
+ for ( const attributeName of originalDomElement.getAttributeNames() ) {
1573
+ newDomElement.setAttribute( attributeName, originalDomElement.getAttribute( attributeName ) );
1574
+ }
1575
+ }
1576
+
1577
+ return newDomElement;
1578
+ }
1376
1579
  }
1377
1580
 
1378
1581
  // Helper function.
@@ -1435,3 +1638,44 @@ function hasBlockParent( domNode, blockElements ) {
1435
1638
  *
1436
1639
  * @typedef {String} module:engine/view/filler~BlockFillerMode
1437
1640
  */
1641
+
1642
+ /**
1643
+ * The {@link module:engine/view/domconverter~DomConverter} detected a `<script>` element that may disrupt the
1644
+ * {@glink framework/guides/architecture/editing-engine#editing-pipeline editing pipeline} of the editor. To avoid this,
1645
+ * the `<script>` element was renamed to `<span data-ck-unsafe-element="script"></span>`.
1646
+ *
1647
+ * @error domconverter-unsafe-element-detected
1648
+ * @param {module:engine/model/element~Element|HTMLElement} unsafeElement The editing view or DOM element
1649
+ * that was renamed.
1650
+ */
1651
+
1652
+ /**
1653
+ * The {@link module:engine/view/domconverter~DomConverter} detected an interactive attribute in the
1654
+ * {@glink framework/guides/architecture/editing-engine#editing-pipeline editing pipeline}. For the best
1655
+ * editing experience, the attribute was renamed to `data-ck-unsafe-attribute-[original attribute name]`.
1656
+ *
1657
+ * If you are the author of the plugin that generated this attribute and you want it to be preserved
1658
+ * in the editing pipeline, you can configure this when creating the element
1659
+ * using {@link module:engine/view/downcastwriter~DowncastWriter} during the
1660
+ * {@glink framework/guides/architecture/editing-engine#conversion model–view conversion}. Methods such as
1661
+ * {@link module:engine/view/downcastwriter~DowncastWriter#createContainerElement},
1662
+ * {@link module:engine/view/downcastwriter~DowncastWriter#createAttributeElement}, or
1663
+ * {@link module:engine/view/downcastwriter~DowncastWriter#createEmptyElement}
1664
+ * accept an option that will disable filtering of specific attributes:
1665
+ *
1666
+ * const paragraph = writer.createContainerElement( 'p',
1667
+ * {
1668
+ * class: 'clickable-paragraph',
1669
+ * onclick: 'alert( "Paragraph clicked!" )'
1670
+ * },
1671
+ * {
1672
+ * // Make sure the "onclick" attribute will pass through.
1673
+ * renderUnsafeAttributes: [ 'onclick' ]
1674
+ * }
1675
+ * );
1676
+ *
1677
+ * @error domconverter-unsafe-attribute-detected
1678
+ * @param {HTMLElement} domElement The DOM element the attribute was set on.
1679
+ * @param {String} key The original name of the attribute
1680
+ * @param {String} value The value of the original attribute
1681
+ */