Coverage Report

Created: 2026-05-30 06:53

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/rust-cssparser/src/serializer.rs
Line
Count
Source
1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
 * License, v. 2.0. If a copy of the MPL was not distributed with this
3
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5
use std::fmt::{self, Write};
6
use std::str;
7
8
#[cfg(feature = "fast_match_byte")]
9
pub use crate::match_byte;
10
11
use super::Token;
12
13
/// Trait for things the can serialize themselves in CSS syntax.
14
pub trait ToCss {
15
    /// Serialize `self` in CSS syntax, writing to `dest`.
16
    fn to_css<W>(&self, dest: &mut W) -> fmt::Result
17
    where
18
        W: fmt::Write;
19
20
    /// Serialize `self` in CSS syntax and return a string.
21
    ///
22
    /// (This is a convenience wrapper for `to_css` and probably should not be overridden.)
23
    #[inline]
24
    fn to_css_string(&self) -> String {
25
        let mut s = String::new();
26
        self.to_css(&mut s).unwrap();
27
        s
28
    }
29
}
30
31
#[inline]
32
68.5M
fn write_numeric<W>(value: f32, int_value: Option<i32>, has_sign: bool, dest: &mut W) -> fmt::Result
33
68.5M
where
34
68.5M
    W: fmt::Write,
35
{
36
68.5M
    if value == 0.0 && value.is_sign_negative() {
37
        // Negative zero. Work around #20596.
38
40.0k
        return dest.write_str("-0");
39
68.5M
    }
40
    // NOTE: `value.value >= 0` is true for negative 0 but we've dealt with it above.
41
68.5M
    if has_sign && value >= 0.0 {
42
5.33M
        dest.write_str("+")?;
43
63.2M
    }
44
45
68.5M
    if let Some(v) = int_value {
46
3.57M
        return write!(dest, "{}", v);
47
64.9M
    }
48
49
64.9M
    let notation = dtoa_short::write(dest, value)?;
50
64.9M
    if value.fract() == 0. && !notation.decimal_point && !notation.scientific {
51
64.7M
        dest.write_str(".0")?;
52
272k
    }
53
64.9M
    Ok(())
54
68.5M
}
55
56
impl ToCss for Token<'_> {
57
89.2M
    fn to_css<W>(&self, dest: &mut W) -> fmt::Result
58
89.2M
    where
59
89.2M
        W: fmt::Write,
