pseudo-dom 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +496 -72
  2. package/browser/pseudo-dom.js +619 -284
  3. package/browser/pseudo-dom.min.js +1 -1
  4. package/dist/classes/PseudoHTMLDocument.d.ts +2 -2
  5. package/dist/classes/PseudoHTMLDocument.js +10 -21
  6. package/dist/classes/PseudoHTMLDocument.min.js +1 -1
  7. package/dist/classes/PseudoNodeList.js +14 -8
  8. package/dist/classes/PseudoNodeList.min.js +1 -1
  9. package/dist/factories/generateNode.d.ts +2 -10
  10. package/dist/factories/generateNode.js +9 -42
  11. package/dist/factories/generateNode.min.js +1 -1
  12. package/dist/functions/getParentNodes.d.ts +8 -6
  13. package/dist/functions/getParentNodes.js +16 -9
  14. package/dist/functions/getParentNodes.min.js +1 -1
  15. package/dist/functions/getParentNodesFromAttribute.d.ts +7 -7
  16. package/dist/functions/getParentNodesFromAttribute.js +21 -1
  17. package/dist/functions/getParentNodesFromAttribute.min.js +1 -1
  18. package/dist/functions.d.ts +1 -1
  19. package/dist/services/DocumentFragmentService.d.ts +9 -0
  20. package/dist/services/DocumentFragmentService.js +16 -1
  21. package/dist/services/DocumentFragmentService.min.js +1 -1
  22. package/dist/services/DocumentService.d.ts +8 -0
  23. package/dist/services/DocumentService.js +15 -1
  24. package/dist/services/DocumentService.min.js +1 -1
  25. package/dist/services/ElementService.d.ts +5 -6
  26. package/dist/services/ElementService.js +17 -13
  27. package/dist/services/ElementService.min.js +1 -1
  28. package/dist/services/NodeService.d.ts +31 -16
  29. package/dist/services/NodeService.js +90 -33
  30. package/dist/services/NodeService.min.js +1 -1
  31. package/package.json +2 -2
@@ -152,13 +152,6 @@
152
152
  2: [function (require, module, exports) {
153
153
  'use strict'
154
154
 
155
- const __importDefault = void 0 && (void 0).__importDefault || function (mod) {
156
- return mod && mod.__esModule
157
- ? mod
158
- : {
159
- default: mod
160
- }
161
- }
162
155
  Object.defineProperty(exports, '__esModule', {
163
156
  value: true
164
157
  })
@@ -172,8 +165,6 @@
172
165
  * @type {PseudoHTMLElement}
173
166
  */
174
167
  const HTMLElementService_1 = require('../services/HTMLElementService')
175
- const generateNodeList_1 = __importDefault(require('../factories/generateNodeList'))
176
- const TreeLinker_1 = require('collect-your-stuff/dist/collections/linked-tree-list/TreeLinker')
177
168
  /**
178
169
  * Simulate the behaviour of the HTMLDocument Class when there is no DOM available.
179
170
  * @author Joshua Heagle <joshuaheagle@gmail.com>
@@ -181,7 +172,7 @@
181
172
  * @augments PseudoHTMLElement
182
173
  * @property {PseudoHTMLElement} head - A reference to the Head child element
183
174
  * @property {PseudoHTMLElement} body - A reference to the Body child element
184
- * @property {function} createElement - Generate a new PseudoHTMLElement with parent of document
175
+ * @property {function} createElement - Generate a new PseudoHTMLElement (which is not in the document until it is appended)
185
176
  */
186
177
  class PseudoHTMLDocument extends HTMLElementService_1.HTMLElementService {
187
178
  /**
@@ -191,43 +182,41 @@
191
182
  constructor () {
192
183
  super()
193
184
  const html = new HTMLElementService_1.HTMLElementService({
194
- tagName: 'html',
195
- parent: this
185
+ tagName: 'html'
196
186
  })
187
+ this.appendChild(html)
197
188
  /**
198
189
  * Create document head element
199
190
  * @type {PseudoHTMLElement}
200
191
  */
201
192
  this.head = new HTMLElementService_1.HTMLElementService({
202
- tagName: 'head',
203
- parent: html
193
+ tagName: 'head'
204
194
  })
195
+ html.appendChild(this.head)
205
196
  /**
206
197
  * Create document body element
207
198
  * @type {PseudoHTMLElement}
208
199
  */
209
200
  this.body = new HTMLElementService_1.HTMLElementService({
210
- tagName: 'body',
211
- parent: html
201
+ tagName: 'body'
212
202
  })
213
- html.children = (0, generateNodeList_1.default)(TreeLinker_1.TreeLinker.fromArray([this.head, this.body]).head)
203
+ html.appendChild(this.body)
214
204
  }
215
205
 
216
206
  /**
217
- * Create and return a PseudoHTMLElement
207
+ * Create and return a PseudoHTMLElement, which is not added to the document until it is appended somewhere
218
208
  * @param {string} tagName - Tag Name is a string representing the type of Dom element this represents
219
209
  * @returns {PseudoHTMLElement}
220
210
  */
221
211
  createElement (tagName = 'div') {
222
- const returnElement = new HTMLElementService_1.HTMLElementService({
212
+ // Like the DOM, the new element is not added anywhere: it has no parent until it is appended
213
+ return new HTMLElementService_1.HTMLElementService({
223
214
  tagName
224
215
  })
225
- returnElement.parent = this
226
- return returnElement
227
216
  }
228
217
  }
229
218
  exports.default = PseudoHTMLDocument
230
- }, { '../factories/generateNodeList': 5, '../services/HTMLElementService': 14, 'collect-your-stuff/dist/collections/linked-tree-list/TreeLinker': 24 }],
219
+ }, { '../services/HTMLElementService': 14 }],
231
220
  3: [function (require, module, exports) {
232
221
  'use strict'
233
222
 
@@ -255,16 +244,22 @@
255
244
  * @returns {Iterator}
256
245
  */
257
246
  [Symbol.iterator] () {
258
- const linkers = super[Symbol.iterator]()
247
+ // Walk the nodes of this list only (the linkers of a child list have no children of their own)
248
+ let current = this.first
259
249
  return {
260
250
  next: () => {
261
- const result = linkers.next()
262
- return result.done
263
- ? result
264
- : {
265
- done: false,
266
- value: result.value.data
267
- }
251
+ if (current === null) {
252
+ return {
253
+ done: true,
254
+ value: undefined
255
+ }
256
+ }
257
+ const result = {
258
+ done: false,
259
+ value: current.data
260
+ }
261
+ current = current.next
262
+ return result
268
263
  }
269
264
  }
270
265
  }
@@ -380,31 +375,58 @@
380
375
  6: [function (require, module, exports) {
381
376
  'use strict'
382
377
 
383
- const __importDefault = void 0 && (void 0).__importDefault || function (mod) {
384
- return mod && mod.__esModule
385
- ? mod
386
- : {
387
- default: mod
388
- }
389
- }
390
378
  Object.defineProperty(exports, '__esModule', {
391
379
  value: true
392
380
  })
393
- const getParentNodesFromAttribute_1 = __importDefault(require('./getParentNodesFromAttribute'))
394
- const getParentNodes = node => (0, getParentNodesFromAttribute_1.default)('', false, node)
381
+ /**
382
+ * Get all of the ancestors of a node, starting with the root of the tree and ending with the node's own parent (the
383
+ * order in which an event travels down through them). A node which has no parent has no ancestors.
384
+ * @function getParentNodes
385
+ * @param {PseudoEventTarget|PseudoNode|*} node The node to find the ancestors of
386
+ * @returns {Array<PseudoNode>}
387
+ */
388
+ const getParentNodes = node => {
389
+ const parents = []
390
+ let current = node && node.parentNode ? node.parentNode : null
391
+ while (current) {
392
+ parents.unshift(current)
393
+ current = current.parentNode
394
+ }
395
+ return parents
396
+ }
395
397
  exports.default = getParentNodes
396
- }, { './getParentNodesFromAttribute': 7 }],
398
+ }, {}],
397
399
  7: [function (require, module, exports) {
398
400
  'use strict'
399
401
 
402
+ require('core-js/modules/esnext.iterator.constructor.js')
403
+ require('core-js/modules/esnext.iterator.filter.js')
404
+ const __importDefault = void 0 && (void 0).__importDefault || function (mod) {
405
+ return mod && mod.__esModule
406
+ ? mod
407
+ : {
408
+ default: mod
409
+ }
410
+ }
400
411
  Object.defineProperty(exports, '__esModule', {
401
412
  value: true
402
413
  })
414
+ const getParentNodes_1 = __importDefault(require('./getParentNodes'))
415
+ /**
416
+ * A selector function for retrieving existing parent PseudoNode from the given child item.
417
+ * This function will check all the parents starting from node, and scan the attributes
418
+ * property for matches. The return array contains all matching parent ancestors, starting with the root of the tree.
419
+ * @function getParentNodesFromAttribute
420
+ * @param {string} attr The property to compare on each ancestor (a missing property counts as false)
421
+ * @param {boolean|number|string} value The value the property must have
422
+ * @param {PseudoEventTarget|PseudoNode|*} node The node to find the matching ancestors of
423
+ * @returns {Array.<PseudoNode>}
424
+ */
403
425
  const getParentNodesFromAttribute = (attr, value, node) => {
404
- return Object.keys(node.parentNode).length ? (node.parentNode[attr] || false) === value ? getParentNodesFromAttribute(attr, value, node.parentNode).concat([node.parentNode]) : getParentNodesFromAttribute(attr, value, node.parentNode) : []
426
+ return (0, getParentNodes_1.default)(node).filter(parent => (parent[attr] || false) === value)
405
427
  }
406
428
  exports.default = getParentNodesFromAttribute
407
- }, {}],
429
+ }, { './getParentNodes': 6, 'core-js/modules/esnext.iterator.constructor.js': 130, 'core-js/modules/esnext.iterator.filter.js': 131 }],
408
430
  8: [function (require, module, exports) {
409
431
  'use strict'
410
432
 
@@ -679,8 +701,6 @@
679
701
  value: true
680
702
  })
