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 {
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 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
91impl<'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 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}