Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
29.41% covered (danger)
29.41%
5 / 17
CRAP
52.00% covered (warning)
52.00%
26 / 50
Attr
0.00% covered (danger)
0.00%
0 / 1
29.41% covered (danger)
29.41%
5 / 17
188.40
52.00% covered (warning)
52.00%
26 / 50
 __construct
0.00% covered (danger)
0.00%
0 / 1
7.23
83.33% covered (warning)
83.33%
10 / 12
 getNodeType
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getNodeName
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getNodeValue
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 1
 setNodeValue
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 2
 getNamespaceURI
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
1 / 1
 getSpecified
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getOwnerElement
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
1 / 1
 getPrefix
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 1
 getLocalName
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 getName
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 getValue
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 setValue
0.00% covered (danger)
0.00%
0 / 1
5.25
78.57% covered (warning)
78.57%
11 / 14
 getTextContent
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 1
 setTextContent
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 2
 _subclass_cloneNodeShallow
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 6
 _subclass_isEqualNode
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 3
<?php
declare( strict_types = 1 );
// phpcs:disable Generic.NamingConventions.CamelCapsFunctionName.ScopeNotCamelCaps
namespace Wikimedia\Dodo;
use Exception;
/**
 * Attr.php
 * --------
 * The Attr class represents a single attribute node.
 *
 * NOTE
 * The definition of the Attr class has undergone some changes in recent
 * revisions of the DOM spec.
 *
 *      DOM-2: Introduced namespaces, and the properties 'namespaceURI',
 *             'localName', and 'prefix' were defined on the Node class.
 *             As a subclass of Node, Attr inherited these.
 *
 *      DOM-4: Attr was no longer classified as a Node. The properties
 *             'namespaceURI', 'localName', and 'prefix' were removed
 *             from the Node class. They were now defined on the Attr
 *             class itself, as well as on the Element class, which
 *             remained a subclass of Node.
 *
 *      DOM-LS: Attr was re-classified as a Node, but the properties
 *              'namespaceURI', 'localName', and 'prefix' remained on
 *              the Attr class (and Element class), and did not re-appear
 *              on the Node class..
 */
/*
 * Qualified Names, Local Names, and Namespace Prefixes
 *
 * An Element or Attribute's qualified name is its local name if its
 * namespace prefix is null, and its namespace prefix, followed by ":",
 * followed by its local name, otherwise.
 */
/*
 * NOTES (taken from Domino.js)
 *
 * Attributes in the DOM are tricky:
 *
 * - there are the 8 basic get/set/has/removeAttribute{NS} methods
 *
 * - but many HTML attributes are also 'reflected' through IDL
 *   attributes which means that they can be queried and set through
 *   regular properties of the element.  There is just one attribute
 *   value, but two ways to get and set it.
 *
 * - Different HTML element types have different sets of reflected
 *   attributes.
 *
 * - attributes can also be queried and set through the .attributes
 *   property of an element.  This property behaves like an array of
 *   Attr objects.  The value property of each Attr is writeable, so
 *   this is a third way to read and write attributes.
 *
 * - for efficiency, we really want to store attributes in some kind
 *   of name->attr map.  But the attributes[] array is an array, not a
 *   map, which is kind of unnatural.
 *
 * - When using namespaces and prefixes, and mixing the NS methods
 *   with the non-NS methods, it is apparently actually possible for
 *   an attributes[] array to have more than one attribute with the
 *   same qualified name.  And certain methods must operate on only
 *   the first attribute with such a name.  So for these methods, an
 *   inefficient array-like data structure would be easier to
 *   implement.
 *
 * - The attributes[] array is live, not a snapshot, so changes to the
 *   attributes must be immediately visible through existing arrays.
 *
 * - When attributes are queried and set through IDL properties
 *   (instead of the get/setAttributes() method or the attributes[]
 *   array) they may be subject to type conversions, URL
 *   normalization, etc., so some extra processing is required in that
 *   case.
 *
 * - But access through IDL properties is probably the most common
 *   case, so we'd like that to be as fast as possible.
 *
 * - We can't just store attribute values in their parsed idl form,
 *   because setAttribute() has to return whatever string is passed to
 *   getAttribute even if it is not a legal, parseable value. So
 *   attribute values must be stored in unparsed string form.
 *
 * - We need to be able to send change notifications or mutation
 *   events of some sort to the renderer whenever an attribute value
 *   changes, regardless of the way in which it changes.
 *
 * - Some attributes, such as id and class affect other parts of the
 *   DOM API, like getElementById and getElementsByClassName and so
 *   for efficiency, we need to specially track changes to these
 *   special attributes.
 *
 * - Some attributes like class have different names (className) when
 *   reflected.
 *
 * - Attributes whose names begin with the string 'data-' are treated
 *   specially.
 *
 * - Reflected attributes that have a boolean type in IDL have special
 *   behavior: setting them to false (in IDL) is the same as removing
 *   them with removeAttribute()
 *
 * - numeric attributes (like HTMLElement.tabIndex) can have default
 *   values that must be returned by the idl getter even if the
 *   content attribute does not exist. (The default tabIndex value
 *   actually varies based on the type of the element, so that is a
 *   tricky one).
 *
 * See
 * http://www.whatwg.org/specs/web-apps/current-work/multipage/urls.html#reflect
 * for rules on how attributes are reflected.
 */
