mediawiki-extensions-Discus.../includes/ImmutableRange.php
Ed Sanders e57cf6c60f ImmutableRange: Add surroundContents method
Also adds extractContents method.

Change-Id: Ie81dab12007b4038ebaaae904a1c032f80ef43cb
2022-10-04 16:51:08 +02:00

837 lines
24 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace MediaWiki\Extension\DiscussionTools;
use Error;
use Exception;
use Wikimedia\Assert\Assert;
use Wikimedia\Parsoid\DOM\CharacterData;
use Wikimedia\Parsoid\DOM\Comment;
use Wikimedia\Parsoid\DOM\Document;
use Wikimedia\Parsoid\DOM\DocumentFragment;
use Wikimedia\Parsoid\DOM\DocumentType;
use Wikimedia\Parsoid\DOM\Node;
use Wikimedia\Parsoid\DOM\ProcessingInstruction;
use Wikimedia\Parsoid\DOM\Text;
/**
* ImmutableRange has a similar API to the DOM Range class.
*
* start/endContainer and offsets can be accessed, as can commonAncestorContainer
* which is lazy evaluated.
*
* setStart and setEnd are still available but return a cloned range.
*
* @property bool $collapsed
* @property Node $commonAncestorContainer
* @property Node $endContainer
* @property int $endOffset
* @property Node $startContainer
* @property int $startOffset
*/
class ImmutableRange {
private $mCommonAncestorContainer;
private $mEndContainer;
private $mEndOffset;
private $mStartContainer;
private $mStartOffset;
/**
* Find the common ancestor container of two nodes
*
* @param Node $a
* @param Node $b
* @return Node Common ancestor container
*/
private static function findCommonAncestorContainer( Node $a, Node $b ): Node {
$ancestorsA = [];
$ancestorsB = [];
do {
$ancestorsA[] = $a;
} while ( ( $a = $a->parentNode ) );
do {
$ancestorsB[] = $b;
} while ( ( $b = $b->parentNode ) );
$node = null;
while ( end( $ancestorsA ) && end( $ancestorsA ) === end( $ancestorsB ) ) {
$node = end( $ancestorsA );
array_pop( $ancestorsA );
array_pop( $ancestorsB );
}
if ( !$node ) {
throw new Error( 'Nodes are not in the same document' );
}
return $node;
}
/**
* Get the root ancestor of a node
*
* @param Node $node
* @return Node
*/
private static function getRootNode( Node $node ): Node {
while ( $node->parentNode ) {
$node = $node->parentNode;
'@phan-var Node $node';
}
return $node;
}
/**
* @param Node $startNode
* @param int $startOffset
* @param Node $endNode
* @param int $endOffset
*/
public function __construct(
Node $startNode, int $startOffset, Node $endNode, int $endOffset
) {
$this->mStartContainer = $startNode;
$this->mStartOffset = $startOffset;
$this->mEndContainer = $endNode;
$this->mEndOffset = $endOffset;
}
/**
* @param string $field Field name
* @return mixed
*/
public function __get( string $field ) {
switch ( $field ) {
case 'collapsed':
return $this->mStartContainer === $this->mEndContainer &&
$this->mStartOffset === $this->mEndOffset;
case 'commonAncestorContainer':
if ( !$this->mCommonAncestorContainer ) {
$this->mCommonAncestorContainer =
static::findCommonAncestorContainer( $this->mStartContainer, $this->mEndContainer );
}
return $this->mCommonAncestorContainer;
case 'endContainer':
return $this->mEndContainer;
case 'endOffset':
return $this->mEndOffset;
case 'startContainer':
return $this->mStartContainer;
case 'startOffset':
return $this->mStartOffset;
default:
throw new Exception( 'Invalid property: ' . $field );
}
}
/**
* Clone range with a new start position
*
* @param Node $startNode
* @param int $startOffset
* @return self
*/
public function setStart( Node $startNode, int $startOffset ): self {
return $this->setStartOrEnd( 'start', $startNode, $startOffset );
}
/**
* Clone range with a new end position
*
* @param Node $endNode
* @param int $endOffset
* @return self
*/
public function setEnd( Node $endNode, int $endOffset ): self {
return $this->setStartOrEnd( 'end', $endNode, $endOffset );
}
/**
* Sets the start or end boundary point for the Range.
*
* Ported from https://github.com/TRowbotham/PHPDOM (MIT)
* @see https://dom.spec.whatwg.org/#concept-range-bp-set
*
* @param string $type Which boundary point should be set. Valid values are start or end.
* @param Node $node The Node that will become the boundary.
* @param int $offset The offset within the given Node that will be the boundary.
* @return self
*/
private function setStartOrEnd( string $type, Node $node, int $offset ): self {
if ( $node instanceof DocumentType ) {
throw new Error();
}
switch ( $type ) {
case 'start':
$endContainer = $this->mEndContainer;
$endOffset = $this->mEndOffset;
if (
self::getRootNode( $this->mStartContainer ) !== self::getRootNode( $node ) ||
$this->computePosition(
$node, $offset, $this->mEndContainer, $this->mEndOffset
) === 'after'
) {
$endContainer = $node;
$endOffset = $offset;
}
return new self(
$node, $offset, $endContainer, $endOffset
);
case 'end':
$startContainer = $this->mStartContainer;
$startOffset = $this->mStartOffset;
if (
self::getRootNode( $this->mStartContainer ) !== self::getRootNode( $node ) ||
$this->computePosition(
$node, $offset, $this->mStartContainer, $this->mStartOffset
) === 'before'
) {
$startContainer = $node;
$startOffset = $offset;
}
return new self(
$startContainer, $startOffset, $node, $offset
);
}
}
/**
* Returns true if only a portion of the Node is contained within the Range.
*
* Ported from https://github.com/TRowbotham/PHPDOM (MIT)
* @see https://dom.spec.whatwg.org/#partially-contained
*
* @param Node $node The Node to check against.
* @return bool
*/
private function isPartiallyContainedNode( Node $node ): bool {
return CommentUtils::contains( $node, $this->mStartContainer ) xor
CommentUtils::contains( $node, $this->mEndContainer );
}
/**
* Returns true if the entire Node is within the Range, otherwise false.
*
* Ported from https://github.com/TRowbotham/PHPDOM (MIT)
* @see https://dom.spec.whatwg.org/#contained
*
* @param Node $node The Node to check against.
* @return bool
*/
private function isFullyContainedNode( Node $node ): bool {
return static::getRootNode( $node ) === static::getRootNode( $this->mStartContainer )
&& $this->computePosition( $node, 0, $this->mStartContainer, $this->mStartOffset ) === 'after'
&& $this->computePosition(
// @phan-suppress-next-line PhanUndeclaredProperty
$node, $node->length ?? $node->childNodes->length,
$this->mEndContainer, $this->mEndOffset
) === 'before';
}
/**
* Extracts the content of the Range from the node tree and places it in a
* DocumentFragment.
*
* Ported from https://github.com/TRowbotham/PHPDOM (MIT)
* @see https://dom.spec.whatwg.org/#dom-range-extractcontents
*
* @return DocumentFragment
*/
public function extractContents(): DocumentFragment {
$fragment = $this->mStartContainer->ownerDocument->createDocumentFragment();
if (
$this->mStartContainer === $this->mEndContainer
&& $this->mStartOffset === $this->mEndOffset
) {
return $fragment;
}
$originalStartNode = $this->mStartContainer;
$originalStartOffset = $this->mStartOffset;
$originalEndNode = $this->mEndContainer;
$originalEndOffset = $this->mEndOffset;
if (
$originalStartNode === $originalEndNode
&& ( $originalStartNode instanceof Text
|| $originalStartNode instanceof ProcessingInstruction
|| $originalStartNode instanceof Comment )
) {
$clone = $originalStartNode->cloneNode();
Assert::precondition( $clone instanceof CharacterData, 'TODO' );
$clone->data = $originalStartNode->substringData(
$originalStartOffset,
$originalEndOffset - $originalStartOffset
);
$fragment->appendChild( $clone );
$originalStartNode->replaceData(
$originalStartOffset,
$originalEndOffset - $originalStartOffset,
''
);
return $fragment;
}
$commonAncestor = $this->commonAncestorContainer;
// It should be impossible for common ancestor to be null here since both nodes should be
// in the same tree.
Assert::precondition( $commonAncestor !== null, 'TODO' );
$firstPartiallyContainedChild = null;
if ( !CommentUtils::contains( $originalStartNode, $originalEndNode ) ) {
foreach ( $commonAncestor->childNodes as $node ) {
if ( $this->isPartiallyContainedNode( $node ) ) {
$firstPartiallyContainedChild = $node;
break;
}
}
}
$lastPartiallyContainedChild = null;
if ( !CommentUtils::contains( $originalEndNode, $originalStartNode ) ) {
$node = $commonAncestor->lastChild;
while ( $node ) {
if ( $this->isPartiallyContainedNode( $node ) ) {
$lastPartiallyContainedChild = $node;
break;
}
$node = $node->previousSibling;
}
}
$containedChildren = [];
foreach ( $commonAncestor->childNodes as $childNode ) {
if ( $this->isFullyContainedNode( $childNode ) ) {
if ( $childNode instanceof DocumentType ) {
throw new Error();
}
$containedChildren[] = $childNode;
}
}
if ( CommentUtils::contains( $originalStartNode, $originalEndNode ) ) {
$newNode = $originalStartNode;
$newOffset = $originalStartOffset;
} else {
$referenceNode = $originalStartNode;
$parent = $referenceNode->parentNode;
while ( $parent && !CommentUtils::contains( $parent, $originalEndNode ) ) {
$referenceNode = $parent;
$parent = $referenceNode->parentNode;
}
// Note: If reference nodes parent is null, it would be the root of range, so would be an inclusive
// ancestor of original end node, and we could not reach this point.
Assert::precondition( $parent !== null, 'TODO' );
$newNode = $parent;
$newOffset = CommentUtils::childIndexOf( $referenceNode ) + 1;
}
if (
$firstPartiallyContainedChild instanceof Text
|| $firstPartiallyContainedChild instanceof ProcessingInstruction
|| $firstPartiallyContainedChild instanceof Comment
) {
// Note: In this case, first partially contained child is original start node.
Assert::precondition( $originalStartNode instanceof CharacterData, 'TODO' );
$clone = $originalStartNode->cloneNode();
Assert::precondition( $clone instanceof CharacterData, 'TODO' );
$clone->data = $originalStartNode->substringData(
$originalStartOffset,
$originalStartNode->length - $originalStartOffset
);
$fragment->appendChild( $clone );
$originalStartNode->replaceData(
$originalStartOffset,
$originalStartNode->length - $originalStartOffset,
''
);
} elseif ( $firstPartiallyContainedChild ) {
$clone = $firstPartiallyContainedChild->cloneNode();
$fragment->appendChild( $clone );
$subrange = clone $this;
$subrange->mStartContainer = $originalStartNode;
$subrange->mStartOffset = $originalStartOffset;
$subrange->mEndContainer = $firstPartiallyContainedChild;
$subrange->mEndOffset = count( $firstPartiallyContainedChild->childNodes );
$subfragment = $subrange->extractContents();
$clone->appendChild( $subfragment );
}
foreach ( $containedChildren as $child ) {
$fragment->appendChild( $child );
}
if (
$lastPartiallyContainedChild instanceof Text
|| $lastPartiallyContainedChild instanceof ProcessingInstruction
|| $lastPartiallyContainedChild instanceof Comment
) {
// Note: In this case, last partially contained child is original end node.
Assert::precondition( $originalEndNode instanceof CharacterData, 'TODO' );
$clone = $originalEndNode->cloneNode();
Assert::precondition( $clone instanceof CharacterData, 'TODO' );
$clone->data = $originalEndNode->substringData( 0, $originalEndOffset );
$fragment->appendChild( $clone );
$originalEndNode->replaceData( 0, $originalEndOffset, '' );
} elseif ( $lastPartiallyContainedChild ) {
$clone = $lastPartiallyContainedChild->cloneNode();
$fragment->appendChild( $clone );
$subrange = clone $this;
$subrange->mStartContainer = $lastPartiallyContainedChild;
$subrange->mStartOffset = 0;
$subrange->mEndContainer = $originalEndNode;
$subrange->mEndOffset = $originalEndOffset;
$subfragment = $subrange->extractContents();
$clone->appendChild( $subfragment );
}
$this->mStartContainer = $newNode;
$this->mStartOffset = $newOffset;
$this->mEndContainer = $newNode;
$this->mEndOffset = $newOffset;
return $fragment;
}
/**
* Ported from https://github.com/TRowbotham/PHPDOM (MIT)
* @see https://dom.spec.whatwg.org/#dom-range-clonecontents
*
* @return DocumentFragment
*/
public function cloneContents(): DocumentFragment {
$ownerDocument = $this->mStartContainer->ownerDocument;
$fragment = $ownerDocument->createDocumentFragment();
if ( $this->mStartContainer === $this->mEndContainer
&& $this->mStartOffset === $this->mEndOffset
) {
return $fragment;
}
$originalStartContainer = $this->mStartContainer;
$originalStartOffset = $this->mStartOffset;
$originalEndContainer = $this->mEndContainer;
$originalEndOffset = $this->mEndOffset;
if ( $originalStartContainer === $originalEndContainer
&& ( $originalStartContainer instanceof Text
|| $originalStartContainer instanceof ProcessingInstruction
|| $originalStartContainer instanceof Comment )
) {
$clone = $originalStartContainer->cloneNode();
$clone->nodeValue = $originalStartContainer->substringData(
$originalStartOffset,
$originalEndOffset - $originalStartOffset
);
$fragment->appendChild( $clone );
return $fragment;
}
$commonAncestor = static::findCommonAncestorContainer(
$originalStartContainer,
$originalEndContainer
);
$firstPartiallyContainedChild = null;
if ( !CommentUtils::contains( $originalStartContainer, $originalEndContainer ) ) {
foreach ( $commonAncestor->childNodes as $node ) {
if ( $this->isPartiallyContainedNode( $node ) ) {
$firstPartiallyContainedChild = $node;
break;
}
}
}
$lastPartiallyContainedChild = null;
// Upstream uses lastChild then iterates over previousSibling, however this
// is much slower that copying all the nodes to an array, at least when using
// a native DOMNode, presumably because previousSibling is lazy-evaluated.
if ( !CommentUtils::contains( $originalEndContainer, $originalStartContainer ) ) {
$childNodes = iterator_to_array( $commonAncestor->childNodes );
foreach ( array_reverse( $childNodes ) as $node ) {
if ( $this->isPartiallyContainedNode( $node ) ) {
$lastPartiallyContainedChild = $node;
break;
}
}
}
$containedChildrenStart = null;
$containedChildrenEnd = null;
$child = $firstPartiallyContainedChild ?: $commonAncestor->firstChild;
for ( ; $child; $child = $child->nextSibling ) {
if ( $this->isFullyContainedNode( $child ) ) {
$containedChildrenStart = $child;
break;
}
}
$child = $lastPartiallyContainedChild ?: $commonAncestor->lastChild;
for ( ; $child !== $containedChildrenStart; $child = $child->previousSibling ) {
if ( $this->isFullyContainedNode( $child ) ) {
$containedChildrenEnd = $child;
break;
}
}
if ( !$containedChildrenEnd ) {
$containedChildrenEnd = $containedChildrenStart;
}
// $containedChildrenStart and $containedChildrenEnd may be null here, but this loop still works correctly
for ( $child = $containedChildrenStart; $child !== $containedChildrenEnd; $child = $child->nextSibling ) {
if ( $child instanceof DocumentType ) {
throw new Error();
}
}
if ( $firstPartiallyContainedChild instanceof Text
|| $firstPartiallyContainedChild instanceof ProcessingInstruction
|| $firstPartiallyContainedChild instanceof Comment
) {
$clone = $originalStartContainer->cloneNode();
Assert::precondition(
$firstPartiallyContainedChild === $originalStartContainer,
'Only possible when the node is the startContainer'
);
$clone->nodeValue = $firstPartiallyContainedChild->substringData(
$originalStartOffset,
$firstPartiallyContainedChild->length - $originalStartOffset
);
$fragment->appendChild( $clone );
} elseif ( $firstPartiallyContainedChild ) {
$clone = $firstPartiallyContainedChild->cloneNode();
$fragment->appendChild( $clone );
$subrange = new self(
$originalStartContainer, $originalStartOffset,
$firstPartiallyContainedChild,
// @phan-suppress-next-line PhanUndeclaredProperty
$firstPartiallyContainedChild->length ?? $firstPartiallyContainedChild->childNodes->length
);
$subfragment = $subrange->cloneContents();
if ( $subfragment->hasChildNodes() ) {
$clone->appendChild( $subfragment );
}
}
// $containedChildrenStart and $containedChildrenEnd may be null here, but this loop still works correctly
for ( $child = $containedChildrenStart; $child !== $containedChildrenEnd; $child = $child->nextSibling ) {
$clone = $child->cloneNode( true );
$fragment->appendChild( $clone );
}
// If not null, this node wasn't processed by the loop
if ( $containedChildrenEnd ) {
$clone = $containedChildrenEnd->cloneNode( true );
$fragment->appendChild( $clone );
}
if ( $lastPartiallyContainedChild instanceof Text
|| $lastPartiallyContainedChild instanceof ProcessingInstruction
|| $lastPartiallyContainedChild instanceof Comment
) {
Assert::precondition(
$lastPartiallyContainedChild === $originalEndContainer,
'Only possible when the node is the endContainer'
);
$clone = $lastPartiallyContainedChild->cloneNode();
$clone->nodeValue = $lastPartiallyContainedChild->substringData(
0,
$originalEndOffset
);
$fragment->appendChild( $clone );
} elseif ( $lastPartiallyContainedChild ) {
$clone = $lastPartiallyContainedChild->cloneNode();
$fragment->appendChild( $clone );
$subrange = new self(
$lastPartiallyContainedChild, 0,
$originalEndContainer, $originalEndOffset
);
$subfragment = $subrange->cloneContents();
if ( $subfragment->hasChildNodes() ) {
$clone->appendChild( $subfragment );
}
}
return $fragment;
}
/**
* Inserts a new Node into at the start of the Range.
*
* Ported from https://github.com/TRowbotham/PHPDOM (MIT)
*
* @see https://dom.spec.whatwg.org/#dom-range-insertnode
*
* @param Node $node The Node to be inserted.
* @return void
*/
public function insertNode( Node $node ): void {
if ( ( $this->mStartContainer instanceof ProcessingInstruction
|| $this->mStartContainer instanceof Comment )
|| ( $this->mStartContainer instanceof Text
&& $this->mStartContainer->parentNode === null )
) {
throw new Error();
}
$referenceNode = null;
if ( $this->mStartContainer instanceof Text ) {
$referenceNode = $this->mStartContainer;
} else {
$referenceNode = $this
->mStartContainer
->childNodes
->item( $this->mStartOffset );
}
$parent = !$referenceNode
? $this->mStartContainer
: $referenceNode->parentNode;
// TODO: Restore this validation check?
// $parent->ensurePreinsertionValidity( $node, $referenceNode );
if ( $this->mStartContainer instanceof Text ) {
$referenceNode = $this->mStartContainer->splitText( $this->mStartOffset );
}
if ( $node === $referenceNode ) {
$referenceNode = $referenceNode->nextSibling;
}
if ( $node->parentNode ) {
$node->parentNode->removeChild( $node );
}
// TODO: Restore this validation check?
// $parent->preinsertNode( $node, $referenceNode );
// $referenceNode may be null, this is okay
$parent->insertBefore( $node, $referenceNode );
}
/**
* Wraps the content of Range in a new Node and inserts it in to the Document.
*
* Ported from https://github.com/TRowbotham/PHPDOM (MIT)
*
* @see https://dom.spec.whatwg.org/#dom-range-surroundcontents
*
* @param Node $newParent New parent node for contents
* @return void
*/
public function surroundContents( Node $newParent ): void {
$commonAncestor = $this->commonAncestorContainer;
if ( $commonAncestor ) {
$tw = new TreeWalker( $commonAncestor );
$node = $tw->nextNode();
while ( $node ) {
if ( !$node instanceof Text && $this->isPartiallyContainedNode( $node ) ) {
throw new Error();
}
$node = $tw->nextNode();
}
}
if (
$newParent instanceof Document
|| $newParent instanceof DocumentType
|| $newParent instanceof DocumentFragment
) {
throw new Error();
}
$fragment = $this->extractContents();
while ( $newParent->firstChild ) {
$newParent->removeChild( $newParent->firstChild );
}
$this->insertNode( $newParent );
$newParent->appendChild( $fragment );
// TODO: Return new range?
}
/**
* Compares the position of two boundary points.
*
* Ported from https://github.com/TRowbotham/PHPDOM (MIT)
* @internal
*
* @see https://dom.spec.whatwg.org/#concept-range-bp-position
*
* @param Node $nodeA
* @param int $offsetA
* @param Node $nodeB
* @param int $offsetB
* @return string 'before'|'after'|'equal'
*/
private function computePosition(
Node $nodeA, int $offsetA, Node $nodeB, int $offsetB
): string {
// 1. Assert: nodeA and nodeB have the same root.
// Removed, not necessary for our usage
// 2. If nodeA is nodeB, then return equal if offsetA is offsetB, before if offsetA is less than offsetB, and
// after if offsetA is greater than offsetB.
if ( $nodeA === $nodeB ) {
if ( $offsetA === $offsetB ) {
return 'equal';
} elseif ( $offsetA < $offsetB ) {
return 'before';
} else {
return 'after';
}
}
$commonAncestor = $this->findCommonAncestorContainer( $nodeB, $nodeA );
if ( $commonAncestor === $nodeA ) {
$AFollowsB = false;
} elseif ( $commonAncestor === $nodeB ) {
$AFollowsB = true;
} else {
// A was not found inside B. Traverse both A & B up to the nodes
// before their common ancestor, then see if A is in the nextSibling
// chain of B.
$b = $nodeB;
while ( $b->parentNode !== $commonAncestor ) {
$b = $b->parentNode;
}
$a = $nodeA;
while ( $a->parentNode !== $commonAncestor ) {
$a = $a->parentNode;
}
$AFollowsB = false;
while ( $b ) {
if ( $a === $b ) {
$AFollowsB = true;
break;
}
$b = $b->nextSibling;
}
}
if ( $AFollowsB ) {
// Swap variables
[ $nodeB, $nodeA ] = [ $nodeA, $nodeB ];
[ $offsetB, $offsetA ] = [ $offsetA, $offsetB ];
}
$ancestor = $nodeB->parentNode;
while ( $ancestor ) {
if ( $ancestor === $nodeA ) {
break;
}
$ancestor = $ancestor->parentNode;
}
if ( $ancestor ) {
$child = $nodeB;
while ( $child ) {
if ( $child->parentNode === $nodeA ) {
break;
}
$child = $child->parentNode;
}
// Phan complains that $child may be null here, but that can't happen, because at this point
// we know that $nodeA is an ancestor of $nodeB, so the loop above will stop before the root.
// @phan-suppress-next-line PhanTypeMismatchArgumentNullable
if ( CommentUtils::childIndexOf( $child ) < $offsetA ) {
return $AFollowsB ? 'before' : 'after';
}
}
return $AFollowsB ? 'after' : 'before';
}
public const START_TO_START = 0;
public const START_TO_END = 1;
public const END_TO_END = 2;
public const END_TO_START = 3;
/**
* Compares the boundary points of this Range with another Range.
*
* Ported from https://github.com/TRowbotham/PHPDOM (MIT)
*
* @see https://dom.spec.whatwg.org/#dom-range-compareboundarypoints
*
* @param int $how One of ImmutableRange::END_TO_END, ImmutableRange::END_TO_START,
* ImmutableRange::START_TO_END, ImmutableRange::START_TO_START
* @param ImmutableRange $sourceRange A Range whose boundary points are to be compared.
* @return int -1, 0, or 1
*/
public function compareBoundaryPoints( int $how, self $sourceRange ): int {
if ( static::getRootNode( $this->mStartContainer ) !== static::getRootNode( $sourceRange->startContainer ) ) {
throw new Error();
}
switch ( $how ) {
case static::START_TO_START:
$thisPoint = [ $this->mStartContainer, $this->mStartOffset ];
$otherPoint = [ $sourceRange->startContainer, $sourceRange->startOffset ];
break;
case static::START_TO_END:
$thisPoint = [ $this->mEndContainer, $this->mEndOffset ];
$otherPoint = [ $sourceRange->startContainer, $sourceRange->startOffset ];
break;
case static::END_TO_END:
$thisPoint = [ $this->mEndContainer, $this->mEndOffset ];
$otherPoint = [ $sourceRange->endContainer, $sourceRange->endOffset ];
break;
case static::END_TO_START:
$thisPoint = [ $this->mStartContainer, $this->mStartOffset ];
$otherPoint = [ $sourceRange->endContainer, $sourceRange->endOffset ];
break;
default:
throw new Error();
}
switch ( $this->computePosition( ...$thisPoint, ...$otherPoint ) ) {
case 'before':
return -1;
case 'equal':
return 0;
case 'after':
return 1;
default:
throw new Error();
}
}
}