]> BookStack Code Mirror - bookstack/blob - app/Util/HtmlDocument.php
b8c53d43916294dccf63ed3821222f2888edf22d
[bookstack] / app / Util / HtmlDocument.php
1 <?php
2
3 namespace BookStack\Util;
4
5 use DOMDocument;
6 use DOMElement;
7 use DOMNode;
8 use DOMNodeList;
9 use DOMXPath;
10
11 /**
12  * HtmlDocument is a thin wrapper around DOMDocument built
13  * specifically for loading, querying and generating HTML content.
14  */
15 class HtmlDocument
16 {
17     protected DOMDocument $document;
18     protected ?DOMXPath $xpath = null;
19     protected int $loadOptions;
20
21     public function __construct(string $partialHtml = '', int $loadOptions = 0)
22     {
23         libxml_use_internal_errors(true);
24         $this->document = new DOMDocument();
25         $this->loadOptions = $loadOptions;
26
27         if ($partialHtml) {
28             $this->loadPartialHtml($partialHtml);
29         }
30     }
31
32     /**
33      * Load some HTML content that's part of a document (e.g. body content)
34      * into the current document.
35      */
36     public function loadPartialHtml(string $html): void
37     {
38         $html = '<?xml encoding="utf-8" ?><body>' . $html . '</body>';
39         $this->document->loadHTML($html, $this->loadOptions);
40         $this->xpath = null;
41     }
42
43     /**
44      * Load a complete page of HTML content into the document.
45      */
46     public function loadCompleteHtml(string $html): void
47     {
48         $html = '<?xml encoding="utf-8" ?>' . $html;
49         $this->document->loadHTML($html, $this->loadOptions);
50         $this->xpath = null;
51     }
52
53     /**
54      * Start an XPath query on the current document.
55      */
56     public function queryXPath(string $expression): DOMNodeList
57     {
58         if (is_null($this->xpath)) {
59             $this->xpath = new DOMXPath($this->document);
60         }
61
62         $result = $this->xpath->query($expression);
63         if ($result === false) {
64             throw new \InvalidArgumentException("XPath query for expression [$expression] failed to execute");
65         }
66
67         return $result;
68     }
69
70     /**
71      * Create a new DOMElement instance within the document.
72      */
73     public function createElement(string $localName, string $value = ''): DOMElement
74     {
75         $element = $this->document->createElement($localName, $value);
76
77         if ($element === false) {
78             throw new \InvalidArgumentException("Failed to create element of name [$localName] and value [$value]");
79         }
80
81         return $element;
82     }
83
84     /**
85      * Get an element within the document of the given ID.
86      */
87     public function getElementById(string $elementId): ?DOMElement
88     {
89         return $this->document->getElementById($elementId);
90     }
91
92     /**
93      * Get the DOMNode that represents the HTML body.
94      */
95     public function getBody(): DOMNode
96     {
97         return $this->document->getElementsByTagName('body')[0];
98     }
99
100     /**
101      * Get the nodes that are a direct child of the body.
102      * This is usually all the content nodes if loaded partially.
103      */
104     public function getBodyChildren(): DOMNodeList
105     {
106         return $this->getBody()->childNodes;
107     }
108
109     /**
110      * Get the inner HTML content of the body.
111      * This is usually all the content if loaded partially.
112      */
113     public function getBodyInnerHtml(): string
114     {
115         $html = '';
116         foreach ($this->getBodyChildren() as $child) {
117             $html .= $this->document->saveHTML($child);
118         }
119
120         return $html;
121     }
122
123     /**
124      * Get the HTML content of the whole document.
125      */
126     public function getHtml(): string
127     {
128         return $this->document->saveHTML($this->document->documentElement);
129     }
130
131     /**
132      * Get the inner HTML for the given node.
133      */
134     public function getNodeInnerHtml(DOMNode $node): string
135     {
136         $html = '';
137
138         foreach ($node->childNodes as $childNode) {
139             $html .= $this->document->saveHTML($childNode);
140         }
141
142         return $html;
143     }
144
145     /**
146      * Get the outer HTML for the given node.
147      */
148     public function getNodeOuterHtml(DOMNode $node): string
149     {
150         return $this->document->saveHTML($node);
151     }
152 }