1<?php 2 3declare( strict_types = 1 ); 4 5namespace Wikimedia\Dodo; 6 7use Wikimedia\Dodo\Internal\WhatWG; 8 9/** 10 * The ContainerNode class defines common functionality for node subtypes 11 * that can have children. We factor this out so that leaf nodes can 12 * save the space required to maintain the list of children. There are 13 * a lot of leaf nodes in a document, so this space can add up. 14 * 15 * The list of children is kept primarily as a circular linked list of 16 * siblings. We fail over to an array of children (which makes 17 * insertion and removal much more expensive) only when required. 18 */ 19abstract class ContainerNode extends Node { 20 /** 21 * @var ?Node The first child, if we're using the linked list representation 22 */ 23 public $_firstChild; 24 25 /** 26 * @var ?NodeList An array of children, or null to indicate we're using 27 * the linked list representation. 28 */ 29 public $_childNodes; 30 31 /** 32 * @param Document $nodeDocument 33 */ 34 public function __construct( Document $nodeDocument ) { 35 parent::__construct( $nodeDocument ); 36 /* Our children */ 37 $this->_firstChild = null; 38 $this->_childNodes = null; 39 } 40 41 /** 42 * Return true iff there are children of this node. 43 * 44 * Note that we must test `_childNodes` to determine which representation 45 * to consult. 46 * 47 * @inheritDoc 48 */ 49 public function hasChildNodes(): bool { 50 if ( $this->_childNodes === null ) { 51 // We're using the linked list representation. 52 return $this->_firstChild !== null; 53 } else { 54 // We're using the NodeList representation 55 return $this->_childNodes->getLength() > 0; 56 } 57 } 58 59 /** @inheritDoc */ 60 public function _length(): int { 61 return $this->getChildNodes()->getLength(); 62 } 63 64 /** @inheritDoc */ 65 public function _empty(): bool { 66 return !$this->hasChildNodes(); 67 } 68 69 /** 70 * Keeping child nodes as an array makes insertion/removal of nodes 71 * quite expensive. So we try *never* to create this array, if 72 * possible, keeping `$this->childNodes` set to null. If someone 73 * actually fetches the childNodes list we lazily create it. 74 * It then has to be live, and so we must update it whenever 75 * nodes are appended or removed. 76 * 77 * @inheritDoc 78 */ 79 public function getChildNodes(): NodeList { 80 if ( $this->_childNodes === null ) { 81 // If childNodes has never been created, we've now created it. 82 $this->_childNodes = new NodeList(); 83 // optimized circular linked list traversal 84 $childNodes = new NodeList(); 85 $first = $this->_firstChild; 86 $kid = $first; 87 if ( $kid !== null ) { 88 do { 89 $childNodes->_append( $kid ); 90 $kid = $kid->_nextSibling; 91 } while ( $kid !== $first ); // circular linked list 92 } 93 $this->_childNodes = $childNodes; 94 // The first child could later be removed, but we'd still be 95 // holding on to a reference. So set _firstChild to null to 96 // allow freeing up that memory. 97 $this->_firstChild = null; 98 } 99 return $this->_childNodes; 100 } 101 102 /** 103 * Be careful to use this method in most cases rather than directly 104 * access `_firstChild`. 105 * 106 * @inheritDoc 107 */ 108 public function getFirstChild(): ?Node { 109 $kids = $this->_childNodes; 110 if ( $kids === null ) { 111 /* 112 * If we are using the Linked List representation, then just return 113 * the backing property (may still be null). 114 */ 115 return $this->_firstChild; 116 } 117 $len = $kids->getLength(); 118 return $len === 0 ? null : $kids->item( 0 ); 119 } 120 121 /** 122 * @inheritDoc 123 */ 124 public function getLastChild(): ?Node { 125 $kids = $this->_childNodes; 126 if ( $kids !== null ) { 127 // We are using the NodeList representation. 128 $len = $kids->getLength(); 129 return $len === 0 ? null : $kids->item( $len - 1 ); 130 } 131 // We are using the Linked List representation. 132 if ( $this->_firstChild === null ) { 133 return null; 134 } 135 // If we have a firstChild, its _previousSibling is the last child, 136 // because this is a circularly linked list. 137 return $this->_firstChild->_previousSibling; 138 } 139 140 // These next methods are defined on Element and DocumentFragment with 141 // identical behavior. Note that they are defined differently on Document, 142 // however, so we need to override this definition in that class. 143 144 /** 145 * Generic implementation of ::getTextContent to be used by Element 146 * and DocumentFragment (but not Document!). 147 * @see https://dom.spec.whatwg.org/#dom-node-textcontent 148 * @return ?string 149 */ 150 public function getTextContent(): ?string { 151 $text = []; 152 WhatWG::descendantTextContent( $this, $text ); 153 return implode( "", $text ); 154 } 155 156 /** 157 * Generic implementation of ::setTextContent to be used by Element 158 * and DocumentFragment (but not Document!). 159 * @see https://dom.spec.whatwg.org/#dom-node-textcontent 160 * @param ?string $value 161 */ 162 public function setTextContent( ?string $value ): void { 163 $value = $value ?? ''; 164 $this->_removeChildren(); 165 if ( $value !== "" ) { 166 /* Equivalent to Node:: appendChild without checks! */ 167 $this->_unsafeAppendChild( 168 $this->_nodeDocument->createTextNode( $value ) 169 ); 170 } 171 } 172 173} 174