Skip to main content

css_ast/types/
unicode_range.rs

1use super::prelude::*;
2use css_lexer::{Feature as LexerFeature, Lexer, SourceOffset};
3
4/// `<unicode-range-token>` as defined in [css-syntax-3](https://drafts.csswg.org/css-syntax-3/#typedef-unicode-range-token).
5///
6/// ```text,ignore
7/// U+0-7F | U+30?? | U+4E00-9FFF
8/// ```
9///
10/// The token only exists in the `unicode-range` descriptor, where the value is re-tokenized with
11/// unicode ranges allowed. Ordinary tokenization would split `U+4E00-9FFF` into an ident, a number
12/// and a dimension, so parsing re-lexes the source from the current position and consumes every
13/// token the re-lexed range covers, keeping a single [`Cursor`] over the whole range.
14#[node]
15#[derive(IntoCursor, ToSpan, SemanticEq, ToCursors, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
17#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
18#[derive(csskit_derives::NodeWithMetadata)]
19pub struct UnicodeRange(#[metadata(skip)] Cursor);
20
21impl UnicodeRange {
22	/// The first code point of the range.
23	pub fn start(&self) -> u32 {
24		self.0.token().unicode_range_start()
25	}
26
27	/// The last code point of the range.
28	pub fn end(&self) -> u32 {
29		self.0.token().unicode_range_end()
30	}
31}
32
33impl<'a> Peek<'a> for UnicodeRange {
34	const PEEK_KINDSET: KindSet = KindSet::new(&[Kind::Ident]);
35
36	fn peek<I>(p: &Parser<'a, I>, c: Cursor) -> bool
37	where
38		I: Iterator<Item = Cursor> + Clone,
39	{
40		if c != Kind::Ident {
41			return false;
42		}
43		let source = p.source_text();
44		if !c.str_slice(source).eq_ignore_ascii_case("u") {
45			return false;
46		}
47		source[c.end_offset().0 as usize..].starts_with('+')
48	}
49}
50
51impl<'a> Parse<'a> for UnicodeRange {
52	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
53	where
54		I: Iterator<Item = Cursor> + Clone,
55	{
56		let c = p.peek_n(1);
57		if !p.peek::<Self>() {
58			Err(Diagnostic::new(c, Diagnostic::invalid_unicode_range))?
59		}
60		let offset = c.offset().0 as usize;
61		let token =
62			Lexer::new_with_features(&CssAtomSet::ATOMS, &p.source_text()[offset..], LexerFeature::UnicodeRange)
63				.advance();
64		if token != Kind::UnicodeRange || token.is_bad() {
65			Err(Diagnostic::new(c, Diagnostic::invalid_unicode_range))?
66		}
67		let end = offset + token.len() as usize;
68		let skip = p.set_skip(KindSet::NONE);
69		let result = loop {
70			let consumed = p.next();
71			match consumed.end_offset().0 as usize {
72				at if at == end => break Ok(()),
73				at if at > end || consumed == Kind::Eof => {
74					break Err(Diagnostic::new(c, Diagnostic::invalid_unicode_range));
75				}
76				_ => {}
77			}
78		};
79		p.set_skip(skip);
80		result?;
81		Ok(Self(Cursor::new(SourceOffset(offset as u32), token)))
82	}
83}
84
85#[cfg(test)]
86mod tests {
87	use super::*;
88	use css_parse::{assert_parse, assert_parse_error, assert_peek_false};
89
90	#[test]
91	fn test_writes() {
92		assert_parse!(CssAtomSet::ATOMS, UnicodeRange, "U+26");
93		assert_parse!(CssAtomSet::ATOMS, UnicodeRange, "U+0-7F");
94		assert_parse!(CssAtomSet::ATOMS, UnicodeRange, "u+590-5ff");
95		assert_parse!(CssAtomSet::ATOMS, UnicodeRange, "U+30??");
96		assert_parse!(CssAtomSet::ATOMS, UnicodeRange, "U+4E00-9FFF");
97		assert_parse!(CssAtomSet::ATOMS, UnicodeRange, "U+0-10FFFF");
98		assert_parse!(CssAtomSet::ATOMS, UnicodeRange, "U+??????");
99	}
100
101	#[test]
102	fn test_values() {
103		let alloc = css_parse::Arena::new();
104		let source = "U+4E00-9FFF";
105		let lexer = Lexer::new(&CssAtomSet::ATOMS, source);
106		let mut p = Parser::new(&alloc, source, lexer);
107		let range = p.parse_entirely::<UnicodeRange>().output.expect("parses");
108		assert_eq!(range.start(), 0x4E00);
109		assert_eq!(range.end(), 0x9FFF);
110	}
111
112	#[test]
113	fn test_errors() {
114		assert_peek_false!(CssAtomSet::ATOMS, UnicodeRange, "U");
115		assert_peek_false!(CssAtomSet::ATOMS, UnicodeRange, "url(a)");
116		assert_peek_false!(CssAtomSet::ATOMS, UnicodeRange, "1px");
117		assert_parse_error!(CssAtomSet::ATOMS, UnicodeRange, "U+ZZ");
118	}
119}