/*
 * SPEC NOTE
 * Attr has gone back and forth between
 * extending Node and being its own
 * class in recent specs. As of the
 * most recent DOM-LS at the time of this
 * writing (2021-02-11), it extends Node.
 */
class Attr extends Node implements \Wikimedia\IDLeDOM\Attr {
    // Stub out methods not yet implemented.
    use \Wikimedia\IDLeDOM\Stub\Attr;
    use UnimplementedTrait;
    // Helper functions from IDLeDOM
    use \Wikimedia\IDLeDOM\Helper\Attr;
    /**
     * @var string|null
     * Should be considered readonly, if a string its non-empty
     */
    protected $_namespaceURI = null;
    /**
     * @var string|null
     * Should be considered readonly, if a string its non-empty
     */
    protected $_prefix = null;
    /**
     * @var string
     * Should be considered readonly, always non-empty
     */
    protected $_localName = '';
    /**
     * @var string
     * Should be considered readonly, always non-empty
     */
    protected $_name = '';
    /** @var string */
    protected $_value = '';
    /**
     * @var Element|null
     * Should be considered readonly
     */
    protected $_ownerElement = null;
    /**
     * @var bool
     * Should be considered readonly, always true - TODO make a constant
     */
    protected $_specified = true; /* readonly const true */
    /**
     * @param ?Element $ownerElement
     * @param string $localName
     * @param ?string $prefix
     * @param ?string $namespaceURI
     * @param string $value
     */
    public function __construct(
        ?Element $ownerElement,
        string $localName,
        ?string $prefix = null,
        ?string $namespaceURI = null,
        string $value = ""
    ) {
        if ( $localName !== '' ) {
            /* DOM-LS: Non-empty string */
            $this->_localName = $localName;
        } else {
            throw new Exception( "Attr local name must be non-empty" );
        }
        if ( $namespaceURI !== '' ) {
            /* DOM-LS: NULL or non-empty string */
            $this->_namespaceURI = $namespaceURI;
        }
        if ( $prefix !== '' ) {
            /* DOM-LS: null or non-empty string */
            $this->_prefix = $prefix;
            /* DOM-LS: qualified name:
             *      namespace prefix, followed by ":",
             *      followed by local name, if prefix is not null.
             */
            $this->_name = "$prefix:$localName";
        } else {
            /* DOM-LS: qualified name: localName if prefix is null */
            $this->_name = $localName;
        }
        /* DOM-LS: null or Element */
        $this->_ownerElement = $ownerElement;
        /* DOM-LS: string */
        $this->_value = $value;
    }
    /*
     * ACCESSORS
     */
    /**
     * @copydoc Node::getNodeType()
     * @inheritDoc
     */
    final public function getNodeType() : int {
        return Node::ATTRIBUTE_NODE;
    }
    /**
     * @copydoc Node::getNodeName()
     * @inheritDoc
     */
    final public function getNodeName() : string {
        return $this->_name;
    }
    /** @inheritDoc */
    final public function getNodeValue() : ?string {
        return $this->_value;
    }
    /** @inheritDoc */
    final public function setNodeValue( ?string $value ) : void {
        $this->setValue( $value ?? '' );
    }
    /** @inheritDoc */
    public function getNamespaceURI(): ?string {
        return $this->_namespaceURI;
    }
    /** @inheritDoc */
    public function getSpecified(): bool {
        return $this->_specified;
    }
    /** @inheritDoc */
    public function getOwnerElement(): ?Element {
        return $this->_ownerElement;
    }
    /** @inheritDoc */
    public function getPrefix(): ?string {
        return $this->_prefix;
    }
    /** @inheritDoc */
    public function getLocalName(): string {
        return $this->_localName;
    }
    /** @inheritDoc */
    public function getName(): string {
        return $this->_name;
    }
    /** @inheritDoc */
    public function getValue() : string {
        return $this->_value;
    }
    /** @inheritDoc */
    public function setValue( string $value = null ) : void {
        /*
         * NOTE
         * You can unset an attribute by calling Attr::value("");
         */
        if ( $this->_value === $value ) {
            return;
        }
        $old = $this->_value;
        $this->_value = $value;
        if ( $this->_ownerElement
             && ( isset( $this->_ownerElement->__onchange_attr[$this->_localName] ) )
        ) {
            /*
             * Elements must take special action if the
             * value of certain attributes are updated.
             * This allows the Attr to inform the Element
             * it has been updated, so the Element can
             * take the appropriate steps.
             *
             * For example, updating the 'id' attribute
             * will cause a rooted Element to delete its
             * old id from and add its new id to its
             * ownerDocument's node id cache.
             *
             * WARNING: This is only fired when we modify
             * the attribute using .value(). This is not
             * fired when we call Element::removeAttribute,
             * but that's okay for 'id' and 'class'.
             */
            $this->_ownerElement->__onchange_attr[$this->_localName](
                $this->_ownerElement,
                $old,
                $value
            );
        }
        if ( $this->_ownerElement->__is_rooted() ) {
            /*
             * Documents must also sometimes take special action
             * and be aware of mutations occurring in their tree.
             * These methods are for that.
             *
             * WARNING: This is only fired when we modify
             * the attribute using .value(). This is not
             * fired when we call Element::removeAttribute,
             * but that's okay for 'id' and 'class'.
             *
             * TODO: These two mutation handling things
             * should be combined.
             *
             * TODO: Is this trying to implement spec,
             * or are we just doing this for our own use?
             */
            $doc = $this->_ownerElement->getOwnerDocument();
            '@phan-var Document $doc'; // @var Document $doc
            $doc->__mutate_attr( $this, $old );
        }
    }
    /**
     * @inheritDoc
     */
    public function getTextContent() : ?string {
        return $this->getValue();
    }
    /**
     * @inheritDoc
     */
    public function setTextContent( ?string $val ) : void {
        $this->setValue( $val ?? '' );
    }
    /**
     * Delegated from Node
     *
     * @return ?Node always Attr
     */
    protected function _subclass_cloneNodeShallow(): ?Node {
        return new Attr(
            null,
            $this->_localName,
            $this->_prefix,
            $this->_namespaceURI,
            $this->_value
        );
    }
    /**
     * Delegated from Node
     *
     * @param Node $node
     * @return bool
     */
    protected function _subclass_isEqualNode( Node $node ): bool {
        '@phan-var Attr $node';
        /** @var Attr $node */
        return (
            $this->_namespaceURI === $node->_namespaceURI
            && $this->_localName === $node->_localName
            && $this->_value === $node->_value
        );
    }
}