Skip to main content

ferroui_base/media/
unicode_range.rs

1use std::rc::Rc;
2use std::str::FromStr;
3
4use crate::utilities::FormatError;
5
6/// The `UnicodeRange` class is used to specify which specific characters
7/// should be considered in a font fallback.
8#[derive(Clone, Debug, PartialEq, Eq, Hash)]
9pub struct UnicodeRange {
10    single: UnicodeRangeSegment,
11    segments: Option<Rc<[UnicodeRangeSegment]>>,
12}
13
14impl UnicodeRange {
15    /// Creates a range with a single segment.
16    pub fn new(start: i32, end: i32) -> Self {
17        Self { single: UnicodeRangeSegment::new(start, end), segments: None }
18    }
19
20    /// Creates a range from one segment.
21    pub fn from_segment(single: UnicodeRangeSegment) -> Self {
22        Self { single, segments: None }
23    }
24
25    /// Creates a range from several segments.
26    ///
27    /// Panics when `segments` is empty.
28    pub fn from_segments(segments: Vec<UnicodeRangeSegment>) -> Self {
29        if segments.is_empty() {
30            panic!("segments must not be empty");
31        }
32
33        Self { single: segments[0], segments: Some(Rc::from(segments)) }
34    }
35
36    /// The default range, `0-10FFFD`.
37    pub fn default_range() -> Self {
38        Self::new(0, 0x10FFFD)
39    }
40
41    #[allow(dead_code)] // internal accessor kept for parity; used by font collections
42    pub(crate) fn single(&self) -> UnicodeRangeSegment {
43        self.single
44    }
45
46    #[allow(dead_code)] // internal accessor kept for parity; used by font collections
47    pub(crate) fn segments(&self) -> Option<&[UnicodeRangeSegment]> {
48        self.segments.as_deref()
49    }
50
51    /// Determines if given value is in range.
52    pub fn is_in_range(&self, value: i32) -> bool {
53        match &self.segments {
54            None => self.single.is_in_range(value),
55            Some(segments) => segments.iter().any(|segment| segment.is_in_range(value)),
56        }
57    }
58
59    /// Parses a `UnicodeRange`.
60    pub fn parse(s: &str) -> Result<UnicodeRange, FormatError> {
61        if s.is_empty() {
62            return Err(FormatError::new("Could not parse specified Unicode range."));
63        }
64
65        let mut parts = s.split(',');
66
67        if !s.contains(',') {
68            return Ok(UnicodeRange::from_segment(UnicodeRangeSegment::parse(s)?));
69        }
70
71        let segments = parts.by_ref().map(|part| UnicodeRangeSegment::parse(part.trim())).collect::<Result<Vec<_>, _>>()?;
72
73        Ok(UnicodeRange::from_segments(segments))
74    }
75}
76
77impl Default for UnicodeRange {
78    fn default() -> Self {
79        Self::default_range()
80    }
81}
82
83impl FromStr for UnicodeRange {
84    type Err = FormatError;
85
86    fn from_str(s: &str) -> Result<Self, Self::Err> {
87        Self::parse(s)
88    }
89}
90
91/// One contiguous range of code points.
92#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
93pub struct UnicodeRangeSegment {
94    start: i32,
95    end: i32,
96}
97
98impl UnicodeRangeSegment {
99    pub const fn new(start: i32, end: i32) -> Self {
100        Self { start, end }
101    }
102
103    /// Get the start of the unicode range.
104    pub const fn start(&self) -> i32 {
105        self.start
106    }
107
108    /// Get the end of the unicode range.
109    pub const fn end(&self) -> i32 {
110        self.end
111    }
112
113    /// Determines if given value is in range.
114    pub const fn is_in_range(&self, value: i32) -> bool {
115        self.start <= value && value <= self.end
116    }
117
118    /// Matches `^(?:[uU]\+)?([0-9a-fA-F][0-9a-fA-F?]{1,5})?$` and returns the
119    /// captured hex digits (one hex digit followed by one to five hex digits or
120    /// wildcards, or a single hex digit).
121    fn match_value(s: &str) -> Option<&str> {
122        let value = s.strip_prefix("u+").or_else(|| s.strip_prefix("U+")).unwrap_or(s);
123        let bytes = value.as_bytes();
124
125        if bytes.is_empty() || bytes.len() > 6 || !bytes[0].is_ascii_hexdigit() {
126            return None;
127        }
128
129        if bytes[1..].iter().all(|b| b.is_ascii_hexdigit() || *b == b'?') {
130            Some(value)
131        } else {
132            None
133        }
134    }
135
136    fn parse_hex(value: &str) -> Result<i32, FormatError> {
137        i32::from_str_radix(value, 16)
138            .map_err(|_| FormatError::new("Could not parse specified Unicode range segment."))
139    }
140
141    /// Parses a `UnicodeRangeSegment`: `U+20`, `U+30??` or `0-10FFFD`.
142    pub fn parse(s: &str) -> Result<UnicodeRangeSegment, FormatError> {
143        const ERROR: FormatError = FormatError::new("Could not parse specified Unicode range segment.");
144
145        if s.is_empty() {
146            return Err(ERROR);
147        }
148
149        let mut parts = s.split('-');
150        let first = parts.next().unwrap_or("");
151        let second = parts.next();
152
153        if parts.next().is_some() {
154            return Err(ERROR);
155        }
156
157        match second {
158            None => {
159                // e.g. U+20, U+3F U+30??
160                let single = Self::match_value(first).ok_or(ERROR)?;
161
162                if !single.contains('?') {
163                    let start = Self::parse_hex(single)?;
164                    Ok(UnicodeRangeSegment::new(start, start))
165                } else {
166                    let start = Self::parse_hex(&single.replace('?', "0"))?;
167                    let end = Self::parse_hex(&single.replace('?', "F"))?;
168                    Ok(UnicodeRangeSegment::new(start, end))
169                }
170            }
171            Some(second) => {
172                let first = Self::match_value(first).ok_or(ERROR)?;
173                let second = Self::match_value(second).ok_or(ERROR)?;
174
175                Ok(UnicodeRangeSegment::new(Self::parse_hex(first)?, Self::parse_hex(second)?))
176            }
177        }
178    }
179}
180
181impl FromStr for UnicodeRangeSegment {
182    type Err = FormatError;
183
184    fn from_str(s: &str) -> Result<Self, Self::Err> {
185        Self::parse(s)
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn should_parse_segments() {
195        let range = UnicodeRange::parse("U+0, U+1, U+2, U+3").unwrap();
196
197        let segments = range.segments().expect("the range has segments");
198
199        assert_eq!(segments.iter().map(UnicodeRangeSegment::start).collect::<Vec<_>>(), [0, 1, 2, 3]);
200    }
201
202    #[test]
203    fn should_parse() {
204        for (s, expected_start, expected_end) in
205            [("u+00-FF", 0, 255), ("U+00-FF", 0, 255), ("U+00-U+FF", 0, 255), ("U+AB??", 43776, 44031)]
206        {
207            let segment = UnicodeRangeSegment::parse(s).unwrap();
208
209            assert_eq!(segment.start(), expected_start, "{s}");
210            assert_eq!(segment.end(), expected_end, "{s}");
211        }
212    }
213
214    #[test]
215    fn in_range_should_return_false_for_values_outside_range() {
216        let segment = UnicodeRangeSegment::new(20, 25);
217
218        for value in [0, 19, 26, 100] {
219            assert!(!segment.is_in_range(value));
220        }
221    }
222
223    #[test]
224    fn in_range_should_return_true_for_values_within_range() {
225        let segment = UnicodeRangeSegment::new(20, 22);
226
227        for value in [20, 21, 22] {
228            assert!(segment.is_in_range(value));
229        }
230    }
231
232    #[test]
233    fn malformed_ranges_are_errors() {
234        for s in ["", "U+", "U+G", "U+1-2-3", "U+1234567", "x"] {
235            assert!(UnicodeRangeSegment::parse(s).is_err(), "{s}");
236        }
237
238        assert!(UnicodeRange::parse("").is_err());
239        assert!(UnicodeRange::parse("U+1, x").is_err());
240        assert!(UnicodeRange::default().is_in_range(0x10FFFD));
241        assert!(UnicodeRange::parse("U+41").unwrap().is_in_range(0x41));
242    }
243}