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