1use super::prelude::*;
2use crate::EncodingLabel;
3use css_lexer::{QuoteStyle, Whitespace};
4
5#[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 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 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 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
92impl<'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 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}