Skip to main content

rama_http/protocols/html/selector/
display.rs

1//! Serialization of parsed selectors back to canonical CSS text.
2//!
3//! Output follows the CSSOM "serialize a selector" rules closely enough to
4//! round-trip: parsing the [`Display`] output yields an equal AST. The
5//! canonical form is not byte-identical to arbitrary input (whitespace,
6//! component order and quoting are normalized).
7
8use std::fmt::{self, Write as _};
9
10use super::ast::{
11    AttributeOperator, AttributeSelector, CaseSensitivity, Combinator, ComplexSelector, Compound,
12    Nth, Selector,
13};
14
15impl fmt::Display for Selector {
16    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17        for (i, complex) in self.selectors.iter().enumerate() {
18            if i != 0 {
19                f.write_str(", ")?;
20            }
21            write_complex(f, complex)?;
22        }
23        Ok(())
24    }
25}
26
27fn write_complex(f: &mut fmt::Formatter<'_>, complex: &ComplexSelector) -> fmt::Result {
28    for part in &complex.parts {
29        match part.combinator {
30            None => {}
31            Some(Combinator::Descendant) => f.write_str(" ")?,
32            Some(Combinator::Child) => f.write_str(" > ")?,
33        }
34        write_compound(f, &part.compound)?;
35    }
36    Ok(())
37}
38
39fn write_compound(f: &mut fmt::Formatter<'_>, compound: &Compound) -> fmt::Result {
40    if let Some(name) = &compound.name {
41        write_ident(f, name.as_str())?;
42    } else if compound.explicit_universal {
43        f.write_str("*")?;
44    }
45    if let Some(id) = &compound.id {
46        f.write_str("#")?;
47        write_ident(f, id)?;
48    }
49    for class in &compound.classes {
50        f.write_str(".")?;
51        write_ident(f, class)?;
52    }
53    for attr in &compound.attributes {
54        write_attribute(f, attr)?;
55    }
56    for nth in &compound.nth {
57        write_nth(f, *nth)?;
58    }
59    for negation in &compound.negations {
60        f.write_str(":not(")?;
61        write_compound(f, negation)?;
62        f.write_str(")")?;
63    }
64    Ok(())
65}
66
67fn write_attribute(f: &mut fmt::Formatter<'_>, attr: &AttributeSelector) -> fmt::Result {
68    f.write_str("[")?;
69    write_ident(f, &attr.name)?;
70    if let Some(operator) = attr.operator {
71        f.write_str(match operator {
72            AttributeOperator::Equals => "=",
73            AttributeOperator::Includes => "~=",
74            AttributeOperator::DashMatch => "|=",
75            AttributeOperator::Prefix => "^=",
76            AttributeOperator::Suffix => "$=",
77            AttributeOperator::Substring => "*=",
78        })?;
79        write_string(f, &attr.value)?;
80        if matches!(attr.case, CaseSensitivity::AsciiCaseInsensitive) {
81            f.write_str(" i")?;
82        }
83    }
84    f.write_str("]")
85}
86
87fn write_nth(f: &mut fmt::Formatter<'_>, nth: Nth) -> fmt::Result {
88    let pseudo = match nth.ty {
89        super::ast::NthType::Child => "nth-child",
90        super::ast::NthType::OfType => "nth-of-type",
91    };
92    write!(f, ":{pseudo}(")?;
93    if nth.a == 0 {
94        write!(f, "{}", nth.b)?;
95    } else {
96        match nth.a {
97            1 => f.write_str("n")?,
98            -1 => f.write_str("-n")?,
99            a => write!(f, "{a}n")?,
100        }
101        match nth.b {
102            0 => {}
103            b if b > 0 => write!(f, "+{b}")?,
104            b => write!(f, "{b}")?,
105        }
106    }
107    f.write_str(")")
108}
109
110/// Serializes a CSS identifier (CSSOM "serialize an identifier").
111fn write_ident(f: &mut fmt::Formatter<'_>, ident: &str) -> fmt::Result {
112    for (i, c) in ident.chars().enumerate() {
113        match c {
114            '\0' => f.write_str("\u{FFFD}")?,
115            c if is_control(c) => write_codepoint_escape(f, c)?,
116            c if c.is_ascii_digit() && i == 0 => write_codepoint_escape(f, c)?,
117            c if c.is_ascii_digit() && i == 1 && ident.starts_with('-') => {
118                write_codepoint_escape(f, c)?;
119            }
120            '-' if i == 0 && ident.len() == 1 => f.write_str("\\-")?,
121            c if c == '-' || c == '_' || c.is_ascii_alphanumeric() || !c.is_ascii() => {
122                f.write_char(c)?;
123            }
124            c => write!(f, "\\{c}")?,
125        }
126    }
127    Ok(())
128}
129
130/// Serializes a CSS string (CSSOM "serialize a string").
131fn write_string(f: &mut fmt::Formatter<'_>, value: &str) -> fmt::Result {
132    f.write_str("\"")?;
133    for c in value.chars() {
134        match c {
135            '\0' => f.write_str("\u{FFFD}")?,
136            c if is_control(c) => write_codepoint_escape(f, c)?,
137            '"' => f.write_str("\\\"")?,
138            '\\' => f.write_str("\\\\")?,
139            c => f.write_char(c)?,
140        }
141    }
142    f.write_str("\"")
143}
144
145fn write_codepoint_escape(f: &mut fmt::Formatter<'_>, c: char) -> fmt::Result {
146    write!(f, "\\{:x} ", c as u32)
147}
148
149fn is_control(c: char) -> bool {
150    let n = c as u32;
151    (0x1..=0x1F).contains(&n) || n == 0x7F
152}