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