60
    {
61
89.2M
        match *self {
62
604k
            Token::Ident(ref value) => serialize_identifier(value, dest)?,
63
253k
            Token::AtKeyword(ref value) => {
64
253k
                dest.write_str("@")?;
65
253k
                serialize_identifier(value, dest)?;
66
            }
67
37.9k
            Token::Hash(ref value) => {
68
37.9k
                dest.write_str("#")?;
69
37.9k
                serialize_name(value, dest)?;
70
            }
71
69.4k
            Token::IDHash(ref value) => {
72
69.4k
                dest.write_str("#")?;
73
69.4k
                serialize_identifier(value, dest)?;
74
            }
75
417k
            Token::QuotedString(ref value) => serialize_string(value, dest)?,
76
34.3k
            Token::UnquotedUrl(ref value) => {
77
34.3k
                dest.write_str("url(")?;
78
34.3k
                serialize_unquoted_url(value, dest)?;
79
34.3k
                dest.write_str(")")?;
80
            }
81
16.7M
            Token::Delim(value) => dest.write_char(value)?,
82
83
            Token::Number {
84
68.2M
                value,
85
68.2M
                int_value,
86
68.2M
                has_sign,
87
68.2M
            } => write_numeric(value, int_value, has_sign, dest)?,
88
            Token::Percentage {
89
160k
                unit_value,
90
160k
                int_value,
91
160k
                has_sign,
92
            } => {
93
160k
                write_numeric(unit_value * 100., int_value, has_sign, dest)?;
94
160k
                dest.write_str("%")?;
95
            }
96
            Token::Dimension {
97
226k
                value,
98
226k
                int_value,
99
226k
                has_sign,
100
226k
                ref unit,
101
            } => {
102
226k
                write_numeric(value, int_value, has_sign, dest)?;
103
                // Disambiguate with scientific notation.
104
226k
                let unit = &**unit;
105
                // TODO(emilio): This doesn't handle e.g. 100E1m, which gets us
106
                // an unit of "E1m"...
107
226k
                if unit == "e" || unit == "E" || unit.starts_with("e-") || unit.starts_with("E-") {
108
118k
                    dest.write_str("\\65 ")?;
109
118k
                    serialize_name(&unit[1..], dest)?;
110
                } else {
111
107k
                    serialize_identifier(unit, dest)?;
112
                }
113
            }
114
115
201k
            Token::WhiteSpace(content) => dest.write_str(content)?,
116
0
            Token::Comment(content) => {
117
0
                dest.write_str("/*")?;
118
0
                dest.write_str(content)?;
119
0
                dest.write_str("*/")?
120
            }
121
167k
            Token::Colon => dest.write_str(":")?,
122
263k
            Token::Semicolon => dest.write_str(";")?,
123
222k
            Token::Comma => dest.write_str(",")?,
124
159k
            Token::IncludeMatch => dest.write_str("~=")?,
125
121k
            Token::DashMatch => dest.write_str("|=")?,
126
115k
            Token::PrefixMatch => dest.write_str("^=")?,
127
92.5k
            Token::SuffixMatch => dest.write_str("$=")?,
128
2.25k
            Token::SubstringMatch => dest.write_str("*=")?,
129
35.0k
            Token::CDO => dest.write_str("<!--")?,
130
50.9k
            Token::CDC => dest.write_str("-->")?,
131
132
40.6k
            Token::Function(ref name) => {
133
40.6k
                serialize_identifier(name, dest)?;
134
40.6k
                dest.write_str("(")?;
135
            }
136
46.1k
            Token::ParenthesisBlock => dest.write_str("(")?,
137
37.1k
            Token::SquareBracketBlock => dest.write_str("[")?,
138
424k
            Token::CurlyBracketBlock => dest.write_str("{")?,
139
140
0
            Token::BadUrl(ref contents) => {
141
0
                dest.write_str("url(")?;
142
0
                dest.write_str(contents)?;
143
0
                dest.write_char(')')?;
144
            }
145
0
            Token::BadString(ref value) => {
146
                // During tokenization, an unescaped newline after a quote causes
147
                // the token to be a BadString instead of a QuotedString.
148
                // The BadString token ends just before the newline
149
                // (which is in a separate WhiteSpace token),
150
                // and therefore does not have a closing quote.
151
0
                dest.write_char('"')?;
152
0
                CssStringWriter::new(dest).write_str(value)?;
153
            }
154
79.8k
            Token::CloseParenthesis => dest.write_str(")")?,
155
36.8k
            Token::CloseSquareBracket => dest.write_str("]")?,
156
421k
            Token::CloseCurlyBracket => dest.write_str("}")?,
157
        }
158
89.2M
        Ok(())
159
89.2M
    }
160
}
161
162
485k
fn hex_escape<W>(ascii_byte: u8, dest: &mut W) -> fmt::Result
163
485k
where
164
485k
    W: fmt::Write,
165
{
166
    static HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
167
    let b3;
168
    let b4;
169
485k
    let bytes = if ascii_byte > 0x0F {
170
208k
        let high = (ascii_byte >> 4) as usize;
171
208k
        let low = (ascii_byte & 0x0F) as usize;
172
208k
        b4 = [b'\\', HEX_DIGITS[high], HEX_DIGITS[low], b' '];
173
208k
        &b4[..]
174
    } else {
175
277k
        b3 = [b'\\', HEX_DIGITS[ascii_byte as usize], b' '];
176
277k
        &b3[..]
177
    };
178
485k
    dest.write_str(unsafe { str::from_utf8_unchecked(bytes) })
179
485k
}
180
181
212k
fn char_escape<W>(ascii_byte: u8, dest: &mut W) -> fmt::Result
182
212k
where
183
212k
    W: fmt::Write,
184
{
185
212k
    let bytes = [b'\\', ascii_byte];
186
212k
    dest.write_str(unsafe { str::from_utf8_unchecked(&bytes) })
187
212k
}
188
189
/// Write a CSS identifier, escaping characters as necessary.
190
1.07M
pub fn serialize_identifier<W>(mut value: &str, dest: &mut W) -> fmt::Result
191
1.07M
where
192
1.07M
    W: fmt::Write,
