@@ -74,44 +74,6 @@ const unencodedElements = new Set([
74
74
"noscript" ,
75
75
] ) ;
76
76
77
- function replaceQuotes ( value : string ) : string {
78
- return value . replace ( / " / g, """ ) ;
79
- }
80
-
81
- /**
82
- * Format attributes
83
- */
84
- function formatAttributes (
85
- attributes : Record < string , string | null > | undefined ,
86
- opts : DomSerializerOptions
87
- ) {
88
- if ( ! attributes ) return ;
89
-
90
- const encode =
91
- ( opts . encodeEntities ?? opts . decodeEntities ) === false
92
- ? replaceQuotes
93
- : opts . xmlMode || opts . encodeEntities !== "utf8"
94
- ? encodeXML
95
- : escapeAttribute ;
96
-
97
- return Object . keys ( attributes )
98
- . map ( ( key ) => {
99
- const value = attributes [ key ] ?? "" ;
100
-
101
- if ( opts . xmlMode === "foreign" ) {
102
- /* Fix up mixed-case attribute names */
103
- key = attributeNames . get ( key ) ?? key ;
104
- }
105
-
106
- if ( ! opts . emptyAttrs && ! opts . xmlMode && value === "" ) {
107
- return key ;
108
- }
109
-
110
- return `${ key } ="${ encode ( value ) } "` ;
111
- } )
112
- . join ( " " ) ;
113
- }
114
-
115
77
/**
116
78
* Self-enclosing tags
117
79
*/
@@ -137,52 +99,6 @@ const singleTag = new Set([
137
99
"wbr" ,
138
100
] ) ;
139
101
140
- /**
141
- * Renders a DOM node or an array of DOM nodes to a string.
142
- *
143
- * Can be thought of as the equivalent of the `outerHTML` of the passed node(s).
144
- *
145
- * @param node Node to be rendered.
146
- * @param options Changes serialization behavior
147
- */
148
- export function render (
149
- node : AnyNode | ArrayLike < AnyNode > ,
150
- options : DomSerializerOptions = { }
151
- ) : string {
152
- const nodes = "length" in node ? node : [ node ] ;
153
-
154
- let output = "" ;
155
-
156
- for ( let i = 0 ; i < nodes . length ; i ++ ) {
157
- output += renderNode ( nodes [ i ] , options ) ;
158
- }
159
-
160
- return output ;
161
- }
162
-
163
- export default render ;
164
-
165
- function renderNode ( node : AnyNode , options : DomSerializerOptions ) : string {
166
- switch ( node . type ) {
167
- case ElementType . Root :
168
- return render ( node . children , options ) ;
169
- // @ts -expect-error We don't use `Doctype` yet
170
- case ElementType . Doctype :
171
- case ElementType . Directive :
172
- return renderDirective ( node ) ;
173
- case ElementType . Comment :
174
- return renderComment ( node ) ;
175
- case ElementType . CDATA :
176
- return renderCdata ( node ) ;
177
- case ElementType . Script :
178
- case ElementType . Style :
179
- case ElementType . Tag :
180
- return renderTag ( node , options ) ;
181
- case ElementType . Text :
182
- return renderText ( node , options ) ;
183
- }
184
- }
185
-
186
102
const foreignModeIntegrationPoints = new Set ( [
187
103
"mi" ,
188
104
"mo" ,
@@ -197,83 +113,196 @@ const foreignModeIntegrationPoints = new Set([
197
113
198
114
const foreignElements = new Set ( [ "svg" , "math" ] ) ;
199
115
200
- function renderTag ( elem : Element , opts : DomSerializerOptions ) {
201
- // Handle SVG / MathML in HTML
202
- if ( opts . xmlMode === "foreign" ) {
203
- /* Fix up mixed-case element names */
204
- elem . name = elementNames . get ( elem . name ) ?? elem . name ;
205
- /* Exit foreign mode at integration points */
206
- if (
207
- elem . parent &&
208
- foreignModeIntegrationPoints . has ( ( elem . parent as Element ) . name )
209
- ) {
210
- opts = { ...opts , xmlMode : false } ;
116
+ export class DomSerializer {
117
+ protected output : string ;
118
+ protected options : DomSerializerOptions ;
119
+
120
+ /**
121
+ * Creates a serializer instance
122
+ *
123
+ * @param options Changes serialization behavior
124
+ */
125
+ constructor ( options : DomSerializerOptions = { } ) {
126
+ this . options = options ;
127
+ this . output = "" ;
128
+ }
129
+
130
+ /**
131
+ * Renders a DOM node or an array of DOM nodes to a string.
132
+ *
133
+ * Can be thought of as the equivalent of the `outerHTML` of the passed node(s).
134
+ *
135
+ * @param node Node to be rendered.
136
+ */
137
+ render ( node : AnyNode | ArrayLike < AnyNode > ) : string {
138
+ const nodes = "length" in node ? node : [ node ] ;
139
+
140
+ this . output = "" ;
141
+
142
+ for ( let i = 0 ; i < nodes . length ; i ++ ) {
143
+ this . renderNode ( nodes [ i ] ) ;
211
144
}
145
+
146
+ return this . output ;
212
147
}
213
- if ( ! opts . xmlMode && foreignElements . has ( elem . name ) ) {
214
- opts = { ...opts , xmlMode : "foreign" } ;
148
+
149
+ renderNode ( node : AnyNode ) : void {
150
+ switch ( node . type ) {
151
+ case ElementType . Root :
152
+ this . render ( node . children ) ;
153
+ break ;
154
+ // @ts -expect-error We don't use `Doctype` yet
155
+ case ElementType . Doctype :
156
+ case ElementType . Directive :
157
+ this . renderDirective ( node ) ;
158
+ break ;
159
+ case ElementType . Comment :
160
+ this . renderComment ( node ) ;
161
+ break ;
162
+ case ElementType . CDATA :
163
+ this . renderCdata ( node ) ;
164
+ break ;
165
+ case ElementType . Script :
166
+ case ElementType . Style :
167
+ case ElementType . Tag :
168
+ this . renderTag ( node ) ;
169
+ break ;
170
+ case ElementType . Text :
171
+ this . renderText ( node ) ;
172
+ break ;
173
+ }
215
174
}
216
175
217
- let tag = `<${ elem . name } ` ;
218
- const attribs = formatAttributes ( elem . attribs , opts ) ;
176
+ renderTag ( elem : Element ) : void {
177
+ // Handle SVG / MathML in HTML
178
+ if ( this . options . xmlMode === "foreign" ) {
179
+ /* Fix up mixed-case element names */
180
+ elem . name = elementNames . get ( elem . name ) ?? elem . name ;
181
+ /* Exit foreign mode at integration points */
182
+ if (
183
+ elem . parent &&
184
+ foreignModeIntegrationPoints . has ( ( elem . parent as Element ) . name )
185
+ ) {
186
+ this . options = { ...this . options , xmlMode : false } ;
187
+ }
188
+ }
189
+ if ( ! this . options . xmlMode && foreignElements . has ( elem . name ) ) {
190
+ this . options = { ...this . options , xmlMode : "foreign" } ;
191
+ }
219
192
220
- if ( attribs ) {
221
- tag += ` ${ attribs } ` ;
222
- }
193
+ this . output += `<${ elem . name } ` ;
194
+ const attribs = this . formatAttributes ( elem . attribs ) ;
223
195
224
- if (
225
- elem . children . length === 0 &&
226
- ( opts . xmlMode
227
- ? // In XML mode or foreign mode, and user hasn't explicitly turned off self-closing tags
228
- opts . selfClosingTags !== false
229
- : // User explicitly asked for self-closing tags, even in HTML mode
230
- opts . selfClosingTags && singleTag . has ( elem . name ) )
231
- ) {
232
- if ( ! opts . xmlMode ) tag += " " ;
233
- tag += "/>" ;
234
- } else {
235
- tag += ">" ;
236
- if ( elem . children . length > 0 ) {
237
- tag += render ( elem . children , opts ) ;
196
+ if ( attribs ) {
197
+ this . output += ` ${ attribs } ` ;
238
198
}
239
199
240
- if ( opts . xmlMode || ! singleTag . has ( elem . name ) ) {
241
- tag += `</${ elem . name } >` ;
200
+ if (
201
+ elem . children . length === 0 &&
202
+ ( this . options . xmlMode
203
+ ? // In XML mode or foreign mode, and user hasn't explicitly turned off self-closing tags
204
+ this . options . selfClosingTags !== false
205
+ : // User explicitly asked for self-closing tags, even in HTML mode
206
+ this . options . selfClosingTags && singleTag . has ( elem . name ) )
207
+ ) {
208
+ if ( ! this . options . xmlMode ) this . output += " " ;
209
+ this . output += "/>" ;
210
+ } else {
211
+ this . output += ">" ;
212
+ if ( elem . children . length > 0 ) {
213
+ this . output += render ( elem . children , this . options ) ;
214
+ }
215
+
216
+ if ( this . options . xmlMode || ! singleTag . has ( elem . name ) ) {
217
+ this . output += `</${ elem . name } >` ;
218
+ }
242
219
}
243
220
}
244
221
245
- return tag ;
246
- }
222
+ renderDirective ( elem : ProcessingInstruction ) : void {
223
+ this . output += `<${ elem . data } >` ;
224
+ }
247
225
248
- function renderDirective ( elem : ProcessingInstruction ) {
249
- return `<${ elem . data } >` ;
250
- }
226
+ renderText ( elem : Text ) : void {
227
+ let data = elem . data || "" ;
228
+
229
+ // If entities weren't decoded, no need to encode them back
230
+ if (
231
+ ( this . options . encodeEntities ?? this . options . decodeEntities ) !== false &&
232
+ ! (
233
+ ! this . options . xmlMode &&
234
+ elem . parent &&
235
+ unencodedElements . has ( ( elem . parent as Element ) . name )
236
+ )
237
+ ) {
238
+ data =
239
+ this . options . xmlMode || this . options . encodeEntities !== "utf8"
240
+ ? encodeXML ( data )
241
+ : escapeText ( data ) ;
242
+ }
251
243
252
- function renderText ( elem : Text , opts : DomSerializerOptions ) {
253
- let data = elem . data || "" ;
254
-
255
- // If entities weren't decoded, no need to encode them back
256
- if (
257
- ( opts . encodeEntities ?? opts . decodeEntities ) !== false &&
258
- ! (
259
- ! opts . xmlMode &&
260
- elem . parent &&
261
- unencodedElements . has ( ( elem . parent as Element ) . name )
262
- )
263
- ) {
264
- data =
265
- opts . xmlMode || opts . encodeEntities !== "utf8"
266
- ? encodeXML ( data )
267
- : escapeText ( data ) ;
244
+ this . output += data ;
268
245
}
269
246
270
- return data ;
271
- }
247
+ renderCdata ( elem : CDATA ) : void {
248
+ this . output += `<![CDATA[${ ( elem . children [ 0 ] as Text ) . data } ]]>` ;
249
+ }
272
250
273
- function renderCdata ( elem : CDATA ) {
274
- return `<![CDATA[${ ( elem . children [ 0 ] as Text ) . data } ]]>` ;
251
+ renderComment ( elem : Comment ) : void {
252
+ this . output += `<!--${ elem . data } -->` ;
253
+ }
254
+
255
+ replaceQuotes ( value : string ) : string {
256
+ return value . replace ( / " / g, """ ) ;
257
+ }
258
+
259
+ /**
260
+ * Format attributes
261
+ */
262
+ formatAttributes (
263
+ attributes : Record < string , string | null > | undefined
264
+ ) : string | undefined {
265
+ if ( ! attributes ) return ;
266
+
267
+ const encode =
268
+ ( this . options . encodeEntities ?? this . options . decodeEntities ) === false
269
+ ? this . replaceQuotes
270
+ : this . options . xmlMode || this . options . encodeEntities !== "utf8"
271
+ ? encodeXML
272
+ : escapeAttribute ;
273
+
274
+ return Object . keys ( attributes )
275
+ . map ( ( key ) => {
276
+ const value = attributes [ key ] ?? "" ;
277
+
278
+ if ( this . options . xmlMode === "foreign" ) {
279
+ /* Fix up mixed-case attribute names */
280
+ key = attributeNames . get ( key ) ?? key ;
281
+ }
282
+
283
+ if ( ! this . options . emptyAttrs && ! this . options . xmlMode && value === "" ) {
284
+ return key ;
285
+ }
286
+
287
+ return `${ key } ="${ encode ( value ) } "` ;
288
+ } )
289
+ . join ( " " ) ;
290
+ }
275
291
}
276
292
277
- function renderComment ( elem : Comment ) {
278
- return `<!--${ elem . data } -->` ;
293
+ /**
294
+ * Renders a DOM node or an array of DOM nodes to a string.
295
+ *
296
+ * Can be thought of as the equivalent of the `outerHTML` of the passed node(s).
297
+ *
298
+ * @param node Node to be rendered.
299
+ * @param options Changes serialization behavior
300
+ */
301
+ export function render (
302
+ node : AnyNode | ArrayLike < AnyNode > ,
303
+ options : DomSerializerOptions = { }
304
+ ) : string {
305
+ return new DomSerializer ( options ) . render ( node ) ;
279
306
}
307
+
308
+ export default render ;
0 commit comments