Skip to main content

css_ast/rules/
charset.rs

1use super::prelude::*;
2use crate::EncodingLabel;
3use css_lexer::{QuoteStyle, Whitespace};
4
5/// <https://drafts.csswg.org/css-syntax-3/#charset-rule>
6#[node]
7#[derive(Peek, ToSpan, ToCursors, SemanticEq, Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
9#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
10#[cfg_attr(feature = "css_feature_data", derive(::csskit_derives::ToCSSFeature), css_feature("css.at-rules.charset"))]
11#[derive(csskit_derives::NodeWithMetadata)]
12#[metadata(node_kinds = AtRule, used_at_rules = Charset)]
13pub struct CharsetRule {
14	#[atom(CssAtomSet::Charset)]
15	pub at_keyword: T![AtKeyword],
16	pub space: T![' '],
17	pub string: T![String],
18	#[semantic_eq(skip)]
19	pub semicolon: Option<T![;]>,
20}
21
22impl CharsetRule {
23	/// Checks this rule is the literal byte sequence that [determining the fallback encoding] looks
24	/// for: a lowercase `@charset`, one space, a double quoted label, and a semicolon.
25	///
26	/// Any other spelling reaches no browser, because the sequence is matched byte by byte before
27	/// the stylesheet is parsed. Such a rule has no effect.
28	///
29	/// [determining the fallback encoding]: https://drafts.csswg.org/css-syntax-3/#determine-the-fallback-encoding
30	pub fn is_byte_sequence(&self) -> bool {
31		let at_keyword: Cursor = self.at_keyword.into();
32		let space: Cursor = self.space.into();
33		let string: Cursor = self.string.into();
34		at_keyword.token().is_lower_case()
35			&& !at_keyword.token().contains_escape_chars()
36			&& space.token().len() == 1
37			&& space.token() == Whitespace::Space
38			&& string == QuoteStyle::Double
39			&& !string.token().contains_escape_chars()
40			&& self.semicolon.is_some()
41	}
42
43	/// Gives the label between the quotes, as written.
44	///
45	/// ```rust
46	/// # use css_ast::{CharsetRule, CssAtomSet};
47	/// # use css_parse::{Arena, Parser};
48	/// # use css_lexer::{AtomSet, Lexer};
49	/// let source_text = "@charset \"ISO-8859-1\";";
50	/// let alloc = Arena::default();
51	/// let lexer = Lexer::new(&CssAtomSet::ATOMS, source_text);
52	/// let rule = Parser::new(&alloc, source_text, lexer).parse_entirely::<CharsetRule>().output.unwrap();
53	/// assert_eq!(rule.label(source_text), "ISO-8859-1");
54	/// ```
55	pub fn label<'a>(&self, source_text: &'a str) -> &'a str {
56		let c: Cursor = self.string.into();
57		let source = c.str_slice(source_text);
58		&source[c.token().leading_len() as usize..source.len() - c.token().trailing_len() as usize]
59	}
60
61	/// Gives the encoding this rule sets, as the shortest label naming it.
62	///
63	/// Gives [EncodingLabel::Unknown] when the rule has no effect: it is not the byte sequence (see
64	/// [CharsetRule::is_byte_sequence]), its label is not in the [Encoding Standard], or its label
65	/// names an encoding that resolves to UTF-8, which is the default.
66	///
67	/// [Encoding Standard]: https://encoding.spec.whatwg.org/#names-and-labels
68	///
69	/// ```rust
70	/// # use css_ast::{AtomSet, CharsetRule, CssAtomSet, EncodingLabel};
71	/// # use css_parse::{Arena, Parser};
72	/// # use css_lexer::Lexer;
73	/// # fn encoding(source_text: &str) -> EncodingLabel {
74	/// # let alloc = Arena::default();
75	/// # let lexer = Lexer::new(&CssAtomSet::ATOMS, source_text);
76	/// # let rule = Parser::new(&alloc, source_text, lexer).parse_entirely::<CharsetRule>().output.unwrap();
77	/// # rule.encoding(source_text)
78	/// # }
79	/// assert_eq!(encoding("@charset \"ISO-8859-1\";").to_str(), "l1");
80	/// assert_eq!(encoding("@charset \"UTF-8\";"), EncodingLabel::Unknown);
81	/// assert_eq!(encoding("@charset 'gbk';"), EncodingLabel::Unknown);
82	/// ```
83	pub fn encoding(&self, source_text: &str) -> EncodingLabel {
84		if !self.is_byte_sequence() {
85			return EncodingLabel::Unknown;
86		}
87		EncodingLabel::from_label(self.label(source_text)).compact()
88	}
89}
90
91// CharsetRule is a special rule which means it cannot use standard AtRule parsing... comments below
92// https://drafts.csswg.org/css-syntax-3/#determine-the-fallback-encoding
93impl<'a> Parse<'a> for CharsetRule {
94	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
95	where
96		I: Iterator<Item = Cursor> + Clone,
97	{
98		let at_keyword = p.parse::<T![AtKeyword]>()?;
99		let c: Cursor = at_keyword.into();
100		if !p.equals_atom(c, &CssAtomSet::Charset) {
101			Err(Diagnostic::new(c, Diagnostic::unexpected))?;
102		}
103		// Charsets MUST have a space between the at keyword and the string. This
104		// isn't necessary in other at rules where an at keyword can align with other
105		// delims (e.g. `(`) or unambinguous tokens like strings.
106		let space = p.parse::<T![' ']>()?;
107		let string = p.parse::<T![String]>()?;
108		let semicolon = p.parse::<T![;]>().ok();
109		Ok(Self { at_keyword, space, string, semicolon })
110	}
111}
112
113#[cfg(test)]
114mod tests {
115	use super::*;
116	use crate::CssAtomSet;
117	use css_parse::{Parser, assert_parse};
118
119	fn is_byte_sequence(source_text: &str) -> bool {
120		let alloc = css_parse::Arena::default();
121		let lexer = css_lexer::Lexer::new(&CssAtomSet::ATOMS, source_text);
122		let mut parser = Parser::new(&alloc, source_text, lexer);
123		parser.parse_entirely::<CharsetRule>().output.expect("did not parse").is_byte_sequence()
124	}
125
126	#[test]
127	fn test_writes() {
128		assert_parse!(CssAtomSet::ATOMS, CharsetRule, "@charset \"utf-8\";");
129		assert_parse!(CssAtomSet::ATOMS, CharsetRule, "@charset \"UTF-8\";");
130	}
131
132	#[test]
133	fn test_is_byte_sequence() {
134		assert!(is_byte_sequence("@charset \"utf-8\";"));
135		assert!(is_byte_sequence("@charset \"UTF-8\";"));
136	}
137
138	#[test]
139	fn test_is_not_byte_sequence() {
140		assert!(!is_byte_sequence("@CHARSET \"utf-8\";"));
141		assert!(!is_byte_sequence("@charset 'utf-8';"));
142		assert!(!is_byte_sequence("@charset \"\\75 tf-8\";"));
143		assert!(!is_byte_sequence("@charset\t\"utf-8\";"));
144		assert!(!is_byte_sequence("@charset  \"utf-8\";"));
145		assert!(!is_byte_sequence("@charset \"utf-8\""));
146	}
147}