1 use std::{
2     fmt::{self, Display, Write},
3     hash::{Hash, Hasher},
4 };
5 
6 use codemap::Span;
7 
8 use crate::{
9     common::QuoteKind, error::SassResult, parse::Parser, utils::is_ident, value::Value, Token,
10 };
11 
12 use super::{Namespace, QualifiedName};
13 
14 #[derive(Clone, Debug)]
15 pub(crate) struct Attribute {
16     attr: QualifiedName,
17     value: String,
18     modifier: Option<char>,
19     op: AttributeOp,
20     span: Span,
21 }
22 
23 impl PartialEq for Attribute {
eq(&self, other: &Self) -> bool24     fn eq(&self, other: &Self) -> bool {
25         self.attr == other.attr
26             && self.value == other.value
27             && self.modifier == other.modifier
28             && self.op == other.op
29     }
30 }
31 
32 impl Eq for Attribute {}
33 
34 impl Hash for Attribute {
hash<H: Hasher>(&self, state: &mut H)35     fn hash<H: Hasher>(&self, state: &mut H) {
36         self.attr.hash(state);
37         self.value.hash(state);
38         self.modifier.hash(state);
39         self.op.hash(state);
40     }
41 }
42 
attribute_name(parser: &mut Parser, start: Span) -> SassResult<QualifiedName>43 fn attribute_name(parser: &mut Parser, start: Span) -> SassResult<QualifiedName> {
44     let next = parser.toks.peek().ok_or(("Expected identifier.", start))?;
45     if next.kind == '*' {
46         parser.toks.next();
47         parser.expect_char('|')?;
48 
49         let ident = parser.parse_identifier()?.node;
50         return Ok(QualifiedName {
51             ident,
52             namespace: Namespace::Asterisk,
53         });
54     }
55     parser.span_before = next.pos;
56     let name_or_namespace = parser.parse_identifier()?;
57     match parser.toks.peek() {
58         Some(v) if v.kind != '|' => {
59             return Ok(QualifiedName {
60                 ident: name_or_namespace.node,
61                 namespace: Namespace::None,
62             });
63         }
64         Some(..) => {}
65         None => return Err(("expected more input.", name_or_namespace.span).into()),
66     }
67     match parser.toks.peek_forward(1) {
68         Some(v) if v.kind == '=' => {
69             parser.toks.reset_cursor();
70             return Ok(QualifiedName {
71                 ident: name_or_namespace.node,
72                 namespace: Namespace::None,
73             });
74         }
75         Some(..) => {
76             parser.toks.reset_cursor();
77         }
78         None => return Err(("expected more input.", name_or_namespace.span).into()),
79     }
80     parser.span_before = parser.toks.next().unwrap().pos();
81     let ident = parser.parse_identifier()?.node;
82     Ok(QualifiedName {
83         ident,
84         namespace: Namespace::Other(name_or_namespace.node.into_boxed_str()),
85     })
86 }
87 
attribute_operator(parser: &mut Parser) -> SassResult<AttributeOp>88 fn attribute_operator(parser: &mut Parser) -> SassResult<AttributeOp> {
89     let op = match parser.toks.next() {
90         Some(Token { kind: '=', .. }) => return Ok(AttributeOp::Equals),
91         Some(Token { kind: '~', .. }) => AttributeOp::Include,
92         Some(Token { kind: '|', .. }) => AttributeOp::Dash,
93         Some(Token { kind: '^', .. }) => AttributeOp::Prefix,
94         Some(Token { kind: '$', .. }) => AttributeOp::Suffix,
95         Some(Token { kind: '*', .. }) => AttributeOp::Contains,
96         Some(..) | None => return Err(("Expected \"]\".", parser.span_before).into()),
97     };
98 
99     parser.expect_char('=')?;
100 
101     Ok(op)
102 }
103 impl Attribute {
from_tokens(parser: &mut Parser) -> SassResult<Attribute>104     pub fn from_tokens(parser: &mut Parser) -> SassResult<Attribute> {
105         let start = parser.span_before;
106         parser.whitespace();
107         let attr = attribute_name(parser, start)?;
108         parser.whitespace();
109         if parser
110             .toks
111             .peek()
112             .ok_or(("expected more input.", start))?
113             .kind
114             == ']'
115         {
116             parser.toks.next();
117             return Ok(Attribute {
118                 attr,
119                 value: String::new(),
120                 modifier: None,
121                 op: AttributeOp::Any,
122                 span: start,
123             });
124         }
125 
126         parser.span_before = start;
127         let op = attribute_operator(parser)?;
128         parser.whitespace();
129 
130         let peek = parser.toks.peek().ok_or(("expected more input.", start))?;
131         parser.span_before = peek.pos;
132         let value = match peek.kind {
133             q @ '\'' | q @ '"' => {
134                 parser.toks.next();
135                 match parser.parse_quoted_string(q)?.node {
136                     Value::String(s, ..) => s,
137                     _ => unreachable!(),
138                 }
139             }
140             _ => parser.parse_identifier()?.node,
141         };
142         parser.whitespace();
143 
144         let modifier = match parser.toks.peek() {
145             Some(Token {
146                 kind: c @ 'a'..='z',
147                 ..
148             })
149             | Some(Token {
150                 kind: c @ 'A'..='Z',
151                 ..
152             }) => {
153                 parser.toks.next();
154                 parser.whitespace();
155                 Some(c)
156             }
157             _ => None,
158         };
159 
160         parser.expect_char(']')?;
161 
162         Ok(Attribute {
163             op,
164             attr,
165             value,
166             modifier,
167             span: start,
168         })
169     }
170 }
171 
172 impl Display for Attribute {
173     #[allow(clippy::branches_sharing_code)]
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result174     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175         f.write_char('[')?;
176         write!(f, "{}", self.attr)?;
177 
178         if self.op != AttributeOp::Any {
179             f.write_str(self.op.into())?;
180             if is_ident(&self.value) && !self.value.starts_with("--") {
181                 f.write_str(&self.value)?;
182 
183                 if self.modifier.is_some() {
184                     f.write_char(' ')?;
185                 }
186             } else {
187                 // todo: remove unwrap by not doing this in display
188                 // or having special emitter for quoted strings?
189                 // (also avoids the clone because we can consume/modify self)
190                 f.write_str(
191                     &Value::String(self.value.clone(), QuoteKind::Quoted)
192                         .to_css_string(self.span, false)
193                         .unwrap(),
194                 )?;
195                 // todo: this space is not emitted when `compressed` output
196                 if self.modifier.is_some() {
197                     f.write_char(' ')?;
198                 }
199             }
200 
201             if let Some(c) = self.modifier {
202                 f.write_char(c)?;
203             }
204         }
205 
206         f.write_char(']')?;
207 
208         Ok(())
209     }
210 }
211 
212 #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
213 enum AttributeOp {
214     /// \[attr\]
215     ///
216     /// Represents elements with an attribute name of `attr`
217     Any,
218 
219     /// [attr=value]
220     ///
221     /// Represents elements with an attribute name of `attr`
222     /// whose value is exactly `value`
223     Equals,
224 
225     /// [attr~=value]
226     ///
227     /// Represents elements with an attribute name of `attr`
228     /// whose value is a whitespace-separated list of words,
229     /// one of which is exactly `value`
230     Include,
231 
232     /// [attr|=value]
233     ///
234     /// Represents elements with an attribute name of `attr`
235     /// whose value can be exactly value or can begin with
236     /// `value` immediately followed by a hyphen (`-`)
237     Dash,
238 
239     /// [attr^=value]
240     Prefix,
241 
242     /// [attr$=value]
243     Suffix,
244 
245     /// [attr*=value]
246     ///
247     /// Represents elements with an attribute name of `attr`
248     /// whose value contains at least one occurrence of
249     /// `value` within the string
250     Contains,
251 }
252 
253 impl From<AttributeOp> for &'static str {
254     #[inline]
from(op: AttributeOp) -> Self255     fn from(op: AttributeOp) -> Self {
256         match op {
257             AttributeOp::Any => "",
258             AttributeOp::Equals => "=",
259             AttributeOp::Include => "~=",
260             AttributeOp::Dash => "|=",
261             AttributeOp::Prefix => "^=",
262             AttributeOp::Suffix => "$=",
263             AttributeOp::Contains => "*=",
264         }
265     }
266 }
267