@ckeditor/ckeditor5-engine 44.2.1 → 44.3.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1677 -1258
- package/dist/index.js.map +1 -1
- package/package.json +2 -4
- package/src/conversion/downcasthelpers.js +14 -42
- package/src/conversion/viewconsumable.d.ts +65 -97
- package/src/conversion/viewconsumable.js +217 -215
- package/src/view/attributeelement.d.ts +14 -0
- package/src/view/attributeelement.js +26 -0
- package/src/view/domconverter.js +72 -7
- package/src/view/downcastwriter.d.ts +32 -20
- package/src/view/downcastwriter.js +18 -142
- package/src/view/element.d.ts +309 -17
- package/src/view/element.js +432 -137
- package/src/view/matcher.d.ts +38 -16
- package/src/view/matcher.js +91 -166
- package/src/view/stylesmap.d.ts +68 -6
- package/src/view/stylesmap.js +144 -8
- package/src/view/tokenlist.d.ts +109 -0
- package/src/view/tokenlist.js +196 -0
package/src/view/element.js
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
import Node from './node.js';
|
|
9
9
|
import Text from './text.js';
|
|
10
10
|
import TextProxy from './textproxy.js';
|
|
11
|
-
import { isIterable,
|
|
12
|
-
import { default as Matcher } from './matcher.js';
|
|
11
|
+
import { isIterable, toMap } from '@ckeditor/ckeditor5-utils';
|
|
12
|
+
import { default as Matcher, isPatternMatched } from './matcher.js';
|
|
13
13
|
import { default as StylesMap } from './stylesmap.js';
|
|
14
|
+
import TokenList from './tokenlist.js';
|
|
14
15
|
// @if CK_DEBUG_ENGINE // const { convertMapToTags } = require( '../dev-utils/utils' );
|
|
15
16
|
/**
|
|
16
17
|
* View element.
|
|
@@ -36,6 +37,22 @@ import { default as StylesMap } from './stylesmap.js';
|
|
|
36
37
|
* should be used to create generic view elements.
|
|
37
38
|
*/
|
|
38
39
|
export default class Element extends Node {
|
|
40
|
+
/**
|
|
41
|
+
* Set of classes associated with element instance.
|
|
42
|
+
*
|
|
43
|
+
* Note that this is just an alias for `this._attrs.get( 'class' );`
|
|
44
|
+
*/
|
|
45
|
+
get _classes() {
|
|
46
|
+
return this._attrs.get('class');
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Normalized styles.
|
|
50
|
+
*
|
|
51
|
+
* Note that this is just an alias for `this._attrs.get( 'style' );`
|
|
52
|
+
*/
|
|
53
|
+
get _styles() {
|
|
54
|
+
return this._attrs.get('style');
|
|
55
|
+
}
|
|
39
56
|
/**
|
|
40
57
|
* Creates a view element.
|
|
41
58
|
*
|
|
@@ -73,24 +90,11 @@ export default class Element extends Node {
|
|
|
73
90
|
*/
|
|
74
91
|
this._customProperties = new Map();
|
|
75
92
|
this.name = name;
|
|
76
|
-
this._attrs =
|
|
93
|
+
this._attrs = this._parseAttributes(attrs);
|
|
77
94
|
this._children = [];
|
|
78
95
|
if (children) {
|
|
79
96
|
this._insertChild(0, children);
|
|
80
97
|
}
|
|
81
|
-
this._classes = new Set();
|
|
82
|
-
if (this._attrs.has('class')) {
|
|
83
|
-
// Remove class attribute and handle it by class set.
|
|
84
|
-
const classString = this._attrs.get('class');
|
|
85
|
-
parseClasses(this._classes, classString);
|
|
86
|
-
this._attrs.delete('class');
|
|
87
|
-
}
|
|
88
|
-
this._styles = new StylesMap(this.document.stylesProcessor);
|
|
89
|
-
if (this._attrs.has('style')) {
|
|
90
|
-
// Remove style attribute and handle it by styles map.
|
|
91
|
-
this._styles.setTo(this._attrs.get('style'));
|
|
92
|
-
this._attrs.delete('style');
|
|
93
|
-
}
|
|
94
98
|
}
|
|
95
99
|
/**
|
|
96
100
|
* Number of element's children.
|
|
@@ -136,13 +140,19 @@ export default class Element extends Node {
|
|
|
136
140
|
* @returns Keys for attributes.
|
|
137
141
|
*/
|
|
138
142
|
*getAttributeKeys() {
|
|
139
|
-
|
|
143
|
+
// This is yielded in this specific order to maintain backward compatibility of data.
|
|
144
|
+
// Otherwise, we could simply just have the `for` loop only inside this method.
|
|
145
|
+
if (this._classes) {
|
|
140
146
|
yield 'class';
|
|
141
147
|
}
|
|
142
|
-
if (
|
|
148
|
+
if (this._styles) {
|
|
143
149
|
yield 'style';
|
|
144
150
|
}
|
|
145
|
-
|
|
151
|
+
for (const key of this._attrs.keys()) {
|
|
152
|
+
if (key != 'class' && key != 'style') {
|
|
153
|
+
yield key;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
146
156
|
}
|
|
147
157
|
/**
|
|
148
158
|
* Returns iterator that iterates over this element's attributes.
|
|
@@ -151,12 +161,8 @@ export default class Element extends Node {
|
|
|
151
161
|
* This format is accepted by native `Map` object and also can be passed in `Node` constructor.
|
|
152
162
|
*/
|
|
153
163
|
*getAttributes() {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
yield ['class', this.getAttribute('class')];
|
|
157
|
-
}
|
|
158
|
-
if (!this._styles.isEmpty) {
|
|
159
|
-
yield ['style', this.getAttribute('style')];
|
|
164
|
+
for (const [name, value] of this._attrs.entries()) {
|
|
165
|
+
yield [name, String(value)];
|
|
160
166
|
}
|
|
161
167
|
}
|
|
162
168
|
/**
|
|
@@ -166,17 +172,7 @@ export default class Element extends Node {
|
|
|
166
172
|
* @returns Attribute value.
|
|
167
173
|
*/
|
|
168
174
|
getAttribute(key) {
|
|
169
|
-
|
|
170
|
-
if (this._classes.size > 0) {
|
|
171
|
-
return [...this._classes].join(' ');
|
|
172
|
-
}
|
|
173
|
-
return undefined;
|
|
174
|
-
}
|
|
175
|
-
if (key == 'style') {
|
|
176
|
-
const inlineStyle = this._styles.toString();
|
|
177
|
-
return inlineStyle == '' ? undefined : inlineStyle;
|
|
178
|
-
}
|
|
179
|
-
return this._attrs.get(key);
|
|
175
|
+
return this._attrs.has(key) ? String(this._attrs.get(key)) : undefined;
|
|
180
176
|
}
|
|
181
177
|
/**
|
|
182
178
|
* Returns a boolean indicating whether an attribute with the specified key exists in the element.
|
|
@@ -184,14 +180,19 @@ export default class Element extends Node {
|
|
|
184
180
|
* @param key Attribute key.
|
|
185
181
|
* @returns `true` if attribute with the specified key exists in the element, `false` otherwise.
|
|
186
182
|
*/
|
|
187
|
-
hasAttribute(key) {
|
|
188
|
-
if (key
|
|
189
|
-
return
|
|
183
|
+
hasAttribute(key, token) {
|
|
184
|
+
if (!this._attrs.has(key)) {
|
|
185
|
+
return false;
|
|
190
186
|
}
|
|
191
|
-
if (
|
|
192
|
-
|
|
187
|
+
if (token !== undefined) {
|
|
188
|
+
if (usesStylesMap(this.name, key) || usesTokenList(this.name, key)) {
|
|
189
|
+
return this._attrs.get(key).has(token);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
return this._attrs.get(key) === token;
|
|
193
|
+
}
|
|
193
194
|
}
|
|
194
|
-
return
|
|
195
|
+
return true;
|
|
195
196
|
}
|
|
196
197
|
/**
|
|
197
198
|
* Checks if this element is similar to other element.
|
|
@@ -211,26 +212,21 @@ export default class Element extends Node {
|
|
|
211
212
|
return false;
|
|
212
213
|
}
|
|
213
214
|
// Check number of attributes, classes and styles.
|
|
214
|
-
if (this._attrs.size !== otherElement._attrs.size
|
|
215
|
-
this._styles.size !== otherElement._styles.size) {
|
|
215
|
+
if (this._attrs.size !== otherElement._attrs.size) {
|
|
216
216
|
return false;
|
|
217
217
|
}
|
|
218
218
|
// Check if attributes are the same.
|
|
219
219
|
for (const [key, value] of this._attrs) {
|
|
220
|
-
|
|
220
|
+
const otherValue = otherElement._attrs.get(key);
|
|
221
|
+
if (otherValue === undefined) {
|
|
221
222
|
return false;
|
|
222
223
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return false;
|
|
224
|
+
if (typeof value == 'string' || typeof otherValue == 'string') {
|
|
225
|
+
if (otherValue !== value) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
228
|
}
|
|
229
|
-
|
|
230
|
-
// Check if styles are the same.
|
|
231
|
-
for (const property of this._styles.getStyleNames()) {
|
|
232
|
-
if (!otherElement._styles.has(property) ||
|
|
233
|
-
otherElement._styles.getAsString(property) !== this._styles.getAsString(property)) {
|
|
229
|
+
else if (!value.isSimilar(otherValue)) {
|
|
234
230
|
return false;
|
|
235
231
|
}
|
|
236
232
|
}
|
|
@@ -247,7 +243,7 @@ export default class Element extends Node {
|
|
|
247
243
|
*/
|
|
248
244
|
hasClass(...className) {
|
|
249
245
|
for (const name of className) {
|
|
250
|
-
if (!this._classes.has(name)) {
|
|
246
|
+
if (!this._classes || !this._classes.has(name)) {
|
|
251
247
|
return false;
|
|
252
248
|
}
|
|
253
249
|
}
|
|
@@ -257,10 +253,15 @@ export default class Element extends Node {
|
|
|
257
253
|
* Returns iterator that contains all class names.
|
|
258
254
|
*/
|
|
259
255
|
getClassNames() {
|
|
260
|
-
|
|
256
|
+
const array = this._classes ? this._classes.keys() : [];
|
|
257
|
+
// This is overcomplicated because we need to be backward compatible for use cases when iterator is expected.
|
|
258
|
+
const iterator = array[Symbol.iterator]();
|
|
259
|
+
return Object.assign(array, {
|
|
260
|
+
next: iterator.next.bind(iterator)
|
|
261
|
+
});
|
|
261
262
|
}
|
|
262
263
|
/**
|
|
263
|
-
* Returns style value for the given property
|
|
264
|
+
* Returns style value for the given property name.
|
|
264
265
|
* If the style does not exist `undefined` is returned.
|
|
265
266
|
*
|
|
266
267
|
* **Note**: This method can work with normalized style names if
|
|
@@ -285,7 +286,7 @@ export default class Element extends Node {
|
|
|
285
286
|
* ```
|
|
286
287
|
*/
|
|
287
288
|
getStyle(property) {
|
|
288
|
-
return this._styles.getAsString(property);
|
|
289
|
+
return this._styles && this._styles.getAsString(property);
|
|
289
290
|
}
|
|
290
291
|
/**
|
|
291
292
|
* Returns a normalized style object or single style value.
|
|
@@ -322,15 +323,15 @@ export default class Element extends Node {
|
|
|
322
323
|
* @param property Name of CSS property
|
|
323
324
|
*/
|
|
324
325
|
getNormalizedStyle(property) {
|
|
325
|
-
return this._styles.getNormalized(property);
|
|
326
|
+
return this._styles && this._styles.getNormalized(property);
|
|
326
327
|
}
|
|
327
328
|
/**
|
|
328
|
-
* Returns
|
|
329
|
+
* Returns an array that contains all style names.
|
|
329
330
|
*
|
|
330
331
|
* @param expand Expand shorthand style properties and return all equivalent style representations.
|
|
331
332
|
*/
|
|
332
333
|
getStyleNames(expand) {
|
|
333
|
-
return this._styles.getStyleNames(expand);
|
|
334
|
+
return this._styles ? this._styles.getStyleNames(expand) : [];
|
|
334
335
|
}
|
|
335
336
|
/**
|
|
336
337
|
* Returns true if style keys are present.
|
|
@@ -343,7 +344,7 @@ export default class Element extends Node {
|
|
|
343
344
|
*/
|
|
344
345
|
hasStyle(...property) {
|
|
345
346
|
for (const name of property) {
|
|
346
|
-
if (!this._styles.has(name)) {
|
|
347
|
+
if (!this._styles || !this._styles.has(name)) {
|
|
347
348
|
return false;
|
|
348
349
|
}
|
|
349
350
|
}
|
|
@@ -407,9 +408,12 @@ export default class Element extends Node {
|
|
|
407
408
|
* **Note**: Classes, styles and other attributes are sorted alphabetically.
|
|
408
409
|
*/
|
|
409
410
|
getIdentity() {
|
|
410
|
-
const classes =
|
|
411
|
-
const styles = this._styles.
|
|
412
|
-
const attributes = Array.from(this._attrs)
|
|
411
|
+
const classes = this._classes ? this._classes.keys().sort().join(',') : '';
|
|
412
|
+
const styles = this._styles && String(this._styles);
|
|
413
|
+
const attributes = Array.from(this._attrs)
|
|
414
|
+
.filter(([key]) => key != 'style' && key != 'class')
|
|
415
|
+
.map(i => `${i[0]}="${i[1]}"`)
|
|
416
|
+
.sort().join(' ');
|
|
413
417
|
return this.name +
|
|
414
418
|
(classes == '' ? '' : ` class="${classes}"`) +
|
|
415
419
|
(!styles ? '' : ` style="${styles}"`) +
|
|
@@ -443,10 +447,6 @@ export default class Element extends Node {
|
|
|
443
447
|
}
|
|
444
448
|
// ContainerElement and AttributeElement should be also cloned properly.
|
|
445
449
|
const cloned = new this.constructor(this.document, this.name, this._attrs, childrenClone);
|
|
446
|
-
// Classes and styles are cloned separately - this solution is faster than adding them back to attributes and
|
|
447
|
-
// parse once again in constructor.
|
|
448
|
-
cloned._classes = new Set(this._classes);
|
|
449
|
-
cloned._styles.set(this._styles.getNormalized());
|
|
450
450
|
// Clone custom properties.
|
|
451
451
|
cloned._customProperties = new Map(this._customProperties);
|
|
452
452
|
// Clone filler offset method.
|
|
@@ -522,19 +522,37 @@ export default class Element extends Node {
|
|
|
522
522
|
* @internal
|
|
523
523
|
* @param key Attribute key.
|
|
524
524
|
* @param value Attribute value.
|
|
525
|
+
* @param overwrite Whether tokenized attribute should override the attribute value or just add a token.
|
|
525
526
|
* @fires change
|
|
526
527
|
*/
|
|
527
|
-
_setAttribute(key, value) {
|
|
528
|
-
const stringValue = String(value);
|
|
528
|
+
_setAttribute(key, value, overwrite = true) {
|
|
529
529
|
this._fireChange('attributes', this);
|
|
530
|
-
if (key
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
530
|
+
if (usesStylesMap(this.name, key) || usesTokenList(this.name, key)) {
|
|
531
|
+
let currentValue = this._attrs.get(key);
|
|
532
|
+
if (!currentValue) {
|
|
533
|
+
currentValue = usesStylesMap(this.name, key) ?
|
|
534
|
+
new StylesMap(this.document.stylesProcessor) :
|
|
535
|
+
new TokenList();
|
|
536
|
+
this._attrs.set(key, currentValue);
|
|
537
|
+
}
|
|
538
|
+
if (overwrite) {
|
|
539
|
+
// If reset is set then value have to be a string to tokenize.
|
|
540
|
+
currentValue.setTo(String(value));
|
|
541
|
+
}
|
|
542
|
+
else if (usesStylesMap(this.name, key)) {
|
|
543
|
+
if (Array.isArray(value)) {
|
|
544
|
+
currentValue.set(value[0], value[1]);
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
currentValue.set(value);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
else { // TokenList.
|
|
551
|
+
currentValue.set(typeof value == 'string' ? value.split(/\s+/) : value);
|
|
552
|
+
}
|
|
535
553
|
}
|
|
536
554
|
else {
|
|
537
|
-
this._attrs.set(key,
|
|
555
|
+
this._attrs.set(key, String(value));
|
|
538
556
|
}
|
|
539
557
|
}
|
|
540
558
|
/**
|
|
@@ -543,28 +561,26 @@ export default class Element extends Node {
|
|
|
543
561
|
* @see module:engine/view/downcastwriter~DowncastWriter#removeAttribute
|
|
544
562
|
* @internal
|
|
545
563
|
* @param key Attribute key.
|
|
564
|
+
* @param tokens Attribute value tokens to remove. The whole attribute is removed if not specified.
|
|
546
565
|
* @returns Returns true if an attribute existed and has been removed.
|
|
547
566
|
* @fires change
|
|
548
567
|
*/
|
|
549
|
-
_removeAttribute(key) {
|
|
568
|
+
_removeAttribute(key, tokens) {
|
|
550
569
|
this._fireChange('attributes', this);
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
if (
|
|
554
|
-
|
|
555
|
-
return true;
|
|
570
|
+
if (tokens !== undefined && (usesStylesMap(this.name, key) || usesTokenList(this.name, key))) {
|
|
571
|
+
const currentValue = this._attrs.get(key);
|
|
572
|
+
if (!currentValue) {
|
|
573
|
+
return false;
|
|
556
574
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
if (
|
|
562
|
-
this.
|
|
563
|
-
return true;
|
|
575
|
+
if (usesTokenList(this.name, key) && typeof tokens == 'string') {
|
|
576
|
+
tokens = tokens.split(/\s+/);
|
|
577
|
+
}
|
|
578
|
+
currentValue.remove(tokens);
|
|
579
|
+
if (currentValue.isEmpty) {
|
|
580
|
+
return this._attrs.delete(key);
|
|
564
581
|
}
|
|
565
582
|
return false;
|
|
566
583
|
}
|
|
567
|
-
// Remove other attributes.
|
|
568
584
|
return this._attrs.delete(key);
|
|
569
585
|
}
|
|
570
586
|
/**
|
|
@@ -580,10 +596,7 @@ export default class Element extends Node {
|
|
|
580
596
|
* @fires change
|
|
581
597
|
*/
|
|
582
598
|
_addClass(className) {
|
|
583
|
-
this.
|
|
584
|
-
for (const name of toArray(className)) {
|
|
585
|
-
this._classes.add(name);
|
|
586
|
-
}
|
|
599
|
+
this._setAttribute('class', className, false);
|
|
587
600
|
}
|
|
588
601
|
/**
|
|
589
602
|
* Removes specified class.
|
|
@@ -598,18 +611,14 @@ export default class Element extends Node {
|
|
|
598
611
|
* @fires change
|
|
599
612
|
*/
|
|
600
613
|
_removeClass(className) {
|
|
601
|
-
this.
|
|
602
|
-
for (const name of toArray(className)) {
|
|
603
|
-
this._classes.delete(name);
|
|
604
|
-
}
|
|
614
|
+
this._removeAttribute('class', className);
|
|
605
615
|
}
|
|
606
616
|
_setStyle(property, value) {
|
|
607
|
-
this._fireChange('attributes', this);
|
|
608
617
|
if (typeof property != 'string') {
|
|
609
|
-
this.
|
|
618
|
+
this._setAttribute('style', property, false);
|
|
610
619
|
}
|
|
611
620
|
else {
|
|
612
|
-
this.
|
|
621
|
+
this._setAttribute('style', [property, value], false);
|
|
613
622
|
}
|
|
614
623
|
}
|
|
615
624
|
/**
|
|
@@ -629,9 +638,281 @@ export default class Element extends Node {
|
|
|
629
638
|
* @fires change
|
|
630
639
|
*/
|
|
631
640
|
_removeStyle(property) {
|
|
641
|
+
this._removeAttribute('style', property);
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Used by the {@link module:engine/view/matcher~Matcher Matcher} to collect matching attribute tuples
|
|
645
|
+
* (attribute name and optional token).
|
|
646
|
+
*
|
|
647
|
+
* Normalized patterns can be used in following ways:
|
|
648
|
+
* - to match any attribute name with any or no value:
|
|
649
|
+
*
|
|
650
|
+
* ```ts
|
|
651
|
+
* patterns: [
|
|
652
|
+
* [ true, true ]
|
|
653
|
+
* ]
|
|
654
|
+
* ```
|
|
655
|
+
*
|
|
656
|
+
* - to match a specific attribute with any value:
|
|
657
|
+
*
|
|
658
|
+
* ```ts
|
|
659
|
+
* patterns: [
|
|
660
|
+
* [ 'required', true ]
|
|
661
|
+
* ]
|
|
662
|
+
* ```
|
|
663
|
+
*
|
|
664
|
+
* - to match an attribute name with a RegExp with any value:
|
|
665
|
+
*
|
|
666
|
+
* ```ts
|
|
667
|
+
* patterns: [
|
|
668
|
+
* [ /h[1-6]/, true ]
|
|
669
|
+
* ]
|
|
670
|
+
* ```
|
|
671
|
+
*
|
|
672
|
+
* - to match a specific attribute with the exact value:
|
|
673
|
+
*
|
|
674
|
+
* ```ts
|
|
675
|
+
* patterns: [
|
|
676
|
+
* [ 'rel', 'nofollow' ]
|
|
677
|
+
* ]
|
|
678
|
+
* ```
|
|
679
|
+
*
|
|
680
|
+
* - to match a specific attribute with a value matching a RegExp:
|
|
681
|
+
*
|
|
682
|
+
* ```ts
|
|
683
|
+
* patterns: [
|
|
684
|
+
* [ 'src', /^https/ ]
|
|
685
|
+
* ]
|
|
686
|
+
* ```
|
|
687
|
+
*
|
|
688
|
+
* - to match an attribute name with a RegExp and the exact value:
|
|
689
|
+
*
|
|
690
|
+
* ```ts
|
|
691
|
+
* patterns: [
|
|
692
|
+
* [ /^data-property-/, 'foobar' ],
|
|
693
|
+
* ]
|
|
694
|
+
* ```
|
|
695
|
+
*
|
|
696
|
+
* - to match an attribute name with a RegExp and match a value with another RegExp:
|
|
697
|
+
*
|
|
698
|
+
* ```ts
|
|
699
|
+
* patterns: [
|
|
700
|
+
* [ /^data-property-/, /^foo/ ]
|
|
701
|
+
* ]
|
|
702
|
+
* ```
|
|
703
|
+
*
|
|
704
|
+
* - to match a specific style property with the value matching a RegExp:
|
|
705
|
+
*
|
|
706
|
+
* ```ts
|
|
707
|
+
* patterns: [
|
|
708
|
+
* [ 'style', 'font-size', /px$/ ]
|
|
709
|
+
* ]
|
|
710
|
+
* ```
|
|
711
|
+
*
|
|
712
|
+
* - to match a specific class (class attribute is tokenized so it matches tokens individually):
|
|
713
|
+
*
|
|
714
|
+
* ```ts
|
|
715
|
+
* patterns: [
|
|
716
|
+
* [ 'class', 'foo' ]
|
|
717
|
+
* ]
|
|
718
|
+
* ```
|
|
719
|
+
*
|
|
720
|
+
* @internal
|
|
721
|
+
* @param patterns An array of normalized patterns (tuples of 2 or 3 items depending on if tokenized attribute value match is needed).
|
|
722
|
+
* @param match An array to populate with matching tuples.
|
|
723
|
+
* @param exclude Array of attribute names to exclude from match.
|
|
724
|
+
* @returns `true` if element matches all patterns. The matching tuples are pushed to the `match` array.
|
|
725
|
+
*/
|
|
726
|
+
_collectAttributesMatch(patterns, match, exclude) {
|
|
727
|
+
for (const [keyPattern, tokenPattern, valuePattern] of patterns) {
|
|
728
|
+
let hasKey = false;
|
|
729
|
+
let hasValue = false;
|
|
730
|
+
for (const [key, value] of this._attrs) {
|
|
731
|
+
if (exclude && exclude.includes(key) || !isPatternMatched(keyPattern, key)) {
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
hasKey = true;
|
|
735
|
+
if (typeof value == 'string') {
|
|
736
|
+
if (isPatternMatched(tokenPattern, value)) {
|
|
737
|
+
match.push([key]);
|
|
738
|
+
hasValue = true;
|
|
739
|
+
}
|
|
740
|
+
else if (!(keyPattern instanceof RegExp)) {
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
const tokenMatch = value._getTokensMatch(tokenPattern, valuePattern || true);
|
|
746
|
+
if (tokenMatch) {
|
|
747
|
+
hasValue = true;
|
|
748
|
+
for (const tokenMatchItem of tokenMatch) {
|
|
749
|
+
match.push([key, tokenMatchItem]);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
else if (!(keyPattern instanceof RegExp)) {
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (!hasKey || !hasValue) {
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return true;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Used by the {@link module:engine/conversion/viewconsumable~ViewConsumable} to collect the
|
|
765
|
+
* {@link module:engine/view/element~NormalizedConsumables} for the element.
|
|
766
|
+
*
|
|
767
|
+
* When `key` and `token` parameters are provided the output is filtered for the specified attribute and it's tokens and related tokens.
|
|
768
|
+
*
|
|
769
|
+
* @internal
|
|
770
|
+
* @param key Attribute name.
|
|
771
|
+
* @param token Reference token to collect all related tokens.
|
|
772
|
+
*/
|
|
773
|
+
_getConsumables(key, token) {
|
|
774
|
+
const attributes = [];
|
|
775
|
+
if (key) {
|
|
776
|
+
const value = this._attrs.get(key);
|
|
777
|
+
if (value !== undefined) {
|
|
778
|
+
if (typeof value == 'string') {
|
|
779
|
+
attributes.push([key]);
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
for (const prop of value._getConsumables(token)) {
|
|
783
|
+
attributes.push([key, prop]);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
for (const [key, value] of this._attrs) {
|
|
790
|
+
if (typeof value == 'string') {
|
|
791
|
+
attributes.push([key]);
|
|
792
|
+
}
|
|
793
|
+
else {
|
|
794
|
+
for (const prop of value._getConsumables()) {
|
|
795
|
+
attributes.push([key, prop]);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
return {
|
|
801
|
+
name: !key,
|
|
802
|
+
attributes
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Verify if the given element can be merged without conflicts into the element.
|
|
807
|
+
*
|
|
808
|
+
* Note that this method is extended by the {@link module:engine/view/attributeelement~AttributeElement} implementation.
|
|
809
|
+
*
|
|
810
|
+
* This method is used by the {@link module:engine/view/downcastwriter~DowncastWriter} while down-casting
|
|
811
|
+
* an {@link module:engine/view/attributeelement~AttributeElement} to merge it with other AttributeElement.
|
|
812
|
+
*
|
|
813
|
+
* @internal
|
|
814
|
+
* @returns Returns `true` if elements can be merged.
|
|
815
|
+
*/
|
|
816
|
+
_canMergeAttributesFrom(otherElement) {
|
|
817
|
+
if (this.name != otherElement.name) {
|
|
818
|
+
return false;
|
|
819
|
+
}
|
|
820
|
+
for (const [key, otherValue] of otherElement._attrs) {
|
|
821
|
+
const value = this._attrs.get(key);
|
|
822
|
+
if (value === undefined) {
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
if (typeof value == 'string' || typeof otherValue == 'string') {
|
|
826
|
+
if (value !== otherValue) {
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
else if (!value._canMergeFrom(otherValue)) {
|
|
831
|
+
return false;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return true;
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Merges attributes of a given element into the element.
|
|
838
|
+
* This includes also tokenized attributes like style and class.
|
|
839
|
+
*
|
|
840
|
+
* Note that you should make sure there are no conflicts before merging (see {@link #_canMergeAttributesFrom}).
|
|
841
|
+
*
|
|
842
|
+
* This method is used by the {@link module:engine/view/downcastwriter~DowncastWriter} while down-casting
|
|
843
|
+
* an {@link module:engine/view/attributeelement~AttributeElement} to merge it with other AttributeElement.
|
|
844
|
+
*
|
|
845
|
+
* @internal
|
|
846
|
+
*/
|
|
847
|
+
_mergeAttributesFrom(otherElement) {
|
|
632
848
|
this._fireChange('attributes', this);
|
|
633
|
-
|
|
634
|
-
|
|
849
|
+
// Move all attributes/classes/styles from wrapper to wrapped AttributeElement.
|
|
850
|
+
for (const [key, otherValue] of otherElement._attrs) {
|
|
851
|
+
const value = this._attrs.get(key);
|
|
852
|
+
if (value === undefined || typeof value == 'string' || typeof otherValue == 'string') {
|
|
853
|
+
this._setAttribute(key, otherValue);
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
value._mergeFrom(otherValue);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Verify if the given element attributes can be fully subtracted from the element.
|
|
862
|
+
*
|
|
863
|
+
* Note that this method is extended by the {@link module:engine/view/attributeelement~AttributeElement} implementation.
|
|
864
|
+
*
|
|
865
|
+
* This method is used by the {@link module:engine/view/downcastwriter~DowncastWriter} while down-casting
|
|
866
|
+
* an {@link module:engine/view/attributeelement~AttributeElement} to unwrap the AttributeElement.
|
|
867
|
+
*
|
|
868
|
+
* @internal
|
|
869
|
+
* @returns Returns `true` if elements attributes can be fully subtracted.
|
|
870
|
+
*/
|
|
871
|
+
_canSubtractAttributesOf(otherElement) {
|
|
872
|
+
if (this.name != otherElement.name) {
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
for (const [key, otherValue] of otherElement._attrs) {
|
|
876
|
+
const value = this._attrs.get(key);
|
|
877
|
+
if (value === undefined) {
|
|
878
|
+
return false;
|
|
879
|
+
}
|
|
880
|
+
if (typeof value == 'string' || typeof otherValue == 'string') {
|
|
881
|
+
if (value !== otherValue) {
|
|
882
|
+
return false;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
else if (!value._isMatching(otherValue)) {
|
|
886
|
+
return false;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return true;
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Removes (subtracts) corresponding attributes of the given element from the element.
|
|
893
|
+
* This includes also tokenized attributes like style and class.
|
|
894
|
+
* All attributes, classes and styles from given element should be present inside the element being unwrapped.
|
|
895
|
+
*
|
|
896
|
+
* Note that you should make sure all attributes could be subtracted before subtracting them (see {@link #_canSubtractAttributesOf}).
|
|
897
|
+
*
|
|
898
|
+
* This method is used by the {@link module:engine/view/downcastwriter~DowncastWriter} while down-casting
|
|
899
|
+
* an {@link module:engine/view/attributeelement~AttributeElement} to unwrap the AttributeElement.
|
|
900
|
+
*
|
|
901
|
+
* @internal
|
|
902
|
+
*/
|
|
903
|
+
_subtractAttributesOf(otherElement) {
|
|
904
|
+
this._fireChange('attributes', this);
|
|
905
|
+
for (const [key, otherValue] of otherElement._attrs) {
|
|
906
|
+
const value = this._attrs.get(key);
|
|
907
|
+
if (typeof value == 'string' || typeof otherValue == 'string') {
|
|
908
|
+
this._attrs.delete(key);
|
|
909
|
+
}
|
|
910
|
+
else {
|
|
911
|
+
value.remove(otherValue.keys());
|
|
912
|
+
if (value.isEmpty) {
|
|
913
|
+
this._attrs.delete(key);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
635
916
|
}
|
|
636
917
|
}
|
|
637
918
|
/**
|
|
@@ -654,6 +935,40 @@ export default class Element extends Node {
|
|
|
654
935
|
_removeCustomProperty(key) {
|
|
655
936
|
return this._customProperties.delete(key);
|
|
656
937
|
}
|
|
938
|
+
/**
|
|
939
|
+
* Parses attributes provided to the element constructor before they are applied to an element. If attributes are passed
|
|
940
|
+
* as an object (instead of `Iterable`), the object is transformed to the map. Attributes with `null` value are removed.
|
|
941
|
+
* Attributes with non-`String` value are converted to `String`.
|
|
942
|
+
*
|
|
943
|
+
* @param attrs Attributes to parse.
|
|
944
|
+
* @returns Parsed attributes.
|
|
945
|
+
*/
|
|
946
|
+
_parseAttributes(attrs) {
|
|
947
|
+
const attrsMap = toMap(attrs);
|
|
948
|
+
for (const [key, value] of attrsMap) {
|
|
949
|
+
if (value === null) {
|
|
950
|
+
attrsMap.delete(key);
|
|
951
|
+
}
|
|
952
|
+
else if (usesStylesMap(this.name, key)) {
|
|
953
|
+
// This is either an element clone so we need to clone styles map, or a new instance which requires value to be parsed.
|
|
954
|
+
const newValue = value instanceof StylesMap ?
|
|
955
|
+
value._clone() :
|
|
956
|
+
new StylesMap(this.document.stylesProcessor).setTo(String(value));
|
|
957
|
+
attrsMap.set(key, newValue);
|
|
958
|
+
}
|
|
959
|
+
else if (usesTokenList(this.name, key)) {
|
|
960
|
+
// This is either an element clone so we need to clone token list, or a new instance which requires value to be parsed.
|
|
961
|
+
const newValue = value instanceof TokenList ?
|
|
962
|
+
value._clone() :
|
|
963
|
+
new TokenList().setTo(String(value));
|
|
964
|
+
attrsMap.set(key, newValue);
|
|
965
|
+
}
|
|
966
|
+
else if (typeof value != 'string') {
|
|
967
|
+
attrsMap.set(key, String(value));
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
return attrsMap;
|
|
971
|
+
}
|
|
657
972
|
}
|
|
658
973
|
// The magic of type inference using `is` method is centralized in `TypeCheckable` class.
|
|
659
974
|
// Proper overload would interfere with that.
|
|
@@ -667,38 +982,6 @@ Element.prototype.is = function (type, name) {
|
|
|
667
982
|
return name === this.name && (type === 'element' || type === 'view:element');
|
|
668
983
|
}
|
|
669
984
|
};
|
|
670
|
-
/**
|
|
671
|
-
* Parses attributes provided to the element constructor before they are applied to an element. If attributes are passed
|
|
672
|
-
* as an object (instead of `Iterable`), the object is transformed to the map. Attributes with `null` value are removed.
|
|
673
|
-
* Attributes with non-`String` value are converted to `String`.
|
|
674
|
-
*
|
|
675
|
-
* @param attrs Attributes to parse.
|
|
676
|
-
* @returns Parsed attributes.
|
|
677
|
-
*/
|
|
678
|
-
function parseAttributes(attrs) {
|
|
679
|
-
const attrsMap = toMap(attrs);
|
|
680
|
-
for (const [key, value] of attrsMap) {
|
|
681
|
-
if (value === null) {
|
|
682
|
-
attrsMap.delete(key);
|
|
683
|
-
}
|
|
684
|
-
else if (typeof value != 'string') {
|
|
685
|
-
attrsMap.set(key, String(value));
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
return attrsMap;
|
|
689
|
-
}
|
|
690
|
-
/**
|
|
691
|
-
* Parses class attribute and puts all classes into classes set.
|
|
692
|
-
* Classes set s cleared before insertion.
|
|
693
|
-
*
|
|
694
|
-
* @param classesSet Set to insert parsed classes.
|
|
695
|
-
* @param classesString String with classes to parse.
|
|
696
|
-
*/
|
|
697
|
-
function parseClasses(classesSet, classesString) {
|
|
698
|
-
const classArray = classesString.split(/\s+/);
|
|
699
|
-
classesSet.clear();
|
|
700
|
-
classArray.forEach(name => classesSet.add(name));
|
|
701
|
-
}
|
|
702
985
|
/**
|
|
703
986
|
* Converts strings to Text and non-iterables to arrays.
|
|
704
987
|
*/
|
|
@@ -724,3 +1007,15 @@ function normalize(document, nodes) {
|
|
|
724
1007
|
}
|
|
725
1008
|
return normalizedNodes;
|
|
726
1009
|
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Returns `true` if an attribute on a given element should be handled as a TokenList.
|
|
1012
|
+
*/
|
|
1013
|
+
function usesTokenList(elementName, key) {
|
|
1014
|
+
return key == 'class' || elementName == 'a' && key == 'rel';
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Returns `true` if an attribute on a given element should be handled as a StylesMap.
|
|
1018
|
+
*/
|
|
1019
|
+
function usesStylesMap(elementName, key) {
|
|
1020
|
+
return key == 'style';
|
|
1021
|
+
}
|