193
{
194
1.07M
    if value.is_empty() {
195
0
        return Ok(());
196
1.07M
    }
197
198
1.07M
    if let Some(value) = value.strip_prefix("--") {
199
39.0k
        dest.write_str("--")?;
200
39.0k
        serialize_name(value, dest)
201
1.03M
    } else if value == "-" {
202
26.6k
        dest.write_str("\\-")
203
    } else {
204
1.01M
        if value.as_bytes()[0] == b'-' {
205
121k
            dest.write_str("-")?;
206
121k
            value = &value[1..];
207
888k
        }
208
1.01M
        if let digit @ b'0'..=b'9' = value.as_bytes()[0] {
209
384
            hex_escape(digit, dest)?;
210
384
            value = &value[1..];
211
1.00M
        }
212
1.01M
        serialize_name(value, dest)
213
    }
214
1.07M
}
215
216
/// Write a CSS name, like a custom property name.
217
///
218
/// You should only use this when you know what you're doing, when in doubt,
219
/// consider using `serialize_identifier`.
220
1.20M
pub fn serialize_name<W>(value: &str, dest: &mut W) -> fmt::Result
221
1.20M
where
222
1.20M
    W: fmt::Write,
223
{
224
1.20M
    let mut chunk_start = 0;
225
184M
    for (i, b) in value.bytes().enumerate() {
226
184M
        let escaped = match_byte! { b,
227
22.4M
            b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'_' | b'-' => continue,
228
0
            b'\0' => Some("\u{FFFD}"),
229
184M
            b => {
230
162M
                if !b.is_ascii() {
231
161M
                    continue;
232
464k
                }
233
464k
                None
234
            },
235
        };
236
464k
        dest.write_str(&value[chunk_start..i])?;
237
464k
        if let Some(escaped) = escaped {
238
0
            dest.write_str(escaped)?;
239
464k
        } else if (b'\x01'..=b'\x1F').contains(&b) || b == b'\x7F' {
240
256k
            hex_escape(b, dest)?;
241
        } else {
242
208k
            char_escape(b, dest)?;
243
        }
244
464k
        chunk_start = i + 1;
245
    }
246
1.20M
    dest.write_str(&value[chunk_start..])
247
1.20M
}
248
249
34.3k
fn serialize_unquoted_url<W>(value: &str, dest: &mut W) -> fmt::Result
250
34.3k
where
251
34.3k
    W: fmt::Write,
252
{
253
34.3k
    let mut chunk_start = 0;
254
150M
    for (i, b) in value.bytes().enumerate() {
255
150M
        let hex = match_byte! { b,
256
34.5k
            b'\0'..=b' ' | b'\x7F' => true,
257
4.40k
            b'(' | b')' | b'"' | b'\'' | b'\\' => false,
258
150M
            _ => continue,
259
        };
260
38.9k
        dest.write_str(&value[chunk_start..i])?;
261
38.9k
        if hex {
262
34.5k
            hex_escape(b, dest)?;
263
        } else {
264
4.40k
            char_escape(b, dest)?;
265
        }
266
38.9k
        chunk_start = i + 1;
267
    }
268
34.3k
    dest.write_str(&value[chunk_start..])
269
34.3k
}
270
271
/// Write a double-quoted CSS string token, escaping content as necessary.
272
417k
pub fn serialize_string<W>(value: &str, dest: &mut W) -> fmt::Result
273
417k
where
274
417k
    W: fmt::Write,
275
{
276
417k
    dest.write_str("\"")?;
277
417k
    CssStringWriter::new(dest).write_str(value)?;
278
417k
    dest.write_str("\"")?;
279
417k
    Ok(())
280
417k
}
281
282
/// A `fmt::Write` adapter that escapes text for writing as a double-quoted CSS string.
283
/// Quotes are not included.
284
///
285
/// Typical usage:
286
///
287
/// ```rust,ignore
288
/// fn write_foo<W>(foo: &Foo, dest: &mut W) -> fmt::Result where W: fmt::Write {
289
///     dest.write_str("\"")?;
290
///     {
291
///         let mut string_dest = CssStringWriter::new(dest);
292
///         // Write into string_dest...
293
///     }
294
///     dest.write_str("\"")?;
295
///     Ok(())
296
/// }
297
/// ```
298
pub struct CssStringWriter<'a, W> {
299
    inner: &'a mut W,
