nextNoah. * * This is optimised for the most common case of inserting into a bucket * with zero members, and deleting a bucket containing one member. In the * worst case, iteration through the list is still O(1) in the document * size, since each bucket can have at most 3 members. * * @var array> */ private $noahTableStack = [ [] ]; /** * Manually unlink the doubly-linked list, since otherwise, it is not freed * due to reference cycles. */ public function __destruct() { for ( $node = $this->head; $node; $node = $next ) { $next = $node->nextAFE; $node->prevAFE = $node->nextAFE = $node->nextNoah = null; } // @phan-suppress-next-line PhanTypeMismatchProperty $this->head = $this->tail = $this->noahTableStack = null; } /** * Insert a marker */ public function insertMarker() { $elt = new Marker( 'marker' ); if ( $this->tail ) { $this->tail->nextAFE = $elt; $elt->prevAFE = $this->tail; } else { $this->head = $elt; } $this->tail = $elt; $this->noahTableStack[] = []; } /** * Follow the steps required when the spec requires us to "push onto the * list of active formatting elements". * @param Element $elt */ public function push( Element $elt ) { // Must not be in the list already if ( $elt->prevAFE !== null || $this->head === $elt ) { throw new \Exception( 'Cannot insert a node into the AFE list twice' ); } // "Noah's Ark clause" -- if there are already three copies of // this element before we encounter a marker, then drop the last // one. $noahKey = $elt->getNoahKey(); $table =& $this->noahTableStack[ count( $this->noahTableStack ) - 1 ]; if ( !isset( $table[$noahKey] ) ) { $table[$noahKey] = $elt; } else { $count = 1; $head = $tail = $table[$noahKey]; while ( $tail->nextNoah ) { $tail = $tail->nextNoah; $count++; } if ( $count >= 3 ) { $this->remove( $head ); } $tail->nextNoah = $elt; } // Add to the main AFE list if ( $this->tail ) { $this->tail->nextAFE = $elt; $elt->prevAFE = $this->tail; } else { $this->head = $elt; } $this->tail = $elt; } /** * Follow the steps required when the spec asks us to "clear the list of * active formatting elements up to the last marker". */ public function clearToMarker() { // Iterate back through the list starting from the tail $tail = $this->tail; while ( $tail && !( $tail instanceof Marker ) ) { // Unlink the element $prev = $tail->prevAFE; $tail->prevAFE = null; if ( $prev ) { $prev->nextAFE = null; } $tail->nextNoah = null; $tail = $prev; } // If we finished on a marker, unlink it and pop it off the Noah table stack if ( $tail ) { $prev = $tail->prevAFE; if ( $prev ) { $prev->nextAFE = null; } $tail = $prev; array_pop( $this->noahTableStack ); } else { // No marker: wipe the top-level Noah table (which is the only one) $this->noahTableStack[0] = []; } // If we removed all the elements, clear the head pointer if ( !$tail ) { $this->head = null; } $this->tail = $tail; } /** * Find and return the last element with the specified name between the * end of the list and the last marker on the list. * Used when parsing "in body mode". * @param string $name * @return Element|null */ public function findElementByName( $name ) { $elt = $this->tail; while ( $elt && !( $elt instanceof Marker ) ) { if ( $elt->htmlName === $name ) { return $elt; } $elt = $elt->prevAFE; } return null; } /** * Determine whether an element is in the list of formatting elements. * @param Element $elt * @return bool */ public function isInList( Element $elt ) { return $this->head === $elt || $elt->prevAFE; } /** * Find the element $elt in the list and remove it. * Used when parsing in body mode. * * @param FormattingElement $elt */ public function remove( FormattingElement $elt ) { '@phan-var Marker|Element $elt'; /** @var Marker|Element $elt */ if ( $this->head !== $elt && !$elt->prevAFE ) { throw new TreeBuilderError( "Attempted to remove an element which is not in the AFE list" ); } // Update head and tail pointers if ( $this->head === $elt ) { $this->head = $elt->nextAFE; } if ( $this->tail === $elt ) { $this->tail = $elt->prevAFE; } // Update previous element if ( $elt->prevAFE ) { $elt->prevAFE->nextAFE = $elt->nextAFE; } // Update next element if ( $elt->nextAFE ) { $elt->nextAFE->prevAFE = $elt->prevAFE; } // Clear pointers so that isInList() etc. will work $elt->prevAFE = $elt->nextAFE = null; // Update Noah list if ( $elt instanceof Element ) { $this->removeFromNoahList( $elt ); } } /** * Add an element to a bucket of elements which are alike for the purposes * of the Noah's Ark clause. * * @param Element $elt */ private function addToNoahList( Element $elt ) { $noahKey = $elt->getNoahKey(); $table =& $this->noahTableStack[ count( $this->noahTableStack ) - 1 ]; if ( !isset( $table[$noahKey] ) ) { $table[$noahKey] = $elt; } else { $tail = $table[$noahKey]; while ( $tail->nextNoah ) { $tail = $tail->nextNoah; } $tail->nextNoah = $elt; } } /** * Remove an element from its Noah's Ark bucket. * * @param Element $elt */ private function removeFromNoahList( Element $elt ) { $table =& $this->noahTableStack[ count( $this->noahTableStack ) - 1 ]; $key = $elt->getNoahKey(); $noahElt = $table[$key]; if ( $noahElt === $elt ) { if ( $noahElt->nextNoah ) { $table[$key] = $noahElt->nextNoah; $noahElt->nextNoah = null; } else { unset( $table[$key] ); } } else { do { $prevNoahElt = $noahElt; $noahElt = $prevNoahElt->nextNoah; if ( $noahElt === $elt ) { // Found it, unlink $prevNoahElt->nextNoah = $elt->nextNoah; $elt->nextNoah = null; break; } } while ( $noahElt ); } } /** * Find element $a in the list and replace it with element $b * * @param FormattingElement $a * @param FormattingElement $b */ public function replace( FormattingElement $a, FormattingElement $b ) { '@phan-var Marker|Element $a'; /** @var Marker|Element $a */ '@phan-var Marker|Element $b'; /** @var Marker|Element $b */ if ( $this->head !== $a && !$a->prevAFE ) { throw new TreeBuilderError( "Attempted to replace an element which is not in the AFE list" ); } // Update head and tail pointers if ( $this->head === $a ) { $this->head = $b; } if ( $this->tail === $a ) { $this->tail = $b; } // Update previous element if ( $a->prevAFE ) { $a->prevAFE->nextAFE = $b; } // Update next element if ( $a->nextAFE ) { $a->nextAFE->prevAFE = $b; } $b->prevAFE = $a->prevAFE; $b->nextAFE = $a->nextAFE; $a->nextAFE = $a->prevAFE = null; // Update Noah list if ( $a instanceof Element ) { $this->removeFromNoahList( $a ); } if ( $b instanceof Element ) { $this->addToNoahList( $b ); } } /** * Find $a in the list and insert $b after it. * * @param FormattingElement $a * @param FormattingElement $b */ public function insertAfter( FormattingElement $a, FormattingElement $b ) { '@phan-var Marker|Element $a'; /** @var Marker|Element $a */ '@phan-var Marker|Element $b'; /** @var Marker|Element $b */ if ( $this->head !== $a && !$a->prevAFE ) { throw new TreeBuilderError( "Attempted to insert after an element which is not in the AFE list" ); } if ( $this->tail === $a ) { $this->tail = $b; } if ( $a->nextAFE ) { $a->nextAFE->prevAFE = $b; } $b->nextAFE = $a->nextAFE; $b->prevAFE = $a; $a->nextAFE = $b; if ( $b instanceof Element ) { $this->addToNoahList( $b ); } } /** * Get a string representation of the AFE list, for debugging * @return string */ public function dump() { $prev = null; $s = ''; for ( $node = $this->head; $node; $prev = $node, $node = $node->nextAFE ) { if ( $node instanceof Marker ) { $s .= "MARKER\n"; continue; } /** @var Element $node */ $s .= $node->getDebugTag(); if ( $node->nextNoah ) { $s .= " (noah sibling: " . $node->nextNoah->getDebugTag() . ')'; } if ( $node->nextAFE && $node->nextAFE->prevAFE !== $node ) { $s .= " (reverse link is wrong!)"; } $s .= "\n"; } if ( $prev !== $this->tail ) { $s .= "(tail pointer is wrong!)\n"; } return $s; } /** * Get the most recently inserted element in the list * @return Element|null */ public function getTail() { return $this->tail; } }