Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
| Total | |
100.00% |
1 / 1 |
|
100.00% |
22 / 22 |
CRAP | |
100.00% |
91 / 91 |
| SDom\Node\Element | |
100.00% |
1 / 1 |
|
100.00% |
22 / 22 |
47 | |
100.00% |
91 / 91 |
| __construct | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
| __toString | |
100.00% |
1 / 1 |
5 | |
100.00% |
12 / 12 |
|||
| __clone | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| getTag | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| hasAttribute | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| setAttribute | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
| getAttribute | |
100.00% |
1 / 1 |
3 | |
100.00% |
3 / 3 |
|||
| removeAttribute | |
100.00% |
1 / 1 |
2 | |
100.00% |
3 / 3 |
|||
| parent | |
100.00% |
1 / 1 |
2 | |
100.00% |
1 / 1 |
|||
| attach | |
100.00% |
1 / 1 |
4 | |
100.00% |
14 / 14 |
|||
| detach | |
100.00% |
1 / 1 |
3 | |
100.00% |
6 / 6 |
|||
| clone | |
100.00% |
1 / 1 |
3 | |
100.00% |
6 / 6 |
|||
| getIterator | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| count | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| insertAfter | |
100.00% |
1 / 1 |
3 | |
100.00% |
7 / 7 |
|||
| insertBefore | |
100.00% |
1 / 1 |
3 | |
100.00% |
8 / 8 |
|||
| isChild | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| get | |
100.00% |
1 / 1 |
4 | |
100.00% |
7 / 7 |
|||
| index | |
100.00% |
1 / 1 |
2 | |
100.00% |
4 / 4 |
|||
| removeChild | |
100.00% |
1 / 1 |
2 | |
100.00% |
6 / 6 |
|||
| clear | |
100.00% |
1 / 1 |
2 | |
100.00% |
3 / 3 |
|||
| isVoid | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| <?php | |
| namespace SDom\Node; | |
| /** | |
| * Node representing an HTML element. | |
| * | |
| * Class Element | |
| * @package SDom\Node | |
| */ | |
| class Element implements | |
| NodeInterface, | |
| \IteratorAggregate, | |
| \Countable | |
| { | |
| /** | |
| * @var string[] | |
| */ | |
| protected static $void = [ | |
| 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr' | |
| ]; | |
| /** | |
| * @var string | |
| */ | |
| protected $tag; | |
| /** | |
| * @var Element | |
| */ | |
| protected $parent; | |
| /** | |
| * @var string[] | |
| */ | |
| protected $attributes = []; | |
| /** | |
| * @var NodeInterface[] | |
| */ | |
| protected $children = []; | |
| /** | |
| * Element constructor. | |
| * | |
| * @param string $tag | |
| */ | |
| public function __construct(string $tag) | |
| { | |
| $this->tag = strtolower($tag); | |
| } | |
| /** | |
| * @inheritDoc | |
| */ | |
| public function __toString(): string | |
| { | |
| $html = '<' . $this->tag; | |
| /** | |
| * @var string $key | |
| * @var string $value | |
| */ | |
| foreach ($this->attributes as $key => $value) { | |
| $html .= ' ' . htmlspecialchars($key); | |
| if ('' !== $value) { | |
| $html .= '="' . htmlspecialchars($value, ENT_QUOTES) . '"'; | |
| } | |
| } | |
| if ($this->isVoid()) { | |
| $html .= '/>'; | |
| } else { | |
| $html .= '>'; | |
| foreach ($this->children as $child) { | |
| $html .= (string) $child; | |
| } | |
| $html .= '</' . $this->tag . '>'; | |
| } | |
| return $html; | |
| } | |
| /** | |
| * @inheritDoc | |
| */ | |
| public function __clone() | |
| { | |
| throw new \BadMethodCallException('Native cloning is not allowed, use clone() instead.'); | |
| } | |
| /** | |
| * Retrieve the tag name of the element. | |
| * | |
| * @return string | |
| */ | |
| public function getTag(): string | |
| { | |
| return $this->tag; | |
| } | |
| /** | |
| * Return TRUE if the specified name exists as attribute. | |
| * The attribute name is lowercased. | |
| * | |
| * @param string $name | |
| * @return bool | |
| */ | |
| public function hasAttribute(string $name): bool | |
| { | |
| return array_key_exists(strtolower($name), $this->attributes); | |
| } | |
| /** | |
| * Set the specified value for the specified attribute name. | |
| * Attributes with no value, or an empty string as value are rendered without the ="..." part. | |
| * The attribute name is lowercased. | |
| * | |
| * @param string $name | |
| * @param string $value | |
| * @return Element | |
| */ | |
| public function setAttribute(string $name, string $value = ''): Element | |
| { | |
| $this->attributes[strtolower($name)] = $value; | |
| return $this; | |
| } | |
| /** | |
| * Retrieve the value of the specified attribute name, or NULL if the attribute does not exist. | |
| * The attribute name is lowercased. | |
| * | |
| * @param string $name | |
| * @return null|string | |
| */ | |
| public function getAttribute(string $name): ?string | |
| { | |
| if (!$this->hasAttribute($name)) { | |
| return null; | |
| } | |
| return $this->attributes[strtolower($name)]; | |
| } | |
| /** | |
| * Remove an attribute with the specified name. | |
| * The attribute name is lowercased. | |
| * | |
| * @param string $name | |
| * @return Element | |
| */ | |
| public function removeAttribute(string $name): Element | |
| { | |
| if ($this->hasAttribute($name)) { | |
| unset($this->attributes[strtolower($name)]); | |
| } | |
| return $this; | |
| } | |
| /** | |
| * @inheritDoc | |
| */ | |
| public function parent(): ?NodeInterface | |
| { | |
| return $this->parent; | |
| } | |
| /** | |
| * @inheritDoc | |
| */ | |
| public function attach(NodeInterface $parent): NodeInterface | |
| { | |
| if (!$parent instanceof self) { | |
| throw new \InvalidArgumentException(sprintf( | |
| 'Only node of type %s can be parent to a %s node.', | |
| Element::class, | |
| self::class | |
| )); | |
| } | |
| if ($parent->isVoid()) { | |
| throw new \InvalidArgumentException(sprintf( | |
| 'Node of type %s (void) cannot be parent to a %s node.', | |
| Element::class, | |
| self::class | |
| )); | |
| } | |
| if (isset($this->parent)) { | |
| $this->parent->removeChild($this); | |
| } | |
| $this->parent = $parent; | |
| return $this; | |
| } | |
| /** | |
| * @inheritDoc | |
| */ | |
| public function detach(): NodeInterface | |
| { | |
| if (isset($this->parent)) { | |
| $parent = $this->parent; | |
| $this->parent = null; | |
| // if detach() is called from a removeChild(), then isChild will fail - disregarding it will cause recursion | |
| if ($parent->isChild($this)) { | |
| $parent->removeChild($this); | |
| } | |
| } | |
| return $this; | |
| } | |
| /** | |
| * @inheritDoc | |
| */ | |
| public function clone(): NodeInterface | |
| { | |
| $clone = new static($this->tag); | |
| /** | |
| * Inherit attributes. | |
| * | |
| * @var string $name | |
| * @var string $value | |
| */ | |
| foreach ($this->attributes as $name => $value) { | |
| $clone->setAttribute($name, $value); | |
| } | |
| /** | |
| * Inherit cloned child nodes. | |
| * | |
| * @var int $index | |
| * @var NodeInterface $child | |
| */ | |
| foreach ($this->children as $index => $child) { | |
| $clone->insertAfter($child->clone()); | |
| } | |
| return $clone; | |
| } | |
| /** | |
| * @inheritDoc | |
| */ | |
| public function getIterator() | |
| { | |
| return new \ArrayIterator($this->children); | |
| } | |
| /** | |
| * @inheritDoc | |
| */ | |
| public function count() | |
| { | |
| return count($this->children); | |
| } | |
| /** | |
| * Insert content at the end of the list of child nodes, or after the specified target node. | |
| * If the target node is not an immediate child node of this one, an exception will be thrown. | |
| * Attach this node as parent to the inserted node. | |
| * | |
| * @param NodeInterface $node | |
| * @param NodeInterface|null $after | |
| * @return Element | |
| */ | |
| public function insertAfter(NodeInterface $node, NodeInterface $after = null): Element | |
| { | |
| $index = count($this->children); | |
| if (isset($after)) { | |
| $index = array_search($after, $this->children, true); | |
| if (false === $index) { | |
| throw new \InvalidArgumentException('Only immediate child nodes can be used as insertAfter anchor.'); | |
| } | |
| } | |
| array_splice($this->children, $index + 1, 0, [$node->attach($this)]); | |
| return $this; | |
| } | |
| /** | |
| * Insert content at the beginning of the list of child nodes, or before the specified target node. | |
| * If the target node is not an immediate child node of this one, an exception will be thrown. | |
| * Attach this node as parent to the inserted node. | |
| * | |
| * @param NodeInterface $node | |
| * @param NodeInterface|null $before | |
| * @return Element | |
| */ | |
| public function insertBefore(NodeInterface $node, NodeInterface $before = null): Element | |
| { | |
| $index = 0; | |
| if (isset($before)) { | |
| $index = array_search($before, $this->children, true); | |
| if (false === $index) { | |
| throw new \InvalidArgumentException('Only immediate child nodes can be used as insertBefore anchor.'); | |
| } | |
| $index = (int) $index; | |
| } | |
| array_splice($this->children, $index, 0, [$node->attach($this)]); | |
| return $this; | |
| } | |
| /** | |
| * Returns TRUE if the specified node is an immediate child of the current node. | |
| * | |
| * @param NodeInterface $node | |
| * @return bool | |
| */ | |
| public function isChild(NodeInterface $node): bool | |
| { | |
| return in_array($node, $this->children); | |
| } | |
| /** | |
| * Retrieve a NodeInterface instance (immediate child node) for the specified index. | |
| * Throw \OutOfBoundsException exception if the specified index is out of bounds. | |
| * | |
| * @param int $index | |
| * @return NodeInterface | |
| * @throws \OutOfBoundsException | |
| */ | |
| public function get(int $index): NodeInterface | |
| { | |
| $count = count($this); | |
| if ($index < 0 || $index >= $count) { | |
| throw new \OutOfBoundsException(sprintf( | |
| 'The requested node index %d is out of the child list bounds [%s].', | |
| $index, | |
| 0 < $count ? '[0; ' . ($count - 1) . ']' : '(empty child list)' | |
| )); | |
| } | |
| return $this->children[$index]; | |
| } | |
| /** | |
| * Retrieve the positional index of the specified NodeInterface in the list of immediate child nodes. | |
| * If the target node is not an immediate child node of this one, an exception will be thrown. | |
| * | |
| * @param NodeInterface $node | |
| * @return int | |
| * @throws \InvalidArgumentException | |
| */ | |
| public function index(NodeInterface $node): int | |
| { | |
| $index = array_search($node, $this->children, true); | |
| if (false === $index) { | |
| throw new \InvalidArgumentException('The specified node is not an immediate child node.'); | |
| } | |
| return (int) $index; | |
| } | |
| /** | |
| * Remove the specified node from the list of immediate children of this node. | |
| * If the target node is not an immediate child node of this one, an exception will be thrown. | |
| * The node's detach() method will also be called to release the parent reference if such is set. | |
| * | |
| * @param NodeInterface $node | |
| * @return Element | |
| */ | |
| public function removeChild(NodeInterface $node): Element | |
| { | |
| $index = $this->index($node); | |
| $child = $this->children[$index]; | |
| array_splice($this->children, $index, 1); | |
| if (null !== $child->parent()) { | |
| $child->detach(); | |
| } | |
| return $this; | |
| } | |
| /** | |
| * Remove all child nodes. | |
| * | |
| * @return Element | |
| */ | |
| public function clear(): Element | |
| { | |
| /** @var NodeInterface $node */ | |
| foreach ($this as $node) { | |
| $this->removeChild($node); | |
| } | |
| return $this; | |
| } | |
| /** | |
| * Returns TRUE if the element's tag matches the list of void element tags. | |
| * | |
| * @return bool | |
| */ | |
| public function isVoid(): bool | |
| { | |
| return in_array($this->tag, static::$void); | |
| } | |
| } |