-
Notifications
You must be signed in to change notification settings - Fork 3.6k
/
nodelist.ts
192 lines (166 loc) · 5.26 KB
/
nodelist.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
/**
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @module engine/model/nodelist
*/
import Node from './node.js';
import { CKEditorError, spliceArray } from '@ckeditor/ckeditor5-utils';
/**
* Provides an interface to operate on a list of {@link module:engine/model/node~Node nodes}. `NodeList` is used internally
* in classes like {@link module:engine/model/element~Element Element}
* or {@link module:engine/model/documentfragment~DocumentFragment DocumentFragment}.
*/
export default class NodeList implements Iterable<Node> {
/**
* Nodes contained in this node list.
*/
private _nodes: Array<Node> = [];
/**
* Creates an empty node list.
*
* @internal
* @param nodes Nodes contained in this node list.
*/
constructor( nodes?: Iterable<Node> ) {
if ( nodes ) {
this._insertNodes( 0, nodes );
}
}
/**
* Iterable interface.
*
* Iterates over all nodes contained inside this node list.
*/
public [ Symbol.iterator ](): IterableIterator<Node> {
return this._nodes[ Symbol.iterator ]();
}
/**
* Number of nodes contained inside this node list.
*/
public get length(): number {
return this._nodes.length;
}
/**
* Sum of {@link module:engine/model/node~Node#offsetSize offset sizes} of all nodes contained inside this node list.
*/
public get maxOffset(): number {
return this._nodes.reduce( ( sum, node ) => sum + node.offsetSize, 0 );
}
/**
* Gets the node at the given index. Returns `null` if incorrect index was passed.
*/
public getNode( index: number ): Node | null {
return this._nodes[ index ] || null;
}
/**
* Returns an index of the given node. Returns `null` if given node is not inside this node list.
*/
public getNodeIndex( node: Node ): number | null {
const index = this._nodes.indexOf( node );
return index == -1 ? null : index;
}
/**
* Returns the starting offset of given node. Starting offset is equal to the sum of
* {@link module:engine/model/node~Node#offsetSize offset sizes} of all nodes that are before this node in this node list.
*/
public getNodeStartOffset( node: Node ): number | null {
const index = this.getNodeIndex( node );
return index === null ? null : this._nodes.slice( 0, index ).reduce( ( sum, node ) => sum + node.offsetSize, 0 );
}
/**
* Converts index to offset in node list.
*
* Returns starting offset of a node that is at given index. Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
* `model-nodelist-index-out-of-bounds` if given index is less than `0` or more than {@link #length}.
*/
public indexToOffset( index: number ): number {
if ( index == this._nodes.length ) {
return this.maxOffset;
}
const node = this._nodes[ index ];
if ( !node ) {
/**
* Given index cannot be found in the node list.
*
* @error model-nodelist-index-out-of-bounds
*/
throw new CKEditorError( 'model-nodelist-index-out-of-bounds', this );
}
return this.getNodeStartOffset( node )!;
}
/**
* Converts offset in node list to index.
*
* Returns index of a node that occupies given offset. Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
* `model-nodelist-offset-out-of-bounds` if given offset is less than `0` or more than {@link #maxOffset}.
*/
public offsetToIndex( offset: number ): number {
let totalOffset = 0;
for ( const node of this._nodes ) {
if ( offset >= totalOffset && offset < totalOffset + node.offsetSize ) {
return this.getNodeIndex( node )!;
}
totalOffset += node.offsetSize;
}
if ( totalOffset != offset ) {
/**
* Given offset cannot be found in the node list.
*
* @error model-nodelist-offset-out-of-bounds
* @param offset
* @param nodeList Stringified node list.
*/
throw new CKEditorError( 'model-nodelist-offset-out-of-bounds',
this,
{
offset,
nodeList: this
}
);
}
return this.length;
}
/**
* Inserts given nodes at given index.
*
* @internal
* @param index Index at which nodes should be inserted.
* @param nodes Nodes to be inserted.
*/
public _insertNodes( index: number, nodes: Iterable<Node> ): void {
// Validation.
for ( const node of nodes ) {
if ( !( node instanceof Node ) ) {
/**
* Trying to insert an object which is not a Node instance.
*
* @error model-nodelist-insertnodes-not-node
*/
throw new CKEditorError( 'model-nodelist-insertnodes-not-node', this );
}
}
this._nodes = spliceArray<Node>( this._nodes, Array.from( nodes ), index, 0 );
}
/**
* Removes one or more nodes starting at the given index.
*
* @internal
* @param indexStart Index of the first node to remove.
* @param howMany Number of nodes to remove.
* @returns Array containing removed nodes.
*/
public _removeNodes( indexStart: number, howMany: number = 1 ): Array<Node> {
return this._nodes.splice( indexStart, howMany );
}
/**
* Converts `NodeList` instance to an array containing nodes that were inserted in the node list. Nodes
* are also converted to their plain object representation.
*
* @returns `NodeList` instance converted to `Array`.
*/
public toJSON(): unknown {
return this._nodes.map( node => node.toJSON() );
}
}