Coverage Report

Created: 2026-02-14 06:22

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