Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
21.43% |
3 / 14 |
CRAP | |
25.00% |
22 / 88 |
NamedNodeMap | |
0.00% |
0 / 1 |
|
21.43% |
3 / 14 |
750.17 | |
25.00% |
22 / 88 |
__construct | |
100.00% |
1 / 1 |
2 | |
100.00% |
2 / 2 |
|||
_append | |
0.00% |
0 / 1 |
3.51 | |
61.54% |
8 / 13 |
|||
_replace | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 12 |
|||
_remove | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 15 |
|||
getLength | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
item | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
_hasNamedItem | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 3 |
|||
_hasNamedItemNS | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 2 |
|||
getNamedItem | |
0.00% |
0 / 1 |
9.66 | |
42.86% |
3 / 7 |
|||
getNamedItemNS | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 2 |
|||
setNamedItem | |
0.00% |
0 / 1 |
5.68 | |
70.00% |
7 / 10 |
|||
setNamedItemNS | |
0.00% |
0 / 1 |
30 | |
0.00% |
0 / 10 |
|||
removeNamedItem | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 5 |
|||
removeNamedItemNS | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 5 |
<?php | |
declare( strict_types = 1 ); | |
// phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName | |
// phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore | |
// phpcs:disable PSR2.Methods.MethodDeclaration.Underscore | |
// phpcs:disable Squiz.PHP.NonExecutableCode.Unreachable | |
namespace Wikimedia\Dodo; | |
/****************************************************************************** | |
* NamedNodeMap.php | |
* ---------------- | |
* Implements a NamedNodeMap. Used to represent Element::attributes. | |
* | |
* NOTE: Why is it called NamedNodeMap? | |
* | |
* NamedNodeMap has nothing to do with Nodes, it's a collection | |
* of Attrs. But once upon a time, an Attr was a type of Node called a | |
* NamedNode. But then DOM-4 came along and said that an Attr is no | |
* longer a subclass of Node. But then DOM-LS came and change it again, | |
* and said it was a subclass of Node. NamedNode was forgotten, but it | |
* lives on in this interface's name! How confusing! | |
* | |
* NOTE: This looks different from Domino.js! | |
* | |
* In Domino.js, NamedNodeMap was only implemented to satisfy | |
* 'instanceof' type-checking. Almost all of the methods were | |
* stubbed, except for 'length' and 'item'. The tables that | |
* stored an Element's attributes were located directly on the | |
* Element itself. | |
* | |
* Because there are so many attribute handling methods on an | |
* Element, each with little differences, this meant replicating | |
* a bunch of the book-keeping inside those methods. The negative | |
* impact on code maintainability was pronounced, so the book-keeping | |
* was transferred to the NamedNodeMap itself, and its methods were | |
* properly implemented, which made it much easier to read and write | |
* the attribute methods on the Element class. | |
* | |
*/ | |
class NamedNodeMap implements \Wikimedia\IDLeDOM\NamedNodeMap { | |
// Stub out methods not yet implemented. | |
use \Wikimedia\IDLeDOM\Stub\NamedNodeMap; | |
use UnimplementedTrait; | |
// Helper functions from IDLeDOM | |
use \Wikimedia\IDLeDOM\Helper\NamedNodeMap; | |
/** | |
* qname => Attr | |
* | |
* @var array entries are either Attr objects, or arrays of Attr objects on collisions | |
*/ | |
private $_qname_to_attr = []; | |
/** | |
* ns|lname => Attr | |
* | |
* @var Attr[] | |
*/ | |
private $_lname_to_attr = []; | |
/** | |
* ns|lname => index number | |
* | |
* @var int[] | |
*/ | |
private $_lname_to_index = []; | |
/** | |
* index number => Attr | |
* @var Attr[] | |
*/ | |
public $_index_to_attr = []; | |
/** | |
* DOM-LS associated element, defined in spec but not given property. | |
* | |
* @var ?Element | |
*/ | |
public $_element = null; | |
/** | |
* @param ?Element $element | |
*/ | |
public function __construct( ?Element $element = null ) { | |
$this->_element = $element; | |
} | |
/********************************************************************** | |
* DODO INTERNAL BOOK-KEEPING | |
*/ | |
/** | |
* @param Attr $a | |
*/ | |
private function _append( Attr $a ) { | |
$qname = $a->getName(); | |
/* NO COLLISION */ | |
if ( !isset( $this->_qname_to_attr[$qname] ) ) { | |
$this->_qname_to_attr[$qname] = $a; | |
/* COLLISION */ | |
} else { | |
if ( is_array( $this->_qname_to_attr[$qname] ) ) { | |
$this->_qname_to_attr[$qname][] = $a; | |
} else { | |
$this->_qname_to_attr[$qname] = [ | |
$this->_qname_to_attr[$qname], | |
$a | |
]; | |
} | |
} | |
$key = $a->getNamespaceURI() . '|' . $a->getLocalName(); | |
$this->_lname_to_attr[$key] = $a; | |
$this->_lname_to_index[$key] = count( $this->_index_to_attr ); | |
$this->_index_to_attr[] = $a; | |
} | |
/** | |
* @param Attr $a | |
*/ | |
private function _replace( Attr $a ) { | |
$qname = $a->getName(); | |
/* NO COLLISION */ | |
if ( !isset( $this->_qname_to_attr[$qname] ) ) { | |
$this->_qname_to_attr[$qname] = $a; | |
/* COLLISION */ | |
} else { | |
if ( is_array( $this->_qname_to_attr[$qname] ) ) { | |
$this->_qname_to_attr[$qname][] = $a; | |
} else { | |
$this->_qname_to_attr[$qname] = [ | |
$this->_qname_to_attr[$qname], | |
$a | |
]; | |
} | |
} | |
$key = $a->getNamespaceURI() . '|' . $a->getLocalName(); | |
$this->_lname_to_attr[$key] = $a; | |
$this->_index_to_attr[$this->_lname_to_index[$key]] = $a; | |
} | |
/** | |
* @internal | |
* @param Attr $a | |
*/ | |
public function _remove( Attr $a ) : void { | |
$qname = $a->getName(); | |
$key = $a->getNamespaceURI() . '|' . $a->getLocalName(); | |
unset( $this->_lname_to_attr[$key] ); | |
$i = $this->_lname_to_index[$key]; | |
unset( $this->_lname_to_index[$key] ); | |
array_splice( $this->_index_to_attr, $i, 1 ); | |
if ( isset( $this->_qname_to_attr[$qname] ) ) { | |
if ( is_array( $this->_qname_to_attr[$qname] ) ) { | |
$i = array_search( $a, $this->_qname_to_attr[$qname] ); | |
if ( $i !== false ) { | |
array_splice( $this->_qname_to_attr[$qname], $i, 1 ); | |
} | |
} else { | |
unset( $this->_qname_to_attr[$qname] ); | |
} | |
return; | |
} | |
Util::error( 'NotFoundError' ); | |
} | |
/* | |
* DOM-LS Methods | |
*/ | |
/** @inheritDoc */ | |
public function getLength(): int { | |
return count( $this->_index_to_attr ); | |
} | |
/** @inheritDoc */ | |
public function item( int $index ) { | |
return $this->_index_to_attr[$index] ?? null; | |
} | |
/** | |
* Nonstandard. | |
* @inheritDoc | |
*/ | |
public function _hasNamedItem( string $qname ): bool { | |
/* | |
* Per HTML spec, we normalize qname before lookup, | |
* even though XML itself is case-sensitive. | |
*/ | |
if ( !ctype_lower( $qname ) && $this->_element->_isHTMLElement() ) { | |
$qname = Util::ascii_to_lowercase( $qname ); | |
} | |
return isset( $this->_qname_to_attr[$qname] ); | |
} | |
/** | |
* Nonstandard. | |
* @inheritDoc | |
*/ | |
public function _hasNamedItemNS( ?string $ns, string $lname ): bool { | |
$ns = $ns ?? ""; | |
return isset( $this->_lname_to_attr["$ns|$lname"] ); | |
} | |
/** @inheritDoc */ | |
public function getNamedItem( string $qname ) { | |
/* | |
* Per HTML spec, we normalize qname before lookup, | |
* even though XML itself is case-sensitive. | |
*/ | |
if ( !ctype_lower( $qname ) && $this->_element->_isHTMLElement() ) { | |
$qname = Util::ascii_to_lowercase( $qname ); | |
} | |
if ( !isset( $this->_qname_to_attr[$qname] ) ) { | |
return null; | |
} | |
if ( is_array( $this->_qname_to_attr[$qname] ) ) { | |
return $this->_qname_to_attr[$qname][0]; | |
} else { | |
return $this->_qname_to_attr[$qname]; | |
} | |
} | |
/** @inheritDoc */ | |
public function getNamedItemNS( ?string $ns, string $lname ) { | |
$ns = $ns ?? ""; | |
return $this->_lname_to_attr["$ns|$lname"] ?? null; | |
} | |
/** @inheritDoc */ | |
public function setNamedItem( $attr ) { | |
'@phan-var Attr $attr'; // @var Attr $attr | |
$owner = $attr->getOwnerElement(); | |
if ( $owner !== null && $owner !== $this->_element ) { | |
Util::error( "InUseAttributeError" ); | |
} | |
$oldAttr = $this->getNamedItem( $attr->getName() ); | |
if ( $oldAttr == $attr ) { | |
return $attr; | |
} | |
if ( $oldAttr !== null ) { | |
$this->_replace( $attr ); | |
} else { | |
$this->_append( $attr ); | |
} | |
return $oldAttr; | |
} | |
/** @inheritDoc */ | |
public function setNamedItemNS( $attr ) { | |
'@phan-var Attr $attr'; // @var Attr $attr | |
$owner = $attr->getOwnerElement(); | |
if ( $owner !== null && $owner !== $this->_element ) { | |
Util::error( "InUseAttributeError" ); | |
} | |
$oldAttr = $this->getNamedItemNS( $attr->getNamespaceURI(), $attr->getLocalName() ); | |
if ( $oldAttr == $attr ) { | |
return $attr; | |
} | |
if ( $oldAttr !== null ) { | |
$this->_replace( $attr ); | |
} else { | |
$this->_append( $attr ); | |
} | |
return $oldAttr; | |
} | |
/** | |
* Note: qname may be lowercase or normalized in various ways | |
* | |
* @inheritDoc | |
*/ | |
public function removeNamedItem( string $qname ) { | |
$attr = $this->getNamedItem( $qname ); | |
if ( $attr !== null ) { | |
'@phan-var Attr $attr'; // @var Attr $attr | |
$this->_remove( $attr ); | |
} else { | |
Util::error( "NotFoundError" ); | |
// Lie to phan about types since the above should never return | |
'@phan-var Attr $attr'; // @var Attr $attr | |
} | |
return $attr; | |
} | |
/** | |
* Note: lname may be lowercase or normalized in various ways | |
* | |
* @inheritDoc | |
*/ | |
public function removeNamedItemNS( ?string $ns, string $lname ) { | |
$attr = $this->getNamedItemNS( $ns, $lname ); | |
if ( $attr !== null ) { | |
'@phan-var Attr $attr'; // @var Attr $attr | |
$this->_remove( $attr ); | |
} else { | |
Util::error( "NotFoundError" ); | |
// Lie to phan about types since the above should never return | |
'@phan-var Attr $attr'; // @var Attr $attr | |
} | |
return $attr; | |
} | |
} |