Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
15.15% covered (danger)
15.15%
5 / 33
CRAP
22.22% covered (danger)
22.22%
34 / 153
Element
0.00% covered (danger)
0.00%
0 / 1
15.15% covered (danger)
15.15%
5 / 33
4900.65
22.22% covered (danger)
22.22%
34 / 153
 __construct
0.00% covered (danger)
0.00%
0 / 1
15.69
61.54% covered (warning)
61.54%
16 / 26
 getNodeType
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 getNodeName
100.00% covered (success)
100.00%
1 / 1
1
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
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getNamespaceURI
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
1 / 1
 getTagName
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 id
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 3
 getTextContent
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 3
 setTextContent
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 5
 _subclass_cloneNodeShallow
0.00% covered (danger)
0.00%
0 / 1
56
0.00% covered (danger)
0.00%
0 / 14
 _subclass_isEqualNode
0.00% covered (danger)
0.00%
0 / 1
72
0.00% covered (danger)
0.00%
0 / 11
 getAttribute
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
2 / 2
 setAttribute
0.00% covered (danger)
0.00%
0 / 1
5.20
80.00% covered (warning)
80.00%
8 / 10
 removeAttribute
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 hasAttribute
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 toggleAttribute
0.00% covered (danger)
0.00%
0 / 1
72
0.00% covered (danger)
0.00%
0 / 12
 getAttributeNS
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 2
 setAttributeNS
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 9
 removeAttributeNS
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 2
 hasAttributeNS
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 1
 getAttributeNode
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 1
 setAttributeNode
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 removeAttributeNode
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getAttributeNodeNS
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 1
 setAttributeNodeNS
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 hasAttributes
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getAttributeNames
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 classList
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 5
 matches
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 closest
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 6
 _isHTMLElement
0.00% covered (danger)
0.00%
0 / 1
4.13
80.00% covered (warning)
80.00%
4 / 5
 _nextElement
0.00% covered (danger)
0.00%
0 / 1
72
0.00% covered (danger)
0.00%
0 / 16
<?php
declare( strict_types = 1 );
// @phan-file-suppress PhanParamSignatureMismatch
// @phan-file-suppress PhanParamTooFew
// @phan-file-suppress PhanTypeMismatchArgument
// @phan-file-suppress PhanTypeMismatchArgumentReal
// @phan-file-suppress PhanUndeclaredClassMethod
// @phan-file-suppress PhanUndeclaredMethod
// @phan-file-suppress PhanUndeclaredVariable
// phpcs:disable Generic.NamingConventions.CamelCapsFunctionName.ScopeNotCamelCaps
// phpcs:disable MediaWiki.Commenting.FunctionComment.MissingDocumentationPublic
// phpcs:disable MediaWiki.Commenting.FunctionComment.MissingParamTag
// phpcs:disable MediaWiki.Commenting.FunctionComment.MissingReturn
// phpcs:disable MediaWiki.Commenting.FunctionComment.SpacingAfter
// phpcs:disable MediaWiki.Commenting.FunctionComment.WrongStyle
// phpcs:disable MediaWiki.Commenting.PropertyDocumentation.MissingDocumentationPublic
// phpcs:disable MediaWiki.Commenting.PropertyDocumentation.WrongStyle
namespace Wikimedia\Dodo;
use Wikimedia\Zest\Zest;
/******************************************************************************
 * Element.php
 * -----------
 * Defines an "Element"
 */
/******************************************************************************
 *
 * Where a specification is implemented, the following annotations appear.
 *
 * DOM-1     W3C DOM Level 1              http://w3.org/TR/DOM-Level-1/
 * DOM-2     W3C DOM Level 2 Core         http://w3.org/TR/DOM-Level-2-Core/
 * DOM-3     W3C DOM Level 3 Core         http://w3.org/TR/DOM-Level-3-Core/
 * DOM-4     W3C DOM Level 4              http://w3.org/TR/dom/
 * DOM-LS    WHATWG DOM Living Standard      http://dom.spec.whatwg.org/
 * DOM-PS-WD W3C DOM Parsing & Serialization http://w3.org/TR/DOM-Parsing/
 * WEBIDL-1  W3C WebIDL Level 1                 http://w3.org/TR/WebIDL-1/
 * XML-NS    W3C XML Namespaces             http://w3.org/TR/xml-names/
 * CSS-OM    CSS Object Model                http://drafts.csswg.org/cssom-view/
 * HTML-LS   HTML Living Standard            https://html.spec.whatwg.org/
 *
 */
