swc_xml_codegen/
lib.rs

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    /// Should be used only for `DocumentFragment` code generation
28    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("&lt;");
359            }
360            '>' => {
361                result.push_str("&gt;");
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                // HTML CODE
377                // Prevent `&amp;#38;` -> `&#38`
378                Some(number @ '0'..='9') => {
379                    result.push_str("&amp;");
380                    result.push(hash);
381                    result.push(number);
382                }
383                Some(x @ 'x' | x @ 'X') => {
384                    match chars.peek() {
385                        // HEX CODE
386                        // Prevent `&amp;#x38;` -> `&#x38`
387                        Some(c) if c.is_ascii_hexdigit() => {
388                            result.push_str("&amp;");
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        // Named entity
410        // Prevent `&amp;current` -> `&current`
411        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
432// Escaping a string (for the purposes of the algorithm above) consists of
433// running the following steps:
434//
435// 1. Replace any occurrence of the "&" character by the string "&amp;".
436//
437// 2. Replace any occurrences of the U+00A0 NO-BREAK SPACE character by the
438// string "&nbsp;".
439//
440// 3. If the algorithm was invoked in the attribute mode, replace any
441// occurrences of the """ character by the string "&quot;".
442//
443// 4. If the algorithm was not invoked in the attribute mode, replace any
444// occurrences of the "<" character by the string "&lt;", and any occurrences of
445// the ">" character by the string "&gt;".
446fn 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("&amp;");
453            }
454            '"' if is_attribute_mode => result.push_str("&quot;"),
455            '<' => {
456                result.push_str("&lt;");
457            }
458            '>' if !is_attribute_mode => {
459                result.push_str("&gt;");
460            }
461            _ => result.push(c),
462        }
463    }
464
465    result
466}