681
703
  exports.ElementService = void 0
682
- const generateNodeList_1 = __importDefault(require('../factories/generateNodeList'))
683
- const TreeLinker_1 = require('collect-your-stuff/dist/collections/linked-tree-list/TreeLinker')
684
704
  const NodeService_1 = require('./NodeService')
685
705
  const AttrService_1 = require('./AttrService')
686
706
  const DOMTokenListService_1 = require('./DOMTokenListService')
@@ -706,8 +726,8 @@
706
726
  * @param {Object} [settings={}]
707
727
  * @param {string} [settings.tagName=''] The name of the tag this element represents
708
728
  * @param {Array<{name: string, value: *}>} [settings.attributes=[]] The attributes (also assigned as properties) to start with
709
- * @param {PseudoNode|null} [settings.parent=null] The parent node
710
- * @param {Array} [settings.children=[]] The values or nodes to start as children
729
+ * @param {PseudoNode|null} [settings.parent=null] The node to add this element to as its last child
730
+ * @param {Array<PseudoNode>} [settings.children=[]] The nodes to start as children
711
731
  * @constructor
712
732
  */
713
733
  constructor ({
@@ -718,8 +738,6 @@
718
738
  } = {}) {
719
739
  super()
720
740
  this.tokenList = new DOMTokenListService_1.DOMTokenListService()
721
- this.parent = parent
722
- this.children = (0, generateNodeList_1.default)(TreeLinker_1.TreeLinker.fromArray(children).head)
723
741
  this.tag = tagName
724
742
  this.attributeList = attributes.concat([{
725
743
  name: 'className',
@@ -740,6 +758,15 @@
740
758
  }) => {
741
759
  this[name] = value
742
760
  })
761
+ children.forEach(child => {
762
+ if (!child || typeof child.nodeType !== 'number') {
763
+ throw new TypeError('The children of an element must be nodes.')
764
+ }
765
+ this.appendChild(child)
766
+ })
767
+ if (parent) {
768
+ parent.appendChild(this)
769
+ }
743
770
  }
744
771
 
745
772
  get tagName () {
@@ -795,14 +822,13 @@
795
822
  }
796
823
 
797
824
  /**
798
- *
799
- * @param {PseudoNode|ElementService} childElement
800
- * @returns {PseudoNode}
825
+ * An element which is added as a child gets its default events (for example a submit button submits its form).
826
+ * @param {NodeService} child The node which was inserted
801
827
  */
