1#![deny(clippy::all)]
2#![allow(clippy::needless_update)]
3#![allow(non_local_definitions)]
4
5pub use std::fmt::Result;
6use std::{iter::Peekable, str::Chars};
7
8use swc_common::Spanned;
9use swc_xml_ast::*;
10use swc_xml_codegen_macros::emitter;
11use writer::XmlWriter;
12
13pub use self::emit::*;
14use self::{ctx::Ctx, list::ListFormat};
15
16#[macro_use]
17mod macros;
18mod ctx;
19mod emit;
20mod list;
21pub mod writer;
22
23#[derive(Debug, Clone, Default)]
24pub struct CodegenConfig<'a> {
25 pub minify: bool,
26 pub scripting_enabled: bool,
27 pub context_element: Option<&'a Element>,
29}
30
31#[derive(Debug)]
32pub struct CodeGenerator<'a, W>
33where
34 W: XmlWriter,
35{
36 wr: W,
37 config: CodegenConfig<'a>,
38 ctx: Ctx,
39}
40
41impl<'a, W> CodeGenerator<'a, W>
42where
43 W: XmlWriter,
44{
45 pub fn new(wr: W, config: CodegenConfig<'a>) -> Self {
46 CodeGenerator {
47 wr,
48 config,
49 ctx: Default::default(),
50 }
51 }
52
53 #[emitter]
54 fn emit_document(&mut self, n: &Document) -> Result {
55 self.emit_list(&n.children, ListFormat::NotDelimited)?;
56 }
57
58 #[emitter]
59 fn emit_child(&mut self, n: &Child) -> Result {
60 match n {
61 Child::DocumentType(n) => emit!(self, n),
62 Child::Element(n) => emit!(self, n),
63 Child::Text(n) => emit!(self, n),
64 Child::Comment(n) => emit!(self, n),
65 Child::ProcessingInstruction(n) => emit!(self, n),
66 Child::CdataSection(n) => emit!(self, n),
67 }
68 }
69
70 #[emitter]
71 fn emit_document_doctype(&mut self, n: &DocumentType) -> Result {
72 let mut doctype = String::with_capacity(
73 10 + if let Some(name) = &n.name {
74 name.len() + 1
75 } else {
76 0
77 } + if let Some(public_id) = &n.public_id {
78 let mut len = public_id.len() + 10;
79
80 if let Some(system_id) = &n.system_id {
81 len += system_id.len() + 3
82 }
83
84 len
85 } else if let Some(system_id) = &n.system_id {
86 system_id.len() + 10
87 } else {
88 0
89 },
90 );
91
92 doctype.push('<');
93 doctype.push('!');
94
95 if self.config.minify {
96 doctype.push_str("doctype");
97 } else {
98 doctype.push_str("DOCTYPE");
99 }
100
101 if let Some(name) = &n.name {
102 doctype.push(' ');
103 doctype.push_str(name);
104 }
105
106 if let Some(public_id) = &n.public_id {
107 doctype.push(' ');
108
109 if self.config.minify {
110 doctype.push_str("public");
111 } else {
112 doctype.push_str("PUBLIC");
113 }
114
115 doctype.push(' ');
116
117 let public_id_quote = if public_id.contains('"') { '\'' } else { '"' };
118
119 doctype.push(public_id_quote);
120 doctype.push_str(public_id);
121 doctype.push(public_id_quote);
122
123 if let Some(system_id) = &n.system_id {
124 doctype.push(' ');
125
126 let system_id_quote = if system_id.contains('"') { '\'' } else { '"' };
127
128 doctype.push(system_id_quote);
129 doctype.push_str(system_id);
130 doctype.push(system_id_quote);
131 }
132 } else if let Some(system_id) = &n.system_id {
133 doctype.push(' ');
134
135 if self.config.minify {
136 doctype.push_str("system");
137 } else {
138 doctype.push_str("SYSTEM");
139 }
140
141 doctype.push(' ');
142
143 let system_id_quote = if system_id.contains('"') { '\'' } else { '"' };
144
145 doctype.push(system_id_quote);
146 doctype.push_str(system_id);
147 doctype.push(system_id_quote);
148 }
149
150 doctype.push('>');
151
152 write_raw!(self, n.span, &doctype);
153 formatting_newline!(self);
154 }
155
156 fn basic_emit_element(&mut self, n: &Element) -> Result {
157 let has_attributes = !n.attributes.is_empty();
158 let is_void_element = n.children.is_empty();
159
160 write_raw!(self, "<");
161 write_raw!(self, &n.tag_name);
162
163 if has_attributes {
164 space!(self);
165
166 self.emit_list(&n.attributes, ListFormat::SpaceDelimited)?;
167 }
168
169 if is_void_element {
170 if !self.config.minify {
171 write_raw!(self, " ");
172 }
173
174 write_raw!(self, "/");
175 }
176
177 write_raw!(self, ">");
178
179 if is_void_element {
180 return Ok(());
181 }
182
183 if !n.children.is_empty() {
184 let ctx = self.create_context_for_element(n);
185
186 self.with_ctx(ctx)
187 .emit_list(&n.children, ListFormat::NotDelimited)?;
188 }
189
190 write_raw!(self, "<");
191 write_raw!(self, "/");
192 write_raw!(self, &n.tag_name);
193 write_raw!(self, ">");
194
195 Ok(())
196 }
197
198 #[emitter]
199 fn emit_element(&mut self, n: &Element) -> Result {
200 self.basic_emit_element(n)?;
201 }
202
203 #[emitter]
204 fn emit_attribute(&mut self, n: &Attribute) -> Result {
205 let mut attribute = String::with_capacity(
206 if let Some(prefix) = &n.prefix {
207 prefix.len() + 1
208 } else {
209 0
210 } + n.name.len()
211 + if let Some(value) = &n.value {
212 value.len() + 1
213 } else {
214 0
215 },
216 );
217
218 if let Some(prefix) = &n.prefix {
219 attribute.push_str(prefix);
220 attribute.push(':');
221 }
222
223 attribute.push_str(&n.name);
224
225 if let Some(value) = &n.value {
226 attribute.push('=');
227
228 let normalized = normalize_attribute_value(value);
229
230 attribute.push_str(&normalized);
231 }
232
233 write_multiline_raw!(self, n.span, &attribute);
234 }
235
236 #[emitter]
237 fn emit_text(&mut self, n: &Text) -> Result {
238 if self.ctx.need_escape_text {
239 let mut data = String::with_capacity(n.data.len());
240
241 if self.config.minify {
242 data.push_str(&minify_text(&n.data));
243 } else {
244 data.push_str(&escape_string(&n.data, false));
245 }
246
247 write_multiline_raw!(self, n.span, &data);
248 } else {
249 write_multiline_raw!(self, n.span, &n.data);
250 }
251 }
252
253 #[emitter]
254 fn emit_comment(&mut self, n: &Comment) -> Result {
255 let mut comment = String::with_capacity(n.data.len() + 7);
256
257 comment.push_str("<!--");
258 comment.push_str(&n.data);
259 comment.push_str("-->");
260
261 write_multiline_raw!(self, n.span, &comment);
262 }
263
264 #[emitter]
265 fn emit_processing_instruction(&mut self, n: &ProcessingInstruction) -> Result {
266 let mut processing_instruction = String::with_capacity(n.target.len() + n.data.len() + 5);
267
268 processing_instruction.push_str("<?");
269 processing_instruction.push_str(&n.target);
270 processing_instruction.push(' ');
271 processing_instruction.push_str(&n.data);
272 processing_instruction.push_str("?>");
273
274 write_multiline_raw!(self, n.span, &processing_instruction);
275 }
276
277 #[emitter]
278 fn emit_cdata_section(&mut self, n: &CdataSection) -> Result {
279 let mut cdata_section = String::with_capacity(n.data.len() + 12);
280
281 cdata_section.push_str("<![CDATA[");
282 cdata_section.push_str(&n.data);
283 cdata_section.push_str("]]>");
284
285 write_multiline_raw!(self, n.span, &cdata_section);
286 }
287
288 fn create_context_for_element(&self, n: &Element) -> Ctx {
289 let need_escape_text = match &*n.tag_name {
290 "noscript" => !self.config.scripting_enabled,
291 _ => true,
292 };
293
294 Ctx {
295 need_escape_text,
296 ..self.ctx
297 }
298 }
299
300 fn emit_list<N>(&mut self, nodes: &[N], format: ListFormat) -> Result
301 where
302 Self: Emit<N>,
303 N: Spanned,
304 {
305 for (idx, node) in nodes.iter().enumerate() {
306 if idx != 0 {
307 self.write_delim(format)?;
308
309 if format & ListFormat::LinesMask == ListFormat::MultiLine {
310 formatting_newline!(self);
311 }
312 }
313
314 emit!(self, node)
315 }
316
317 Ok(())
318 }
319
320 fn write_delim(&mut self, f: ListFormat) -> Result {
321 match f & ListFormat::DelimitersMask {
322 ListFormat::None => {}
323 ListFormat::SpaceDelimited => {
324 space!(self)
325 }
326 _ => unreachable!(),
327 }
328
329 Ok(())
330 }
331}
332
333fn normalize_attribute_value(value: &str) -> String {
334 if value.is_empty() {
335 return "\"\"".to_string();
336 }
337
338 let mut normalized = String::with_capacity(value.len() + 2);
339
340 normalized.push('"');
341 normalized.push_str(&escape_string(value, true));
342 normalized.push('"');
343
344 normalized
345}
346
347#[allow(clippy::unused_peekable)]
348fn minify_text(value: &str) -> String {
349 let mut result = String::with_capacity(value.len());
350 let mut chars = value.chars().peekable();
351
352 while let Some(c) = chars.next() {
353 match c {
354 '&' => {
355 result.push_str(&minify_amp(&mut chars));
356 }
357 '<' => {
358 result.push_str("<");
359 }
360 '>' => {
361 result.push_str(">");
362 }
363 _ => result.push(c),
364 }
365 }
366
367 result
368}
369
370fn minify_amp(chars: &mut Peekable<Chars>) -> String {
371 let mut result = String::with_capacity(7);
372
373 match chars.next() {
374 Some(hash @ '#') => {
375 match chars.next() {
376 Some(number @ '0'..='9') => {
379 result.push_str("&");
380 result.push(hash);
381 result.push(number);
382 }
383 Some(x @ 'x' | x @ 'X') => {
384 match chars.peek() {
385 Some(c) if c.is_ascii_hexdigit() => {
388 result.push_str("&");
389 result.push(hash);
390 result.push(x);
391 }
392 _ => {
393 result.push('&');
394 result.push(hash);
395 result.push(x);
396 }
397 }
398 }
399 any => {
400 result.push('&');
401 result.push(hash);
402
403 if let Some(any) = any {
404 result.push(any);
405 }
406 }
407 }
408 }
409 Some(c @ 'a'..='z') | Some(c @ 'A'..='Z') => {
412 let mut entity_temporary_buffer = String::with_capacity(33);
413
414 entity_temporary_buffer.push('&');
415 entity_temporary_buffer.push(c);
416
417 result.push('&');
418 result.push_str(&entity_temporary_buffer[1..]);
419 }
420 any => {
421 result.push('&');
422
423 if let Some(any) = any {
424 result.push(any);
425 }
426 }
427 }
428
429 result
430}
431
432fn escape_string(value: &str, is_attribute_mode: bool) -> String {
447 let mut result = String::with_capacity(value.len());
448
449 for c in value.chars() {
450 match c {
451 '&' => {
452 result.push_str("&");
453 }
454 '"' if is_attribute_mode => result.push_str("""),
455 '<' => {
456 result.push_str("<");
457 }
458 '>' if !is_attribute_mode => {
459 result.push_str(">");
460 }
461 _ => result.push(c),
462 }
463 }
464
465 result
466}