300
}
301
302
impl<'a, W> CssStringWriter<'a, W>
303
where
304
    W: fmt::Write,
305
{
306
    /// Wrap a text writer to create a `CssStringWriter`.
307
417k
    pub fn new(inner: &'a mut W) -> CssStringWriter<'a, W> {
308
417k
        CssStringWriter { inner }
309
417k
    }
310
}
311
312
impl<W> fmt::Write for CssStringWriter<'_, W>
313
where
314
    W: fmt::Write,
315
{
316
417k
    fn write_str(&mut self, s: &str) -> fmt::Result {
317
417k
        let mut chunk_start = 0;
318
82.7M
        for (i, b) in s.bytes().enumerate() {
319
82.7M
            let escaped = match_byte! { b,
320
189k
                b'"' => Some("\\\""),
321
2.73k
                b'\\' => Some("\\\\"),
322
0
                b'\0' => Some("\u{FFFD}"),
323
194k
                b'\x01'..=b'\x1F' | b'\x7F' => None,
324
82.3M
                _ => continue,
325
            };
326
386k
            self.inner.write_str(&s[chunk_start..i])?;
327
386k
            match escaped {
328
192k
                Some(x) => self.inner.write_str(x)?,
329
194k
                None => hex_escape(b, self.inner)?,
330
            };
331
386k
            chunk_start = i + 1;
332
        }
333
417k
        self.inner.write_str(&s[chunk_start..])
334
417k
    }
335
}
336
337
macro_rules! impl_tocss_for_int {
338
    ($T: ty) => {
339
        impl ToCss for $T {
340
            fn to_css<W>(&self, dest: &mut W) -> fmt::Result
341
            where
342
                W: fmt::Write,
343
            {
344
                let mut buf = itoa::Buffer::new();
345
                dest.write_str(buf.format(*self))
346
            }
347
        }
348
    };
349
}
350
351
impl_tocss_for_int!(i8);
352
impl_tocss_for_int!(u8);
353
impl_tocss_for_int!(i16);
354
impl_tocss_for_int!(u16);
355
impl_tocss_for_int!(i32);
356
impl_tocss_for_int!(u32);
357
impl_tocss_for_int!(i64);
358
impl_tocss_for_int!(u64);
359
360
macro_rules! impl_tocss_for_float {
361
    ($T: ty) => {
362
        impl ToCss for $T {
363
            fn to_css<W>(&self, dest: &mut W) -> fmt::Result
364
            where
365
                W: fmt::Write,
366
            {
367
                dtoa_short::write(dest, *self).map(|_| ())
368
            }
369
        }
370
    };
371
}
372
373
impl_tocss_for_float!(f32);
374
impl_tocss_for_float!(f64);
375
376
/// A category of token. See the `needs_separator_when_before` method.
377
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
378
pub enum TokenSerializationType {
379
    /// No token serialization type.
380
    #[default]
381
    Nothing,
382
383
    /// The [`<whitespace-token>`](https://drafts.csswg.org/css-syntax/#whitespace-token-diagram)
384
    /// type.
385
    WhiteSpace,
386
387
    /// The [`<at-keyword-token>`](https://drafts.csswg.org/css-syntax/#at-keyword-token-diagram)
388
    /// type, the "[`<hash-token>`](https://drafts.csswg.org/css-syntax/#hash-token-diagram) with
389
    /// the type flag set to 'unrestricted'" type, or the
390
    /// "[`<hash-token>`](https://drafts.csswg.org/css-syntax/#hash-token-diagram) with the type
391
    /// flag set to 'id'" type.
392
    AtKeywordOrHash,
393
394
    /// The [`<number-token>`](https://drafts.csswg.org/css-syntax/#number-token-diagram) type.
395
    Number,
396
397
    /// The [`<dimension-token>`](https://drafts.csswg.org/css-syntax/#dimension-token-diagram)
398
    /// type.
399
    Dimension,
400
401
    /// The [`<percentage-token>`](https://drafts.csswg.org/css-syntax/#percentage-token-diagram)
402
    /// type.
403
    Percentage,
404
405
    /// The [`<url-token>`](https://drafts.csswg.org/css-syntax/#url-token-diagram) or
406
    /// `<bad-url-token>` type.
407
    UrlOrBadUrl,
408
409
    /// The [`<function-token>`](https://drafts.csswg.org/css-syntax/#function-token-diagram) type.
410
    Function,
411
412
    /// The [`<ident-token>`](https://drafts.csswg.org/css-syntax/#ident-token-diagram) type.
413
    Ident,
414
415
    /// The `-->` [`<CDC-token>`](https://drafts.csswg.org/css-syntax/#CDC-token-diagram) type.
416
    CDC,
417
418
    /// The `|=`
419
    /// [`<dash-match-token>`](https://drafts.csswg.org/css-syntax/#dash-match-token-diagram) type.
420
    DashMatch,
421
422
    /// The `*=`
423
    /// [`<substring-match-token>`](https://drafts.csswg.org/css-syntax/#substring-match-token-diagram)
424
    /// type.
425
    SubstringMatch,
426
427
    /// The `<(-token>` type.
428
    OpenParen,
429
430
    /// The `#` `<delim-token>` type.
431
    DelimHash,
432
433
    /// The `@` `<delim-token>` type.
434
    DelimAt,
435
436
    /// The `.` or `+` `<delim-token>` type.
437
    DelimDotOrPlus,
438
439
    /// The `-` `<delim-token>` type.
440
    DelimMinus,
441
442
    /// The `?` `<delim-token>` type.
443
    DelimQuestion,
444
445
    /// The `$`, `^`, or `~` `<delim-token>` type.
446
    DelimAssorted,
447
448
    /// The `=` `<delim-token>` type.
449
    DelimEquals,
450
451
    /// The `|` `<delim-token>` type.
452
    DelimBar,
453
454
    /// The `/` `<delim-token>` type.
455
    DelimSlash,
456
457
    /// The `*` `<delim-token>` type.
458
    DelimAsterisk,
459
460
    /// The `%` `<delim-token>` type.
461
    DelimPercent,
462
463
    /// A type indicating any other token.
464
    Other,
465
}
466
467
#[cfg(feature = "malloc_size_of")]
468
malloc_size_of::malloc_size_of_is_0!(TokenSerializationType);
469
470
impl TokenSerializationType {
471
    /// Return a value that represents the absence of a token, e.g. before the start of the input.
472
    #[deprecated(
473
        since = "0.32.1",
474
        note = "use TokenSerializationType::Nothing or TokenSerializationType::default() instead"
475
    )]
