lewp_css/domain/properties/
specified_value.rs

1// This file is part of css. It is subject to the license terms in the COPYRIGHT file found in the top-level directory of this distribution and at https://raw.githubusercontent.com/lemonrock/css/master/COPYRIGHT. No part of predicator, including this file, may be copied, modified, propagated, or distributed except according to the terms contained in the COPYRIGHT file.
2// Copyright © 2017 The developers of css. See the COPYRIGHT file in the top-level directory of this distribution and at https://raw.githubusercontent.com/lemonrock/css/master/COPYRIGHT.
3
4use {
5    crate::{domain::Atom, parsers::ParserContext, CustomParseError},
6    cssparser::{Delimiter, ParseError, Parser, ToCss, TokenSerializationType},
7    std::{borrow::Cow, collections::HashSet, fmt},
8};
9
10/// A specified value for a property is just a set of tokens.
11///
12/// The original CSS is preserved for serialization, as are variable references to other property names.
13#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
14pub struct SpecifiedValue {
15    pub originalCss: String,
16    //first_token_type: TokenSerializationType,
17    //last_token_type: TokenSerializationType,
18    // References to property names in var() functions.
19    //pub references: HashSet<Atom>,
20}
21
22impl ToCss for SpecifiedValue {
23    fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
24        dest.write_str(&self.originalCss)
25    }
26}
27
28impl SpecifiedValue {
29    /// Parse a custom property SpecifiedValue.
30    pub(crate) fn parse<'i, 't>(
31        _context: &ParserContext,
32        input: &mut Parser<'i, 't>,
33    ) -> Result<Self, ParseError<'i, CustomParseError<'i>>> {
34        let mut references = Some(HashSet::new());
35        let (_first, css, _last) =
36            Self::parse_self_contained_declaration_value(
37                input,
38                &mut references,
39            )?;
40        Ok(SpecifiedValue {
41            originalCss: css.into_owned(),
42            //first_token_type: first,
43            //last_token_type: last,
44            //references: references.unwrap(),
45        })
46    }
47
48    fn parse_self_contained_declaration_value<'i, 't>(
49        input: &mut Parser<'i, 't>,
50        references: &mut Option<HashSet<Atom>>,
51    ) -> Result<
52        (TokenSerializationType, Cow<'i, str>, TokenSerializationType),
53        ParseError<'i, CustomParseError<'i>>,
54    > {
55        let start_position = input.position();
56        let mut missing_closing_characters = String::new();
57        let (first, last) = Self::parse_declaration_value(
58            input,
59            references,
60            &mut missing_closing_characters,
61        )?;
62        let mut css: Cow<str> = input.slice_from(start_position).into();
63        if !missing_closing_characters.is_empty() {
64            // Unescaped backslash at EOF in a quoted string is ignored.
65            if css.ends_with('\\') {
66                let first = missing_closing_characters.as_bytes()[0];
67                if first == b'"' || first == b'\'' {
68                    css.to_mut().pop();
69                }
70            }
71            css.to_mut().push_str(&missing_closing_characters);
72        }
73        Ok((first, css, last))
74    }
75
76    /// <https://drafts.csswg.org/css-syntax-3/#typedef-declaration-value>
77    fn parse_declaration_value<'i, 't>(
78        input: &mut Parser<'i, 't>,
79        references: &mut Option<HashSet<Atom>>,
80        missing_closing_characters: &mut String,
81    ) -> Result<
82        (TokenSerializationType, TokenSerializationType),
83        ParseError<'i, CustomParseError<'i>>,
84    > {
85        input.parse_until_before(
86            Delimiter::Bang | Delimiter::Semicolon,
87            |input| {
88                // Need at least one token
89                let start = input.state();
90                input.next_including_whitespace()?;
91                input.reset(&start);
92
93                Self::parse_declaration_value_block(
94                    input,
95                    references,
96                    missing_closing_characters,
97                )
98            },
99        )
100    }
101
102    // Like parse_declaration_value, but accept `!` and `;` since they are only invalid at the top level
103    fn parse_declaration_value_block<'i, 't>(
104        input: &mut Parser<'i, 't>,
105        references: &mut Option<HashSet<Atom>>,
106        missing_closing_characters: &mut String,
107    ) -> Result<
108        (TokenSerializationType, TokenSerializationType),
109        ParseError<'i, CustomParseError<'i>>,
110    > {
111        let mut token_start = input.position();
112        let mut token = match input.next_including_whitespace_and_comments() {
113            Ok(token) => token.clone(),
114            Err(_) => {
115                return Ok((
116                    TokenSerializationType::nothing(),
117                    TokenSerializationType::nothing(),
118                ))
119            }
120        };
121
122        let first_token_type = token.serialization_type();
123        loop {
124            macro_rules! nested {
125                () => {
126                    input.parse_nested_block(|input| {
127                        Self::parse_declaration_value_block(
128                            input,
129                            references,
130                            missing_closing_characters,
131                        )
132                    })?
133                };
134            }
135
136            macro_rules! check_closed {
137                ($closing: expr) => {
138                    if !input.slice_from(token_start).ends_with($closing) {
139                        missing_closing_characters.push_str($closing)
140                    }
141                };
142            }
143
144            use cssparser::Token::*;
145
146            let last_token_type = match token {
147                Comment(_) => {
148                    let token_slice = input.slice_from(token_start);
149                    if !token_slice.ends_with("*/") {
150                        missing_closing_characters.push_str(if token_slice.ends_with('*') {
151                            "/"
152                        } else {
153                            "*/"
154                        })
155                    }
156                    token.serialization_type()
157                }
158
159                BadUrl(url) => {
160                    return Err(ParseError::from(
161                        CustomParseError::BadUrlInDeclarationValueBlock(url),
162                    ))
163                }
164
165                BadString(string) => {
166                    return Err(ParseError::from(
167                        CustomParseError::BadStringInDeclarationValueBlock(string),
168                    ))
169                }
170
171                CloseParenthesis => {
172                    return Err(ParseError::from(
173                        CustomParseError::UnbalancedCloseParenthesisInDeclarationValueBlock,
174                    ))
175                }
176
177                CloseSquareBracket => {
178                    return Err(ParseError::from(
179                        CustomParseError::UnbalancedCloseSquareBracketInDeclarationValueBlock,
180                    ))
181                }
182
183                CloseCurlyBracket => {
184                    return Err(ParseError::from(
185                        CustomParseError::UnbalancedCloseCurlyBracketInDeclarationValueBlock,
186                    ))
187                }
188
189                Function(ref name) => {
190                    if name.eq_ignore_ascii_case("var") {
191                        let args_start = input.state();
192                        input.parse_nested_block(|input| {
193                            Self::parse_var_function(input, references)
194                        })?;
195                        input.reset(&args_start);
196                    }
197                    nested!();
198                    check_closed!(")");
199                    CloseParenthesis.serialization_type()
200                }
201
202                ParenthesisBlock => {
203                    nested!();
204                    check_closed!(")");
205                    CloseParenthesis.serialization_type()
206                }
207
208                CurlyBracketBlock => {
209                    nested!();
210                    check_closed!("}");
211                    CloseCurlyBracket.serialization_type()
212                }
213
214                SquareBracketBlock => {
215                    nested!();
216                    check_closed!("]");
217                    CloseSquareBracket.serialization_type()
218                }
219
220                QuotedString(_) => {
221                    let token_slice = input.slice_from(token_start);
222                    let quote = &token_slice[..1];
223                    if !(token_slice.ends_with(quote) && token_slice.len() > 1) {
224                        missing_closing_characters.push_str(quote)
225                    }
226                    token.serialization_type()
227                }
228
229                Ident(ref value)
230                | AtKeyword(ref value)
231                | Hash(ref value)
232                | IDHash(ref value)
233                | UnquotedUrl(ref value)
234                | Dimension {
235                    unit: ref value, ..
236                } => {
237                    if value.ends_with('�') && input.slice_from(token_start).ends_with('\\') {
238                        // Unescaped backslash at EOF in these contexts is interpreted as U+FFFD
239                        // Check the value in case the final backslash was itself escaped.
240                        // Serialize as escaped U+FFFD, which is also interpreted as U+FFFD.
241                        // (Unescaped U+FFFD would also work, but removing the backslash is annoying.)
242                        missing_closing_characters.push('�')
243                    }
244
245                    match token {
246                        UnquotedUrl(_) => check_closed!(")"),
247                        _ => {}
248                    }
249
250                    token.serialization_type()
251                }
252                _ => token.serialization_type(),
253            };
254
255            token_start = input.position();
256            token = match input.next_including_whitespace_and_comments() {
257                Ok(token) => token.clone(),
258                Err(..) => return Ok((first_token_type, last_token_type)),
259            };
260        }
261    }
262
263    // If the var function is valid, return Ok((custom_property_name, fallback))
264    fn parse_var_function<'i, 't>(
265        input: &mut Parser<'i, 't>,
266        references: &mut Option<HashSet<Atom>>,
267    ) -> Result<(), ParseError<'i, CustomParseError<'i>>> {
268        let name = input.expect_ident_cloned()?;
269        if input.r#try(|input| input.expect_comma()).is_ok() {
270            // Exclude `!` and `;` at the top level
271            // https://drafts.csswg.org/css-syntax/#typedef-declaration-value
272            input.parse_until_before(
273                Delimiter::Bang | Delimiter::Semicolon,
274                |input| {
275                    // At least one non-comment token.
276                    input.next_including_whitespace()?;
277
278                    // Skip until the end.
279                    while input.next_including_whitespace_and_comments().is_ok()
280                    {
281                    }
282
283                    Ok(())
284                },
285            )?;
286        }
287        if let Some(ref mut refs) = *references {
288            refs.insert(Atom::from(name));
289        }
290        Ok(())
291    }
292}