@ckeditor/ckeditor5-engine 35.0.1 → 35.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/CHANGELOG.md +4 -4
- package/package.json +30 -24
- package/src/controller/datacontroller.js +467 -561
- package/src/controller/editingcontroller.js +168 -204
- package/src/conversion/conversion.js +541 -565
- package/src/conversion/conversionhelpers.js +24 -28
- package/src/conversion/downcastdispatcher.js +457 -686
- package/src/conversion/downcasthelpers.js +1583 -1965
- package/src/conversion/mapper.js +518 -707
- package/src/conversion/modelconsumable.js +240 -283
- package/src/conversion/upcastdispatcher.js +372 -718
- package/src/conversion/upcasthelpers.js +707 -818
- package/src/conversion/viewconsumable.js +524 -581
- package/src/dataprocessor/basichtmlwriter.js +12 -16
- package/src/dataprocessor/dataprocessor.js +5 -0
- package/src/dataprocessor/htmldataprocessor.js +100 -116
- package/src/dataprocessor/htmlwriter.js +1 -18
- package/src/dataprocessor/xmldataprocessor.js +116 -137
- package/src/dev-utils/model.js +260 -352
- package/src/dev-utils/operationreplayer.js +106 -126
- package/src/dev-utils/utils.js +34 -51
- package/src/dev-utils/view.js +632 -753
- package/src/index.js +0 -11
- package/src/model/batch.js +111 -127
- package/src/model/differ.js +988 -1233
- package/src/model/document.js +340 -449
- package/src/model/documentfragment.js +327 -364
- package/src/model/documentselection.js +996 -1189
- package/src/model/element.js +306 -410
- package/src/model/history.js +224 -262
- package/src/model/item.js +5 -0
- package/src/model/liveposition.js +84 -145
- package/src/model/liverange.js +108 -185
- package/src/model/markercollection.js +379 -480
- package/src/model/model.js +883 -1034
- package/src/model/node.js +419 -463
- package/src/model/nodelist.js +175 -201
- package/src/model/operation/attributeoperation.js +153 -182
- package/src/model/operation/detachoperation.js +64 -83
- package/src/model/operation/insertoperation.js +135 -166
- package/src/model/operation/markeroperation.js +114 -140
- package/src/model/operation/mergeoperation.js +163 -191
- package/src/model/operation/moveoperation.js +157 -187
- package/src/model/operation/nooperation.js +28 -38
- package/src/model/operation/operation.js +106 -125
- package/src/model/operation/operationfactory.js +30 -34
- package/src/model/operation/renameoperation.js +109 -135
- package/src/model/operation/rootattributeoperation.js +155 -188
- package/src/model/operation/splitoperation.js +196 -232
- package/src/model/operation/transform.js +1833 -2204
- package/src/model/operation/utils.js +140 -204
- package/src/model/position.js +899 -1053
- package/src/model/range.js +910 -1028
- package/src/model/rootelement.js +77 -97
- package/src/model/schema.js +1189 -1835
- package/src/model/selection.js +745 -862
- package/src/model/text.js +90 -114
- package/src/model/textproxy.js +204 -240
- package/src/model/treewalker.js +316 -397
- package/src/model/typecheckable.js +16 -0
- package/src/model/utils/autoparagraphing.js +32 -44
- package/src/model/utils/deletecontent.js +334 -418
- package/src/model/utils/findoptimalinsertionrange.js +25 -36
- package/src/model/utils/getselectedcontent.js +96 -118
- package/src/model/utils/insertcontent.js +654 -773
- package/src/model/utils/insertobject.js +96 -119
- package/src/model/utils/modifyselection.js +120 -158
- package/src/model/utils/selection-post-fixer.js +153 -201
- package/src/model/writer.js +1305 -1474
- package/src/view/attributeelement.js +189 -225
- package/src/view/containerelement.js +75 -85
- package/src/view/document.js +172 -215
- package/src/view/documentfragment.js +200 -249
- package/src/view/documentselection.js +338 -367
- package/src/view/domconverter.js +1370 -1617
- package/src/view/downcastwriter.js +1747 -2076
- package/src/view/editableelement.js +81 -97
- package/src/view/element.js +739 -890
- package/src/view/elementdefinition.js +5 -0
- package/src/view/emptyelement.js +82 -92
- package/src/view/filler.js +35 -50
- package/src/view/item.js +5 -0
- package/src/view/matcher.js +260 -559
- package/src/view/node.js +274 -360
- package/src/view/observer/arrowkeysobserver.js +19 -28
- package/src/view/observer/bubblingemittermixin.js +120 -263
- package/src/view/observer/bubblingeventinfo.js +47 -55
- package/src/view/observer/clickobserver.js +7 -13
- package/src/view/observer/compositionobserver.js +14 -24
- package/src/view/observer/domeventdata.js +57 -67
- package/src/view/observer/domeventobserver.js +40 -64
- package/src/view/observer/fakeselectionobserver.js +81 -96
- package/src/view/observer/focusobserver.js +45 -61
- package/src/view/observer/inputobserver.js +7 -13
- package/src/view/observer/keyobserver.js +17 -27
- package/src/view/observer/mouseobserver.js +7 -14
- package/src/view/observer/mutationobserver.js +220 -315
- package/src/view/observer/observer.js +81 -102
- package/src/view/observer/selectionobserver.js +191 -246
- package/src/view/observer/tabobserver.js +23 -36
- package/src/view/placeholder.js +128 -173
- package/src/view/position.js +350 -401
- package/src/view/range.js +453 -513
- package/src/view/rawelement.js +85 -112
- package/src/view/renderer.js +874 -1018
- package/src/view/rooteditableelement.js +80 -90
- package/src/view/selection.js +608 -689
- package/src/view/styles/background.js +43 -44
- package/src/view/styles/border.js +220 -276
- package/src/view/styles/margin.js +8 -17
- package/src/view/styles/padding.js +8 -16
- package/src/view/styles/utils.js +127 -160
- package/src/view/stylesmap.js +728 -905
- package/src/view/text.js +102 -126
- package/src/view/textproxy.js +144 -170
- package/src/view/treewalker.js +383 -479
- package/src/view/typecheckable.js +19 -0
- package/src/view/uielement.js +166 -187
- package/src/view/upcastwriter.js +395 -449
- package/src/view/view.js +569 -664
- package/src/dataprocessor/dataprocessor.jsdoc +0 -64
- package/src/model/item.jsdoc +0 -14
- package/src/view/elementdefinition.jsdoc +0 -59
- package/src/view/item.jsdoc +0 -14
package/src/dev-utils/view.js
CHANGED
|
@@ -2,17 +2,13 @@
|
|
|
2
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
|
-
|
|
6
5
|
/**
|
|
7
6
|
* @module engine/dev-utils/view
|
|
8
7
|
*/
|
|
9
|
-
|
|
10
8
|
/* globals document */
|
|
11
|
-
|
|
12
9
|
/**
|
|
13
10
|
* Collection of methods for manipulating the {@link module:engine/view/view view} for testing purposes.
|
|
14
11
|
*/
|
|
15
|
-
|
|
16
12
|
import View from '../view/view';
|
|
17
13
|
import ViewDocument from '../view/document';
|
|
18
14
|
import ViewDocumentFragment from '../view/documentfragment';
|
|
@@ -27,26 +23,24 @@ import EmptyElement from '../view/emptyelement';
|
|
|
27
23
|
import UIElement from '../view/uielement';
|
|
28
24
|
import RawElement from '../view/rawelement';
|
|
29
25
|
import { StylesProcessor } from '../view/stylesmap';
|
|
30
|
-
|
|
31
26
|
const ELEMENT_RANGE_START_TOKEN = '[';
|
|
32
27
|
const ELEMENT_RANGE_END_TOKEN = ']';
|
|
33
28
|
const TEXT_RANGE_START_TOKEN = '{';
|
|
34
29
|
const TEXT_RANGE_END_TOKEN = '}';
|
|
35
30
|
const allowedTypes = {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
'container': ContainerElement,
|
|
32
|
+
'attribute': AttributeElement,
|
|
33
|
+
'empty': EmptyElement,
|
|
34
|
+
'ui': UIElement,
|
|
35
|
+
'raw': RawElement
|
|
41
36
|
};
|
|
42
37
|
// Returns simplified implementation of {@link module:engine/view/domconverter~DomConverter#setContentOf DomConverter.setContentOf} method.
|
|
43
38
|
// Used to render UIElement and RawElement.
|
|
44
39
|
const domConverterStub = {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
setContentOf: (node, html) => {
|
|
41
|
+
node.innerHTML = html;
|
|
42
|
+
}
|
|
48
43
|
};
|
|
49
|
-
|
|
50
44
|
/**
|
|
51
45
|
* Writes the content of the {@link module:engine/view/document~Document document} to an HTML-like string.
|
|
52
46
|
*
|
|
@@ -71,32 +65,28 @@ const domConverterStub = {
|
|
|
71
65
|
* i.e. with view data filtering. Otherwise the simple stub is used.
|
|
72
66
|
* @returns {String} The stringified data.
|
|
73
67
|
*/
|
|
74
|
-
export function getData(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
getData._stringify( root, null, stringifyOptions ) :
|
|
94
|
-
getData._stringify( root, document.selection, stringifyOptions );
|
|
68
|
+
export function getData(view, options = {}) {
|
|
69
|
+
if (!(view instanceof View)) {
|
|
70
|
+
throw new TypeError('View needs to be an instance of module:engine/view/view~View.');
|
|
71
|
+
}
|
|
72
|
+
const document = view.document;
|
|
73
|
+
const withoutSelection = !!options.withoutSelection;
|
|
74
|
+
const rootName = options.rootName || 'main';
|
|
75
|
+
const root = document.getRoot(rootName);
|
|
76
|
+
const stringifyOptions = {
|
|
77
|
+
showType: options.showType,
|
|
78
|
+
showPriority: options.showPriority,
|
|
79
|
+
renderUIElements: options.renderUIElements,
|
|
80
|
+
renderRawElements: options.renderRawElements,
|
|
81
|
+
ignoreRoot: true,
|
|
82
|
+
domConverter: options.domConverter
|
|
83
|
+
};
|
|
84
|
+
return withoutSelection ?
|
|
85
|
+
getData._stringify(root, null, stringifyOptions) :
|
|
86
|
+
getData._stringify(root, document.selection, stringifyOptions);
|
|
95
87
|
}
|
|
96
|
-
|
|
97
88
|
// Set stringify as getData private method - needed for testing/spying.
|
|
98
89
|
getData._stringify = stringify;
|
|
99
|
-
|
|
100
90
|
/**
|
|
101
91
|
* Sets the content of a view {@link module:engine/view/document~Document document} provided as an HTML-like string.
|
|
102
92
|
*
|
|
@@ -106,27 +96,22 @@ getData._stringify = stringify;
|
|
|
106
96
|
* @param {String} [options.rootName='main'] The root name where parsed data will be stored. If not provided,
|
|
107
97
|
* the default `main` name will be used.
|
|
108
98
|
*/
|
|
109
|
-
export function setData(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
writer.setSelection( result.selection );
|
|
123
|
-
}
|
|
124
|
-
} );
|
|
99
|
+
export function setData(view, data, options = {}) {
|
|
100
|
+
if (!(view instanceof View)) {
|
|
101
|
+
throw new TypeError('View needs to be an instance of module:engine/view/view~View.');
|
|
102
|
+
}
|
|
103
|
+
const document = view.document;
|
|
104
|
+
const rootName = options.rootName || 'main';
|
|
105
|
+
const root = document.getRoot(rootName);
|
|
106
|
+
view.change(writer => {
|
|
107
|
+
const result = setData._parse(data, { rootElement: root });
|
|
108
|
+
if (result.view && result.selection) {
|
|
109
|
+
writer.setSelection(result.selection);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
125
112
|
}
|
|
126
|
-
|
|
127
113
|
// Set parse as setData private method - needed for testing/spying.
|
|
128
114
|
setData._parse = parse;
|
|
129
|
-
|
|
130
115
|
/**
|
|
131
116
|
* Converts view elements to HTML-like string representation.
|
|
132
117
|
*
|
|
@@ -257,23 +242,18 @@ setData._parse = parse;
|
|
|
257
242
|
* i.e. with view data filtering. Otherwise the simple stub is used.
|
|
258
243
|
* @returns {String} An HTML-like string representing the view.
|
|
259
244
|
*/
|
|
260
|
-
export function stringify(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const viewStringify = new ViewStringify( node, selection, options );
|
|
273
|
-
|
|
274
|
-
return viewStringify.stringify();
|
|
245
|
+
export function stringify(node, selectionOrPositionOrRange = null, options = {}) {
|
|
246
|
+
let selection;
|
|
247
|
+
if (selectionOrPositionOrRange instanceof Position ||
|
|
248
|
+
selectionOrPositionOrRange instanceof Range) {
|
|
249
|
+
selection = new DocumentSelection(selectionOrPositionOrRange);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
selection = selectionOrPositionOrRange;
|
|
253
|
+
}
|
|
254
|
+
const viewStringify = new ViewStringify(node, selection, options);
|
|
255
|
+
return viewStringify.stringify();
|
|
275
256
|
}
|
|
276
|
-
|
|
277
257
|
/**
|
|
278
258
|
* Parses an HTML-like string and returns a view tree.
|
|
279
259
|
* A simple string will be converted to a {@link module:engine/view/text~Text text} node:
|
|
@@ -349,610 +329,528 @@ export function stringify( node, selectionOrPositionOrRange = null, options = {}
|
|
|
349
329
|
* Returns the parsed view node or an object with two fields: `view` and `selection` when selection ranges were included in the data
|
|
350
330
|
* to parse.
|
|
351
331
|
*/
|
|
352
|
-
export function parse(
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
return {
|
|
394
|
-
view,
|
|
395
|
-
selection
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// If single element is returned without selection - remove it from parent and return detached element.
|
|
400
|
-
if ( view.parent ) {
|
|
401
|
-
view._remove();
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
return view;
|
|
332
|
+
export function parse(data, options = {}) {
|
|
333
|
+
const viewDocument = new ViewDocument(new StylesProcessor());
|
|
334
|
+
options.order = options.order || [];
|
|
335
|
+
const rangeParser = new RangeParser({
|
|
336
|
+
sameSelectionCharacters: options.sameSelectionCharacters
|
|
337
|
+
});
|
|
338
|
+
const processor = new XmlDataProcessor(viewDocument, {
|
|
339
|
+
namespaces: Object.keys(allowedTypes)
|
|
340
|
+
});
|
|
341
|
+
// Convert data to view.
|
|
342
|
+
let view = processor.toView(data);
|
|
343
|
+
// At this point we have a view tree with Elements that could have names like `attribute:b:1`. In the next step
|
|
344
|
+
// we need to parse Element's names and convert them to AttributeElements and ContainerElements.
|
|
345
|
+
view = _convertViewElements(view);
|
|
346
|
+
// If custom root is provided - move all nodes there.
|
|
347
|
+
if (options.rootElement) {
|
|
348
|
+
const root = options.rootElement;
|
|
349
|
+
const nodes = view._removeChildren(0, view.childCount);
|
|
350
|
+
root._removeChildren(0, root.childCount);
|
|
351
|
+
root._appendChild(nodes);
|
|
352
|
+
view = root;
|
|
353
|
+
}
|
|
354
|
+
// Parse ranges included in view text nodes.
|
|
355
|
+
const ranges = rangeParser.parse(view, options.order);
|
|
356
|
+
// If only one element is returned inside DocumentFragment - return that element.
|
|
357
|
+
if (view.is('documentFragment') && view.childCount === 1) {
|
|
358
|
+
view = view.getChild(0);
|
|
359
|
+
}
|
|
360
|
+
// When ranges are present - return object containing view, and selection.
|
|
361
|
+
if (ranges.length) {
|
|
362
|
+
const selection = new DocumentSelection(ranges, { backward: !!options.lastRangeBackward });
|
|
363
|
+
return {
|
|
364
|
+
view,
|
|
365
|
+
selection
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
// If single element is returned without selection - remove it from parent and return detached element.
|
|
369
|
+
if (view.parent) {
|
|
370
|
+
view._remove();
|
|
371
|
+
}
|
|
372
|
+
return view;
|
|
405
373
|
}
|
|
406
|
-
|
|
407
374
|
/**
|
|
408
375
|
* Private helper class used for converting ranges represented as text inside view {@link module:engine/view/text~Text text nodes}.
|
|
409
376
|
*
|
|
410
377
|
* @private
|
|
411
378
|
*/
|
|
412
379
|
class RangeParser {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
* @private
|
|
588
|
-
* @returns {Array.<module:engine/view/range~Range>}
|
|
589
|
-
*/
|
|
590
|
-
_createRanges() {
|
|
591
|
-
const ranges = [];
|
|
592
|
-
let range = null;
|
|
593
|
-
|
|
594
|
-
for ( const item of this._positions ) {
|
|
595
|
-
// When end of range is found without opening.
|
|
596
|
-
if ( !range && ( item.bracket == ELEMENT_RANGE_END_TOKEN || item.bracket == TEXT_RANGE_END_TOKEN ) ) {
|
|
597
|
-
throw new Error( `Parse error - end of range was found '${ item.bracket }' but range was not started before.` );
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
// When second start of range is found when one is already opened - selection does not allow intersecting
|
|
601
|
-
// ranges.
|
|
602
|
-
if ( range && ( item.bracket == ELEMENT_RANGE_START_TOKEN || item.bracket == TEXT_RANGE_START_TOKEN ) ) {
|
|
603
|
-
throw new Error( `Parse error - start of range was found '${ item.bracket }' but one range is already started.` );
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
if ( item.bracket == ELEMENT_RANGE_START_TOKEN || item.bracket == TEXT_RANGE_START_TOKEN ) {
|
|
607
|
-
range = new Range( item.position, item.position );
|
|
608
|
-
} else {
|
|
609
|
-
range.end = item.position;
|
|
610
|
-
ranges.push( range );
|
|
611
|
-
range = null;
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// Check if all ranges have proper ending.
|
|
616
|
-
if ( range !== null ) {
|
|
617
|
-
throw new Error( 'Parse error - range was started but no end delimiter was found.' );
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
return ranges;
|
|
621
|
-
}
|
|
380
|
+
/**
|
|
381
|
+
* Creates a range parser instance.
|
|
382
|
+
*
|
|
383
|
+
* @param {Object} options The range parser configuration.
|
|
384
|
+
* @param {Boolean} [options.sameSelectionCharacters=false] When set to `true`, the selection inside the text is marked as
|
|
385
|
+
* `{` and `}` and the selection outside the text as `[` and `]`. When set to `false`, both are marked as `[` and `]`.
|
|
386
|
+
*/
|
|
387
|
+
constructor(options) {
|
|
388
|
+
this.sameSelectionCharacters = !!options.sameSelectionCharacters;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Parses the view and returns ranges represented inside {@link module:engine/view/text~Text text nodes}.
|
|
392
|
+
* The method will remove all occurrences of `{`, `}`, `[` and `]` from found text nodes. If a text node is empty after
|
|
393
|
+
* the process, it will be removed, too.
|
|
394
|
+
*
|
|
395
|
+
* @param {module:engine/view/node~Node} node The starting node.
|
|
396
|
+
* @param {Array.<Number>} order The order of ranges. Each element should represent the desired position of the range after
|
|
397
|
+
* sorting. For example: `[2, 3, 1]` means that the first range will be placed as the second, the second as the third and the third
|
|
398
|
+
* as the first.
|
|
399
|
+
* @returns {Array.<module:engine/view/range~Range>} An array with ranges found.
|
|
400
|
+
*/
|
|
401
|
+
parse(node, order) {
|
|
402
|
+
this._positions = [];
|
|
403
|
+
// Remove all range brackets from view nodes and save their positions.
|
|
404
|
+
this._getPositions(node);
|
|
405
|
+
// Create ranges using gathered positions.
|
|
406
|
+
let ranges = this._createRanges();
|
|
407
|
+
// Sort ranges if needed.
|
|
408
|
+
if (order.length) {
|
|
409
|
+
if (order.length != ranges.length) {
|
|
410
|
+
throw new Error(`Parse error - there are ${ranges.length} ranges found, but ranges order array contains ${order.length} elements.`);
|
|
411
|
+
}
|
|
412
|
+
ranges = this._sortRanges(ranges, order);
|
|
413
|
+
}
|
|
414
|
+
return ranges;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Gathers positions of brackets inside the view tree starting from the provided node. The method will remove all occurrences of
|
|
418
|
+
* `{`, `}`, `[` and `]` from found text nodes. If a text node is empty after the process, it will be removed, too.
|
|
419
|
+
*
|
|
420
|
+
* @private
|
|
421
|
+
* @param {module:engine/view/node~Node} node Staring node.
|
|
422
|
+
*/
|
|
423
|
+
_getPositions(node) {
|
|
424
|
+
if (node.is('documentFragment') || node.is('element')) {
|
|
425
|
+
// Copy elements into the array, when nodes will be removed from parent node this array will still have all the
|
|
426
|
+
// items needed for iteration.
|
|
427
|
+
const children = [...node.getChildren()];
|
|
428
|
+
for (const child of children) {
|
|
429
|
+
this._getPositions(child);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (node.is('$text')) {
|
|
433
|
+
const regexp = new RegExp(`[${TEXT_RANGE_START_TOKEN}${TEXT_RANGE_END_TOKEN}\\${ELEMENT_RANGE_END_TOKEN}\\${ELEMENT_RANGE_START_TOKEN}]`, 'g');
|
|
434
|
+
let text = node.data;
|
|
435
|
+
let match;
|
|
436
|
+
let offset = 0;
|
|
437
|
+
const brackets = [];
|
|
438
|
+
// Remove brackets from text and store info about offset inside text node.
|
|
439
|
+
while ((match = regexp.exec(text))) {
|
|
440
|
+
const index = match.index;
|
|
441
|
+
const bracket = match[0];
|
|
442
|
+
brackets.push({
|
|
443
|
+
bracket,
|
|
444
|
+
textOffset: index - offset
|
|
445
|
+
});
|
|
446
|
+
offset++;
|
|
447
|
+
}
|
|
448
|
+
text = text.replace(regexp, '');
|
|
449
|
+
node._data = text;
|
|
450
|
+
const index = node.index;
|
|
451
|
+
const parent = node.parent;
|
|
452
|
+
// Remove empty text nodes.
|
|
453
|
+
if (!text) {
|
|
454
|
+
node._remove();
|
|
455
|
+
}
|
|
456
|
+
for (const item of brackets) {
|
|
457
|
+
// Non-empty text node.
|
|
458
|
+
if (text) {
|
|
459
|
+
if (this.sameSelectionCharacters ||
|
|
460
|
+
(!this.sameSelectionCharacters &&
|
|
461
|
+
(item.bracket == TEXT_RANGE_START_TOKEN || item.bracket == TEXT_RANGE_END_TOKEN))) {
|
|
462
|
+
// Store information about text range delimiter.
|
|
463
|
+
this._positions.push({
|
|
464
|
+
bracket: item.bracket,
|
|
465
|
+
position: new Position(node, item.textOffset)
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
// Check if element range delimiter is not placed inside text node.
|
|
470
|
+
if (!this.sameSelectionCharacters && item.textOffset !== 0 && item.textOffset !== text.length) {
|
|
471
|
+
throw new Error(`Parse error - range delimiter '${item.bracket}' is placed inside text node.`);
|
|
472
|
+
}
|
|
473
|
+
// If bracket is placed at the end of the text node - it should be positioned after it.
|
|
474
|
+
const offset = (item.textOffset === 0 ? index : index + 1);
|
|
475
|
+
// Store information about element range delimiter.
|
|
476
|
+
this._positions.push({
|
|
477
|
+
bracket: item.bracket,
|
|
478
|
+
position: new Position(parent, offset)
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
if (!this.sameSelectionCharacters &&
|
|
484
|
+
item.bracket == TEXT_RANGE_START_TOKEN ||
|
|
485
|
+
item.bracket == TEXT_RANGE_END_TOKEN) {
|
|
486
|
+
throw new Error(`Parse error - text range delimiter '${item.bracket}' is placed inside empty text node. `);
|
|
487
|
+
}
|
|
488
|
+
// Store information about element range delimiter.
|
|
489
|
+
this._positions.push({
|
|
490
|
+
bracket: item.bracket,
|
|
491
|
+
position: new Position(parent, index)
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Sorts ranges in a given order. Range order should be an array and each element should represent the desired position
|
|
499
|
+
* of the range after sorting.
|
|
500
|
+
* For example: `[2, 3, 1]` means that the first range will be placed as the second, the second as the third and the third
|
|
501
|
+
* as the first.
|
|
502
|
+
*
|
|
503
|
+
* @private
|
|
504
|
+
* @param {Array.<module:engine/view/range~Range>} ranges Ranges to sort.
|
|
505
|
+
* @param {Array.<Number>} rangesOrder An array with new range order.
|
|
506
|
+
* @returns {Array} Sorted ranges array.
|
|
507
|
+
*/
|
|
508
|
+
_sortRanges(ranges, rangesOrder) {
|
|
509
|
+
const sortedRanges = [];
|
|
510
|
+
let index = 0;
|
|
511
|
+
for (const newPosition of rangesOrder) {
|
|
512
|
+
if (ranges[newPosition - 1] === undefined) {
|
|
513
|
+
throw new Error('Parse error - provided ranges order is invalid.');
|
|
514
|
+
}
|
|
515
|
+
sortedRanges[newPosition - 1] = ranges[index];
|
|
516
|
+
index++;
|
|
517
|
+
}
|
|
518
|
+
return sortedRanges;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Uses all found bracket positions to create ranges from them.
|
|
522
|
+
*
|
|
523
|
+
* @private
|
|
524
|
+
* @returns {Array.<module:engine/view/range~Range>}
|
|
525
|
+
*/
|
|
526
|
+
_createRanges() {
|
|
527
|
+
const ranges = [];
|
|
528
|
+
let range = null;
|
|
529
|
+
for (const item of this._positions) {
|
|
530
|
+
// When end of range is found without opening.
|
|
531
|
+
if (!range && (item.bracket == ELEMENT_RANGE_END_TOKEN || item.bracket == TEXT_RANGE_END_TOKEN)) {
|
|
532
|
+
throw new Error(`Parse error - end of range was found '${item.bracket}' but range was not started before.`);
|
|
533
|
+
}
|
|
534
|
+
// When second start of range is found when one is already opened - selection does not allow intersecting
|
|
535
|
+
// ranges.
|
|
536
|
+
if (range && (item.bracket == ELEMENT_RANGE_START_TOKEN || item.bracket == TEXT_RANGE_START_TOKEN)) {
|
|
537
|
+
throw new Error(`Parse error - start of range was found '${item.bracket}' but one range is already started.`);
|
|
538
|
+
}
|
|
539
|
+
if (item.bracket == ELEMENT_RANGE_START_TOKEN || item.bracket == TEXT_RANGE_START_TOKEN) {
|
|
540
|
+
range = new Range(item.position, item.position);
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
range.end = item.position;
|
|
544
|
+
ranges.push(range);
|
|
545
|
+
range = null;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// Check if all ranges have proper ending.
|
|
549
|
+
if (range !== null) {
|
|
550
|
+
throw new Error('Parse error - range was started but no end delimiter was found.');
|
|
551
|
+
}
|
|
552
|
+
return ranges;
|
|
553
|
+
}
|
|
622
554
|
}
|
|
623
|
-
|
|
624
555
|
/**
|
|
625
556
|
* Private helper class used for converting the view tree to a string.
|
|
626
557
|
*
|
|
627
558
|
* @private
|
|
628
559
|
*/
|
|
629
560
|
class ViewStringify {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
* Converts the passed {@link module:engine/view/element~Element element} attributes to their string representation.
|
|
923
|
-
* If an element has no attributes, an empty string is returned.
|
|
924
|
-
*
|
|
925
|
-
* @private
|
|
926
|
-
* @param {module:engine/view/element~Element} element
|
|
927
|
-
* @returns {String}
|
|
928
|
-
*/
|
|
929
|
-
_stringifyElementAttributes( element ) {
|
|
930
|
-
const attributes = [];
|
|
931
|
-
const keys = [ ...element.getAttributeKeys() ].sort();
|
|
932
|
-
|
|
933
|
-
for ( const attribute of keys ) {
|
|
934
|
-
let attributeValue;
|
|
935
|
-
|
|
936
|
-
if ( attribute === 'class' ) {
|
|
937
|
-
attributeValue = [ ...element.getClassNames() ]
|
|
938
|
-
.sort()
|
|
939
|
-
.join( ' ' );
|
|
940
|
-
} else if ( attribute === 'style' ) {
|
|
941
|
-
attributeValue = [ ...element.getStyleNames() ]
|
|
942
|
-
.sort()
|
|
943
|
-
.map( style => `${ style }:${ element.getStyle( style ) }` )
|
|
944
|
-
.join( ';' );
|
|
945
|
-
} else {
|
|
946
|
-
attributeValue = element.getAttribute( attribute );
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
attributes.push( `${ attribute }="${ attributeValue }"` );
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
return attributes.join( ' ' );
|
|
953
|
-
}
|
|
561
|
+
/**
|
|
562
|
+
* Creates a view stringify instance.
|
|
563
|
+
*
|
|
564
|
+
* @param root
|
|
565
|
+
* @param {module:engine/view/documentselection~DocumentSelection} selection A selection whose ranges
|
|
566
|
+
* should also be converted to a string.
|
|
567
|
+
* @param {Object} options An options object.
|
|
568
|
+
* @param {Boolean} [options.showType=false] When set to `true`, the type of elements will be printed (`<container:p>`
|
|
569
|
+
* instead of `<p>`, `<attribute:b>` instead of `<b>` and `<empty:img>` instead of `<img>`).
|
|
570
|
+
* @param {Boolean} [options.showPriority=false] When set to `true`, the attribute element's priority will be printed.
|
|
571
|
+
* @param {Boolean} [options.ignoreRoot=false] When set to `true`, the root's element opening and closing tag will not
|
|
572
|
+
* be outputted.
|
|
573
|
+
* @param {Boolean} [options.sameSelectionCharacters=false] When set to `true`, the selection inside the text is marked as
|
|
574
|
+
* `{` and `}` and the selection outside the text as `[` and `]`. When set to `false`, both are marked as `[` and `]`.
|
|
575
|
+
* @param {Boolean} [options.renderUIElements=false] When set to `true`, the inner content of each
|
|
576
|
+
* {@link module:engine/view/uielement~UIElement} will be printed.
|
|
577
|
+
* @param {Boolean} [options.renderRawElements=false] When set to `true`, the inner content of each
|
|
578
|
+
* @param {Object} [options.domConverter={}] When set to an actual {@link module:engine/view/domconverter~DomConverter DomConverter}
|
|
579
|
+
* instance, it lets the conversion go through exactly the same flow the editing view is going through,
|
|
580
|
+
* i.e. with view data filtering. Otherwise the simple stub is used.
|
|
581
|
+
* {@link module:engine/view/rawelement~RawElement} will be printed.
|
|
582
|
+
*/
|
|
583
|
+
constructor(root, selection, options) {
|
|
584
|
+
this.root = root;
|
|
585
|
+
this.selection = selection;
|
|
586
|
+
this.ranges = [];
|
|
587
|
+
if (selection) {
|
|
588
|
+
this.ranges = [...selection.getRanges()];
|
|
589
|
+
}
|
|
590
|
+
this.showType = !!options.showType;
|
|
591
|
+
this.showPriority = !!options.showPriority;
|
|
592
|
+
this.showAttributeElementId = !!options.showAttributeElementId;
|
|
593
|
+
this.ignoreRoot = !!options.ignoreRoot;
|
|
594
|
+
this.sameSelectionCharacters = !!options.sameSelectionCharacters;
|
|
595
|
+
this.renderUIElements = !!options.renderUIElements;
|
|
596
|
+
this.renderRawElements = !!options.renderRawElements;
|
|
597
|
+
this.domConverter = options.domConverter || domConverterStub;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Converts the view to a string.
|
|
601
|
+
*
|
|
602
|
+
* @returns {String} String representation of the view elements.
|
|
603
|
+
*/
|
|
604
|
+
stringify() {
|
|
605
|
+
let result = '';
|
|
606
|
+
this._walkView(this.root, chunk => {
|
|
607
|
+
result += chunk;
|
|
608
|
+
});
|
|
609
|
+
return result;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Executes a simple walker that iterates over all elements in the view tree starting from the root element.
|
|
613
|
+
* Calls the `callback` with parsed chunks of string data.
|
|
614
|
+
*
|
|
615
|
+
* @private
|
|
616
|
+
* @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element|module:engine/view/text~Text} root
|
|
617
|
+
* @param {Function} callback
|
|
618
|
+
*/
|
|
619
|
+
_walkView(root, callback) {
|
|
620
|
+
const ignore = this.ignoreRoot && this.root === root;
|
|
621
|
+
if (root.is('element') || root.is('documentFragment')) {
|
|
622
|
+
if (root.is('element') && !ignore) {
|
|
623
|
+
callback(this._stringifyElementOpen(root));
|
|
624
|
+
}
|
|
625
|
+
if ((this.renderUIElements && root.is('uiElement'))) {
|
|
626
|
+
callback(root.render(document, this.domConverter).innerHTML);
|
|
627
|
+
}
|
|
628
|
+
else if (this.renderRawElements && root.is('rawElement')) {
|
|
629
|
+
// There's no DOM element for "root" to pass to render(). Creating
|
|
630
|
+
// a surrogate container to render the children instead.
|
|
631
|
+
const rawContentContainer = document.createElement('div');
|
|
632
|
+
root.render(rawContentContainer, this.domConverter);
|
|
633
|
+
callback(rawContentContainer.innerHTML);
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
let offset = 0;
|
|
637
|
+
callback(this._stringifyElementRanges(root, offset));
|
|
638
|
+
for (const child of root.getChildren()) {
|
|
639
|
+
this._walkView(child, callback);
|
|
640
|
+
offset++;
|
|
641
|
+
callback(this._stringifyElementRanges(root, offset));
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (root.is('element') && !ignore) {
|
|
645
|
+
callback(this._stringifyElementClose(root));
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (root.is('$text')) {
|
|
649
|
+
callback(this._stringifyTextRanges(root));
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Checks if a given {@link module:engine/view/element~Element element} has a {@link module:engine/view/range~Range#start range start}
|
|
654
|
+
* or a {@link module:engine/view/range~Range#start range end} placed at a given offset and returns its string representation.
|
|
655
|
+
*
|
|
656
|
+
* @private
|
|
657
|
+
* @param {module:engine/view/element~Element} element
|
|
658
|
+
* @param {Number} offset
|
|
659
|
+
*/
|
|
660
|
+
_stringifyElementRanges(element, offset) {
|
|
661
|
+
let start = '';
|
|
662
|
+
let end = '';
|
|
663
|
+
let collapsed = '';
|
|
664
|
+
for (const range of this.ranges) {
|
|
665
|
+
if (range.start.parent == element && range.start.offset === offset) {
|
|
666
|
+
if (range.isCollapsed) {
|
|
667
|
+
collapsed += ELEMENT_RANGE_START_TOKEN + ELEMENT_RANGE_END_TOKEN;
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
start += ELEMENT_RANGE_START_TOKEN;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (range.end.parent === element && range.end.offset === offset && !range.isCollapsed) {
|
|
674
|
+
end += ELEMENT_RANGE_END_TOKEN;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return end + collapsed + start;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Checks if a given {@link module:engine/view/element~Element Text node} has a
|
|
681
|
+
* {@link module:engine/view/range~Range#start range start} or a
|
|
682
|
+
* {@link module:engine/view/range~Range#start range end} placed somewhere inside. Returns a string representation of text
|
|
683
|
+
* with range delimiters placed inside.
|
|
684
|
+
*
|
|
685
|
+
* @private
|
|
686
|
+
* @param {module:engine/view/text~Text} node
|
|
687
|
+
*/
|
|
688
|
+
_stringifyTextRanges(node) {
|
|
689
|
+
const length = node.data.length;
|
|
690
|
+
const data = node.data.split('');
|
|
691
|
+
let rangeStartToken, rangeEndToken;
|
|
692
|
+
if (this.sameSelectionCharacters) {
|
|
693
|
+
rangeStartToken = ELEMENT_RANGE_START_TOKEN;
|
|
694
|
+
rangeEndToken = ELEMENT_RANGE_END_TOKEN;
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
rangeStartToken = TEXT_RANGE_START_TOKEN;
|
|
698
|
+
rangeEndToken = TEXT_RANGE_END_TOKEN;
|
|
699
|
+
}
|
|
700
|
+
// Add one more element for ranges ending after last character in text.
|
|
701
|
+
data[length] = '';
|
|
702
|
+
// Represent each letter as object with information about opening/closing ranges at each offset.
|
|
703
|
+
const result = data.map(letter => {
|
|
704
|
+
return {
|
|
705
|
+
letter,
|
|
706
|
+
start: '',
|
|
707
|
+
end: '',
|
|
708
|
+
collapsed: ''
|
|
709
|
+
};
|
|
710
|
+
});
|
|
711
|
+
for (const range of this.ranges) {
|
|
712
|
+
const start = range.start;
|
|
713
|
+
const end = range.end;
|
|
714
|
+
if (start.parent == node && start.offset >= 0 && start.offset <= length) {
|
|
715
|
+
if (range.isCollapsed) {
|
|
716
|
+
result[end.offset].collapsed += rangeStartToken + rangeEndToken;
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
result[start.offset].start += rangeStartToken;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if (end.parent == node && end.offset >= 0 && end.offset <= length && !range.isCollapsed) {
|
|
723
|
+
result[end.offset].end += rangeEndToken;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return result.map(item => item.end + item.collapsed + item.start + item.letter).join('');
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Converts the passed {@link module:engine/view/element~Element element} to an opening tag.
|
|
730
|
+
*
|
|
731
|
+
* Depending on the current configuration, the opening tag can be simple (`<a>`), contain a type prefix (`<container:p>`,
|
|
732
|
+
* `<attribute:a>` or `<empty:img>`), contain priority information ( `<attribute:a view-priority="20">` ),
|
|
733
|
+
* or contain element id ( `<attribute:span view-id="foo">` ). Element attributes will also be included
|
|
734
|
+
* (`<a href="https://ckeditor.com" name="foobar">`).
|
|
735
|
+
*
|
|
736
|
+
* @private
|
|
737
|
+
* @param {module:engine/view/element~Element} element
|
|
738
|
+
* @returns {String}
|
|
739
|
+
*/
|
|
740
|
+
_stringifyElementOpen(element) {
|
|
741
|
+
const priority = this._stringifyElementPriority(element);
|
|
742
|
+
const id = this._stringifyElementId(element);
|
|
743
|
+
const type = this._stringifyElementType(element);
|
|
744
|
+
const name = [type, element.name].filter(i => i !== '').join(':');
|
|
745
|
+
const attributes = this._stringifyElementAttributes(element);
|
|
746
|
+
const parts = [name, priority, id, attributes];
|
|
747
|
+
return `<${parts.filter(i => i !== '').join(' ')}>`;
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Converts the passed {@link module:engine/view/element~Element element} to a closing tag.
|
|
751
|
+
* Depending on the current configuration, the closing tag can be simple (`</a>`) or contain a type prefix (`</container:p>`,
|
|
752
|
+
* `</attribute:a>` or `</empty:img>`).
|
|
753
|
+
*
|
|
754
|
+
* @private
|
|
755
|
+
* @param {module:engine/view/element~Element} element
|
|
756
|
+
* @returns {String}
|
|
757
|
+
*/
|
|
758
|
+
_stringifyElementClose(element) {
|
|
759
|
+
const type = this._stringifyElementType(element);
|
|
760
|
+
const name = [type, element.name].filter(i => i !== '').join(':');
|
|
761
|
+
return `</${name}>`;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Converts the passed {@link module:engine/view/element~Element element's} type to its string representation
|
|
765
|
+
*
|
|
766
|
+
* Returns:
|
|
767
|
+
* * 'attribute' for {@link module:engine/view/attributeelement~AttributeElement attribute elements},
|
|
768
|
+
* * 'container' for {@link module:engine/view/containerelement~ContainerElement container elements},
|
|
769
|
+
* * 'empty' for {@link module:engine/view/emptyelement~EmptyElement empty elements},
|
|
770
|
+
* * 'ui' for {@link module:engine/view/uielement~UIElement UI elements},
|
|
771
|
+
* * 'raw' for {@link module:engine/view/rawelement~RawElement raw elements},
|
|
772
|
+
* * an empty string when the current configuration is preventing showing elements' types.
|
|
773
|
+
*
|
|
774
|
+
* @private
|
|
775
|
+
* @param {module:engine/view/element~Element} element
|
|
776
|
+
* @returns {String}
|
|
777
|
+
*/
|
|
778
|
+
_stringifyElementType(element) {
|
|
779
|
+
if (this.showType) {
|
|
780
|
+
for (const type in allowedTypes) {
|
|
781
|
+
if (element instanceof allowedTypes[type]) {
|
|
782
|
+
return type;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return '';
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Converts the passed {@link module:engine/view/element~Element element} to its priority representation.
|
|
790
|
+
*
|
|
791
|
+
* The priority string representation will be returned when the passed element is an instance of
|
|
792
|
+
* {@link module:engine/view/attributeelement~AttributeElement attribute element} and the current configuration allows to show the
|
|
793
|
+
* priority. Otherwise returns an empty string.
|
|
794
|
+
*
|
|
795
|
+
* @private
|
|
796
|
+
* @param {module:engine/view/element~Element} element
|
|
797
|
+
* @returns {String}
|
|
798
|
+
*/
|
|
799
|
+
_stringifyElementPriority(element) {
|
|
800
|
+
if (this.showPriority && element.is('attributeElement')) {
|
|
801
|
+
return `view-priority="${element.priority}"`;
|
|
802
|
+
}
|
|
803
|
+
return '';
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Converts the passed {@link module:engine/view/element~Element element} to its id representation.
|
|
807
|
+
*
|
|
808
|
+
* The id string representation will be returned when the passed element is an instance of
|
|
809
|
+
* {@link module:engine/view/attributeelement~AttributeElement attribute element}, the element has an id
|
|
810
|
+
* and the current configuration allows to show the id. Otherwise returns an empty string.
|
|
811
|
+
*
|
|
812
|
+
* @private
|
|
813
|
+
* @param {module:engine/view/element~Element} element
|
|
814
|
+
* @returns {String}
|
|
815
|
+
*/
|
|
816
|
+
_stringifyElementId(element) {
|
|
817
|
+
if (this.showAttributeElementId && element.is('attributeElement') && element.id) {
|
|
818
|
+
return `view-id="${element.id}"`;
|
|
819
|
+
}
|
|
820
|
+
return '';
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Converts the passed {@link module:engine/view/element~Element element} attributes to their string representation.
|
|
824
|
+
* If an element has no attributes, an empty string is returned.
|
|
825
|
+
*
|
|
826
|
+
* @private
|
|
827
|
+
* @param {module:engine/view/element~Element} element
|
|
828
|
+
* @returns {String}
|
|
829
|
+
*/
|
|
830
|
+
_stringifyElementAttributes(element) {
|
|
831
|
+
const attributes = [];
|
|
832
|
+
const keys = [...element.getAttributeKeys()].sort();
|
|
833
|
+
for (const attribute of keys) {
|
|
834
|
+
let attributeValue;
|
|
835
|
+
if (attribute === 'class') {
|
|
836
|
+
attributeValue = [...element.getClassNames()]
|
|
837
|
+
.sort()
|
|
838
|
+
.join(' ');
|
|
839
|
+
}
|
|
840
|
+
else if (attribute === 'style') {
|
|
841
|
+
attributeValue = [...element.getStyleNames()]
|
|
842
|
+
.sort()
|
|
843
|
+
.map(style => `${style}:${element.getStyle(style)}`)
|
|
844
|
+
.join(';');
|
|
845
|
+
}
|
|
846
|
+
else {
|
|
847
|
+
attributeValue = element.getAttribute(attribute);
|
|
848
|
+
}
|
|
849
|
+
attributes.push(`${attribute}="${attributeValue}"`);
|
|
850
|
+
}
|
|
851
|
+
return attributes.join(' ');
|
|
852
|
+
}
|
|
954
853
|
}
|
|
955
|
-
|
|
956
854
|
// Converts {@link module:engine/view/element~Element elements} to
|
|
957
855
|
// {@link module:engine/view/attributeelement~AttributeElement attribute elements},
|
|
958
856
|
// {@link module:engine/view/containerelement~ContainerElement container elements},
|
|
@@ -965,35 +863,31 @@ class ViewStringify {
|
|
|
965
863
|
// rootNode The root node to convert.
|
|
966
864
|
// @returns {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment|
|
|
967
865
|
// module:engine/view/text~Text} The root node of converted elements.
|
|
968
|
-
function _convertViewElements(
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
return rootNode;
|
|
866
|
+
function _convertViewElements(rootNode) {
|
|
867
|
+
if (rootNode.is('element') || rootNode.is('documentFragment')) {
|
|
868
|
+
// Convert element or leave document fragment.
|
|
869
|
+
const convertedElement = rootNode.is('documentFragment') ?
|
|
870
|
+
new ViewDocumentFragment(rootNode.document) :
|
|
871
|
+
_convertElement(rootNode.document, rootNode);
|
|
872
|
+
// Convert all child nodes.
|
|
873
|
+
// Cache the nodes in array. Otherwise, we would skip some nodes because during iteration we move nodes
|
|
874
|
+
// from `rootNode` to `convertedElement`. This would interfere with iteration.
|
|
875
|
+
for (const child of [...rootNode.getChildren()]) {
|
|
876
|
+
if (convertedElement.is('emptyElement')) {
|
|
877
|
+
throw new Error('Parse error - cannot parse inside EmptyElement.');
|
|
878
|
+
}
|
|
879
|
+
else if (convertedElement.is('uiElement')) {
|
|
880
|
+
throw new Error('Parse error - cannot parse inside UIElement.');
|
|
881
|
+
}
|
|
882
|
+
else if (convertedElement.is('rawElement')) {
|
|
883
|
+
throw new Error('Parse error - cannot parse inside RawElement.');
|
|
884
|
+
}
|
|
885
|
+
convertedElement._appendChild(_convertViewElements(child));
|
|
886
|
+
}
|
|
887
|
+
return convertedElement;
|
|
888
|
+
}
|
|
889
|
+
return rootNode;
|
|
995
890
|
}
|
|
996
|
-
|
|
997
891
|
// Converts an {@link module:engine/view/element~Element element} to
|
|
998
892
|
// {@link module:engine/view/attributeelement~AttributeElement attribute element},
|
|
999
893
|
// {@link module:engine/view/containerelement~ContainerElement container element},
|
|
@@ -1017,29 +911,24 @@ function _convertViewElements( rootNode ) {
|
|
|
1017
911
|
// module:engine/view/emptyelement~EmptyElement|module:engine/view/uielement~UIElement|
|
|
1018
912
|
// module:engine/view/containerelement~ContainerElement} A tree view
|
|
1019
913
|
// element converted according to its name.
|
|
1020
|
-
function _convertElement(
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
newElement._setAttribute( attributeKey, viewElement.getAttribute( attributeKey ) );
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
return newElement;
|
|
914
|
+
function _convertElement(viewDocument, viewElement) {
|
|
915
|
+
const info = _convertElementNameAndInfo(viewElement);
|
|
916
|
+
const ElementConstructor = allowedTypes[info.type];
|
|
917
|
+
const newElement = ElementConstructor ? new ElementConstructor(viewDocument, info.name) : new ViewElement(viewDocument, info.name);
|
|
918
|
+
if (newElement.is('attributeElement')) {
|
|
919
|
+
if (info.priority !== null) {
|
|
920
|
+
newElement._priority = info.priority;
|
|
921
|
+
}
|
|
922
|
+
if (info.id !== null) {
|
|
923
|
+
newElement._id = info.id;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
// Move attributes.
|
|
927
|
+
for (const attributeKey of viewElement.getAttributeKeys()) {
|
|
928
|
+
newElement._setAttribute(attributeKey, viewElement.getAttribute(attributeKey));
|
|
929
|
+
}
|
|
930
|
+
return newElement;
|
|
1041
931
|
}
|
|
1042
|
-
|
|
1043
932
|
// Converts the `view-priority` attribute and the {@link module:engine/view/element~Element#name element's name} information needed for
|
|
1044
933
|
// creating {@link module:engine/view/attributeelement~AttributeElement attribute element},
|
|
1045
934
|
// {@link module:engine/view/containerelement~ContainerElement container element},
|
|
@@ -1053,57 +942,47 @@ function _convertElement( viewDocument, viewElement ) {
|
|
|
1053
942
|
// @returns {String} info.name The parsed name of the element.
|
|
1054
943
|
// @returns {String|null} info.type The parsed type of the element. It can be `attribute`, `container` or `empty`.
|
|
1055
944
|
// returns {Number|null} info.priority The parsed priority of the element.
|
|
1056
|
-
function _convertElementNameAndInfo(
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
priority,
|
|
1082
|
-
id
|
|
1083
|
-
};
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
throw new Error( `Parse error - cannot parse element's name: ${ viewElement.name }.` );
|
|
945
|
+
function _convertElementNameAndInfo(viewElement) {
|
|
946
|
+
const parts = viewElement.name.split(':');
|
|
947
|
+
const priority = _convertPriority(viewElement.getAttribute('view-priority'));
|
|
948
|
+
const id = viewElement.hasAttribute('view-id') ? viewElement.getAttribute('view-id') : null;
|
|
949
|
+
viewElement._removeAttribute('view-priority');
|
|
950
|
+
viewElement._removeAttribute('view-id');
|
|
951
|
+
if (parts.length == 1) {
|
|
952
|
+
return {
|
|
953
|
+
name: parts[0],
|
|
954
|
+
type: priority !== null ? 'attribute' : null,
|
|
955
|
+
priority,
|
|
956
|
+
id
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
// Check if type and name: container:div.
|
|
960
|
+
const type = _convertType(parts[0]);
|
|
961
|
+
if (type) {
|
|
962
|
+
return {
|
|
963
|
+
name: parts[1],
|
|
964
|
+
type,
|
|
965
|
+
priority,
|
|
966
|
+
id
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
throw new Error(`Parse error - cannot parse element's name: ${viewElement.name}.`);
|
|
1087
970
|
}
|
|
1088
|
-
|
|
1089
971
|
// Checks if the element's type is allowed. Returns `attribute`, `container`, `empty` or `null`.
|
|
1090
972
|
//
|
|
1091
973
|
// @param {String} type
|
|
1092
974
|
// @returns {String|null}
|
|
1093
|
-
function _convertType(
|
|
1094
|
-
|
|
975
|
+
function _convertType(type) {
|
|
976
|
+
return type in allowedTypes ? type : null;
|
|
1095
977
|
}
|
|
1096
|
-
|
|
1097
978
|
// Checks if a given priority is allowed. Returns null if the priority cannot be converted.
|
|
1098
979
|
//
|
|
1099
980
|
// @param {String} priorityString
|
|
1100
981
|
// returns {Number|null}
|
|
1101
|
-
function _convertPriority(
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
return null;
|
|
982
|
+
function _convertPriority(priorityString) {
|
|
983
|
+
const priority = parseInt(priorityString, 10);
|
|
984
|
+
if (!isNaN(priority)) {
|
|
985
|
+
return priority;
|
|
986
|
+
}
|
|
987
|
+
return null;
|
|
1109
988
|
}
|