grass/src/selector/attribute.rs

271 lines
7.6 KiB
Rust
Raw Normal View History

use std::{
fmt::{self, Display, Write},
hash::{Hash, Hasher},
};
2020-04-12 19:37:12 -04:00
use codemap::Span;
2020-04-02 20:59:37 -04:00
2020-06-16 20:00:11 -04:00
use crate::{common::QuoteKind, error::SassResult, parse::Parser, utils::is_ident, value::Value};
2020-06-16 19:38:30 -04:00
use super::{Namespace, QualifiedName};
2020-04-02 20:59:37 -04:00
#[derive(Clone, Debug)]
2020-04-02 20:59:37 -04:00
pub(crate) struct Attribute {
2020-04-26 00:55:38 -04:00
attr: QualifiedName,
2020-04-02 21:10:45 -04:00
value: String,
modifier: Option<char>,
2020-04-26 00:55:38 -04:00
op: AttributeOp,
span: Span,
2020-04-02 20:59:37 -04:00
}
impl PartialEq for Attribute {
fn eq(&self, other: &Self) -> bool {
self.attr == other.attr
&& self.value == other.value
&& self.modifier == other.modifier
&& self.op == other.op
}
}
impl Eq for Attribute {}
impl Hash for Attribute {
fn hash<H: Hasher>(&self, state: &mut H) {
self.attr.hash(state);
self.value.hash(state);
self.modifier.hash(state);
self.op.hash(state);
}
}
2020-06-16 19:38:30 -04:00
fn attribute_name(parser: &mut Parser<'_>, start: Span) -> SassResult<QualifiedName> {
let next = parser.toks.peek().ok_or(("Expected identifier.", start))?;
2020-04-26 00:55:38 -04:00
if next.kind == '*' {
let pos = next.pos;
2020-06-16 19:38:30 -04:00
parser.toks.next();
if parser.toks.peek().ok_or(("expected \"|\".", pos))?.kind != '|' {
2020-04-26 00:55:38 -04:00
return Err(("expected \"|\".", pos).into());
}
2020-04-26 01:52:43 -04:00
2020-06-16 19:38:30 -04:00
parser.span_before = parser.toks.next().unwrap().pos();
2020-04-26 01:52:43 -04:00
2020-06-16 19:38:30 -04:00
let ident = parser.parse_identifier()?.node;
2020-04-26 00:55:38 -04:00
return Ok(QualifiedName {
ident,
namespace: Namespace::Asterisk,
2020-04-26 00:55:38 -04:00
});
}
2020-06-16 19:38:30 -04:00
parser.span_before = next.pos;
let name_or_namespace = parser.parse_identifier()?;
match parser.toks.peek() {
2020-04-26 00:55:38 -04:00
Some(v) if v.kind != '|' => {
return Ok(QualifiedName {
ident: name_or_namespace.node,
namespace: Namespace::None,
2020-04-26 00:55:38 -04:00
});
}
Some(..) => {}
None => return Err(("expected more input.", name_or_namespace.span).into()),
}
2020-06-16 19:38:30 -04:00
match parser.toks.peek_forward(1) {
2020-04-26 00:55:38 -04:00
Some(v) if v.kind == '=' => {
2020-06-19 22:47:06 -04:00
parser.toks.reset_cursor();
2020-04-26 00:55:38 -04:00
return Ok(QualifiedName {
ident: name_or_namespace.node,
namespace: Namespace::None,
2020-04-26 00:55:38 -04:00
});
}
Some(..) => {
2020-06-19 22:47:06 -04:00
parser.toks.reset_cursor();
2020-04-26 00:55:38 -04:00
}
None => return Err(("expected more input.", name_or_namespace.span).into()),
}
2020-06-16 19:38:30 -04:00
parser.span_before = parser.toks.next().unwrap().pos();
let ident = parser.parse_identifier()?.node;
2020-04-26 00:55:38 -04:00
Ok(QualifiedName {
ident,
namespace: Namespace::Other(name_or_namespace.node.into_boxed_str()),
2020-04-26 00:55:38 -04:00
})
}
2020-06-16 19:38:30 -04:00
fn attribute_operator(parser: &mut Parser<'_>) -> SassResult<AttributeOp> {
let start = parser.span_before;
let op = match parser.toks.next().ok_or(("Expected \"]\".", start))?.kind {
2020-04-26 00:55:38 -04:00
'=' => return Ok(AttributeOp::Equals),
'~' => AttributeOp::Include,
'|' => AttributeOp::Dash,
'^' => AttributeOp::Prefix,
'$' => AttributeOp::Suffix,
'*' => AttributeOp::Contains,
_ => return Err(("Expected \"]\".", start).into()),
};
2020-06-16 19:38:30 -04:00
if parser.toks.next().ok_or(("expected \"=\".", start))?.kind != '=' {
2020-04-26 00:55:38 -04:00
return Err(("expected \"=\".", start).into());
}
Ok(op)
}
2020-04-02 20:59:37 -04:00
impl Attribute {
2020-06-16 19:38:30 -04:00
pub fn from_tokens(parser: &mut Parser<'_>) -> SassResult<Attribute> {
let start = parser.span_before;
parser.whitespace();
let attr = attribute_name(parser, start)?;
parser.whitespace();
if parser
.toks
.peek()
.ok_or(("expected more input.", start))?
.kind
== ']'
{
parser.toks.next();
return Ok(Attribute {
2020-04-26 00:55:38 -04:00
attr,
value: String::new(),
modifier: None,
op: AttributeOp::Any,
span: start,
});
2020-04-02 20:59:37 -04:00
}
2020-06-16 19:38:30 -04:00
parser.span_before = start;
let op = attribute_operator(parser)?;
parser.whitespace();
2020-04-02 20:59:37 -04:00
2020-06-16 19:38:30 -04:00
let peek = parser.toks.peek().ok_or(("expected more input.", start))?;
parser.span_before = peek.pos;
2020-04-26 00:55:38 -04:00
let value = match peek.kind {
q @ '\'' | q @ '"' => {
2020-06-16 19:38:30 -04:00
parser.toks.next();
match parser.parse_quoted_string(q)?.node {
2020-05-22 14:35:41 -04:00
Value::String(s, ..) => s,
2020-04-26 00:55:38 -04:00
_ => unreachable!(),
}
2020-04-02 20:59:37 -04:00
}
2020-06-16 19:38:30 -04:00
_ => parser.parse_identifier()?.node,
2020-04-02 20:59:37 -04:00
};
2020-06-16 19:38:30 -04:00
parser.whitespace();
2020-04-02 20:59:37 -04:00
2020-06-16 19:38:30 -04:00
let peek = parser.toks.peek().ok_or(("expected more input.", start))?;
2020-04-12 19:37:12 -04:00
2020-04-26 00:55:38 -04:00
let modifier = match peek.kind {
c if c.is_alphabetic() => Some(c),
_ => None,
2020-04-02 20:59:37 -04:00
};
2020-04-26 00:55:38 -04:00
let pos = peek.pos();
if modifier.is_some() {
2020-06-16 19:38:30 -04:00
parser.toks.next();
parser.whitespace();
2020-04-26 00:55:38 -04:00
}
2020-06-16 19:38:30 -04:00
if parser.toks.peek().ok_or(("expected \"]\".", pos))?.kind != ']' {
2020-04-26 00:55:38 -04:00
return Err(("expected \"]\".", pos).into());
}
2020-06-16 19:38:30 -04:00
parser.toks.next();
2020-04-26 01:52:43 -04:00
Ok(Attribute {
2020-04-26 00:55:38 -04:00
op,
2020-04-02 20:59:37 -04:00
attr,
value,
modifier,
2020-04-26 00:55:38 -04:00
span: start,
})
2020-04-02 20:59:37 -04:00
}
}
impl Display for Attribute {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2020-04-26 00:55:38 -04:00
f.write_char('[')?;
write!(f, "{}", self.attr)?;
if self.op != AttributeOp::Any {
f.write_str(self.op.into())?;
if is_ident(&self.value) && !self.value.starts_with("--") {
f.write_str(&self.value)?;
if self.modifier.is_some() {
f.write_char(' ')?;
}
} else {
// todo: remove unwrap by not doing this in display
// or having special emitter for quoted strings?
// (also avoids the clone because we can consume/modify self)
f.write_str(
2020-05-22 14:35:41 -04:00
&Value::String(self.value.clone(), QuoteKind::Quoted)
.to_css_string(self.span)
.unwrap(),
2020-04-26 00:55:38 -04:00
)?;
// todo: this space is not emitted when `compressed` output
if self.modifier.is_some() {
f.write_char(' ')?;
}
}
if let Some(c) = self.modifier {
f.write_char(c)?;
}
2020-04-02 20:59:37 -04:00
}
2020-04-26 00:55:38 -04:00
f.write_char(']')?;
Ok(())
2020-04-02 20:59:37 -04:00
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
2020-04-26 00:55:38 -04:00
enum AttributeOp {
2020-04-04 19:07:00 -04:00
/// \[attr\]
2020-04-02 21:10:45 -04:00
///
2020-04-02 20:59:37 -04:00
/// Represents elements with an attribute name of `attr`
Any,
2020-04-02 21:10:45 -04:00
2020-04-02 20:59:37 -04:00
/// [attr=value]
2020-04-02 21:10:45 -04:00
///
/// Represents elements with an attribute name of `attr`
/// whose value is exactly `value`
2020-04-02 20:59:37 -04:00
Equals,
2020-04-02 21:10:45 -04:00
2020-04-02 20:59:37 -04:00
/// [attr~=value]
2020-04-02 21:10:45 -04:00
///
/// Represents elements with an attribute name of `attr`
/// whose value is a whitespace-separated list of words,
/// one of which is exactly `value`
Include,
2020-04-02 20:59:37 -04:00
/// [attr|=value]
2020-04-02 21:10:45 -04:00
///
/// Represents elements with an attribute name of `attr`
/// whose value can be exactly value or can begin with
/// `value` immediately followed by a hyphen (`-`)
Dash,
2020-04-02 20:59:37 -04:00
/// [attr^=value]
2020-04-02 21:10:45 -04:00
Prefix,
2020-04-02 20:59:37 -04:00
/// [attr$=value]
2020-04-02 21:10:45 -04:00
Suffix,
2020-04-02 20:59:37 -04:00
/// [attr*=value]
2020-04-02 21:10:45 -04:00
///
/// Represents elements with an attribute name of `attr`
/// whose value contains at least one occurrence of
/// `value` within the string
2020-04-02 20:59:37 -04:00
Contains,
}
2020-04-26 00:55:38 -04:00
impl Into<&'static str> for AttributeOp {
fn into(self) -> &'static str {
match self {
Self::Any => "",
Self::Equals => "=",
Self::Include => "~=",
Self::Dash => "|=",
Self::Prefix => "^=",
Self::Suffix => "$=",
Self::Contains => "*=",
}
}
}