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