@ckeditor/ckeditor5-engine 29.1.0 → 31.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +22 -20
- package/src/controller/datacontroller.js +50 -1
- package/src/dataprocessor/htmldataprocessor.js +10 -13
- package/src/dataprocessor/xmldataprocessor.js +12 -16
- package/src/dev-utils/view.js +21 -3
- package/src/model/markercollection.js +5 -4
- package/src/model/range.js +1 -1
- package/src/model/selection.js +1 -1
- package/src/model/utils/selection-post-fixer.js +43 -28
- package/src/view/document.js +12 -0
- package/src/view/domconverter.js +262 -9
- package/src/view/downcastwriter.js +32 -1
- package/src/view/element.js +28 -0
- package/src/view/matcher.js +21 -16
- package/src/view/observer/selectionobserver.js +48 -1
- package/src/view/rawelement.js +3 -2
- package/src/view/renderer.js +91 -38
- package/src/view/styles/utils.js +5 -0
- package/src/view/uielement.js +5 -2
- package/src/view/view.js +1 -1
- package/theme/renderer.css +9 -0
- package/CHANGELOG.md +0 -823
package/src/view/domconverter.js
CHANGED
|
@@ -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,6 +24,7 @@ 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,6 +32,8 @@ import isText from '@ckeditor/ckeditor5-utils/src/dom/istext';
|
|
|
31
32
|
const BR_FILLER_REF = BR_FILLER( document ); // eslint-disable-line new-cap
|
|
32
33
|
const NBSP_FILLER_REF = NBSP_FILLER( document ); // eslint-disable-line new-cap
|
|
33
34
|
const MARKED_NBSP_FILLER_REF = MARKED_NBSP_FILLER( document ); // eslint-disable-line new-cap
|
|
35
|
+
const UNSAFE_ATTRIBUTE_NAME_PREFIX = 'data-ck-unsafe-attribute-';
|
|
36
|
+
const UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE = 'data-ck-unsafe-element';
|
|
34
37
|
|
|
35
38
|
/**
|
|
36
39
|
* `DomConverter` is a set of tools to do transformations between DOM nodes and view nodes. It also handles
|
|
@@ -51,7 +54,12 @@ export default class DomConverter {
|
|
|
51
54
|
*
|
|
52
55
|
* @param {module:engine/view/document~Document} document The view document instance.
|
|
53
56
|
* @param {Object} options An object with configuration options.
|
|
54
|
-
* @param {module:engine/view/filler~BlockFillerMode} [options.blockFillerMode
|
|
57
|
+
* @param {module:engine/view/filler~BlockFillerMode} [options.blockFillerMode] The type of the block filler to use.
|
|
58
|
+
* Default value depends on the options.renderingMode:
|
|
59
|
+
* 'nbsp' when options.renderingMode == 'data',
|
|
60
|
+
* 'br' when options.renderingMode == 'editing'.
|
|
61
|
+
* @param {'data'|'editing'} [options.renderingMode='editing'] Whether to leave the View-to-DOM conversion result unchanged
|
|
62
|
+
* or improve editing experience by filtering out interactive data.
|
|
55
63
|
*/
|
|
56
64
|
constructor( document, options = {} ) {
|
|
57
65
|
/**
|
|
@@ -60,12 +68,19 @@ export default class DomConverter {
|
|
|
60
68
|
*/
|
|
61
69
|
this.document = document;
|
|
62
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Whether to leave the View-to-DOM conversion result unchanged or improve editing experience by filtering out interactive data.
|
|
73
|
+
*
|
|
74
|
+
* @member {'data'|'editing'} module:engine/view/domconverter~DomConverter#renderingMode
|
|
75
|
+
*/
|
|
76
|
+
this.renderingMode = options.renderingMode || 'editing';
|
|
77
|
+
|
|
63
78
|
/**
|
|
64
79
|
* The mode of a block filler used by the DOM converter.
|
|
65
80
|
*
|
|
66
81
|
* @member {'br'|'nbsp'|'markedNbsp'} module:engine/view/domconverter~DomConverter#blockFillerMode
|
|
67
82
|
*/
|
|
68
|
-
this.blockFillerMode = options.blockFillerMode || 'br';
|
|
83
|
+
this.blockFillerMode = options.blockFillerMode || ( this.renderingMode === 'editing' ? 'br' : 'nbsp' );
|
|
69
84
|
|
|
70
85
|
/**
|
|
71
86
|
* Elements which are considered pre-formatted elements.
|
|
@@ -221,6 +236,106 @@ export default class DomConverter {
|
|
|
221
236
|
this._viewToDomMapping.set( viewFragment, domFragment );
|
|
222
237
|
}
|
|
223
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Decides whether a given pair of attribute key and value should be passed further down the pipeline.
|
|
241
|
+
*
|
|
242
|
+
* @param {String} attributeKey
|
|
243
|
+
* @param {String} attributeValue
|
|
244
|
+
* @param {String} elementName Element name in lower case.
|
|
245
|
+
* @returns {Boolean}
|
|
246
|
+
*/
|
|
247
|
+
shouldRenderAttribute( attributeKey, attributeValue, elementName ) {
|
|
248
|
+
if ( this.renderingMode === 'data' ) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
attributeKey = attributeKey.toLowerCase();
|
|
253
|
+
|
|
254
|
+
if ( attributeKey.startsWith( 'on' ) ) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (
|
|
259
|
+
attributeKey === 'srcdoc' &&
|
|
260
|
+
attributeValue.match( /\bon\S+\s*=|javascript:|<\s*\/*script/i )
|
|
261
|
+
) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (
|
|
266
|
+
elementName === 'img' &&
|
|
267
|
+
( attributeKey === 'src' || attributeKey === 'srcset' )
|
|
268
|
+
) {
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if ( elementName === 'source' && attributeKey === 'srcset' ) {
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if ( attributeValue.match( /^\s*(javascript:|data:(image\/svg|text\/x?html))/i ) ) {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Set `domElement`'s content using provided `html` argument. Apply necessary filtering for the editing pipeline.
|
|
285
|
+
*
|
|
286
|
+
* @param {Element} domElement DOM element that should have `html` set as its content.
|
|
287
|
+
* @param {String} html Textual representation of the HTML that will be set on `domElement`.
|
|
288
|
+
*/
|
|
289
|
+
setContentOf( domElement, html ) {
|
|
290
|
+
// For data pipeline we pass the HTML as-is.
|
|
291
|
+
if ( this.renderingMode === 'data' ) {
|
|
292
|
+
domElement.innerHTML = html;
|
|
293
|
+
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const document = new DOMParser().parseFromString( html, 'text/html' );
|
|
298
|
+
const fragment = document.createDocumentFragment();
|
|
299
|
+
const bodyChildNodes = document.body.childNodes;
|
|
300
|
+
|
|
301
|
+
while ( bodyChildNodes.length > 0 ) {
|
|
302
|
+
fragment.appendChild( bodyChildNodes[ 0 ] );
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const treeWalker = document.createTreeWalker( fragment, NodeFilter.SHOW_ELEMENT );
|
|
306
|
+
const nodes = [];
|
|
307
|
+
|
|
308
|
+
let currentNode;
|
|
309
|
+
|
|
310
|
+
// eslint-disable-next-line no-cond-assign
|
|
311
|
+
while ( currentNode = treeWalker.nextNode() ) {
|
|
312
|
+
nodes.push( currentNode );
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
for ( const currentNode of nodes ) {
|
|
316
|
+
// Go through nodes to remove those that are prohibited in editing pipeline.
|
|
317
|
+
for ( const attributeName of currentNode.getAttributeNames() ) {
|
|
318
|
+
this.setDomElementAttribute( currentNode, attributeName, currentNode.getAttribute( attributeName ) );
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const elementName = currentNode.tagName.toLowerCase();
|
|
322
|
+
|
|
323
|
+
// There are certain nodes, that should be renamed to <span> in editing pipeline.
|
|
324
|
+
if ( this._shouldRenameElement( elementName ) ) {
|
|
325
|
+
logWarning( 'domconverter-unsafe-element-detected', { unsafeElement: currentNode } );
|
|
326
|
+
|
|
327
|
+
currentNode.replaceWith( this._createReplacementDomElement( elementName, currentNode ) );
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Empty the target element.
|
|
332
|
+
while ( domElement.firstChild ) {
|
|
333
|
+
domElement.firstChild.remove();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
domElement.append( fragment );
|
|
337
|
+
}
|
|
338
|
+
|
|
224
339
|
/**
|
|
225
340
|
* Converts the view to the DOM. For all text nodes, not bound elements and document fragments new items will
|
|
226
341
|
* be created. For bound elements and document fragments the method will return corresponding items.
|
|
@@ -257,7 +372,7 @@ export default class DomConverter {
|
|
|
257
372
|
domElement = domDocument.createComment( viewNode.getCustomProperty( '$rawContent' ) );
|
|
258
373
|
} else {
|
|
259
374
|
// UIElement has its own render() method (see #799).
|
|
260
|
-
domElement = viewNode.render( domDocument );
|
|
375
|
+
domElement = viewNode.render( domDocument, this );
|
|
261
376
|
}
|
|
262
377
|
|
|
263
378
|
if ( options.bind ) {
|
|
@@ -267,7 +382,11 @@ export default class DomConverter {
|
|
|
267
382
|
return domElement;
|
|
268
383
|
} else {
|
|
269
384
|
// Create DOM element.
|
|
270
|
-
if (
|
|
385
|
+
if ( this._shouldRenameElement( viewNode.name ) ) {
|
|
386
|
+
logWarning( 'domconverter-unsafe-element-detected', { unsafeElement: viewNode } );
|
|
387
|
+
|
|
388
|
+
domElement = this._createReplacementDomElement( viewNode.name );
|
|
389
|
+
} else if ( viewNode.hasAttribute( 'xmlns' ) ) {
|
|
271
390
|
domElement = domDocument.createElementNS( viewNode.getAttribute( 'xmlns' ), viewNode.name );
|
|
272
391
|
} else {
|
|
273
392
|
domElement = domDocument.createElement( viewNode.name );
|
|
@@ -276,7 +395,7 @@ export default class DomConverter {
|
|
|
276
395
|
// RawElement take care of their children in RawElement#render() method which can be customized
|
|
277
396
|
// (see https://github.com/ckeditor/ckeditor5/issues/4469).
|
|
278
397
|
if ( viewNode.is( 'rawElement' ) ) {
|
|
279
|
-
viewNode.render( domElement );
|
|
398
|
+
viewNode.render( domElement, this );
|
|
280
399
|
}
|
|
281
400
|
|
|
282
401
|
if ( options.bind ) {
|
|
@@ -285,7 +404,7 @@ export default class DomConverter {
|
|
|
285
404
|
|
|
286
405
|
// Copy element's attributes.
|
|
287
406
|
for ( const key of viewNode.getAttributeKeys() ) {
|
|
288
|
-
|
|
407
|
+
this.setDomElementAttribute( domElement, key, viewNode.getAttribute( key ), viewNode );
|
|
289
408
|
}
|
|
290
409
|
}
|
|
291
410
|
|
|
@@ -299,6 +418,60 @@ export default class DomConverter {
|
|
|
299
418
|
}
|
|
300
419
|
}
|
|
301
420
|
|
|
421
|
+
/**
|
|
422
|
+
* Sets the attribute on a DOM element.
|
|
423
|
+
*
|
|
424
|
+
* **Note**: To remove the attribute, use {@link #removeDomElementAttribute}.
|
|
425
|
+
*
|
|
426
|
+
* @param {HTMLElement} domElement The DOM element the attribute should be set on.
|
|
427
|
+
* @param {String} key The name of the attribute
|
|
428
|
+
* @param {String} value The value of the attribute
|
|
429
|
+
* @param {module:engine/view/element~Element} [relatedViewElement] The view element related to the `domElement` (if there is any).
|
|
430
|
+
* It helps decide whether the attribute set is unsafe. For instance, view elements created via
|
|
431
|
+
* {@link module:engine/view/downcastwriter~DowncastWriter} methods can allow certain attributes that would normally be filtered out.
|
|
432
|
+
*/
|
|
433
|
+
setDomElementAttribute( domElement, key, value, relatedViewElement = null ) {
|
|
434
|
+
const shouldRenderAttribute = this.shouldRenderAttribute( key, value, domElement.tagName.toLowerCase() ) ||
|
|
435
|
+
relatedViewElement && relatedViewElement.shouldRenderUnsafeAttribute( key );
|
|
436
|
+
|
|
437
|
+
if ( !shouldRenderAttribute ) {
|
|
438
|
+
logWarning( 'domconverter-unsafe-attribute-detected', { domElement, key, value } );
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// The old value was safe but the new value is unsafe.
|
|
442
|
+
if ( domElement.hasAttribute( key ) && !shouldRenderAttribute ) {
|
|
443
|
+
domElement.removeAttribute( key );
|
|
444
|
+
}
|
|
445
|
+
// The old value was unsafe (but prefixed) but the new value will be safe (will be unprefixed).
|
|
446
|
+
else if ( domElement.hasAttribute( UNSAFE_ATTRIBUTE_NAME_PREFIX + key ) && shouldRenderAttribute ) {
|
|
447
|
+
domElement.removeAttribute( UNSAFE_ATTRIBUTE_NAME_PREFIX + key );
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// If the attribute should not be rendered, rename it (instead of removing) to give developers some idea of what
|
|
451
|
+
// is going on (https://github.com/ckeditor/ckeditor5/issues/10801).
|
|
452
|
+
domElement.setAttribute( shouldRenderAttribute ? key : UNSAFE_ATTRIBUTE_NAME_PREFIX + key, value );
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Removes an attribute from a DOM element.
|
|
457
|
+
*
|
|
458
|
+
* **Note**: To set the attribute, use {@link #setDomElementAttribute}.
|
|
459
|
+
*
|
|
460
|
+
* @param {HTMLElement} domElement The DOM element the attribute should be removed from.
|
|
461
|
+
* @param {String} key The name of the attribute.
|
|
462
|
+
*/
|
|
463
|
+
removeDomElementAttribute( domElement, key ) {
|
|
464
|
+
// See #_createReplacementDomElement() to learn what this is.
|
|
465
|
+
if ( key == UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE ) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
domElement.removeAttribute( key );
|
|
470
|
+
|
|
471
|
+
// See setDomElementAttribute() to learn what this is.
|
|
472
|
+
domElement.removeAttribute( UNSAFE_ATTRIBUTE_NAME_PREFIX + key );
|
|
473
|
+
}
|
|
474
|
+
|
|
302
475
|
/**
|
|
303
476
|
* Converts children of the view element to DOM using the
|
|
304
477
|
* {@link module:engine/view/domconverter~DomConverter#viewToDom} method.
|
|
@@ -603,10 +776,10 @@ export default class DomConverter {
|
|
|
603
776
|
* If structures are too different and it is not possible to find corresponding position then `null` will be returned.
|
|
604
777
|
*
|
|
605
778
|
* @param {Node} domParent DOM position parent.
|
|
606
|
-
* @param {Number} domOffset DOM position offset.
|
|
779
|
+
* @param {Number} [domOffset=0] DOM position offset. You can skip it when converting the inline filler node.
|
|
607
780
|
* @returns {module:engine/view/position~Position} viewPosition View position.
|
|
608
781
|
*/
|
|
609
|
-
domPositionToView( domParent, domOffset ) {
|
|
782
|
+
domPositionToView( domParent, domOffset = 0 ) {
|
|
610
783
|
if ( this.isBlockFiller( domParent ) ) {
|
|
611
784
|
return this.domPositionToView( domParent.parentNode, indexOf( domParent ) );
|
|
612
785
|
}
|
|
@@ -1373,6 +1546,45 @@ export default class DomConverter {
|
|
|
1373
1546
|
_isViewElementWithRawContent( viewElement, options ) {
|
|
1374
1547
|
return options.withChildren !== false && this._rawContentElementMatcher.match( viewElement );
|
|
1375
1548
|
}
|
|
1549
|
+
|
|
1550
|
+
/**
|
|
1551
|
+
* Checks whether a given element name should be renamed in a current rendering mode.
|
|
1552
|
+
*
|
|
1553
|
+
* @private
|
|
1554
|
+
* @param {String} elementName The name of view element.
|
|
1555
|
+
* @returns {Boolean}
|
|
1556
|
+
*/
|
|
1557
|
+
_shouldRenameElement( elementName ) {
|
|
1558
|
+
return this.renderingMode == 'editing' && elementName.toLowerCase() == 'script';
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
/**
|
|
1562
|
+
* Return a <span> element with a special attribute holding the name of the original element.
|
|
1563
|
+
* Optionally, copy all the attributes of the original element if that element is provided.
|
|
1564
|
+
*
|
|
1565
|
+
* @private
|
|
1566
|
+
* @param {String} elementName The name of view element.
|
|
1567
|
+
* @param {Element} [originalDomElement] The original DOM element to copy attributes and content from.
|
|
1568
|
+
* @returns {Element}
|
|
1569
|
+
*/
|
|
1570
|
+
_createReplacementDomElement( elementName, originalDomElement = null ) {
|
|
1571
|
+
const newDomElement = document.createElement( 'span' );
|
|
1572
|
+
|
|
1573
|
+
// Mark the span replacing a script as hidden.
|
|
1574
|
+
newDomElement.setAttribute( UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE, elementName );
|
|
1575
|
+
|
|
1576
|
+
if ( originalDomElement ) {
|
|
1577
|
+
while ( originalDomElement.firstChild ) {
|
|
1578
|
+
newDomElement.appendChild( originalDomElement.firstChild );
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
for ( const attributeName of originalDomElement.getAttributeNames() ) {
|
|
1582
|
+
newDomElement.setAttribute( attributeName, originalDomElement.getAttribute( attributeName ) );
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
return newDomElement;
|
|
1587
|
+
}
|
|
1376
1588
|
}
|
|
1377
1589
|
|
|
1378
1590
|
// Helper function.
|
|
@@ -1435,3 +1647,44 @@ function hasBlockParent( domNode, blockElements ) {
|
|
|
1435
1647
|
*
|
|
1436
1648
|
* @typedef {String} module:engine/view/filler~BlockFillerMode
|
|
1437
1649
|
*/
|
|
1650
|
+
|
|
1651
|
+
/**
|
|
1652
|
+
* The {@link module:engine/view/domconverter~DomConverter} detected a `<script>` element that may disrupt the
|
|
1653
|
+
* {@glink framework/guides/architecture/editing-engine#editing-pipeline editing pipeline} of the editor. To avoid this,
|
|
1654
|
+
* the `<script>` element was renamed to `<span data-ck-unsafe-element="script"></span>`.
|
|
1655
|
+
*
|
|
1656
|
+
* @error domconverter-unsafe-element-detected
|
|
1657
|
+
* @param {module:engine/model/element~Element|HTMLElement} unsafeElement The editing view or DOM element
|
|
1658
|
+
* that was renamed.
|
|
1659
|
+
*/
|
|
1660
|
+
|
|
1661
|
+
/**
|
|
1662
|
+
* The {@link module:engine/view/domconverter~DomConverter} detected an interactive attribute in the
|
|
1663
|
+
* {@glink framework/guides/architecture/editing-engine#editing-pipeline editing pipeline}. For the best
|
|
1664
|
+
* editing experience, the attribute was renamed to `data-ck-unsafe-attribute-[original attribute name]`.
|
|
1665
|
+
*
|
|
1666
|
+
* If you are the author of the plugin that generated this attribute and you want it to be preserved
|
|
1667
|
+
* in the editing pipeline, you can configure this when creating the element
|
|
1668
|
+
* using {@link module:engine/view/downcastwriter~DowncastWriter} during the
|
|
1669
|
+
* {@glink framework/guides/architecture/editing-engine#conversion model–view conversion}. Methods such as
|
|
1670
|
+
* {@link module:engine/view/downcastwriter~DowncastWriter#createContainerElement},
|
|
1671
|
+
* {@link module:engine/view/downcastwriter~DowncastWriter#createAttributeElement}, or
|
|
1672
|
+
* {@link module:engine/view/downcastwriter~DowncastWriter#createEmptyElement}
|
|
1673
|
+
* accept an option that will disable filtering of specific attributes:
|
|
1674
|
+
*
|
|
1675
|
+
* const paragraph = writer.createContainerElement( 'p',
|
|
1676
|
+
* {
|
|
1677
|
+
* class: 'clickable-paragraph',
|
|
1678
|
+
* onclick: 'alert( "Paragraph clicked!" )'
|
|
1679
|
+
* },
|
|
1680
|
+
* {
|
|
1681
|
+
* // Make sure the "onclick" attribute will pass through.
|
|
1682
|
+
* renderUnsafeAttributes: [ 'onclick' ]
|
|
1683
|
+
* }
|
|
1684
|
+
* );
|
|
1685
|
+
*
|
|
1686
|
+
* @error domconverter-unsafe-attribute-detected
|
|
1687
|
+
* @param {HTMLElement} domElement The DOM element the attribute was set on.
|
|
1688
|
+
* @param {String} key The original name of the attribute
|
|
1689
|
+
* @param {String} value The value of the original attribute
|
|
1690
|
+
*/
|
|
@@ -186,6 +186,8 @@ export default class DowncastWriter {
|
|
|
186
186
|
* @param {Object} [options] Element's options.
|
|
187
187
|
* @param {Number} [options.priority] Element's {@link module:engine/view/attributeelement~AttributeElement#priority priority}.
|
|
188
188
|
* @param {Number|String} [options.id] Element's {@link module:engine/view/attributeelement~AttributeElement#id id}.
|
|
189
|
+
* @param {Array.<String>} [options.renderUnsafeAttributes] A list of attribute names that should be rendered in the editing
|
|
190
|
+
* pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
|
|
189
191
|
* @returns {module:engine/view/attributeelement~AttributeElement} Created element.
|
|
190
192
|
*/
|
|
191
193
|
createAttributeElement( name, attributes, options = {} ) {
|
|
@@ -199,6 +201,10 @@ export default class DowncastWriter {
|
|
|
199
201
|
attributeElement._id = options.id;
|
|
200
202
|
}
|
|
201
203
|
|
|
204
|
+
if ( options.renderUnsafeAttributes ) {
|
|
205
|
+
attributeElement._unsafeAttributesToRender.push( ...options.renderUnsafeAttributes );
|
|
206
|
+
}
|
|
207
|
+
|
|
202
208
|
return attributeElement;
|
|
203
209
|
}
|
|
204
210
|
|
|
@@ -222,6 +228,8 @@ export default class DowncastWriter {
|
|
|
222
228
|
* @param {Boolean} [options.isAllowedInsideAttributeElement=false] Whether an element is
|
|
223
229
|
* {@link module:engine/view/element~Element#isAllowedInsideAttributeElement allowed inside an AttributeElement} and can be wrapped
|
|
224
230
|
* with {@link module:engine/view/attributeelement~AttributeElement} by {@link module:engine/view/downcastwriter~DowncastWriter}.
|
|
231
|
+
* @param {Array.<String>} [options.renderUnsafeAttributes] A list of attribute names that should be rendered in the editing
|
|
232
|
+
* pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
|
|
225
233
|
* @returns {module:engine/view/containerelement~ContainerElement} Created element.
|
|
226
234
|
*/
|
|
227
235
|
createContainerElement( name, attributes, options = {} ) {
|
|
@@ -231,6 +239,10 @@ export default class DowncastWriter {
|
|
|
231
239
|
containerElement._isAllowedInsideAttributeElement = options.isAllowedInsideAttributeElement;
|
|
232
240
|
}
|
|
233
241
|
|
|
242
|
+
if ( options.renderUnsafeAttributes ) {
|
|
243
|
+
containerElement._unsafeAttributesToRender.push( ...options.renderUnsafeAttributes );
|
|
244
|
+
}
|
|
245
|
+
|
|
234
246
|
return containerElement;
|
|
235
247
|
}
|
|
236
248
|
|
|
@@ -245,12 +257,19 @@ export default class DowncastWriter {
|
|
|
245
257
|
*
|
|
246
258
|
* @param {String} name Name of the element.
|
|
247
259
|
* @param {Object} [attributes] Elements attributes.
|
|
260
|
+
* @param {Object} [options] Element's options.
|
|
261
|
+
* @param {Array.<String>} [options.renderUnsafeAttributes] A list of attribute names that should be rendered in the editing
|
|
262
|
+
* pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
|
|
248
263
|
* @returns {module:engine/view/editableelement~EditableElement} Created element.
|
|
249
264
|
*/
|
|
250
|
-
createEditableElement( name, attributes ) {
|
|
265
|
+
createEditableElement( name, attributes, options = {} ) {
|
|
251
266
|
const editableElement = new EditableElement( this.document, name, attributes );
|
|
252
267
|
editableElement._document = this.document;
|
|
253
268
|
|
|
269
|
+
if ( options.renderUnsafeAttributes ) {
|
|
270
|
+
editableElement._unsafeAttributesToRender.push( ...options.renderUnsafeAttributes );
|
|
271
|
+
}
|
|
272
|
+
|
|
254
273
|
return editableElement;
|
|
255
274
|
}
|
|
256
275
|
|
|
@@ -266,6 +285,8 @@ export default class DowncastWriter {
|
|
|
266
285
|
* @param {Boolean} [options.isAllowedInsideAttributeElement=true] Whether an element is
|
|
267
286
|
* {@link module:engine/view/element~Element#isAllowedInsideAttributeElement allowed inside an AttributeElement} and can be wrapped
|
|
268
287
|
* with {@link module:engine/view/attributeelement~AttributeElement} by {@link module:engine/view/downcastwriter~DowncastWriter}.
|
|
288
|
+
* @param {Array.<String>} [options.renderUnsafeAttributes] A list of attribute names that should be rendered in the editing
|
|
289
|
+
* pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
|
|
269
290
|
* @returns {module:engine/view/emptyelement~EmptyElement} Created element.
|
|
270
291
|
*/
|
|
271
292
|
createEmptyElement( name, attributes, options = {} ) {
|
|
@@ -275,6 +296,10 @@ export default class DowncastWriter {
|
|
|
275
296
|
emptyElement._isAllowedInsideAttributeElement = options.isAllowedInsideAttributeElement;
|
|
276
297
|
}
|
|
277
298
|
|
|
299
|
+
if ( options.renderUnsafeAttributes ) {
|
|
300
|
+
emptyElement._unsafeAttributesToRender.push( ...options.renderUnsafeAttributes );
|
|
301
|
+
}
|
|
302
|
+
|
|
278
303
|
return emptyElement;
|
|
279
304
|
}
|
|
280
305
|
|
|
@@ -347,6 +372,8 @@ export default class DowncastWriter {
|
|
|
347
372
|
* @param {Boolean} [options.isAllowedInsideAttributeElement=true] Whether an element is
|
|
348
373
|
* {@link module:engine/view/element~Element#isAllowedInsideAttributeElement allowed inside an AttributeElement} and can be wrapped
|
|
349
374
|
* with {@link module:engine/view/attributeelement~AttributeElement} by {@link module:engine/view/downcastwriter~DowncastWriter}.
|
|
375
|
+
* @param {Array.<String>} [options.renderUnsafeAttributes] A list of attribute names that should be rendered in the editing
|
|
376
|
+
* pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
|
|
350
377
|
* @returns {module:engine/view/rawelement~RawElement} The created element.
|
|
351
378
|
*/
|
|
352
379
|
createRawElement( name, attributes, renderFunction, options = {} ) {
|
|
@@ -358,6 +385,10 @@ export default class DowncastWriter {
|
|
|
358
385
|
rawElement._isAllowedInsideAttributeElement = options.isAllowedInsideAttributeElement;
|
|
359
386
|
}
|
|
360
387
|
|
|
388
|
+
if ( options.renderUnsafeAttributes ) {
|
|
389
|
+
rawElement._unsafeAttributesToRender.push( ...options.renderUnsafeAttributes );
|
|
390
|
+
}
|
|
391
|
+
|
|
361
392
|
return rawElement;
|
|
362
393
|
}
|
|
363
394
|
|
package/src/view/element.js
CHANGED
|
@@ -138,6 +138,21 @@ export default class Element extends Node {
|
|
|
138
138
|
* @member {Boolean}
|
|
139
139
|
*/
|
|
140
140
|
this._isAllowedInsideAttributeElement = false;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* A list of attribute names that should be rendered in the editing pipeline even though filtering mechanisms
|
|
144
|
+
* implemented in the {@link module:engine/view/domconverter~DomConverter} (for instance,
|
|
145
|
+
* {@link module:engine/view/domconverter~DomConverter#shouldRenderAttribute}) would filter them out.
|
|
146
|
+
*
|
|
147
|
+
* These attributes can be specified as an option when the element is created by
|
|
148
|
+
* the {@link module:engine/view/downcastwriter~DowncastWriter}. To check whether an unsafe an attribute should
|
|
149
|
+
* be permitted, use the {@link #shouldRenderUnsafeAttribute} method.
|
|
150
|
+
*
|
|
151
|
+
* @private
|
|
152
|
+
* @readonly
|
|
153
|
+
* @member {Array.<String>}
|
|
154
|
+
*/
|
|
155
|
+
this._unsafeAttributesToRender = [];
|
|
141
156
|
}
|
|
142
157
|
|
|
143
158
|
/**
|
|
@@ -572,6 +587,19 @@ export default class Element extends Node {
|
|
|
572
587
|
( attributes == '' ? '' : ` ${ attributes }` );
|
|
573
588
|
}
|
|
574
589
|
|
|
590
|
+
/**
|
|
591
|
+
* Decides whether an unsafe attribute is whitelisted and should be rendered in the editing pipeline even though filtering mechanisms
|
|
592
|
+
* like {@link module:engine/view/domconverter~DomConverter#shouldRenderAttribute} say it should not.
|
|
593
|
+
*
|
|
594
|
+
* Unsafe attribute names can be specified when creating an element via {@link module:engine/view/downcastwriter~DowncastWriter}.
|
|
595
|
+
*
|
|
596
|
+
* @param {String} attributeName The name of the attribute to be checked.
|
|
597
|
+
* @returns {Boolean}
|
|
598
|
+
*/
|
|
599
|
+
shouldRenderUnsafeAttribute( attributeName ) {
|
|
600
|
+
return this._unsafeAttributesToRender.includes( attributeName );
|
|
601
|
+
}
|
|
602
|
+
|
|
575
603
|
/**
|
|
576
604
|
* Clones provided element.
|
|
577
605
|
*
|
package/src/view/matcher.js
CHANGED
|
@@ -235,7 +235,7 @@ function isElementMatching( element, pattern ) {
|
|
|
235
235
|
function matchName( pattern, name ) {
|
|
236
236
|
// If pattern is provided as RegExp - test against this regexp.
|
|
237
237
|
if ( pattern instanceof RegExp ) {
|
|
238
|
-
return
|
|
238
|
+
return !!name.match( pattern );
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
return pattern === name;
|
|
@@ -394,7 +394,7 @@ function normalizePatterns( patterns ) {
|
|
|
394
394
|
function isKeyMatched( patternKey, itemKey ) {
|
|
395
395
|
return patternKey === true ||
|
|
396
396
|
patternKey === itemKey ||
|
|
397
|
-
patternKey instanceof RegExp &&
|
|
397
|
+
patternKey instanceof RegExp && itemKey.match( patternKey );
|
|
398
398
|
}
|
|
399
399
|
|
|
400
400
|
// @param {String|RegExp} patternValue A pattern representing a value we want to match.
|
|
@@ -408,7 +408,11 @@ function isValueMatched( patternValue, itemKey, valueGetter ) {
|
|
|
408
408
|
|
|
409
409
|
const itemValue = valueGetter( itemKey );
|
|
410
410
|
|
|
411
|
-
|
|
411
|
+
// For now, the reducers are not returning the full tree of properties.
|
|
412
|
+
// Casting to string preserves the old behavior until the root cause is fixed.
|
|
413
|
+
// More can be found in https://github.com/ckeditor/ckeditor5/issues/10399.
|
|
414
|
+
return patternValue === itemValue ||
|
|
415
|
+
patternValue instanceof RegExp && !!String( itemValue ).match( patternValue );
|
|
412
416
|
}
|
|
413
417
|
|
|
414
418
|
// Checks if attributes of provided element can be matched against provided patterns.
|
|
@@ -527,7 +531,7 @@ function matchStyles( patterns, element ) {
|
|
|
527
531
|
* name: 'figure',
|
|
528
532
|
* attributes: [
|
|
529
533
|
* 'title', // Match `title` attribute (can be empty).
|
|
530
|
-
* /^data
|
|
534
|
+
* /^data-*$/ // Match attributes starting with `data-` e.g. `data-foo` with any value (can be empty).
|
|
531
535
|
* ]
|
|
532
536
|
* };
|
|
533
537
|
*
|
|
@@ -537,7 +541,8 @@ function matchStyles( patterns, element ) {
|
|
|
537
541
|
* attributes: [
|
|
538
542
|
* {
|
|
539
543
|
* key: 'type', // Match `type` as an attribute key.
|
|
540
|
-
* value: /^(text|number|date)$/
|
|
544
|
+
* value: /^(text|number|date)$/ // Match `text`, `number` or `date` values.
|
|
545
|
+
* },
|
|
541
546
|
* {
|
|
542
547
|
* key: /^data-.*$/, // Match attributes starting with `data-` e.g. `data-foo`.
|
|
543
548
|
* value: true // Match any value (can be empty).
|
|
@@ -568,7 +573,7 @@ function matchStyles( patterns, element ) {
|
|
|
568
573
|
* // Match view element which has matching styles (Object).
|
|
569
574
|
* const pattern = {
|
|
570
575
|
* name: 'p',
|
|
571
|
-
*
|
|
576
|
+
* styles: {
|
|
572
577
|
* color: /rgb\((\d{1,3}), (\d{1,3}), (\d{1,3})\)/, // Match `color` in RGB format only.
|
|
573
578
|
* 'font-weight': 600, // Match `font-weight` only if it's `600`.
|
|
574
579
|
* 'text-decoration': true // Match any text decoration.
|
|
@@ -578,19 +583,20 @@ function matchStyles( patterns, element ) {
|
|
|
578
583
|
* // Match view element which has matching styles (Array).
|
|
579
584
|
* const pattern = {
|
|
580
585
|
* name: 'p',
|
|
581
|
-
*
|
|
586
|
+
* styles: [
|
|
582
587
|
* 'color', // Match `color` with any value.
|
|
583
|
-
* /^border
|
|
588
|
+
* /^border.*$/ // Match all border properties.
|
|
584
589
|
* ]
|
|
585
590
|
* };
|
|
586
591
|
*
|
|
587
592
|
* // Match view element which has matching styles (key-value pairs).
|
|
588
593
|
* const pattern = {
|
|
589
594
|
* name: 'p',
|
|
590
|
-
*
|
|
595
|
+
* styles: [
|
|
591
596
|
* {
|
|
592
|
-
* key: 'color',
|
|
593
|
-
* value: /rgb\((\d{1,3}), (\d{1,3}), (\d{1,3})\)
|
|
597
|
+
* key: 'color', // Match `color` as an property key.
|
|
598
|
+
* value: /rgb\((\d{1,3}), (\d{1,3}), (\d{1,3})\)/ // Match RGB format only.
|
|
599
|
+
* },
|
|
594
600
|
* {
|
|
595
601
|
* key: /^border.*$/, // Match any border style.
|
|
596
602
|
* value: true // Match any value.
|
|
@@ -643,6 +649,7 @@ function matchStyles( patterns, element ) {
|
|
|
643
649
|
* {
|
|
644
650
|
* key: 'image', // Match `image` class.
|
|
645
651
|
* value: true
|
|
652
|
+
* },
|
|
646
653
|
* {
|
|
647
654
|
* key: /^image-side-(left|right)$/, // Match `image-side-left` or `image-side-right` class.
|
|
648
655
|
* value: true
|
|
@@ -696,11 +703,9 @@ function matchStyles( patterns, element ) {
|
|
|
696
703
|
* @typedef {String|RegExp|Object|Function} module:engine/view/matcher~MatcherPattern
|
|
697
704
|
*
|
|
698
705
|
* @property {String|RegExp} [name] View element name to match.
|
|
699
|
-
* @property {String|RegExp|Array.<String|RegExp>} [classes] View element's
|
|
700
|
-
* @property {Object} [styles]
|
|
701
|
-
*
|
|
702
|
-
* @property {Object} [attributes] Object with key-value pairs representing attributes to match.
|
|
703
|
-
* Each object key represents attribute name. Value can be given as `String` or `RegExp`.
|
|
706
|
+
* @property {Boolean|String|RegExp|Object|Array.<String|RegExp|Object>} [classes] View element's classes to match.
|
|
707
|
+
* @property {Boolean|String|RegExp|Object|Array.<String|RegExp|Object>} [styles] View element's styles to match.
|
|
708
|
+
* @property {Boolean|String|RegExp|Object|Array.<String|RegExp|Object>} [attributes] View element's attributes to match.
|
|
704
709
|
*/
|
|
705
710
|
|
|
706
711
|
/**
|