802
- appendChild (childElement) {
803
- super.appendChild(childElement)
804
- childElement.applyDefaultEvent()
805
- return childElement
828
+ childInserted (child) {
829
+ if (typeof child.applyDefaultEvent === 'function') {
830
+ child.applyDefaultEvent()
831
+ }
806
832
  }
807
833
 
808
834
  /**
@@ -866,7 +892,7 @@
866
892
  }
867
893
  }
868
894
  exports.ElementService = ElementService
869
- }, { '../factories/generateNodeList': 5, '../functions/getParentNodesFromAttribute': 7, './AttrService': 9, './DOMTokenListService': 10, './NamedNodeMapService': 15, './NodeService': 16, 'collect-your-stuff/dist/collections/linked-tree-list/TreeLinker': 24, 'core-js/modules/esnext.iterator.constructor.js': 130, 'core-js/modules/esnext.iterator.find.js': 132, 'core-js/modules/esnext.iterator.for-each.js': 133, 'core-js/modules/esnext.iterator.map.js': 134, 'core-js/modules/esnext.iterator.some.js': 136 }],
895
+ }, { '../functions/getParentNodesFromAttribute': 7, './AttrService': 9, './DOMTokenListService': 10, './NamedNodeMapService': 15, './NodeService': 16, 'core-js/modules/esnext.iterator.constructor.js': 130, 'core-js/modules/esnext.iterator.find.js': 132, 'core-js/modules/esnext.iterator.for-each.js': 133, 'core-js/modules/esnext.iterator.map.js': 134, 'core-js/modules/esnext.iterator.some.js': 136 }],
870
896
  12: [function (require, module, exports) {
871
897
  'use strict'
872
898
 
@@ -1402,8 +1428,6 @@
1402
1428
  16: [function (require, module, exports) {
1403
1429
  'use strict'
1404
1430
 
1405
- require('core-js/modules/esnext.iterator.constructor.js')
1406
- require('core-js/modules/esnext.iterator.for-each.js')
1407
1431
  const __importDefault = void 0 && (void 0).__importDefault || function (mod) {
1408
1432
  return mod && mod.__esModule
1409
1433
  ? mod
@@ -1421,6 +1445,7 @@
1421
1445
  * @version 1.0.0
1422
1446
  */
1423
1447
  const generateNodeList_1 = __importDefault(require('../factories/generateNodeList'))
1448
+ const TreeLinker_1 = require('collect-your-stuff/dist/collections/linked-tree-list/TreeLinker')
1424
1449
  const EventTargetService_1 = __importDefault(require('./EventTargetService'))
1425
1450
  /**
1426
1451
  * Simulate the behaviour of the Node Class when there is no DOM available.
@@ -1443,8 +1468,7 @@
1443
1468
  this.nodeNameValue = ''
1444
1469
  this.children = (0, generateNodeList_1.default)()
1445
1470
  this.parent = null
1446
- this.next = null
1447
- this.prev = null
1471
+ this.listLinker = null
1448
1472
  }
1449
1473
 
1450
1474
  get baseURI () {
@@ -1468,7 +1492,7 @@
1468
1492
  }
1469
1493
 
1470
1494
  get nextSibling () {
1471
- return this.isConnected ? this.next : null
1495
+ return this.listLinker && this.listLinker.next ? this.listLinker.next.data : null
1472
1496
  }
1473
1497
 
1474
1498
  get nodeName () {
@@ -1500,7 +1524,7 @@
1500
1524
  }
1501
1525
 
1502
1526
  get previousSibling () {
1503
- return this.isConnected ? this.prev : null
1527
+ return this.listLinker && this.listLinker.prev ? this.listLinker.prev.data : null
1504
1528
  }
1505
1529
 
1506
1530
  get textContent () {
@@ -1512,15 +1536,20 @@
1512
1536
  }
1513
1537
 
1514
1538
  /**
1515
- *
1516
- * @param {PseudoNode} childNode
1517
- * @returns {PseudoNode}
1539
+ * Add a node as the last child of this node (a node which is already in a tree is moved).
1540
+ * @param {PseudoNode} childNode The node to add
1541
+ * @returns {PseudoNode} The added node
1518
1542
  */
1519
1543
  appendChild (childNode) {
1520
- this.children.append(childNode)
1521
- return childNode
1544
+ return this.insertBefore(childNode, null)
1522
1545
  }
1523
1546
 
1547
+ /**
1548
+ * Called each time a node has been inserted as a child of this node, so that nodes which need to react to children
1549
+ * (for example elements applying default events) can do so.
1550
+ * @param {NodeService} child The node which was inserted
1551
+ */
1552
+ childInserted (child) {}
1524
1553
  /**
1525
1554
  * Not implemented yet.
1526
1555
  * @throws {Error}
@@ -1538,11 +1567,19 @@
1538
1567
  }
1539
1568
 
1540
1569
  /**
1541
- * Not implemented yet.
1542
- * @throws {Error}
1570
+ * Check whether a node is this node or one of its descendants.
1571
+ * @param {PseudoNode|null} otherNode The node to look for
1572
+ * @returns {boolean}
1543
1573
  */
1544
1574
  contains (otherNode) {
1545
- throw new Error('NodeService.contains() is not implemented yet.')
1575
+ let current = otherNode
1576
+ while (current) {
1577
+ if (current === this) {
1578
+ return true
1579
+ }
1580
+ current = current.parentNode
1581
+ }
1582
+ return false
1546
1583
  }
1547
1584
 
1548
1585
  getRootNode (options = {
@@ -1556,11 +1593,44 @@
1556
1593
  }
1557
1594
 
1558
1595
  /**
1559
- * Not implemented yet.
1560
- * @throws {Error}
1596
+ * Insert a node as a child of this node, before the given child (or at the end when there is none). A node which is
1597
+ * already in a tree is moved, and the children of a document fragment are moved in order.
1598
+ * @param {PseudoNode} newNode The node to insert
1599
+ * @param {PseudoNode|null} [referenceNode=null] The child of this node to insert before, or null to insert at the end
1600
+ * @returns {PseudoNode} The inserted node
1601
+ * @throws {Error} When the reference node is not a child of this node, or the new node is this node or contains it
1561
1602
  */
1562
- insertBefore (newNode, referenceNode) {
1563
- throw new Error('NodeService.insertBefore() is not implemented yet.')
1603
+ insertBefore (newNode, referenceNode = null) {
1604
+ if (referenceNode !== null && referenceNode.parentNode !== this) {
1605
+ throw new Error('The node before which the new node is to be inserted is not a child of this node.')
1606
+ }
1607
+ if (newNode === referenceNode) {
1608
+ // Inserting a node before itself leaves it where it is
1609
+ return newNode
1610
+ }
1611
+ if (typeof newNode.contains === 'function' && newNode.contains(this)) {
1612
+ throw new Error('The new node cannot be inserted into itself or one of its own descendants.')
1613
+ }
1614
+ if (newNode.nodeType === NodeService.DOCUMENT_FRAGMENT_NODE) {
1615
+ // The children of a fragment are inserted (moved) in order, and the fragment is left empty
1616
+ while (newNode.firstChild) {
1617
+ this.insertBefore(newNode.firstChild, referenceNode)
1618
+ }
1619
+ return newNode
1620
+ }
1621
+ if (newNode.parentNode) {
1622
+ // A node can only be in one place, so it is moved from where it was
1623
+ newNode.parentNode.removeChild(newNode)
1624
+ }
1625
+ const linker = new TreeLinker_1.TreeLinker({
1626
+ data: newNode
1627
+ })
1628
+ this.children.insertBefore(referenceNode ? referenceNode.listLinker : null, linker)
1629
+ const inserted = newNode
1630
+ inserted.parent = this
1631
+ inserted.listLinker = linker
1632
+ this.childInserted(inserted)
1633
+ return newNode
1564
1634
  }
1565
1635
 
1566
1636
  isDefaultNamespace (namespaceURI) {
@@ -1589,31 +1659,44 @@
1589
1659
 
1590
1660
  normalize () {}
1591
1661
  /**
1592
- * Remove the given child from this node.
1593
- * @param {PseudoNode} childElement The child node, or its TreeLinker from the children list
1594
- * @returns {PseudoNode}
1662
+ * Remove a child from this node, it no longer has a parent or siblings afterwards.
1663
+ * @param {PseudoNode} childElement The child node to remove
1664
+ * @returns {PseudoNode} The removed node
1595
1665
  * @throws {Error} When the node is not a child of this node
1596
1666
  */
1597
1667
  removeChild (childElement) {
1598
- let found = null
1599
- this.children.forEach(linker => {
1600
- if (found === null && (linker === childElement || linker.data === childElement)) {
1601
- found = linker
1602
- }
1603
- })
1604
- if (found === null) {
1668
+ if (!childElement || childElement.parentNode !== this) {
1605
1669
  throw new Error('The node to be removed is not a child of this node.')
1606
1670
  }
1607
- this.children.remove(found)
1608
- return found.data
1671
+ const removed = childElement
1672
+ this.children.remove(removed.listLinker)
1673
+ removed.parent = null
1674
+ removed.listLinker = null
1675
+ return childElement
1609
1676
  }
1610
1677
 
1611
1678
  /**
1612
- * Not implemented yet.
1613
- * @throws {Error}
1679
+ * Replace a child of this node with another node (which is moved if it is already in a tree).
1680
+ * @param {PseudoNode} newChild The node which takes the place
1681
+ * @param {PseudoNode} oldChild The child of this node to replace
1682
+ * @returns {PseudoNode} The replaced node
1683
+ * @throws {Error} When the old node is not a child of this node
1614
1684
  */
1615
1685
  replaceChild (newChild, oldChild) {
1616
- throw new Error('NodeService.replaceChild() is not implemented yet.')
1686
+ if (!oldChild || oldChild.parentNode !== this) {
1687
+ throw new Error('The node to be replaced is not a child of this node.')
1688
+ }
1689
+ if (newChild === oldChild) {
1690
+ return oldChild
1691
+ }
1692
+ // The new node goes where the old one was, which is before the old node's next sibling (unless that is the new node)
1693
+ let reference = oldChild.nextSibling
1694
+ if (reference === newChild) {
1695
+ reference = newChild.nextSibling
1696
+ }
1697
+ this.removeChild(oldChild)
1698
+ this.insertBefore(newChild, reference)
1699
+ return oldChild
1617
1700
  }
1618
1701
  }
1619
1702
  exports.NodeService = NodeService
@@ -1630,7 +1713,7 @@
1630
1713
  NodeService.DOCUMENT_TYPE_NODE = 10
1631
1714
  NodeService.DOCUMENT_FRAGMENT_NODE = 11
1632
1715
  NodeService.NOTATION_NODE = 12
1633
- }, { '../factories/generateNodeList': 5, './EventTargetService': 13, 'core-js/modules/esnext.iterator.constructor.js': 130, 'core-js/modules/esnext.iterator.for-each.js': 133 }],
1716
+ }, { '../factories/generateNodeList': 5, './EventTargetService': 13, 'collect-your-stuff/dist/collections/linked-tree-list/TreeLinker': 24 }],
1634
1717
  17: [function (require, module, exports) {
1635
1718
  'use strict'
1636
1719
 
@@ -1649,7 +1732,9 @@
1649
1732
  * @param {*} [data=null] The data to be stored in this element.
1650
1733
  */
1651
1734
  constructor (data = null) {
1735
+ /** The class used to create this instance, so that it can be recognized as valid without an instanceof check. */
1652
1736
  this.classType = ArrayElement
1737
+ /** The data stored in this element. */
1653
1738
  this.data = null
1654
1739
  this.data = data
1655
1740
  }
@@ -1662,8 +1747,8 @@
1662
1747
  */
1663
1748
  exports.ArrayElement = ArrayElement
1664
1749
  ArrayElement.make = (element, classType = ArrayElement) => {
1665
- if (typeof element !== 'object') {
1666
- // It is not an object, so instantiate the Element with element as the data
1750
+ if (element === null || typeof element !== 'object') {
1751
+ // It is not an object (or it is null), so instantiate the Element with element as the data
1667
1752
  return new classType(element)
1668
1753
  }
1669
1754
  if (element.classType) {
@@ -1719,14 +1804,32 @@
1719
1804
  class Arrayable {
1720
1805
  /**
1721
1806
  * Create the new Arrayable instance, configure the Arrayable class.
1807
+ * @param {ArrayElement} [elementClass=ArrayElement] The class used to wrap given data as elements.
1722
1808
  */
1723
1809
  constructor (elementClass = _ArrayElement.ArrayElement) {
1810
+ /** The class used to create this instance, so that it can be recognized as valid without an instanceof check. */
1724
1811
  this.classType = Arrayable
1812
+ /** The array which stores the elements of this Arrayable. */
1725
1813
  this.innerList = []
1814
+ /** Whether the inner list has been initialized (it can only be initialized once). */
1726
1815
  this.initialized = false
1727
1816
  this.elementClass = elementClass
1728
1817
  }
1729
1818
 
1819
+ /**
1820
+ * Find the position of an element which must be in this list.
1821
+ * @param {ArrayElement} node The element to find
1822
+ * @returns {number}
1823
+ * @throws {Error} When the element is not in this list
1824
+ */
1825
+ indexOfElement (node) {
1826
+ const index = this.innerList.indexOf(node)
1827
+ if (index < 0) {
1828
+ throw new Error('The reference element is not in this list.')
1829
+ }
1830
+ return index
1831
+ }
1832
+
1730
1833
  /**
1731
1834
  * Initialize the inner list, should only run once.
1732
1835
  * @param {Array<ArrayElement>} initialList Give the array of elements to start in this Arrayable.
@@ -1743,7 +1846,7 @@
1743
1846
  }
1744
1847
 
1745
1848
  /**
1746
- * Retrieve a copy of the innerList used.
1849
+ * Retrieve the innerList used (the list itself, not a copy).
1747
1850
  * @returns {Array<ArrayElement>}
1748
1851
  */
1749
1852
  get list () {
@@ -1752,18 +1855,18 @@
1752
1855
 
1753
1856
  /**
1754
1857
  * Retrieve the first Element from the Arrayable
1755
- * @returns {ArrayElement}
1858
+ * @returns {ArrayElement|null} The first element, or null when the Arrayable is empty
1756
1859
  */
1757
1860
  get first () {
1758
- return this.innerList[0]
1861
+ return this.length ? this.innerList[0] : null
1759
1862
  }
1760
1863
 
1761
1864
  /**
1762
1865
  * Retrieve the last Element from the Arrayable
1763
- * @returns {ArrayElement}
1866
+ * @returns {ArrayElement|null} The last element, or null when the Arrayable is empty
1764
1867
  */
1765
1868
  get last () {
1766
- return this.innerList[this.length - 1]
1869
+ return this.length ? this.innerList[this.length - 1] : null
1767
1870
  }
1768
1871
 
1769
1872
  /**
@@ -1776,25 +1879,29 @@
1776
1879
 
1777
1880
  /**
1778
1881
  * Insert a new node (or data) after a node.
1779
- * @param {ArrayElement|*} node The existing node as reference
1882
+ * @param {ArrayElement|null} node The existing node as reference, or null to insert at the start of the list
1780
1883
  * @param {ArrayElement|*} newNode The new node to go after the existing node
1781
1884
  * @returns {Arrayable}
1885
+ * @throws {Error} When the reference node is not in this list
1782
1886
  */
1783
1887
  insertAfter (node, newNode) {
1784
- const insertAt = this.innerList.indexOf(node)
1785
- this.innerList.splice(insertAt + 1, 0, this.elementClass.make(newNode))
1888
+ // With no reference element, the new one goes after nothing: at the start of the list
1889
+ const insertAt = node === null || typeof node === 'undefined' ? -1 : this.indexOfElement(node)
1890
+ this.innerList.splice(insertAt + 1, 0, this.elementClass.make(newNode, this.elementClass))
1786
1891
  return this
1787
1892
  }
1788
1893
 
1789
1894
  /**
1790
1895
  * Insert a new node (or data) before a node.
1791
- * @param {ArrayElement|*} node The existing node as reference
1896
+ * @param {ArrayElement|null} node The existing node as reference, or null to insert at the end of the list
1792
1897
  * @param {ArrayElement|*} newNode The new node to go before the existing node
1793
1898
  * @returns {Arrayable}
1899
+ * @throws {Error} When the reference node is not in this list
1794
1900
  */
1795
1901
  insertBefore (node, newNode) {
1796
- const insertAt = this.innerList.indexOf(node)
1797
- this.innerList.splice(insertAt, 0, this.elementClass.make(newNode))
1902
+ // With no reference element, the new one goes before nothing: at the end of the list
1903
+ const insertAt = node === null || typeof node === 'undefined' ? this.length : this.indexOfElement(node)
1904
+ this.innerList.splice(insertAt, 0, this.elementClass.make(newNode, this.elementClass))
1798
1905
  return this
1799
1906
  }
1800
1907
 
@@ -1805,6 +1912,11 @@
1805
1912
  * @returns {Arrayable}
1806
1913
  */
1807
1914
  append (node, after = this.last) {
1915
+ if (after === this.last) {
1916
+ // Adding to the end does not need to search for where that is
1917
+ this.innerList.push(this.elementClass.make(node, this.elementClass))
1918
+ return this
1919
+ }
1808
1920
  return this.insertAfter(after, node)
1809
1921
  }
1810
1922
 
@@ -1815,16 +1927,24 @@
1815
1927
  * @returns {Arrayable}
1816
1928
  */
1817
1929
  prepend (node, before = this.first) {
1930
+ if (before === this.first) {
1931
+ // Adding to the start does not need to search for where that is
1932
+ this.innerList.unshift(this.elementClass.make(node, this.elementClass))
1933
+ return this
1934
+ }
1818
1935
  return this.insertBefore(before, node)
1819
1936
  }
1820
1937
 
1821
1938
  /**
1822
1939
  * Remove an element from this arrayable.
1823
1940
  * @param {ArrayElement} node The node we wish to remove (and it will be returned after removal)
1824
- * @return {ArrayElement}
1941
+ * @return {ArrayElement|null} The removed node, or null when it was not in this list (nothing is removed)
1825
1942
  */
1826
1943
  remove (node) {
1827
1944
  const deleteAt = this.innerList.indexOf(node)
1945
+ if (deleteAt < 0) {
1946
+ return null
1947
+ }
1828
1948
  this.innerList.splice(deleteAt, 1)
1829
1949
  return node
1830
1950
  }
@@ -1904,7 +2024,7 @@
1904
2024
  class DoubleLinker {
1905
2025
  /**
1906
2026
  * Create the new DoubleLinker instance, provide the data and optionally the next and prev references.
1907
- * @param {Object} [nodeData={}]
2027
+ * @param {Object} [nodeData={}] The settings for the new linker.
1908
2028
  * @param {*} [nodeData.data=null] The data to be stored in this linker
1909
2029
  * @param {DoubleLinker|null} [nodeData.next=null] The reference to the next linker if any
1910
2030
  * @param {DoubleLinker|null} [nodeData.prev=null] The reference to the previous linker if any
@@ -1914,9 +2034,13 @@
1914
2034
  next = null,
1915
2035
  prev = null
1916
2036
  } = {}) {
2037
+ /** The class used to create this instance, so that it can be recognized as valid without an instanceof check. */
1917
2038
  this.classType = DoubleLinker
2039
+ /** The data stored in this linker. */
1918
2040
  this.data = null
2041
+ /** The linker after this one, or null when this is the last. */
1919
2042
  this.next = null
2043
+ /** The linker before this one, or null when this is the first. */
1920
2044
  this.prev = null
1921
2045
  this.data = data
1922
2046
  this.next = next
@@ -1984,11 +2108,19 @@
1984
2108
  class DoublyLinkedList {
1985
2109
  /**
1986
2110
  * Create the new DoublyLinkedList instance.
2111
+ * @param {DoubleLinker} [linkerClass=DoubleLinker] The class used to wrap given data as linkers.
1987
2112
  */
1988
2113
  constructor (linkerClass = _DoubleLinker.DoubleLinker) {
2114
+ /** The class used to create this instance, so that it can be recognized as valid without an instanceof check. */
1989
2115
  this.classType = DoublyLinkedList
2116
+ /** A linker of the list (null when the list is empty); the head is found by walking back from it. */
1990
2117
  this.innerList = null
2118
+ /** Whether the inner list has been initialized (it can only be initialized once). */
1991
2119
  this.initialized = false
2120
+ /** The last linker, remembered so that adding to the end does not need to walk the whole list (null when not known yet). */
2121
+ this.tailCache = null
2122
+ /** The number of linkers, kept up to date by the list's own methods so that the length does not need to walk the whole list (null when not known yet). */
2123
+ this.countCache = null
1992
2124
  this.linkerClass = linkerClass
1993
2125
  }
1994
2126
 
@@ -1998,11 +2130,12 @@
1998
2130
  * @return {DoublyLinkedList}
1999
2131
  */
2000
2132
  initialize (initialList) {
2133
+ // Borrowed from LinkedList, which types its return as a LinkedList although it returns whatever list called it
2001
2134
  return _LinkedList.LinkedList.prototype.initialize.call(this, initialList)
2002
2135
  }
2003
2136
 
2004
2137
  /**
2005
- * Retrieve a copy of the innerList used.
2138
+ * Retrieve the innerList used (the list itself, not a copy).
2006
2139
  * @returns {DoubleLinker}
2007
2140
  */
2008
2141
  get list () {
@@ -2014,91 +2147,122 @@
2014
2147
  * @returns {DoubleLinker}
2015
2148
  */
2016
2149
  get first () {
2017
- return this.reset()
2150
+ let head = this.innerList
2151
+ if (head === null) {
2152
+ return null
2153
+ }
2154
+ // innerList is normally the head already, walking back also finds anything linked on before it outside of this list
2155
+ while (head.prev !== null) {
2156
+ head = head.prev
2157
+ }
2158
+ this.innerList = head
2159
+ return head
2018
2160
  }
2019
2161
 
2020
2162
  /**
2021
- * Retrieve the last DoubleLinker in the list.
2163
+ * Retrieve the last DoubleLinker in the list. The end is remembered, so this does not walk the list.
2022
2164
  * @returns {DoubleLinker}
2023
2165
  */
2024
2166
  get last () {
2025
- let tail = this.innerList
2026
- if (tail === null) {
2167
+ if (this.innerList === null) {
2027
2168
  return null
2028
2169
  }
2029
- let next = tail.next
2030
- while (next !== null) {
2031
- tail = next
2032
- next = tail.next
2170
+ let tail = this.tailCache !== null ? this.tailCache : this.innerList
2171
+ // The remembered tail is normally the end already, walking on from it also finds anything linked on outside of this list
2172
+ while (tail.next !== null) {
2173
+ tail = tail.next
2033
2174
  }
2175
+ this.tailCache = tail
2034
2176
  return tail
2035
2177
  }
2036
2178
 
2037
2179
  /**
2038
- * Return the length of the list.
2180
+ * Return the length of the list. It is kept up to date by the list's own methods, so this does not walk the list
2181
+ * (call reset() after linkers were changed directly).
2039
2182
  * @returns {number}
2040
2183
  */
2041
2184
  get length () {
2042
- let current = this.first
2043
- let length = 0
2044
- while (current !== null) {
2045
- ++length
2046
- current = current.next
2185
+ if (this.countCache === null) {
2186
+ this.reset()
2047
2187
  }
2048
- return length
2188
+ return this.countCache
2049
2189
  }
2050
2190
 
2051
2191
  /**
2052
2192
  * Insert a new node (or data) after a node.
2053
- * @param {DoubleLinker|*} node The existing node as reference
2193
+ * @param {DoubleLinker|*} node The existing node as reference (which must be in this list, this is not checked), or null to insert at the start of the list
2054
2194
  * @param {DoubleLinker|*} newNode The new node to go after the existing node
2055
2195
  * @returns {DoublyLinkedList}
2056
2196
  */
2057
2197
  insertAfter (node, newNode) {
2058
- newNode = this.linkerClass.make(newNode)
2059
- if (node !== null) {
2198
+ newNode = this.linkerClass.make(newNode, this.linkerClass)
2199
+ if (node === null || typeof node === 'undefined') {
2200
+ // After nothing means at the start of the list
2201
+ const head = this.first
2202
+ newNode.prev = null
2203
+ newNode.next = head
2204
+ if (head) {
2205
+ head.prev = newNode
2206
+ } else {
2207
+ this.tailCache = newNode
2208
+ }
2209
+ this.innerList = newNode
2210
+ } else {
2060
2211
  // Ensure the next reference of this node is assigned to the new node
2061
2212
  newNode.next = node.next
2062
2213
  // Ensure this node is assigned as the prev reference of the new node
2063
2214
  newNode.prev = node
2064
2215
  // Then set this node's next reference to the new node
2065
2216
  node.next = newNode
2217
+ if (newNode.next) {
2218
+ // Update the next reference to ensure circular reference for prev points to the new node
2219
+ newNode.next.prev = newNode
2220
+ } else {
2221
+ this.tailCache = newNode
2222
+ }
2066
2223
  }
2067
- if (newNode.next) {
2068
- // Update the next reference to ensure circular reference for prev points to the new node
2069
- newNode.next.prev = newNode
2224
+ if (this.countCache !== null) {
2225
+ ++this.countCache
2070
2226
  }
2071
- if (!this.length) {
2072
- this.innerList = newNode
2073
- }
2074
- this.reset()
2075
2227
  return this
2076
2228
  }
2077
2229
 
2078
2230
  /**
2079
2231
  * Insert a new node (or data) before a node.
2080
- * @param {DoubleLinker|*} node The existing node as reference
2232
+ * @param {DoubleLinker|*} node The existing node as reference (which must be in this list, this is not checked), or null to insert at the end of the list
2081
2233
  * @param {DoubleLinker|*} newNode The new node to go before the existing node
2082
2234
  * @returns {DoublyLinkedList}
2083
2235
  */
2084
2236
  insertBefore (node, newNode) {
2085
- newNode = this.linkerClass.make(newNode)
2086
- if (node !== null) {
2237
+ newNode = this.linkerClass.make(newNode, this.linkerClass)
2238
+ if (node === null || typeof node === 'undefined') {
2239
+ // Before nothing means at the end of the list
2240
+ const tail = this.last
2241
+ newNode.next = null
2242
+ newNode.prev = tail
2243
+ if (tail === null) {
2244
+ this.innerList = newNode
2245
+ } else {
2246
+ tail.next = newNode
2247
+ }
2248
+ this.tailCache = newNode
2249
+ } else {
2087
2250
  // The new node will reference this prev node as prev
2088
2251
  newNode.prev = node.prev
2089
2252
  // The new node will reference this node as next
2090
2253
  newNode.next = node
2091
2254
  // This prev will reference the new node
2092
2255
  node.prev = newNode
2256
+ if (newNode.prev) {
2257
+ // Update the prev reference to ensure circular reference for next points to the new node
2258
+ newNode.prev.next = newNode
2259
+ } else {
2260
+ this.innerList = newNode
2261
+ }
2093
2262
  }
2094
- if (newNode.prev) {
2095
- // Update the prev reference to ensure circular reference for next points to the new node
2096
- newNode.prev.next = newNode
2097
- }
2098
- if (!this.length) {
2099
- this.innerList = newNode
2263
+ if (this.countCache !== null) {
2264
+ ++this.countCache
2100
2265
  }
2101
- this.reset()
2102
2266
  return this
2103
2267
  }
2104
2268
 
@@ -2128,7 +2292,7 @@
2128
2292
  * @return {DoubleLinker}
2129
2293
  */
2130
2294
  remove (node) {
2131
- if (node === null) {
2295
+ if (node === null || typeof node === 'undefined') {
2132
2296
  return null
2133
2297
  }
2134
2298
  if (node.prev) {
@@ -2144,35 +2308,47 @@
2144
2308
  if (this.innerList === node) {
2145
2309
  this.innerList = node.next || node.prev || null
2146
2310
  }
2147
- // Update head reference
2148
- this.reset()
2311
+ if (this.tailCache === node) {
2312
+ this.tailCache = node.prev
2313
+ }
2314
+ if (this.innerList === null) {
2315
+ this.tailCache = null
2316
+ }
2317
+ if (this.countCache !== null) {
2318
+ --this.countCache
2319
+ }
2149
2320
  return node
2150
2321
  }
2151
2322
 
2152
2323
  /**
2153
- * Refresh all references and return head reference.
2154
- * @return {DoubleLinker}
2324
+ * Refresh all references (the head, the end and the length) by walking the list once, and return the head. The list's
2325
+ * own methods keep these up to date, so this is only needed after linkers were changed directly.
2326
+ * @return {DoubleLinker|null}
2155
2327
  */
2156
2328
  reset () {
2157
2329
  // Start at the pointer for the list
2158
2330
  let pointer = this.innerList
2159
2331
  if (pointer === null) {
2332
+ this.countCache = 0
2333
+ this.tailCache = null
2160
2334
  return null
2161
2335
  }
2162
- let next = pointer.next
2163
- // Follow references till the end
2164
- while (next !== null) {
2165
- pointer = next
2166
- next = pointer.next
2167
- }
2168
- let prev = pointer.prev
2169
- // From final reference, follow references back to the beginning
2170
- while (prev !== null) {
2171
- pointer = prev
2172
- prev = pointer.prev
2336
+ // Follow references back to the beginning
2337
+ while (pointer.prev !== null) {
2338
+ pointer = pointer.prev
2173
2339
  }
2174
- // All the live references should have been found, and we are pointing to the true head
2340
+ // We are pointing to the true head, now count along to the end to find the tail and the length
2175
2341
  this.innerList = pointer
2342
+ let count = 0
2343
+ let tail = pointer
2344
+ let current = pointer
2345
+ while (current !== null) {
2346
+ ++count
2347
+ tail = current
2348
+ current = current.next
2349
+ }
2350
+ this.countCache = count
2351
+ this.tailCache = tail
2176
2352
  return pointer
2177
2353
  }
2178
2354
 
@@ -2208,6 +2384,7 @@
2208
2384
  * Be able to run forEach on this DoublyLinkedList to iterate over the DoubleLinker Items.
2209
2385
  * @param {forEachCallback} callback The function to call for-each double linker
2210
2386
  * @param {DoublyLinkedList} thisArg Optional, 'this' reference
2387
+ * @return {DoublyLinkedList} The list which was iterated.
2211
2388
  */
2212
2389
  forEach (callback, thisArg = this) {
2213
2390
  return _LinkedList.LinkedList.prototype.forEach.call(this, callback, thisArg)
@@ -2251,11 +2428,19 @@
2251
2428
  class LinkedList {
2252
2429
  /**
2253
2430
  * Create the new LinkedList instance.
2431
+ * @param {Linker} [linkerClass=Linker] The class used to wrap given data as linkers.
2254
2432
  */
2255
2433
  constructor (linkerClass = _Linker.Linker) {
2434
+ /** The class used to create this instance, so that it can be recognized as valid without an instanceof check. */
2256
2435
  this.classType = LinkedList
2436
+ /** The first linker of the list (null when the list is empty), from which the whole list is reached. */
2257
2437
  this.innerList = null
2438
+ /** Whether the inner list has been initialized (it can only be initialized once). */
2258
2439
  this.initialized = false
2440
+ /** The last linker, remembered so that adding to the end does not need to walk the whole list (null when not known yet). */
2441
+ this.tailCache = null
2442
+ /** The number of linkers, kept up to date by the list's own methods so that the length does not need to walk the whole list (null when not known yet). */
2443
+ this.countCache = null
2259
2444
  this.linkerClass = linkerClass
2260
2445
  }
2261
2446
 
@@ -2265,11 +2450,12 @@
2265
2450
  * @return {LinkedList}
2266
2451
  */
2267
2452
  initialize (initialList) {
2453
+ // Borrowed from Arrayable, which types its return as an Arrayable although it returns whatever list called it
2268
2454
  return _Arrayable.Arrayable.prototype.initialize.call(this, initialList)
2269
2455
  }
2270
2456
 
2271
2457
  /**
2272
- * Retrieve a copy of the innerList used.
2458
+ * Retrieve the innerList used (the list itself, not a copy).
2273
2459
  * @returns {Linker}
2274
2460
  */
2275
2461
  get list () {
@@ -2285,78 +2471,100 @@
2285
2471
  }
2286
2472
 
2287
2473
  /**
2288
- * Retrieve the last Linker in the list.
2474
+ * Retrieve the last Linker in the list. The end is remembered, so this does not walk the list.
2289
2475
  * @returns {Linker}
2290
2476
  */
2291
2477
  get last () {
2292
- let tail = this.innerList
2293
- if (tail === null) {
2478
+ if (this.innerList === null) {
2294
2479
  return null
2295
2480
  }
2296
- let next = tail.next
2297
- while (next !== null) {
2298
- tail = next
2299
- next = tail.next
2481
+ let tail = this.tailCache !== null ? this.tailCache : this.innerList
2482
+ // The remembered tail is normally the end already, walking on from it also finds anything linked on outside of this list
2483
+ while (tail.next !== null) {
2484
+ tail = tail.next
2300
2485
  }
2486
+ this.tailCache = tail
2301
2487
  return tail
2302
2488
  }
2303
2489
 
2304
2490
  /**
2305
- * Return the length of the list.
2491
+ * Return the length of the list. It is kept up to date by the list's own methods, so this does not walk the list
2492
+ * (call reset() after linkers were changed directly).
2306
2493
  * @returns {number}
2307
2494
  */
2308
2495
  get length () {
2309
- let current = this.first
2310
- let length = 0
2311
- while (current !== null) {
2312
- ++length
2313
- current = current.next
2496
+ if (this.countCache === null) {
2497
+ this.reset()
2314
2498
  }
2315
- return length
2499
+ return this.countCache
2316
2500
  }
2317
2501
 
2318
2502
  /**
2319
2503
  * Insert a new node (or data) after a node.
2320
- * @param {Linker|*} node The existing node as reference
2504
+ * @param {Linker|*} node The existing node as reference, or null to insert at the start of the list
2321
2505
  * @param {Linker|*} newNode The new node to go after the existing node
2322
2506
  * @returns {LinkedList}
2323
2507
  */
2324
2508
  insertAfter (node, newNode) {
2325
- newNode = this.linkerClass.make(newNode)
2326
- if (node !== null) {
2327
- // Ensure the next reference of this node is assigned to the new node
2509
+ newNode = this.linkerClass.make(newNode, this.linkerClass)
2510
+ if (node === null || typeof node === 'undefined') {
2511
+ // After nothing means at the start of the list
2512
+ newNode.next = this.innerList
2513
+ if (this.innerList === null) {
2514
+ this.tailCache = newNode
2515
+ }
2516
+ this.innerList = newNode
2517
+ } else {
2328
2518
  newNode.next = node.next
2329
- // Then set this node's next reference to the new node
2330
2519
  node.next = newNode
2520
+ if (newNode.next === null) {
2521
+ this.tailCache = newNode
2522
+ }
2331
2523
  }
2332
- if (!this.length) {
2333
- this.innerList = newNode
2524
+ if (this.countCache !== null) {
2525
+ ++this.countCache
2334
2526
  }
2335
2527
  return this
2336
2528
  }
2337
2529
 
2338
2530
  /**
2339
2531
  * Insert a new node (or data) before a node.
2340
- * @param {Linker|*} node The existing node as reference
2532
+ * @param {Linker|*} node The existing node as reference, or null to insert at the end of the list
2341
2533
  * @param {Linker|*} newNode The new node to go before the existing node
2342
2534
  * @returns {LinkedList}
2535
+ * @throws {Error} When the reference node is not in this list
2343
2536
  */
2344
2537
  insertBefore (node, newNode) {
2345
- newNode = this.linkerClass.make(newNode)
2346
- let prevNode = null
2347
- let currentNode = this.first
2348
- while (currentNode !== node) {
2349
- prevNode = currentNode
2350
- currentNode = currentNode.next
2351
- }
2352
- // The new node will reference this node as next
2353
- newNode.next = node
2354
- if (prevNode) {
2355
- // Ensure the next reference of the previous node is assigned to the new node
2356
- prevNode.next = newNode
2538
+ newNode = this.linkerClass.make(newNode, this.linkerClass)
2539
+ if (node === null || typeof node === 'undefined') {
2540
+ // Before nothing means at the end of the list
2541
+ const tail = this.last
2542
+ newNode.next = null
2543
+ if (tail === null) {
2544
+ this.innerList = newNode
2545
+ } else {
2546
+ tail.next = newNode
2547
+ }
2548
+ this.tailCache = newNode
2549
+ } else {
2550
+ let prevNode = null
2551
+ let currentNode = this.first
2552
+ while (currentNode !== null && currentNode !== node) {
2553
+ prevNode = currentNode
2554
+ currentNode = currentNode.next
2555
+ }
2556
+ if (currentNode === null) {
2557
+ throw new Error('The reference node is not in this list.')
2558
+ }
2559
+ newNode.next = node
2560
+ if (prevNode) {
2561
+ prevNode.next = newNode
2562
+ } else {
2563
+ this.innerList = newNode
2564
+ }
2357
2565
  }
2358
- if (node === this.first || node === null) {
2359
- this.innerList = newNode
2566
+ if (this.countCache !== null) {
2567
+ ++this.countCache
2360
2568
  }
2361
2569
  return this
2362
2570
  }
@@ -2384,26 +2592,58 @@
2384
2592
  /**
2385
2593
  * Remove a linker from this linked list.
2386
2594
  * @param {Linker} node The node we wish to remove (and it will be returned after removal)
2387
- * @return {Linker}
2595
+ * @return {Linker|null} The removed node, or null when it was not in this list (nothing is removed)
2388
2596
  */
2389
2597
  remove (node) {
2598
+ if (node === null || typeof node === 'undefined') {
2599
+ return null
2600
+ }
2390
2601
  let prevNode = null
2391
2602
  let currentNode = this.first
2392
- while (currentNode !== node) {
2603
+ while (currentNode !== null && currentNode !== node) {
2393
2604
  prevNode = currentNode
2394
2605
  currentNode = currentNode.next
2395
2606
  }
2607
+ if (currentNode === null) {
2608
+ // The node is not in this list, so there is nothing to remove
2609
+ return null
2610
+ }
2396
2611
  if (prevNode) {
2397
- // Ensure the next reference of the previous node skips over the removed node
2398
2612
  prevNode.next = node.next
2399
- }
2400
- if (node === this.first && node !== null) {
2401
- // Update list head to point to next if it was this node
2613
+ } else {
2402
2614
  this.innerList = node.next
2403
2615
  }
2616
+ if (this.tailCache === node) {
2617
+ this.tailCache = prevNode
2618
+ }
2619
+ if (this.innerList === null) {
2620
+ this.tailCache = null
2621
+ }
2622
+ if (this.countCache !== null) {
2623
+ --this.countCache
2624
+ }
2404
2625
  return node
2405
2626
  }
2406
2627
 
2628
+ /**
2629
+ * Refresh the remembered end and length of the list by walking it once. The list's own methods keep these up to date,
2630
+ * so this is only needed after linkers were changed directly (for example by setting next on a linker).
2631
+ * @return {Linker|null} The first linker of the list
2632
+ */
2633
+ reset () {
2634
+ let count = 0
2635
+ let tail = null
2636
+ let current = this.innerList
2637
+ while (current !== null) {
2638
+ ++count
2639
+ tail = current
2640
+ current = current.next
2641
+ }
2642
+ this.countCache = count
2643
+ this.tailCache = tail
2644
+ return this.innerList
2645
+ }
2646
+
2407
2647
  /**
2408
2648
  * Retrieve a Linker item from this list by numeric index, otherwise return null.
2409
2649
  * @param {number} index The integer number for retrieving a node by position.
@@ -2486,7 +2726,7 @@
2486
2726
  class Linker {
2487
2727
  /**
2488
2728
  * Create the new Linker instance, provide the data and optionally give the next Linker.
2489
- * @param {Object} [nodeData={}]
2729
+ * @param {Object} [nodeData={}] The settings for the new linker.
2490
2730
  * @param {*} [nodeData.data=null] The data to be stored in this linker
2491
2731
  * @param {Linker|null} [nodeData.next=null] The reference to the next linker if any
2492
2732
  */
@@ -2494,8 +2734,11 @@
2494
2734
  data = null,
2495
2735
  next = null
2496
2736
  } = {}) {
2737
+ /** The class used to create this instance, so that it can be recognized as valid without an instanceof check. */
2497
2738
  this.classType = Linker
2739
+ /** The data stored in this linker. */
2498
2740
  this.data = null
2741
+ /** The linker after this one, or null when this is the last. */
2499
2742
  this.next = null
2500
2743
  this.data = data
2501
2744
  this.next = next
@@ -2509,8 +2752,8 @@
2509
2752
  */
2510
2753
  exports.Linker = Linker
2511
2754
  Linker.make = (linker, classType = Linker) => {
2512
- if (typeof linker !== 'object') {
2513
- // It is not an object, so instantiate the Linker with element as the data
2755
+ if (linker === null || typeof linker !== 'object') {
2756
+ // It is not an object (or it is null), so instantiate the Linker with element as the data
2514
2757
  return new classType({
2515
2758
  data: linker
2516
2759
  })
@@ -2519,7 +2762,8 @@
2519
2762
  // Already valid Linker, return as-is
2520
2763
  return linker
2521
2764
  }
2522
- if (!linker.data) {
2765
+ if (!('data' in linker)) {
2766
+ // Not the settings for a linker (which would have data, even if it is falsy), so it is the data itself
2523
2767
  linker = {
2524
2768
  data: linker
2525
2769
  }
@@ -2533,7 +2777,7 @@
2533
2777
  * @param {IsLinker} [classType=Linker] Provide the type of IsLinker to use.
2534
2778
  * @returns {{head: Linker, tail: Linker}}
2535
2779
  */
2536
- Linker.fromArray = (values, classType = Linker) => values.reduce((references, linker) => {
2780
+ Linker.fromArray = (values = [], classType = Linker) => values.reduce((references, linker) => {
2537
2781
  const newLinker = classType.make(linker, classType)
2538
2782
  if (references.head === null) {
2539
2783
  // Initialize the head and tail with the new node
@@ -2558,6 +2802,8 @@
2558
2802
  value: true
2559
2803
  })
2560
2804
  exports.LinkedTreeList = void 0
2805
+ require('core-js/modules/esnext.iterator.constructor.js')
2806
+ require('core-js/modules/esnext.iterator.for-each.js')
2561
2807
  const _TreeLinker = require('./TreeLinker')
2562
2808
  const _TreeLinkerIterator = require('../../recipes/TreeLinkerIterator')
2563
2809
  const _DoublyLinkedList = require('../doubly-linked-list/DoublyLinkedList')
@@ -2568,6 +2814,13 @@
2568
2814
  * @memberOf module:collect-your-stuff
2569
2815
  */
2570
2816
 
2817
+ /**
2818
+ * Use one of the accessors of DoublyLinkedList (which keeps track of the head, tail and length) for a LinkedTreeList.
2819
+ * @param {string} name The accessor to use
2820
+ * @param {LinkedTreeList} list The list to use it on
2821
+ * @returns {*}
2822
+ */
2823
+ const borrowedGetter = (name, list) => Object.getOwnPropertyDescriptor(_DoublyLinkedList.DoublyLinkedList.prototype, name).get.call(list)
2571
2824
  /**
2572
2825
  * LinkedTreeList represents a collection stored with a root and spreading in branching (tree) formation.
2573
2826
  * @extends DoublyLinkedList
@@ -2575,11 +2828,21 @@
2575
2828
  class LinkedTreeList {
2576
2829
  /**
2577
2830
  * Create the new LinkedTreeList instance, configure the list class.
2831
+ * @param {TreeLinker} [linkerClass=TreeLinker] The class used to wrap given data as tree linkers.
2578
2832
  */
2579
2833
  constructor (linkerClass = _TreeLinker.TreeLinker) {
2834
+ /** The class used to create this instance, so that it can be recognized as valid without an instanceof check. */
2580
2835
  this.classType = LinkedTreeList
2836
+ /** A linker of the list (null when the list is empty); the head is found by walking back from it. */
2581
2837
  this.innerList = null
2838
+ /** Whether the inner list has been initialized (it can only be initialized once). */
2582
2839
  this.initialized = false
2840
+ /** The last linker, remembered so that adding to the end does not need to walk the whole list (null when not known yet). */
2841
+ this.tailCache = null
2842
+ /** The number of linkers, kept up to date by the list's own methods so that the length does not need to walk the whole list (null when not known yet). */
2843
+ this.countCache = null
2844
+ /** The node these linkers are the children of, remembered so that it is known even while the list is empty (undefined until it is known). */
2845
+ this.ownerNode = undefined
2583
2846
  this.linkerClass = linkerClass
2584
2847
  }
2585
2848
 
@@ -2599,7 +2862,7 @@
2599
2862
  }
2600
2863
 
2601
2864
  /**
2602
- * Retrieve a copy of the innerList used.
2865
+ * Retrieve the innerList used (the list itself, not a copy).
2603
2866
  * @returns {TreeLinker}
2604
2867
  */
2605
2868
  get list () {
@@ -2611,57 +2874,46 @@
2611
2874
  * @returns {TreeLinker}
2612
2875
  */
2613
2876
  get first () {
2614
- return this.reset()
2877
+ return borrowedGetter('first', this)
2615
2878
  }
2616
2879
 
2617
2880
  /**
2618
- * Retrieve the last TreeLinker in the list.
2881
+ * Retrieve the last TreeLinker in the list. The end is remembered, so this does not walk the list.
2619
2882
  * @returns {TreeLinker}
2620
2883
  */
2621
2884
  get last () {
2622
- let tail = this.innerList
2623
- if (tail === null) {
2624
- return null
2625
- }
2626
- let next = tail.next
2627
- while (next !== null) {
2628
- tail = next
2629
- next = tail.next
2630
- }
2631
- return tail
2885
+ return borrowedGetter('last', this)
2632
2886
  }
2633
2887
 
2634
2888
  /**
2635
- * Return the length of the list.
2889
+ * Return the length of the list. It is kept up to date by the list's own methods, so this does not walk the list
2890
+ * (call reset() after linkers were changed directly).
2636
2891
  * @returns {number}
2637
2892
  */
2638
2893
  get length () {
2639
- let current = this.first
2640
- let length = 0
2641
- while (current !== null) {
2642
- ++length
2643
- current = current.next
2644
- }
2645
- return length
2894
+ return borrowedGetter('length', this)
2646
2895
  }
2647
2896
 
2648
2897
  /**
2649
- * Get the parent of this tree list.
2650
- * @return {TreeLinker}
2898
+ * Get the parent of this tree list: the node these linkers are the children of (remembered even while the list is
2899
+ * empty), or null for the linkers at the top of a tree.
2900
+ * @return {TreeLinker|null}
2651
2901
  */
2652
2902
  get parent () {
2653
- const first = this.first
2654
- if (first === null) {
2655
- return null
2903
+ if (this.ownerNode !== undefined) {
2904
+ return this.ownerNode
2656
2905
  }
2657
- return this.first.parent
2906
+ const first = this.first
2907
+ return first === null ? null : first.parent
2658
2908
  }
2659
2909
 
2660
2910
  /**
2661
- * Set the parent of this tree list
2662
- * @param {TreeLinker} parent The new node to use as the parent for this group of children
2911
+ * Set the parent of this tree list: every linker in it gets the node as its parent, and the node gets this list as its
2912
+ * children. Linkers added to the list later get this parent too.
2913
+ * @param {TreeLinker|null} parent The new node to use as the parent for this group of children
2663
2914
  */
2664
2915
  set parent (parent) {
2916
+ this.ownerNode = parent
2665
2917
  let current = this.first
2666
2918
  while (current !== null) {
2667
2919
  current.parent = parent
@@ -2691,34 +2943,57 @@
2691
2943
 
2692
2944
  /**
2693
2945
  * Set the children on a parent item.
2694
- * @param {TreeLinker} item The TreeLinker node that will be the parent of the children
2695
- * @param {LinkedTreeList} children The LinkedTreeList which has the child nodes to use
2946
+ * @param {TreeLinker} item The TreeLinker node (one of the linkers of this list) that will be the parent of the children
2947
+ * @param {LinkedTreeList|null} [children=null] The LinkedTreeList which has the child nodes to use, or null to remove the children of the item
2948
+ * @throws {Error} When the item is not one of the linkers of this list
2696
2949
  */
2697
2950
  setChildren (item, children = null) {
2698
- if (Array.from(this).indexOf(item) < 0) {
2699
- console.error('item is not a child of this')
2951
+ // The item must be one of the linkers of this list (only the siblings are checked, not the whole tree)
2952
+ let isChild = false
2953
+ this.forEach(linker => {
2954
+ if (linker === item) {
2955
+ isChild = true
2956
+ }
2957
+ })
2958
+ if (!isChild) {
2959
+ throw new Error('The item is not one of the linkers of this list.')
2960
+ }
2961
+ if (children === null || typeof children === 'undefined') {
2962
+ item.children = null
2963
+ return
2700
2964
  }
2701
2965
  children.parent = item
2702
2966
  }
2703
2967
 
2704
2968
  /**
2705
- * Insert a new node (or data) after a node.
2706
- * @param {TreeLinker|*} node The existing node as reference
2969
+ * Make a linker of the given node (or data) and make this list's parent its parent.
2970
+ * @param {TreeLinker|*} newNode The node (or data) which is being added to this list
2971
+ * @returns {TreeLinker}
2972
+ */
2973
+ adopt (newNode) {
2974
+ const linker = this.linkerClass.make(newNode, this.linkerClass)
2975
+ linker.parent = this.parent
2976
+ return linker
2977
+ }
2978
+
2979
+ /**
2980
+ * Insert a new node (or data) after a node. The new node gets the parent of this list.
2981
+ * @param {TreeLinker|*} node The existing node as reference, or null to insert at the start of the list
2707
2982
  * @param {TreeLinker|*} newNode The new node to go after the existing node
2708
2983
  * @returns {LinkedTreeList}
2709
2984
  */
2710
2985
  insertAfter (node, newNode) {
2711
- return _DoublyLinkedList.DoublyLinkedList.prototype.insertAfter.call(this, node, newNode)
2986
+ return _DoublyLinkedList.DoublyLinkedList.prototype.insertAfter.call(this, node, this.adopt(newNode))
2712
2987
  }
2713
2988
 
2714
2989
  /**
2715
- * Insert a new node (or data) before a node.
2716
- * @param {TreeLinker|*} node The existing node as reference
2990
+ * Insert a new node (or data) before a node. The new node gets the parent of this list.
2991
+ * @param {TreeLinker|*} node The existing node as reference, or null to insert at the end of the list
2717
2992
  * @param {TreeLinker|*} newNode The new node to go before the existing node
2718
2993
  * @returns {LinkedTreeList}
2719
2994
  */
2720
2995
  insertBefore (node, newNode) {
2721
- return _DoublyLinkedList.DoublyLinkedList.prototype.insertBefore.call(this, node, newNode)
2996
+ return _DoublyLinkedList.DoublyLinkedList.prototype.insertBefore.call(this, node, this.adopt(newNode))
2722
2997
  }
2723
2998
 
2724
2999
  /**
@@ -2742,16 +3017,24 @@
2742
3017
  }
2743
3018
 
2744
3019
  /**
2745
- * Remove a linker from this linked list.
3020
+ * Remove a linker from this linked list. The removed node no longer has a parent.
2746
3021
  * @param {TreeLinker} node The node we wish to remove (and it will be returned after removal)
2747
- * @return {TreeLinker}
3022
+ * @return {TreeLinker|null} The removed node, or null when there was nothing to remove
2748
3023
  */
2749
3024
  remove (node) {
2750
- return _DoublyLinkedList.DoublyLinkedList.prototype.remove.call(this, node)
3025
+ const owner = this.parent
3026
+ const removed = _DoublyLinkedList.DoublyLinkedList.prototype.remove.call(this, node)
3027
+ if (removed && removed.parent === owner) {
3028
+ // Remember whose children these are (the list may now be empty), the removed node no longer has that parent
3029
+ this.ownerNode = owner
3030
+ removed.parent = null
3031
+ }
3032
+ return removed
2751
3033
  }
2752
3034
 
2753
3035
  /**
2754
- * Refresh all references and return head reference.
3036
+ * Refresh all references (the head, the end and the length) by walking the list once, and return the head. The
3037
+ * list's own methods keep these up to date, so this is only needed after linkers were changed directly.
2755
3038
  * @return {TreeLinker}
2756
3039
  */
2757
3040
  reset () {
@@ -2771,6 +3054,7 @@
2771
3054
  * Be able to run forEach on this LinkedTreeList to iterate over the TreeLinker Items.
2772
3055
  * @param {forEachCallback} callback The function to call for-each tree node
2773
3056
  * @param {LinkedTreeList} thisArg Optional, 'this' reference
3057
+ * @return {LinkedTreeList} The list which was iterated.
2774
3058
  */
2775
3059
  forEach (callback, thisArg = this) {
2776
3060
  let index = 0
@@ -2784,12 +3068,14 @@
2784
3068
  }
2785
3069
 
2786
3070
  /**
2787
- * Be able to iterate over this class.
3071
+ * Be able to iterate over this class: the linkers of this list and everything below them (left-first). It stays within
3072
+ * this list (it does not start at, or climb up to, the parents), use the parseTree service to parse a whole tree.
2788
3073
  * @returns {Iterator}
2789
3074
  */
2790
3075
  [Symbol.iterator] () {
2791
- const root = this.rootParent
2792
- return new _TreeLinkerIterator.TreeLinkerIterator(root)
3076
+ // The linkers of this list and everything below them, left-first. It stays within this list: it does not start at,
3077
+ // or climb up to, the parents (use the parseTree service to parse a whole tree)
3078
+ return new _TreeLinkerIterator.TreeLinkerIterator(this.first, this.parent)
2793
3079
  }
2794
3080
  }
2795
3081
  /**
@@ -2804,7 +3090,7 @@
2804
3090
  const list = new classType(linkerClass)
2805
3091
  return list.initialize(linkerClass.fromArray(values).head)
2806
3092
  }
2807
- }, { '../../recipes/TreeLinkerIterator': 28, '../doubly-linked-list/DoublyLinkedList': 20, './TreeLinker': 24 }],
3093
+ }, { '../../recipes/TreeLinkerIterator': 28, '../doubly-linked-list/DoublyLinkedList': 20, './TreeLinker': 24, 'core-js/modules/esnext.iterator.constructor.js': 130, 'core-js/modules/esnext.iterator.for-each.js': 133 }],
2808
3094
  24: [function (require, module, exports) {
2809
3095
  'use strict'
2810
3096
 
@@ -2823,7 +3109,7 @@
2823
3109
  class TreeLinker {
2824
3110
  /**
2825
3111
  * Create the new TreeLinker instance, provide the data and optionally set references for next, prev, parent, or children.
2826
- * @param {Object} [settings={}]
3112
+ * @param {Object} [settings={}] The settings for the new tree node.
2827
3113
  * @param {*} [settings.data=null] The data to be stored in this tree node
2828
3114
  * @param {TreeLinker} [settings.next=null] The reference to the next linker if any
2829
3115
  * @param {TreeLinker} [settings.prev=null] The reference to the previous linker if any
@@ -2839,11 +3125,17 @@
2839
3125
  parent = null,
2840
3126
  listClass = _LinkedTreeList.LinkedTreeList
2841
3127
  } = {}) {
3128
+ /** The class used to create this instance, so that it can be recognized as valid without an instanceof check. */
2842
3129
  this.classType = TreeLinker
3130
+ /** The data stored in this tree node. */
2843
3131
  this.data = null
3132
+ /** The sibling after this node, or null when this is the last child. */
2844
3133
  this.next = null
3134
+ /** The sibling before this node, or null when this is the first child. */
2845
3135
  this.prev = null
3136
+ /** The node this node is a child of, or null for a root node. */
2846
3137
  this.parent = null
3138
+ /** The list of the children of this node, or null when it has none. */
2847
3139
  this.children = null
2848
3140
  this.data = data
2849
3141
  this.next = next
@@ -2853,7 +3145,9 @@
2853
3145
  }
2854
3146
 
2855
3147
  /**
2856
- * Create the children for this tree from an array.
3148
+ * Create the children for this tree from an array. Each child becomes a tree linker with this node as its parent: an
3149
+ * existing linker is kept as it is, an object with a data property gives the settings of the linker, and anything
3150
+ * else is the data of the linker.
2857
3151
  * @param {Array|null} children Provide an array of data / linker references to be children of this tree node.
2858
3152
  * @param {IsArrayable<IsTreeNode>} listClass Give the type of list to use for storing the children
2859
3153
  * @return {LinkedTreeList|null}
@@ -2862,10 +3156,17 @@
2862
3156
  if (children === null) {
2863
3157
  return null
2864
3158
  }
2865
- // Creates a linked-tree-list to store the children.
2866
- return listClass.fromArray(children.map(child => Object.assign({}, child, {
2867
- parent: this
2868
- })), this.classType)
3159
+ // Every child is made into a tree linker (an existing one is kept as it is, and a plain value is the data) and is
3160
+ // given this node as its parent
3161
+ const nodes = children.map(child => {
3162
+ const linker = this.classType.make(child, this.classType)
3163
+ linker.parent = this
3164
+ return linker
3165
+ })
3166
+ // Creates a linked-tree-list to store the children, which remembers this node as its parent even when it is empty
3167
+ const list = listClass.fromArray(nodes, this.classType)
3168
+ list.parent = this
3169
+ return list
2869
3170
  }
2870
3171
  }
2871
3172
  /**
@@ -2897,11 +3198,21 @@
2897
3198
  * Class ArrayIterator returns the next value when using elements of array type list.
2898
3199
  */
2899
3200
  class ArrayIterator {
3201
+ /**
3202
+ * Create an iterator over the given array.
3203
+ * @param {Array<IsElement>} innerList The elements to iterate over.
3204
+ * @param {number} [index=0] The position to start from.
3205
+ */
2900
3206
  constructor (innerList, index = 0) {
2901
3207
  this.innerList = innerList
2902
3208
  this.index = index
2903
3209
  }
2904
3210
 
3211
+ /**
3212
+ * Get the next element, moving the iterator forward.
3213
+ * @param {*} [value] Not used, present to match the Iterator interface.
3214
+ * @return {IteratorResult<IsElement>} The next element, or done when there are no more.
3215
+ */
2905
3216
  next (value) {
2906
3217
  if (this.index < this.innerList.length) {
2907
3218
  return {
@@ -2928,10 +3239,19 @@
2928
3239
  * Class DoubleLinkerIterator returns the next value when using linkers of linked type lists.
2929
3240
  */
2930
3241
  class DoubleLinkerIterator {
3242
+ /**
3243
+ * Create an iterator starting at the given item.
3244
+ * @param {IsDoubleLinker} current The item to start from.
3245
+ */
2931
3246
  constructor (current) {
2932
3247
  this.current = current
2933
3248
  }
2934
3249
 
3250
+ /**
3251
+ * Get the current item and move on to the following one.
3252
+ * @param {*} [value] Not used, present to match the Iterator interface.
3253
+ * @return {IteratorResult<IsDoubleLinker>} The current item, or done when there are no more.
3254
+ */
2935
3255
  next (value) {
2936
3256
  const result = {
2937
3257
  value: this.current,
@@ -2954,10 +3274,19 @@
2954
3274
  * Class LinkerIterator returns the next value when using linkers of linked type lists.
2955
3275
  */
2956
3276
  class LinkerIterator {
3277
+ /**
3278
+ * Create an iterator starting at the given item.
3279
+ * @param {IsLinker} current The item to start from.
3280
+ */
2957
3281
  constructor (current) {
2958
3282
  this.current = current
2959
3283
  }
2960
3284
 
3285
+ /**
3286
+ * Get the current item and move on to the following one.
3287
+ * @param {*} [value] Not used, present to match the Iterator interface.
3288
+ * @return {IteratorResult<IsLinker>} The current item, or done when there are no more.
3289
+ */
2961
3290
  next (value) {
2962
3291
  const result = {
2963
3292
  value: this.current,
@@ -2981,16 +3310,27 @@
2981
3310
  * Class TreeLinkerIterator returns the next value taking a left-first approach down a tree.
2982
3311
  */
2983
3312
  class TreeLinkerIterator {
2984
- constructor (current) {
3313
+ /**
3314
+ * Create an iterator starting at the given item.
3315
+ * @param {IsTreeNode} current The item to start from.
3316
+ * @param {IsTreeNode|null} [boundaryParent] The parent of the nodes to stay within (null for the top of a tree), the whole tree when not given.
3317
+ */
3318
+ constructor (current, boundaryParent) {
2985
3319
  this.current = current
3320
+ this.boundaryParent = boundaryParent
2986
3321
  }
2987
3322
 
3323
+ /**
3324
+ * Get the current item and move on to the following one (left-first, down each branch).
3325
+ * @param {*} [value] Not used, present to match the Iterator interface.
3326
+ * @return {IteratorResult<IsTreeNode>} The current item, or done when there are no more.
3327
+ */
2988
3328
  next (value) {
2989
3329
  const result = {
2990
3330
  value: this.current,
2991
3331
  done: !this.current
2992
3332
  }
2993
- this.current = (0, _parseTreeNext.parseTreeNext)(this.current)
3333
+ this.current = (0, _parseTreeNext.parseTreeNext)(this.current, this.boundaryParent)
2994
3334
  return result
2995
3335
  }
2996
3336
  }
@@ -3012,36 +3352,31 @@
3012
3352
  * 5. Repeat 3
3013
3353
  * 6. If no next child, return to parent and repeat 3
3014
3354
  * 7. Stop at root (next is null and parent is null
3355
+ * A boundary can be given to parse only part of a tree: going back up to the parents stops at the boundary, so the
3356
+ * parsing stays within the nodes whose parent is the boundary (and everything below them).
3015
3357
  * @param {IsTreeNode} treeNode Provide a node in a tree and get the next node (left-first approach)
3358
+ * @param {IsTreeNode|null} [boundaryParent] The parent of the nodes to stay within, null for the nodes at the top of a tree. When it is not given the whole tree is parsed.
3016
3359
  * @returns {IsTreeNode|null}
3017
3360
  */
3018
- const parseTreeNext = treeNode => {
3361
+ const parseTreeNext = (treeNode, boundaryParent) => {
3019
3362
  if (!treeNode) {
3020
3363
  return null
3021
3364
  }
3022
- let test = null
3023
3365
  if (treeNode.children && treeNode.children.length) {
3024
- // Go down the left side of the tree
3025
- test = treeNode.children.first
3026
- }
3027
- if (!test) {
3028
- // Reached the bottom, go the next node on the right
3029
- test = treeNode.next
3030
- }
3031
- if (!test && treeNode.parent) {
3032
- // No more child nodes, return to parent and check parent sibling on the right
3033
- let parentNext = treeNode.parent.next
3034
- let parent = treeNode.parent
3035
- while (parent && !parentNext) {
3036
- parentNext = parent.next
3037
- // Keep checking parent next, until there are no more parents, or we find the parent sibling
3038
- parent = parent.parent
3039
- }
3040
- // This may be the parent sibling, or it could be null indicating we are done
3041
- test = parentNext
3042
- }
3043
- // Finally, either use the node we found, or it may be null
3044
- return test
3366
+ return treeNode.children.first
3367
+ }
3368
+ if (treeNode.next) {
3369
+ return treeNode.next
3370
+ }
3371
+ // Nothing more below or beside this node, so go back up until there is a node which has a next (or the boundary)
3372
+ let parent = treeNode.parent
3373
+ while (parent && parent !== boundaryParent) {
3374
+ if (parent.next) {
3375
+ return parent.next
3376
+ }
3377
+ parent = parent.parent
3378
+ }
3379
+ return null
3045
3380
  }
3046
3381
  exports.parseTreeNext = parseTreeNext
3047
3382
  }, {}],