Skip to main content

cssparser/
unicode_range.rs

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//! https://drafts.csswg.org/css-syntax/#urange
6
7use crate::tokenizer::Token;
8use crate::{BasicParseError, Parser, ToCss};
9use std::char;
10use std::fmt;
11
12/// One contiguous range of code points.
13///
14/// Can not be empty. Can represent a single code point when start == end.
15#[derive(PartialEq, Eq, Clone, Hash)]
16#[repr(C)]
17pub struct UnicodeRange {
18    /// Inclusive start of the range. In [0, end].
19    pub start: u32,
20
21    /// Inclusive end of the range. In [0, 0x10FFFF].
22    pub end: u32,
23}
24
25impl UnicodeRange {
26    /// https://drafts.csswg.org/css-syntax/#urange-syntax
27    pub fn parse(input: &mut Parser) -> Result<Self, BasicParseError> {
28        // <urange> =
29        //   u '+' <ident-token> '?'* |
30        //   u <dimension-token> '?'* |
31        //   u <number-token> '?'* |
32        //   u <number-token> <dimension-token> |
33        //   u <number-token> <number-token> |
34        //   u '+' '?'+
35
36        input.expect_ident_matching("u")?;
37        let after_u = input.position();
38        parse_tokens(input)?;
39
40        // This deviates from the spec in case there are CSS comments
41        // between tokens in the middle of one <unicode-range>,
42        // but oh well…
43        let concatenated_tokens = input.slice_from(after_u);
44
45        let range = match parse_concatenated(concatenated_tokens.as_bytes()) {
46            Ok(range) => range,
47            Err(()) => return Err(BasicParseError::unexpected_token()),
48        };
49        if range.end > char::MAX as u32 || range.start > range.end {
50            Err(BasicParseError::unexpected_token())
51        } else {
52            Ok(range)
53        }
54    }
55}
56
57fn parse_tokens(input: &mut Parser) -> Result<(), BasicParseError> {
58    match *input.next_including_whitespace()? {
59        Token::Delim('+') => {
60            match *input.next_including_whitespace()? {
61                Token::Ident(_) => {}
62                Token::Delim('?') => {}
63                _ => return Err(BasicParseError::unexpected_token()),
64            }
65            parse_question_marks(input)
66        }
67        Token::Dimension { .. } => parse_question_marks(input),
68        Token::Number { .. } => {
69            let after_number = input.state();
70            match input.next_including_whitespace() {
71                Ok(&Token::Delim('?')) => parse_question_marks(input),
72                Ok(&Token::Dimension { .. }) => {}
73                Ok(&Token::Number { .. }) => {}
74                _ => input.reset(&after_number),
75            }
76        }
77        _ => return Err(BasicParseError::unexpected_token()),
78    }
79    Ok(())
80}
81
82/// Consume as many '?' as possible
83fn parse_question_marks(input: &mut Parser) {
84    loop {
85        let start = input.state();
86        match input.next_including_whitespace() {
87            Ok(&Token::Delim('?')) => {}
88            _ => {
89                input.reset(&start);
90                return;
91            }
92        }
93    }
94}
95
96fn parse_concatenated(text: &[u8]) -> Result<UnicodeRange, ()> {
97    let mut text = match text.split_first() {
98        Some((&b'+', text)) => text,
99        _ => return Err(()),
100    };
101    let (first_hex_value, hex_digit_count) = consume_hex(&mut text, 6)?;
102    let question_marks = consume_question_marks(&mut text);
103    let consumed = hex_digit_count + question_marks;
104    if consumed == 0 || consumed > 6 {
105        return Err(());
106    }
107
108    if question_marks > 0 {
109        if text.is_empty() {
110            return Ok(UnicodeRange {
111                start: first_hex_value << (question_marks * 4),
112                end: ((first_hex_value + 1) << (question_marks * 4)) - 1,
113            });
114        }
115    } else if text.is_empty() {
116        return Ok(UnicodeRange {
117            start: first_hex_value,
118            end: first_hex_value,
119        });
120    } else if let Some((&b'-', mut text)) = text.split_first() {
121        let (second_hex_value, hex_digit_count) = consume_hex(&mut text, 6)?;
122        if hex_digit_count > 0 && hex_digit_count <= 6 && text.is_empty() {
123            return Ok(UnicodeRange {
124                start: first_hex_value,
125                end: second_hex_value,
126            });
127        }
128    }
129    Err(())
130}
131
132// Consume hex digits, but return an error if more than digit_limit are found.
133fn consume_hex(text: &mut &[u8], digit_limit: usize) -> Result<(u32, usize), ()> {
134    let mut value = 0;
135    let mut digits = 0;
136    while let Some((&byte, rest)) = text.split_first() {
137        if let Some(digit_value) = (byte as char).to_digit(16) {
138            if digits == digit_limit {
139                return Err(());
140            }
141            value = value * 0x10 + digit_value;
142            digits += 1;
143            *text = rest;
144        } else {
145            break;
146        }
147    }
148    Ok((value, digits))
149}
150
151fn consume_question_marks(text: &mut &[u8]) -> usize {
152    let mut question_marks = 0;
153    while let Some((&b'?', rest)) = text.split_first() {
154        question_marks += 1;
155        *text = rest
156    }
157    question_marks
158}
159
160impl fmt::Debug for UnicodeRange {
161    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
162        self.to_css(formatter)
163    }
164}
165
166impl ToCss for UnicodeRange {
167    fn to_css<W>(&self, dest: &mut W) -> fmt::Result
168    where
169        W: fmt::Write,
170    {
171        write!(dest, "U+{:X}", self.start)?;
172        if self.end != self.start {
173            write!(dest, "-{:X}", self.end)?;
174        }
175        Ok(())
176    }
177}