-
-
Notifications
You must be signed in to change notification settings - Fork 28
/
Copy pathDocument.php
366 lines (341 loc) · 13 KB
/
Document.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
<?php
namespace Gt\Dom;
use DOMAttr;
use DOMCdataSection;
use DOMCharacterData;
use DOMComment;
use DOMDocument;
use DOMDocumentFragment;
use DOMDocumentType;
use DOMElement;
use DOMEntity;
use DOMEntityReference;
use DOMException;
use DOMNode;
use DOMNotation;
use DOMProcessingInstruction;
use DOMText;
use Gt\Dom\Exception\HTMLDocumentDoesNotSupportCDATASectionException;
use Gt\Dom\Exception\InvalidCharacterException;
use Gt\Dom\Exception\NotSupportedException;
use Psr\Http\Message\StreamInterface;
use Stringable;
/**
* @property-read ?Element $documentElement Returns the Element that is a direct child of the document. For HTML documents, this is normally the HTMLHtmlElement object representing the document's <html> element.
* @property-read DocumentType $doctype Returns the Document Type Definition (DTD) of the current document.
*
* @method Element importNode(Node|Element $node, bool $deep = false)
* @method DocumentFragment createDocumentFragment()
* @method Text createTextNode(string $data)
*/
abstract class Document extends DOMDocument implements Stringable, StreamInterface {
use DocumentStream;
use ParentNode;
use RegisteredNodeClass;
const NODE_CLASS_LOOKUP = [
DOMDocument::class => self::class,
DOMAttr::class => Attr::class,
DOMCdataSection::class => CdataSection::class,
DOMCharacterData::class => CharacterData::class,
DOMComment::class => Comment::class,
DOMDocumentFragment::class => DocumentFragment::class,
DOMDocumentType::class => DocumentType::class,
DOMElement::class => Element::class,
DOMEntity::class => Entity::class,
DOMEntityReference::class => EntityReference::class,
DOMNode::class => Node::class,
DOMNotation::class => Notation::class,
DOMText::class => Text::class,
DOMProcessingInstruction::class => ProcessingInstruction::class,
];
const DOCTYPE = "<!doctype html>";
public function __construct(
public readonly string $characterSet,
public readonly string $contentType,
) {
parent::__construct("1.0", $this->characterSet);
$this->encoding = $this->characterSet;
$this->substituteEntities = true;
$this->registerNodeClasses();
libxml_use_internal_errors(true);
}
public function __toString():string {
if(get_class($this) === HTMLDocument::class) {
$string = self::DOCTYPE . "\n";
$string .= $this->saveHTML($this->documentElement);
}
else {
$string = $this->saveXML();
}
return trim($string) . "\n";
}
/**
* The Document.createAttribute() method creates a new attribute node,
* and returns it. The object created a node implementing the Attr
* interface. The DOM does not enforce what sort of attributes can be
* added to a particular element in this manner.
*
* @param string $localName name is a string containing the name of the
* attribute.
* @return Attr A Attr node.
* @throws InvalidCharacterException if the parameter contains invalid
* characters for XML attribute
* @link https://developer.mozilla.org/en-US/docs/Web/API/Document/createAttribute
*/
public function createAttribute(string $localName = ""):Attr {
/** @var Attr $attr */
$attr = parent::createAttribute($localName);
return $attr;
}
/**
* Currently undocumented at MDN. Please see W3C spec instead.
* @link https://dom.spec.whatwg.org/#dom-document-createattributens
*
* @param ?string $namespace
* @param string $qualifiedName
* @return Attr
*/
public function createAttributeNS(
?string $namespace,
string $qualifiedName
):Attr {
/** @var Attr $attr */
$attr = parent::createAttributeNS($namespace, $qualifiedName);
return $attr;
}
/**
* Creates a new CDATA section node, and returns it.
* This will only work with XML, not HTML documents (as HTML documents
* do not support CDATA sections); attempting it on an HTML document
* will throw NotSupportedException.
*
* @param string $data data is a string containing the data to be added
* to the CDATA Section.
* @return CdataSection a CDATA Section node.
* @throws NotSupportedException
* @throws InvalidCharacterException if one tries to submit the
* closing CDATA sequence ("]]>") as part of the data, so unescaped
* user-provided data cannot be safely used without this method getting
* this exception (createTextNode() can often be used in its place).
* @link https://developer.mozilla.org/en-US/docs/Web/API/Document/createCDATASection
*/
public function createCDATASection(string $data):CdataSection {
if($this instanceof HTMLDocument) {
throw new HTMLDocumentDoesNotSupportCDATASectionException();
}
$closingTag = "]]>";
if(strstr($data, $closingTag)) {
throw new InvalidCharacterException($closingTag);
}
/** @var CdataSection $cdata */
$cdata = parent::createCDATASection($data);
return $cdata;
}
/**
* In an HTML document, the document.createElement() method creates the
* HTML element specified by tagName, or an HTMLUnknownElement if
* tagName isn't recognized.
*
* @param string $localName A string that specifies the type of element
* to be created. The nodeName of the created element is initialized
* with the value of tagName. Don't use qualified names (like "html:a")
* with this method. When called on an HTML document, createElement()
* converts tagName to lower case before creating the element.
* @param string $value The value of the element. By default, an empty
* element will be created. You can also set the value later with
* DOMElement->nodeValue.
* @return Element The new Element.
* @link https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement
*/
public function createElement(string $localName, string $value = ""):Element {
$localName = strtolower($localName);
try {
/** @var Element $element */
$element = parent::createElement($localName, $value);
return $element;
}
catch(DOMException $exception) {
throw new InvalidCharacterException();
}
}
/**
* Creates an element with the specified namespace URI and
* qualified name.
*
* To create an element without specifying a namespace URI, use
* the createElement() method.
*
* @param string $namespace A string that specifies the namespace URI
* to associate with the element. The namespaceURI property of the
* created element is initialized with the value of namespaceURI.
* @param string $qualifiedName A string that specifies the type of
* element to be created. The nodeName property of the created element
* is initialized with the value of qualifiedName.
* @param string $value The value of the element. By default, an empty
* element will be created. You can also set the value later with
* DOMElement->nodeValue.
* @return Element The new Element.
* @link https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS
*/
public function createElementNS(?string $namespace = "", string $qualifiedName = "", string $value = ""):Element {
/** @var Element $element */
$element = parent::createElementNS($namespace, $qualifiedName, $value);
return $element;
}
/**
* Returns a new NodeIterator object.
*
* @param Node $root The root node at which to begin the NodeIterator's
* traversal.
* @param int $whatToShow Is an optional unsigned long representing a
* bitmask created by combining the constant properties of NodeFilter.
* It is a convenient way of filtering for certain types of node.
* It defaults to 0xFFFFFFFF representing the SHOW_ALL constant.
* @param NodeFilter|callable|null $filter An object implementing the
* NodeFilter interface. Its acceptNode() method will be called for each
* node in the subtree based at root which is accepted as included by
* the whatToShow flag to determine whether or not to include it in the
* list of iterable nodes (a simple callback function may also be used
* instead). The method should return one of NodeFilter.FILTER_ACCEPT,
* NodeFilter.FILTER_REJECT, or NodeFilter.FILTER_SKIP.
* @return NodeIterator
* @link https://developer.mozilla.org/en-US/docs/Web/API/Document/createNodeIterator
*/
public function createNodeIterator(
Node|Element $root,
int $whatToShow = NodeFilter::SHOW_ALL,
NodeFilter|callable|null $filter = null
):NodeIterator {
return NodeIteratorFactory::create($root, $whatToShow, $filter);
}
/**
* createProcessingInstruction() generates a new processing instruction
* node and returns it.
*
* The new node usually will be inserted into an XML document in order
* to accomplish anything with it, such as with node.insertBefore.
*
* @param string $target a string containing the first part of the
* processing instruction (i.e., <?target … ?>)
* @param null|string $data a string containing any information the
* processing instruction should carry, after the target. The data is
* up to you, but it can't contain ?>, since that closes the processing
* instruction.
* @return ProcessingInstruction the resulting ProcessingInstruction
* node.
* @link https://developer.mozilla.org/en-US/docs/Web/API/Document/createProcessingInstruction
*/
public function createProcessingInstruction(
string $target,
?string $data = null
):ProcessingInstruction {
$closingTag = "?>";
if(strstr($data, $closingTag)) {
throw new InvalidCharacterException($closingTag);
}
/** @var ProcessingInstruction $procInstruction */
$procInstruction = parent::createProcessingInstruction($target, $data);
return $procInstruction;
}
/**
* The Document.createTreeWalker() creator method returns a newly
* created TreeWalker object.
*
* @param Node $root A root Node of this TreeWalker traversal. Typically
* this will be an element owned by the document.
* @param int $whatToShow A unsigned long representing a bitmask
* created by combining the constant properties of NodeFilter. It is a
* convenient way of filtering for certain types of node. It defaults
* to 0xFFFFFFFF representing the SHOW_ALL constant.
* @param NodeFilter|callable|null $filter A NodeFilter, that is an
* object with a method acceptNode, which is called by the TreeWalker
* to determine whether or not to accept a node that has passed the
* whatToShow check. Alternatively pass the callable that represents
* `acceptNode` function.
* @return TreeWalker A new TreeWalker object.
* @link https://developer.mozilla.org/en-US/docs/Web/API/Document/createTreeWalker
*/
public function createTreeWalker(
Node|Element $root,
int $whatToShow = NodeFilter::SHOW_ALL,
NodeFilter|callable|null $filter = null
):TreeWalker {
return TreeWalkerFactory::create($root, $whatToShow, $filter);
}
/**
* Returns an XPathResult based on an XPath expression.
*
* @param string $xpathExpression is a string representing the XPath to
* be evaluated.
* @param null|Node|Element $contextNode Leave null to default to $this node
* @return XPathResult
*/
public function evaluate(
string $xpathExpression,
null|Node|Element $contextNode = null
):XPathResult {
if(!$contextNode) {
$contextNode = $this->documentElement;
}
return XPathResultFactory::create(
$xpathExpression,
$this,
$contextNode,
);
}
/**
* The Document method getElementById() returns an Element object
* representing the element whose id property matches the specified
* string. Since element IDs are required to be unique if specified,
* they're a useful way to get access to a specific element quickly.
*
* If you need to get access to an element which doesn't have an ID,
* you can use querySelector() to find the element using any selector.
*
* @param string $elementId The ID of the element to locate. The ID is
* case-sensitive string which is unique within the document; only one
* element may have any given ID.
* @return ?Element An Element object describing the DOM element object
* matching the specified ID, or null if no matching element was found
* in the document.
* @link https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementById
*/
public function getElementById(string $elementId):?Element {
/** @var ?Element $element */
$element = parent::getElementById($elementId);
if(is_null($element)) {
// Known limitation in XML documents: IDs are not always registered.
// Try using XPath instead.
$element = $this->evaluate("//*[@id='$elementId']")->current();
}
return $element;
}
/**
* @see Node::isEqualNode()
*/
public function isEqualNode(
null|Node|Element|Document|DocumentType|Attr|ProcessingInstruction|DOMNode $otherNode
):bool {
return $this->documentElement->isEqualNode($otherNode);
}
/**
* Writes a string of text followed by a newline character to a
* document.
*
* @param string $line line is string containing a line of text.
* @return int The number of bytes written.
* @link https://developer.mozilla.org/en-US/docs/Web/API/Document/writeln
* @noinspection PhpMissingParamTypeInspection
*/
public function writeln($line):int {
return $this->write($line . PHP_EOL);
}
private function registerNodeClasses():void {
foreach(self::NODE_CLASS_LOOKUP as $nativeClass => $gtClass) {
if ($gtClass === self::class) {
$gtClass = static::class;
}
$this->registerNodeClass($nativeClass, $gtClass);
}
}
}