9
9
*/
10
10
namespace Templado \Engine ;
11
11
12
+ use DOMAttr ;
12
13
use DOMDocument ;
14
+ use DOMElement ;
15
+ use DOMNameSpaceNode ;
16
+ use DOMNode ;
17
+ use DOMXPath ;
18
+ use XMLWriter ;
19
+ use function assert ;
20
+ use const LIBXML_NOEMPTYTAG ;
21
+ use const LIBXML_NOXMLDECL ;
13
22
14
23
class HTMLSerializer implements Serializer {
15
24
private bool $ stripRDFaFlag = false ;
@@ -20,6 +29,10 @@ class HTMLSerializer implements Serializer {
20
29
21
30
private bool $ withDoctypeFlag = true ;
22
31
32
+ private const HTMLNS = 'http://www.w3.org/1999/xhtml ' ;
33
+
34
+ private bool $ isFirst ;
35
+
23
36
/** @psalm-var list<Filter> */
24
37
private array $ filters = [];
25
38
@@ -63,13 +76,6 @@ public function addFilter(Filter $filter): self {
63
76
}
64
77
65
78
public function serialize (DOMDocument $ document ): string {
66
- if ($ this ->namespaceCleaningFlag ) {
67
- $ this ->transformations [] = new NamespaceCleaningTransformation ();
68
- }
69
-
70
- if ($ this ->stripRDFaFlag ) {
71
- $ this ->transformations [] = new StripRDFaAttributesTransformation ;
72
- }
73
79
74
80
if (!empty ($ this ->transformations )) {
75
81
(new TransformationProcessor ())->process (
@@ -78,34 +84,140 @@ public function serialize(DOMDocument $document): string {
78
84
);
79
85
}
80
86
87
+ $ xmlString = $ this ->namespaceCleaningFlag ?
88
+ $ this ->serializeToCleanedString ($ document ) :
89
+ $ this ->serializeToBasicString ($ document );
90
+
91
+ $ this ->filters [] = new EmptyElementsFilter ();
92
+
93
+ foreach ($ this ->filters as $ filter ) {
94
+ $ xmlString = $ filter ->apply ($ xmlString );
95
+ }
96
+
97
+ return $ xmlString ;
98
+ }
99
+
100
+ private function serializeToCleanedString (DOMDocument $ document ): string {
101
+ $ writer = new XMLWriter ();
102
+ $ writer ->openMemory ();
103
+ $ writer ->setIndent (true );
104
+ $ writer ->setIndentString (' ' );
105
+
106
+ if ($ this ->keepXMLHeaderFlag ) {
107
+ $ writer ->startDocument ();
108
+ }
109
+
81
110
if ($ this ->withDoctypeFlag ) {
82
- $ document = $ this -> enforceHTML5DocType ( $ document );
111
+ $ writer -> writeDtd ( ' html ' );
83
112
}
84
113
85
- $ document ->formatOutput = true ;
86
- $ xmlString = $ document ->saveXML (options: LIBXML_NOEMPTYTAG );
114
+ $ this ->isFirst = true ;
87
115
88
- $ this ->filters [] = new EmptyElementsFilter ( );
116
+ $ this ->walk ( $ writer , $ document -> documentElement , [] );
89
117
90
- if (! $ this ->keepXMLHeaderFlag ) {
91
- $ this -> filters [] = new XMLHeaderFilter ();
118
+ if ($ this ->keepXMLHeaderFlag ) {
119
+ $ writer -> endDocument ();
92
120
}
93
121
94
- foreach ($ this ->filters as $ filter ) {
95
- $ xmlString = $ filter ->apply ($ xmlString );
122
+ return $ writer ->outputMemory ();
123
+ }
124
+
125
+ private function walk (XMLWriter $ writer , DOMNode $ node , array $ knownPrefixes ):void {
126
+ assert ($ node ->ownerDocument instanceof DOMDocument);
127
+
128
+ if (!$ node instanceof DOMElement) {
129
+ $ writer ->writeRaw (
130
+ $ node ->ownerDocument ->saveXML ($ node )
131
+ );
132
+
133
+ return ;
96
134
}
97
135
98
- return $ xmlString ;
136
+ if ($ node ->namespaceURI === self ::HTMLNS || empty ($ node ->namespaceURI )) {
137
+ $ writer ->startElement ($ node ->localName );
138
+ if ($ this ->isFirst ) {
139
+ $ writer ->writeAttribute ('xmlns ' , self ::HTMLNS );
140
+ $ this ->isFirst = false ;
141
+ }
142
+ } else {
143
+ $ writer ->startElement ($ node ->nodeName );
144
+ if (empty ($ node ->prefix )) {
145
+ $ writer ->writeAttribute ('xmlns ' , $ node ->namespaceURI );
146
+ } elseif (!isset ($ knownPrefixes [$ node ->prefix ])) {
147
+ $ writer ->writeAttribute ('xmlns: ' . $ node ->prefix , $ node ->namespaceURI );
148
+ $ knownPrefixes [$ node ->prefix ] = $ node ->namespaceURI ;
149
+ }
150
+ }
151
+
152
+ foreach ($ node ->attributes as $ attribute ) {
153
+ assert ($ attribute instanceof DOMAttr);
154
+
155
+ if ($ this ->stripRDFaFlag && in_array ($ attribute ->name , ['property ' , 'resource ' , 'prefix ' , 'typeof ' , 'vocab ' ])) {
156
+ continue ;
157
+ }
158
+
159
+ if (empty ($ attribute ->prefix )) {
160
+ $ writer ->writeAttribute ($ attribute ->name , $ attribute ->value );
161
+ continue ;
162
+ }
163
+
164
+ if (!isset ($ knownPrefixes [$ attribute ->prefix ])) {
165
+ $ knownPrefixes [$ attribute ->prefix ] = $ node ->lookupNamespaceURI ($ attribute ->prefix );
166
+ $ writer ->writeAttribute ('xmlns: ' . $ attribute ->prefix , $ node ->lookupNamespaceURI ($ attribute ->prefix ));
167
+ }
168
+
169
+ $ writer ->writeAttribute (
170
+ $ attribute ->nodeName ,
171
+ $ attribute ->value
172
+ );
173
+ }
174
+
175
+ foreach ((new DOMXPath ($ node ->ownerDocument ))->query ('./namespace::* ' , $ node ) as $ nsNode ) {
176
+ assert ($ nsNode instanceof DOMNameSpaceNode);
177
+
178
+ if (empty ($ nsNode ->prefix ) || $ nsNode ->prefix === 'xml ' ) {
179
+ continue ;
180
+ }
181
+
182
+ if ($ nsNode ->nodeValue === self ::HTMLNS ) {
183
+ continue ;
184
+ }
185
+
186
+ if (isset ($ knownPrefixes [$ nsNode ->prefix ])) {
187
+ continue ;
188
+ }
189
+
190
+ assert ($ nsNode ->nodeValue !== null );
191
+ $ writer ->writeAttribute ('xmlns: ' . $ nsNode ->prefix , $ nsNode ->nodeValue );
192
+ $ knownPrefixes [$ nsNode ->prefix ] = $ nsNode ->nodeValue ;
193
+
194
+ }
195
+
196
+ if ($ node ->hasChildNodes ()) {
197
+ foreach ($ node ->childNodes as $ childNode ) {
198
+ $ this ->walk ($ writer , $ childNode , $ knownPrefixes );
199
+ }
200
+ }
201
+
202
+ $ writer ->fullEndElement ();
99
203
}
100
204
101
- private function enforceHTML5DocType (DOMDocument $ document ): DOMDocument {
102
- $ tmp = new DOMDocument ();
103
- $ tmp ->loadXML ('<?xml version="1.0" ?><!DOCTYPE html><html /> ' );
104
- $ tmp ->replaceChild (
105
- $ tmp ->importNode ($ document ->documentElement , true ),
106
- $ tmp ->documentElement
107
- );
205
+ private function serializeToBasicString (DOMDocument $ document ): string {
206
+ $ document ->formatOutput = true ;
207
+ $ xmlString = $ document ->saveXML ($ document ->documentElement , options: LIBXML_NOEMPTYTAG );
108
208
109
- return $ tmp ;
209
+ if ($ this ->withDoctypeFlag ) {
210
+ $ xmlString = "<!DOCTYPE html> \n" . $ xmlString ;
211
+ }
212
+
213
+ if ($ this ->keepXMLHeaderFlag ) {
214
+ $ xmlString = sprintf (
215
+ '<?xml version="1.0" encoding="%s" ?> ' ,
216
+ $ document ->encoding ?? 'utf-8 '
217
+ ) . "\n" . $ xmlString ;
218
+ }
219
+
220
+ return $ xmlString . "\n" ;
110
221
}
222
+
111
223
}
0 commit comments