476
2.42k
    pub fn nothing() -> TokenSerializationType {
477
2.42k
        Default::default()
478
2.42k
    }
479
480
    /// If this value is `TokenSerializationType::Nothing`, set it to the given value instead.
481
0
    pub fn set_if_nothing(&mut self, new_value: TokenSerializationType) {
482
0
        if matches!(self, TokenSerializationType::Nothing) {
483
0
            *self = new_value
484
0
        }
485
0
    }
486
487
    /// Return true if, when a token of category `self` is serialized just before
488
    /// a token of category `other` with no whitespace in between,
489
    /// an empty comment `/**/` needs to be inserted between them
490
    /// so that they are not re-parsed as a single token.
491
    ///
492
    /// See https://drafts.csswg.org/css-syntax/#serialization
493
    ///
494
    /// See https://github.com/w3c/csswg-drafts/issues/4088 for the
495
    /// `DelimPercent` bits.
496
88.7M
    pub fn needs_separator_when_before(self, other: TokenSerializationType) -> bool {
497
        use self::TokenSerializationType::*;
498
88.7M
        match self {
499
597k
            Ident => matches!(
500
604k
                other,
501
                Ident
502
                    | Function
503
                    | UrlOrBadUrl
504
                    | DelimMinus
505
                    | Number
506
                    | Percentage
507
                    | Dimension
508
                    | CDC
509
                    | OpenParen
510
            ),
511
496k
            AtKeywordOrHash | Dimension => matches!(
512
586k
                other,
513
                Ident | Function | UrlOrBadUrl | DelimMinus | Number | Percentage | Dimension | CDC
514
            ),
515
580k
            DelimHash | DelimMinus => matches!(
516
3.14M
                other,
517
                Ident | Function | UrlOrBadUrl | DelimMinus | Number | Percentage | Dimension
518
            ),
519
335k
            Number => matches!(
520
68.2M
                other,
521
                Ident
522
                    | Function
523
                    | UrlOrBadUrl
524
                    | DelimMinus
525
                    | Number
526
                    | Percentage
527
                    | DelimPercent
528
                    | Dimension
529
            ),
530
151k
            DelimAt => matches!(other, Ident | Function | UrlOrBadUrl | DelimMinus),
531
27.1k
            DelimDotOrPlus => matches!(other, Number | Percentage | Dimension),
532
447k
            DelimAssorted | DelimAsterisk => matches!(other, DelimEquals),
533
11.9M
            DelimBar => matches!(other, DelimEquals | DelimBar | DashMatch),
534
66.9k
            DelimSlash => matches!(other, DelimAsterisk | SubstringMatch),
535
            Nothing | WhiteSpace | Percentage | UrlOrBadUrl | Function | CDC | OpenParen
536
            | DashMatch | SubstringMatch | DelimQuestion | DelimEquals | DelimPercent | Other => {
537
3.56M
                false
538
            }
539
        }
540
88.7M
    }