/*
 * 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.
 */
class Element extends Node implements \Wikimedia\IDLeDOM\Element {
    // DOM mixins
    use ChildNode;
    use NonDocumentTypeChildNode;
    use ParentNode;
    use Slottable;
    // Stub out methods not yet implemented.
    use \Wikimedia\IDLeDOM\Stub\Element;
    use UnimplementedTrait;
    // Helper functions from IDLeDOM
    use \Wikimedia\IDLeDOM\Helper\Element;
    /* Provided by Node
       public $_ownerDocument = NULL;
    */
    // XXX figure out how to save storage by only storing this for
    // HTMLUnknownElement etc; for specific HTML*Element subclasses
    // we should be able to get this from the object type.
    /** @var string */
    public $_nodeName; // HTML-uppercased qualified name
    /* Required by Element */
    public $_namespaceURI = null;
    public $_localName = null;
    public $_prefix = null;
    /* Actually attached without an accessor */
    public $attributes = null;
    /* Watch these attributes */
    public $__onchange_attr = [];
    /*
    * OPTIMIZATION: When we create a DOM tree, we will likely create many
    * elements with the same tag name / qualified name, and thus need to
    * repeatedly convert those strings to ASCII uppercase to form the
    * HTML-uppercased qualified name.
    *
    * This table caches the results of that ASCII uppercase conversion,
    * turning subsequent calls into O(1) table lookups.
    *
    * @var array<string,string>
    */
    private static $UC_Cache = [];
    /**
     * Element constructor
     *
     * @param Document $doc
     * @param string $lname
     * @param ?string $ns
     * @param ?string $prefix
     * @return void
     */
    public function __construct( Document $doc, string $lname, ?string $ns, ?string $prefix = null ) {
        parent::__construct();
        $this->__onchange_attr = [
            "id" => function ( $elem, $old, $new ) {
                if ( !$elem->__is_rooted() ) {
                    return;
                }
                if ( $old ) {
                    $elem->_ownerDocument->__remove_from_id_table( $old, $elem );
                }
                if ( $new ) {
                    $elem->_ownerDocument->__add_to_id_table( $new, $elem );
                }
            },
            "class" => function ( $elem, $old, $new ) {
                if ( $elem->_classList ) {
                    $elem->_classList->_update();
                }
            }
        ];
        /*
         * TODO
         * DOM-LS: "Elements have an associated namespace, namespace
         * prefix, local name, custom element state, custom element
         * definition, is value. When an element is created, all of
         * these values are initialized.
         */
        $this->_namespaceURI  = $ns;
        $this->_prefix        = $prefix;
        $this->_localName     = $lname;
        $this->_ownerDocument = $doc;
        /*
         * DOM-LS: "An Element'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."
         */
        $qname = ( $prefix === null ) ? $lname : "$prefix:$lname";
        /*
         * DOM-LS: "An element's tagName is its HTML-uppercased
         * qualified name".
         *
         * DOM-LS: "If an Element is in the HTML namespace and its node
         * document is an HTML document, then its HTML-uppercased
         * qualified name is its qualified name in ASCII uppercase.
         * Otherwise, its HTML-uppercased qualified name is its
         * qualified name."
         */
        if ( $this->_isHTMLElement() ) {
            if ( !isset( self::$UC_Cache[$qname] ) ) {
                $uc_qname = Util::ascii_to_uppercase( $qname );
                self::$UC_Cache[$qname] = $uc_qname;
            } else {
                $uc_qname = self::$UC_Cache[$qname];
            }
        } else {
            /* If not an HTML element, don't uppercase. */
            $uc_qname = $qname;
        }
        /*
         * DOM-LS: "User agents could optimize qualified name and
         * HTML-uppercased qualified name by storing them in internal
         * slots."
         */
        $this->_nodeName = $uc_qname;
        /*
         * DOM-LS: "Elements also have an attribute list, which is
         * a list exposed through a NamedNodeMap. Unless explicitly
         * given when an element is created, its attribute list is
         * empty."
         */
        $this->attributes = new NamedNodeMap( $this );
    }
    /**********************************************************************
     * ACCESSORS
     */
    /**
     * @inheritDoc
     */
    final public function getNodeType() : int {
        return Node::ELEMENT_NODE;
    }
    /**
     * @inheritDoc
     */
    final public function getNodeName() : string {
        return $this->_nodeName;
    }
    /* TODO: Also in Attr... are they part of Node ? */
    public function getPrefix(): ?string {
        return $this->_prefix;
    }
    /* TODO: Also in Attr... are they part of Node ? */
    public function getLocalName(): string {
        return $this->_localName;
    }
    /* TODO: Also in Attr... are they part of Node ? */
    public function getNamespaceURI(): ?string {
        return $this->_namespaceURI;
    }
    public function getTagName(): string {
        return $this->getNodeName();
    }
    public function id( ?string $v = null ) {
        if ( $v === null ) {
            return $this->getAttribute( "id" );
        } else {
            return $this->setAttribute( "id", $v );
        }
    }
    /** @inheritDoc */
    public function getTextContent() : ?string {
        $text = [];
        Algorithm::descendant_text_content( $this, $text );
        return implode( "", $text );
    }
    /** @inheritDoc */
    public function setTextContent( ?string $value ) : void {
        $value = $value ?? '';
        $this->__remove_children();
        if ( $value !== "" ) {
            /* Equivalent to Node:: appendChild without checks! */
            WhatWG::insert_before_or_replace( $node, $this->_ownerDocument->createTextNode( $value ), null );
        }
    }
    /**********************************************************************
     * METHODS DELEGATED FROM NODE
     */
    public function _subclass_cloneNodeShallow(): ?Node {
        /*
         * XXX:
         * Modify this to use the constructor directly or avoid
         * error checking in some other way. In case we try
         * to clone an invalid node that the parser inserted.
         */
        if ( $this->getNamespaceURI() !== Util::NAMESPACE_HTML
             || $this->getPrefix()
             || !$this->getOwnerDocument()->isHTMLDocument() ) {
            if ( $this->getPrefix() === null ) {
                $name = $this->getLocalName();
            } else {
                $name = $this->getPrefix() . ':' . $this->getLocalName();
            }
            $clone = $this->getOwnerDocument()->createElementNS(
                $this->getNamespaceURI(),
                $name
            );
        } else {
            $clone = $this->getOwnerDocument()->createElement(
                $this->getLocalName()
            );
        }
        '@phan-var Element $clone'; // @var Element $clone
        foreach ( $this->attributes as $a ) {
            $clone->setAttributeNodeNS( $a->cloneNode() );
        }
        return $clone;
    }
    public function _subclass_isEqualNode( Node $node ): bool {
        if ( $this->getLocalName() !== $node->getLocalName()
             || $this->getNamespaceURI() !== $node->getNamespaceURI()
             || $this->getPrefix() !== $node->getPrefix()
             || count( $this->attributes ) !== count( $node->attributes ) ) {
            return false;
        }
        /*
         * Compare the sets of attributes, ignoring order
         * and ignoring attribute prefixes.
         */
        foreach ( $this->attributes as $a ) {
            if ( !$node->hasAttributeNS( $a->getNamespaceURI(), $a->getLocalName() ) ) {
                return false;
            }
            if ( $node->getAttributeNS( $a->getNamespaceURI(), $a->getLocalName() ) !== $a->getValue() ) {
                return false;
            }
        }
        return true;
    }
    /**********************************************************************
     * ATTRIBUTE: get/set/remove/has/toggle
     */
    /**
     * Fetch the value of an attribute with the given qualified name
     *
     * param string $qname The attribute's qualifiedName
     * return ?string the value of the attribute
     */
    public function getAttribute( string $qname ): ?string {
        $attr = $this->attributes->getNamedItem( $qname );
        return $attr ? $attr->getValue() : null;
    }
    /**
     * Set the value of first attribute with a particular qualifiedName
     *
     * spec DOM-LS
     *
     * NOTES
     * Per spec, $value is not a string, but the string value of
     * whatever is passed.
     *
     * TODO: DRY with this and setAttributeNS?
     *
     * @inheritDoc
     */
    public function setAttribute( string $qname, string $value ) : void {
        if ( !WhatWG::is_valid_xml_name( $qname ) ) {
            Util::error( "InvalidCharacterError" );
        }
        if ( !ctype_lower( $qname ) && $this->_isHTMLElement() ) {
            $qname = Util::ascii_to_lowercase( $qname );
        }
        $attr = $this->attributes->getNamedItem( $qname );
        if ( $attr === null ) {
            $attr = new Attr( $this, $qname, null, null );
        }
        $attr->setValue( $value ); /* Triggers __onchange_attr */
        $this->attributes->setNamedItem( $attr );
    }
    /**
     * Remove the first attribute given a particular qualifiedName
     *
     * spec DOM-LS
     *
     */
    public function removeAttribute( string $qname ): void {
        $this->attributes->removeNamedItem( $qname );
    }
    /**
     * Test Element for attribute with the given qualified name
     *
     * spec DOM-LS
     *
     * @param string $qname Qualified name of attribute
     * @return bool
     */
    public function hasAttribute( string $qname ): bool {
        return $this->attributes->hasNamedItem( $qname );
    }
    /**
     * Toggle the first attribute with the given qualified name
     *
     * spec DOM-LS
     *
     * @param string $qname qualified name
     * @param bool|null $force whether to set if no attribute exists
     * @return bool whether we set or removed an attribute
     */
    public function toggleAttribute( string $qname, ?bool $force = null ): bool {
        if ( !WhatWG::is_valid_xml_name( $qname ) ) {
            Util::error( "InvalidCharacterError" );
        }
        $a = $this->attributes->getNamedItem( $qname );
        if ( $a === null ) {
            if ( $force === null || $force === true ) {
                $this->setAttribute( $qname, "" );
                return true;
            }
            return false;
        } else {
            if ( $force === null || $force === false ) {
                $this->removeAttribute( $qname );
                return false;
            }
            return true;
        }
    }
    /**********************************************************************
     * ATTRIBUTE NS: get/set/remove/has
     */
    /**
     * Fetch value of attribute with the given namespace and localName
     *
     * spec DOM-LS
     *
     * @param ?string $ns The attribute's namespace
     * @param string $lname The attribute's local name
     * @return ?string the value of the attribute
     */
    public function getAttributeNS( ?string $ns, string $lname ): ?string {
        $attr = $this->attributes->getNamedItemNS( $ns, $lname );
        return $attr ? $attr->getValue() : null;
    }
    /**
     * Set value of attribute with a particular namespace and localName
     *
     * spec DOM-LS
     *
     * NOTES
     * Per spec, $value is not a string, but the string value of
     * whatever is passed.
     *
     * @inheritDoc
     */
    public function setAttributeNS( ?string $ns, string $qname, string $value ) : void {
        $lname = null;
        $prefix = null;
        WhatWG::validate_and_extract( $ns, $qname, $prefix, $lname );
        $attr = $this->attributes->getNamedItemNS( $ns, $qname );
        if ( $attr === null ) {
            $attr = new Attr( $this, $lname, $prefix, $ns );
        }
        $attr->setValue( $value );
        $this->attributes->setNamedItemNS( $attr );
    }
    /**
     * Remove attribute given a particular namespace and localName
     *
     * spec DOM-LS
     *
     * @inheritDoc
     */
    public function removeAttributeNS( ?string $ns, string $lname ) : void {
        $this->attributes->removeNamedItemNS( $ns, $lname );
    }
    /**
     * Test Element for attribute with the given namespace and localName
     *
     * spec DOM-LS
     *
     * @param ?string $ns the namespace
     * @param string $lname the localName
     * @return bool
     */
    public function hasAttributeNS( ?string $ns, string $lname ): bool {
        return $this->attributes->hasNamedItemNS( $ns, $lname );
    }
    /**********************************************************************
     * ATTRIBUTE NODE: get/set/remove
     */
    /**
     * Fetch the Attr node with the given qualifiedName
     *
     * param string $lname The attribute's local name
     * return ?Attr the attribute node, or NULL
     * spec DOM-LS
     */
    public function getAttributeNode( string $qname ): ?Attr {
        return $this->attributes->getNamedItem( $qname );
    }
    /**
     * Add an Attr node to an Element node
     *
     * @inheritDoc
     */
    public function setAttributeNode( $attr ) {
        return $this->attributes->setNamedItem( $attr );
    }
    /**
     * Remove the given attribute node from this Element
     *
     * spec DOM-LS
     *
     * @inheritDoc
     */
    public function removeAttributeNode( $attr ) {
        /* TODO: This is not a public function */
        $this->attributes->_remove( $attr );
        return $attr;
    }
    /**********************************************************************
     * ATTRIBUTE NODE NS: get/set
     */
    /**
     * Fetch the Attr node with the given namespace and localName
     *
     * spec DOM-LS
     *
     * @param ?string $ns The attribute's local name
     * @param string $lname The attribute's local name
     * @return ?Attr the attribute node, or NULL
     */
    public function getAttributeNodeNS( ?string $ns, string $lname ): ?Attr {
        return $this->attributes->getNamedItemNS( $ns, $lname );
    }
    /**
     * Add a namespace-aware Attr node to an Element node
     *
     * @param Attr $attr
     * @return ?Attr
     */
    public function setAttributeNodeNS( $attr ) {
        return $this->attributes->setNamedItemNS( $attr );
    }
    /*********************************************************************
     * OTHER
     */
    /**
     * Test whether this Element has any attributes
     *
     * spec DOM-LS
     *
     * @return bool
     */
    public function hasAttributes(): bool {
        return !empty( $this->attributes->index_to_attr );
    }
    /**
     * Fetch the qualified names of all attributes on this Element
     *
     * spec DOM-LS
     *
     * NOTE
     * The names are *not* guaranteed to be unique.
     *
     * @return array of strings, or empty array if no attributes.
     */
    public function getAttributeNames(): array {
        /*
         * Note that per spec, these are not guaranteed to be
         * unique.
         */
        $ret = [];
        foreach ( $this->attributes as $a ) {
            $ret[] = $a->getName();
        }
        return $ret;
    }
    public function classList() {
        $self = $this;
        if ( $this->_classList ) {
            return $this->_classList;
        }
        /* TODO: FIx this into PHP
           $dtlist = new DOMTokenList(
           function() {
           return self.className || "";
           },
           function(v) {
           self.className = v;
           }
           );
        */
        $this->_classList = $dtlist;
        return $dtlist;
    }
    /**
     * @param string $selectors
     * @return bool
     */
    public function matches( string $selectors ) : bool {
        return Zest::matches( $this, $selectors );
    }
    /**
     * @param string $selectors
     * @return ?Element
     */
    public function closest( string $selectors ) {
        $el = $this;
        do {
            if ( $el instanceof Element && $el->matches( $selectors ) ) {
                return $el;
            }
            $el = $el->getParentElement() ?? $el->getParentNode();
        } while ( $el !== null && $el->getNodeType() == Node::ELEMENT_NODE );
        return null;
    }
    /*********************************************************************
     * DODO EXTENSIONS
     */
    /* Calls isHTMLDocument() on ownerDocument */
    public function _isHTMLElement() {
        if ( $this->_namespaceURI === Util::NAMESPACE_HTML
             && $this->_ownerDocument
             && $this->_ownerDocument->isHTMLDocument() ) {
            return true;
        }
        return false;
    }
    /*
     * Return the next element, in source order, after this one or
     * null if there are no more.  If root element is specified,
     * then don't traverse beyond its subtree.
     *
     * This is not a DOM method, but is convenient for
     * lazy traversals of the tree.
     * TODO: Change its name to __next_element then!
     */
    public function _nextElement( $root ) {
        if ( !$root ) {
            $root = $this->getOwnerDocument()->getDocumentElement();
        }
        $next = $this->firstElementChild();
        if ( !$next ) {
            /* don't use sibling if we're at root */
            if ( $this === $root ) {
                return null;
            }
            $next = $this->getNextElementSibling();
        }
        if ( $next ) {
            return $next;
        }
        /*
         * If we can't go down or across, then we have to go up
         * and across to the parent sibling or another ancestor's
         * sibling. Be careful, though: if we reach the root
         * element, or if we reach the documentElement, then
         * the traversal ends.
         */
        for (
            $parent = $this->getParentElement();
            $parent && $parent !== $root;
            $parent = $parent->getParentElement()
        ) {
            $next = $parent->getNextElementSibling();
            if ( $next ) {
                return $next;
            }
        }
        return null;
    }
}