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 atom_set::AtomSet;
49	/// # use css_lexer::Lexer;
50	/// let source_text = "@charset \"ISO-8859-1\";";
51	/// let alloc = Arena::default();
52	/// let lexer = Lexer::new(&CssAtomSet::ATOMS, source_text);
53	/// let rule = Parser::new(&alloc, source_text, lexer).parse_entirely::<CharsetRule>().output.unwrap();
54	/// assert_eq!(rule.label(source_text), "ISO-8859-1");
55	/// ```
56	pub fn label<'a>(&self, source_text: &'a str) -> &'a str {
57		let c: Cursor = self.string.into();
58		let source = c.str_slice(source_text);
59		&source[c.token().leading_len() as usize..source.len() - c.token().trailing_len() as usize]
60	}
61
62	/// Gives the encoding this rule sets, as the shortest label naming it.
63	///
64	/// Gives [EncodingLabel::Unknown] when the rule has no effect: it is not the byte sequence (see
65	/// [CharsetRule::is_byte_sequence]), its label is not in the [Encoding Standard], or its label
66	/// names an encoding that resolves to UTF-8, which is the default.
67	///
68	/// [Encoding Standard]: https://encoding.spec.whatwg.org/#names-and-labels
69	///
70	/// ```rust
71	/// # use css_ast::{AtomSet, CharsetRule, CssAtomSet, EncodingLabel};
72	/// # use css_parse::{Arena, Parser};
73	/// # use css_lexer::Lexer;
74	/// # fn encoding(source_text: &str) -> EncodingLabel {
75	/// # let alloc = Arena::default();
76	/// # let lexer = Lexer::new(&CssAtomSet::ATOMS, source_text);
77	/// # let rule = Parser::new(&alloc, source_text, lexer).parse_entirely::<CharsetRule>().output.unwrap();
78	/// # rule.encoding(source_text)
79	/// # }
80	/// assert_eq!(encoding("@charset \"ISO-8859-1\";").to_str(), "l1");
81	/// assert_eq!(encoding("@charset \"UTF-8\";"), EncodingLabel::Unknown);
82	/// assert_eq!(encoding("@charset 'gbk';"), EncodingLabel::Unknown);
83	/// ```
84	pub fn encoding(&self, source_text: &str) -> EncodingLabel {
85		if !self.is_byte_sequence() {
86			return EncodingLabel::Unknown;
87		}
88		EncodingLabel::from_label(self.label(source_text)).compact()
89	}
90}
91
92// CharsetRule is a special rule which means it cannot use standard AtRule parsing... comments below
93// https://drafts.csswg.org/css-syntax-3/#determine-the-fallback-encoding
94impl<'a> Parse<'a> for CharsetRule {
95	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
96	where
97		I: Iterator<Item = Cursor> + Clone,
98	{
99		let at_keyword = p.parse::<T![AtKeyword]>()?;
100		let c: Cursor = at_keyword.into();
101		if !p.equals_atom(c, &CssAtomSet::Charset) {
102			Err(Diagnostic::new(c, Diagnostic::unexpected))?;
103		}
104		// Charsets MUST have a space between the at keyword and the string. This
105		// isn't necessary in other at rules where an at keyword can align with other
106		// delims (e.g. `(`) or unambinguous tokens like strings.
107		let space = p.parse::<T![' ']>()?;
108		let string = p.parse::<T![String]>()?;
109		let semicolon = p.parse::<T![;]>().ok();
110		Ok(Self { at_keyword, space, string, semicolon })
111	}
112}
113
114#[cfg(test)]
115mod tests {
116	use super::*;
117	use crate::CssAtomSet;
118	use css_parse::{Parser, assert_parse};
119
120	fn is_byte_sequence(source_text: &str) -> bool {
121		let alloc = css_parse::Arena::default();
122		let lexer = css_lexer::Lexer::new(&CssAtomSet::ATOMS, source_text);
123		let mut parser = Parser::new(&alloc, source_text, lexer);
124		parser.parse_entirely::<CharsetRule>().output.expect("did not parse").is_byte_sequence()
125	}
126
127	#[test]
128	fn test_writes() {
129		assert_parse!(CssAtomSet::ATOMS, CharsetRule, "@charset \"utf-8\";");
130		assert_parse!(CssAtomSet::ATOMS, CharsetRule, "@charset \"UTF-8\";");
131	}
132
133	#[test]
134	fn test_is_byte_sequence() {
135		assert!(is_byte_sequence("@charset \"utf-8\";"));
136		assert!(is_byte_sequence("@charset \"UTF-8\";"));
137	}
138
139	#[test]
140	fn test_is_not_byte_sequence() {
141		assert!(!is_byte_sequence("@CHARSET \"utf-8\";"));
142		assert!(!is_byte_sequence("@charset 'utf-8';"));
143		assert!(!is_byte_sequence("@charset \"\\75 tf-8\";"));
144		assert!(!is_byte_sequence("@charset\t\"utf-8\";"));
145		assert!(!is_byte_sequence("@charset  \"utf-8\";"));
146		assert!(!is_byte_sequence("@charset \"utf-8\""));
147	}
148}