541
}
542
543
impl Token<'_> {
544
    /// Categorize a token into a type that determines when `/**/` needs to be inserted
545
    /// between two tokens when serialized next to each other without whitespace in between.
546
    ///
547
    /// See the `TokenSerializationType::needs_separator_when_before` method.
548
88.7M
    pub fn serialization_type(&self) -> TokenSerializationType {
549
        use self::TokenSerializationType::*;
550
88.7M
        match self {
551
604k
            Token::Ident(_) => Ident,
552
360k
            Token::AtKeyword(_) | Token::Hash(_) | Token::IDHash(_) => AtKeywordOrHash,
553
34.3k
            Token::UnquotedUrl(_) | Token::BadUrl(_) => UrlOrBadUrl,
554
10.8k
            Token::Delim('#') => DelimHash,
555
151k
            Token::Delim('@') => DelimAt,
556
27.2k
            Token::Delim('.') | Token::Delim('+') => DelimDotOrPlus,
557
3.13M
            Token::Delim('-') => DelimMinus,
558
16.6k
            Token::Delim('?') => DelimQuestion,
559
189k
            Token::Delim('$') | Token::Delim('^') | Token::Delim('~') => DelimAssorted,
560
18.2k
            Token::Delim('%') => DelimPercent,
561
47.2k
            Token::Delim('=') => DelimEquals,
562
11.9M
            Token::Delim('|') => DelimBar,
563
66.9k
            Token::Delim('/') => DelimSlash,
564
258k
            Token::Delim('*') => DelimAsterisk,
565
68.2M
            Token::Number { .. } => Number,
566
160k
            Token::Percentage { .. } => Percentage,
567
226k
            Token::Dimension { .. } => Dimension,
568
201k
            Token::WhiteSpace(_) => WhiteSpace,
569
0
            Token::Comment(_) => DelimSlash,
570
121k
            Token::DashMatch => DashMatch,
571
2.25k
            Token::SubstringMatch => SubstringMatch,
572
50.9k
            Token::CDC => CDC,
573
40.6k
            Token::Function(_) => Function,
574
46.1k
            Token::ParenthesisBlock => OpenParen,
575
            Token::SquareBracketBlock
576
            | Token::CurlyBracketBlock
577
            | Token::CloseParenthesis
578
            | Token::CloseSquareBracket
579
            | Token::CloseCurlyBracket
580
            | Token::QuotedString(_)
581
            | Token::BadString(_)
582
            | Token::Delim(_)
583
            | Token::Colon
584
            | Token::Semicolon
585
            | Token::Comma
586
            | Token::CDO
587
            | Token::IncludeMatch
588
            | Token::PrefixMatch
589
2.82M
            | Token::SuffixMatch => Other,
590
        }
591
88.7M
    }
592
}