Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.71% covered (warning)
85.71%
36 / 42
84.62% covered (warning)
84.62%
11 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
Document
85.71% covered (warning)
85.71%
36 / 42
84.62% covered (warning)
84.62%
11 / 13
36.17
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 nodeType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 nodeName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createElement
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
6
 createTextNode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createComment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createDocumentFragment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getElementsByTagName
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getElementById
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findHtmlChild
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
6.84
 collectByTagName
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
6
 findById
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
6
 shallowClone
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\Html\Dom;
6
7/**
8 * The root of a parsed HTML document.
9 *
10 * Per the contract decision (Q1), Document is mutable post-parse.
11 */
12final class Document extends Node
13{
14    public const string HTML_NS = 'http://www.w3.org/1999/xhtml';
15    public const string SVG_NS = 'http://www.w3.org/2000/svg';
16    public const string MATHML_NS = 'http://www.w3.org/1998/Math/MathML';
17    public const string XML_NS = 'http://www.w3.org/XML/1998/namespace';
18    public const string XMLNS_NS = 'http://www.w3.org/2000/xmlns/';
19    public const string XLINK_NS = 'http://www.w3.org/1999/xlink';
20
21    public DocumentMode $mode = DocumentMode::NoQuirks;
22    public string $characterSet = 'UTF-8';
23
24    public ?DocumentType $doctype {
25        get {
26            for ($n = $this->firstChild; $n !== null; $n = $n->nextSibling) {
27                if ($n instanceof DocumentType) {
28                    return $n;
29                }
30            }
31            return null;
32        }
33    }
34
35    public ?Element $documentElement {
36        get {
37            for ($n = $this->firstChild; $n !== null; $n = $n->nextSibling) {
38                if ($n instanceof Element) {
39                    return $n;
40                }
41            }
42            return null;
43        }
44    }
45
46    public ?Element $head {
47        get => $this->findHtmlChild('head');
48    }
49
50    public ?Element $body {
51        get => $this->findHtmlChild('body');
52    }
53
54    public ?string $title {
55        get {
56            $head = $this->head;
57            if ($head === null) {
58                return null;
59            }
60            for ($n = $head->firstChild; $n !== null; $n = $n->nextSibling) {
61                if ($n instanceof Element && $n->localName === 'title') {
62                    return $n->textContent();
63                }
64            }
65            return null;
66        }
67    }
68
69    public function __construct()
70    {
71        // Document has no owner — passing null is allowed; the ownerDocument
72        // accessor on Node returns $this for Document instances.
73        parent::__construct(null);
74    }
75
76    public function nodeType(): NodeType
77    {
78        return NodeType::Document;
79    }
80
81    public function nodeName(): string
82    {
83        return '#document';
84    }
85
86    public function createElement(string $localName, string $namespace = self::HTML_NS): Element
87    {
88        // Only lower-case for HTML — SVG and MathML are case-sensitive
89        // (linearGradient, foreignObject, etc.). The parser hands us the
90        // canonical case in those namespaces via its case-correction tables.
91        $name = $namespace === self::HTML_NS ? strtolower($localName) : $localName;
92        // Dispatch to specialised element classes for HTML elements with
93        // dedicated DOM behaviour (slot distribution, template content).
94        if ($namespace === self::HTML_NS) {
95            return match ($name) {
96                'template' => new HTMLTemplateElement($this, $name, $namespace),
97                'slot' => new HTMLSlotElement($this, $name, $namespace),
98                default => new Element($this, $name, $namespace),
99            };
100        }
101        return new Element($this, $name, $namespace);
102    }
103
104    public function createTextNode(string $data): Text
105    {
106        return new Text($this, $data);
107    }
108
109    public function createComment(string $data): Comment
110    {
111        return new Comment($this, $data);
112    }
113
114    public function createDocumentFragment(): DocumentFragment
115    {
116        return new DocumentFragment($this);
117    }
118
119    /**
120     * Find descendants by lower-cased HTML tag name. Depth-first traversal.
121     *
122     * @return list<Element>
123     */
124    public function getElementsByTagName(string $localName): array
125    {
126        $localName = strtolower($localName);
127        $out = [];
128        $this->collectByTagName($this, $localName, $out);
129        return $out;
130    }
131
132    public function getElementById(string $id): ?Element
133    {
134        return $this->findById($this, $id);
135    }
136
137    private function findHtmlChild(string $localName): ?Element
138    {
139        $root = $this->documentElement;
140        if ($root === null) {
141            return null;
142        }
143        for ($n = $root->firstChild; $n !== null; $n = $n->nextSibling) {
144            if ($n instanceof Element && $n->localName === $localName && $n->namespaceURI === self::HTML_NS) {
145                return $n;
146            }
147        }
148        return null;
149    }
150
151    /** @param list<Element> $out */
152    private function collectByTagName(Node $scope, string $localName, array &$out): void
153    {
154        for ($n = $scope->firstChild; $n !== null; $n = $n->nextSibling) {
155            if ($n instanceof Element && ($localName === '*' || $n->localName === $localName)) {
156                $out[] = $n;
157            }
158            if ($n->hasChildNodes()) {
159                $this->collectByTagName($n, $localName, $out);
160            }
161        }
162    }
163
164    private function findById(Node $scope, string $id): ?Element
165    {
166        for ($n = $scope->firstChild; $n !== null; $n = $n->nextSibling) {
167            if ($n instanceof Element && $n->id === $id) {
168                return $n;
169            }
170            if ($n->hasChildNodes()) {
171                $found = $this->findById($n, $id);
172                if ($found !== null) {
173                    return $found;
174                }
175            }
176        }
177        return null;
178    }
179
180    protected function shallowClone(): static
181    {
182        $copy = new self();
183        $copy->mode = $this->mode;
184        $copy->characterSet = $this->characterSet;
185        /** @var static $copy */
186        return $copy;
187    }
188}