improve parsing of media queries

This commit is contained in:
ConnorSkees 2020-06-24 11:39:32 -04:00
parent e5e3943e5c
commit 9512e9f39f
3 changed files with 279 additions and 13 deletions

View File

@ -15,7 +15,7 @@ enum Toplevel {
body: Vec<Stmt>,
},
Media {
params: String,
query: String,
body: Vec<Stmt>,
},
Supports {
@ -94,8 +94,8 @@ impl Css {
Stmt::RuleSet { .. } => vals.extend(self.parse_stmt(rule, extender)?),
Stmt::Style(s) => vals.get_mut(0).unwrap().push_style(*s)?,
Stmt::Comment(s) => vals.get_mut(0).unwrap().push_comment(s),
Stmt::Media { params, body, .. } => {
vals.push(Toplevel::Media { params, body })
Stmt::Media { query, body, .. } => {
vals.push(Toplevel::Media { query, body })
}
Stmt::Supports { params, body, .. } => {
vals.push(Toplevel::Supports { params, body })
@ -114,7 +114,7 @@ impl Css {
}
Stmt::Comment(s) => vec![Toplevel::MultilineComment(s)],
Stmt::Style(s) => vec![Toplevel::Style(s)],
Stmt::Media { params, body, .. } => vec![Toplevel::Media { params, body }],
Stmt::Media { query, body, .. } => vec![Toplevel::Media { query, body }],
Stmt::Supports { params, body, .. } => vec![Toplevel::Supports { params, body }],
Stmt::UnknownAtRule {
params, name, body, ..
@ -239,7 +239,7 @@ impl Css {
)?;
writeln!(buf, "{}}}", padding)?;
}
Toplevel::Media { params, body } => {
Toplevel::Media { query, body } => {
if body.is_empty() {
continue;
}
@ -247,7 +247,7 @@ impl Css {
should_emit_newline = false;
writeln!(buf)?;
}
writeln!(buf, "{}@media {} {{", padding, params)?;
writeln!(buf, "{}@media {} {{", padding, query)?;
Css::from_stmts(body, extender)?._inner_pretty_print(
buf,
map,

248
src/parse/media.rs Normal file
View File

@ -0,0 +1,248 @@
use std::fmt;
use crate::{
error::SassResult,
utils::{is_name_start, peek_ident_no_interpolation, read_until_closing_paren},
{Cow, Token},
};
use super::Parser;
#[derive(Debug, Eq, PartialEq, Clone)]
pub(super) struct MediaQuery {
/// The modifier, probably either "not" or "only".
///
/// This may be `None` if no modifier is in use.
modifier: Option<String>,
/// The media type, for example "screen" or "print".
///
/// This may be `None`. If so, `self.features` will not be empty.
media_type: Option<String>,
/// Feature queries, including parentheses.
features: Vec<String>,
}
#[allow(dead_code)]
impl MediaQuery {
pub fn is_condition(&self) -> bool {
self.modifier.is_none() && self.media_type.is_none()
}
pub fn matches_all_types(&self) -> bool {
self.media_type.is_none()
|| self
.media_type
.as_ref()
.map_or(false, |v| v.to_ascii_lowercase() == "all")
}
pub fn condition(features: Vec<String>) -> Self {
Self {
modifier: None,
media_type: None,
features,
}
}
#[allow(dead_code, unused_variables)]
pub fn merge(other: &Self) -> Self {
todo!()
}
}
impl fmt::Display for MediaQuery {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(modifier) = &self.modifier {
f.write_str(modifier)?;
}
if let Some(media_type) = &self.media_type {
f.write_str(media_type)?;
if !&self.features.is_empty() {
f.write_str(" and ")?;
}
}
f.write_str(&self.features.join(" and "))
}
}
impl<'a> Parser<'a> {
pub fn scan_identifier(&mut self, ident: &str) -> SassResult<bool> {
let peeked_identifier =
match peek_ident_no_interpolation(self.toks, false, self.span_before) {
Ok(v) => v.node,
Err(..) => return Ok(false),
};
if peeked_identifier == ident {
self.toks.take(ident.chars().count()).for_each(drop);
self.toks.reset_cursor();
return Ok(true);
}
self.toks.reset_cursor();
Ok(false)
}
pub fn expect_char(&mut self, c: char) -> SassResult<()> {
if let Some(Token { kind, .. }) = self.toks.peek() {
if *kind == c {
self.toks.next();
return Ok(());
}
}
Err((format!("expected \"{}\".", c), self.span_before).into())
}
pub fn scan_char(&mut self, c: char) -> bool {
if let Some(Token { kind, .. }) = self.toks.peek() {
if *kind == c {
self.toks.next();
return true;
}
}
false
}
pub fn expression_until_comparison(&mut self) -> SassResult<Cow<'static, str>> {
let mut toks = Vec::new();
while let Some(tok) = self.toks.peek().cloned() {
match tok.kind {
'=' => {
self.toks.advance_cursor();
if matches!(self.toks.peek(), Some(Token { kind: '=', .. })) {
self.toks.reset_cursor();
break;
}
self.toks.reset_cursor();
toks.push(tok);
toks.push(tok);
self.toks.next();
self.toks.next();
}
'>' | '<' | ':' => {
break;
}
_ => {
toks.push(tok);
self.toks.next();
}
}
}
self.parse_value_as_string_from_vec(toks)
}
pub(super) fn parse_media_query_list(&mut self) -> SassResult<String> {
let mut buf = String::new();
loop {
self.whitespace();
buf.push_str(&self.parse_single_media_query()?);
if !self.scan_char(',') {
break;
}
buf.push(',');
buf.push(' ');
}
Ok(buf)
}
fn parse_media_feature(&mut self) -> SassResult<String> {
if let Some(Token { kind: '#', .. }) = self.toks.peek() {
if let Some(Token { kind: '{', .. }) = self.toks.peek_forward(1) {
self.toks.next();
self.toks.next();
return Ok(self.parse_interpolation_as_string()?.into_owned());
}
todo!()
}
let mut buf = String::with_capacity(2);
self.expect_char('(')?;
buf.push('(');
self.whitespace();
buf.push_str(&self.expression_until_comparison()?);
if let Some(Token { kind: ':', .. }) = self.toks.peek() {
self.toks.next();
self.whitespace();
buf.push(':');
buf.push(' ');
let mut toks = read_until_closing_paren(self.toks)?;
if let Some(tok) = toks.pop() {
if tok.kind != ')' {
todo!()
}
}
buf.push_str(&self.parse_value_as_string_from_vec(toks)?);
self.whitespace();
buf.push(')');
return Ok(buf);
} else {
let next_tok = self.toks.peek().cloned();
let is_angle = next_tok.map_or(false, |t| t.kind == '<' || t.kind == '>');
if is_angle || matches!(next_tok, Some(Token { kind: '=', .. })) {
buf.push(' ');
// todo: remove this unwrap
buf.push(self.toks.next().unwrap().kind);
if is_angle && self.scan_char('=') {
buf.push('=');
}
buf.push(' ');
self.whitespace();
buf.push_str(&self.expression_until_comparison()?);
}
}
self.expect_char(')')?;
self.whitespace();
buf.push(')');
Ok(buf)
}
fn parse_single_media_query(&mut self) -> SassResult<String> {
let mut buf = String::new();
if !matches!(self.toks.peek(), Some(Token { kind: '(', .. })) {
buf.push_str(&self.parse_identifier()?);
self.whitespace();
if let Some(tok) = self.toks.peek() {
if !is_name_start(tok.kind) {
return Ok(buf);
}
}
let ident = self.parse_identifier()?;
self.whitespace();
if ident.to_ascii_lowercase() == "and" {
buf.push_str(" and ");
} else {
buf.push_str(&ident);
if self.scan_identifier("and")? {
self.whitespace();
buf.push_str(" and ");
} else {
return Ok(buf);
}
}
}
loop {
self.whitespace();
buf.push_str(&self.parse_media_feature()?);
self.whitespace();
if !self.scan_identifier("and")? {
break;
}
buf.push_str(" and ");
}
Ok(buf)
}
}

View File

@ -29,6 +29,7 @@ pub mod common;
mod function;
mod ident;
mod import;
mod media;
mod mixin;
mod style;
mod value;
@ -48,7 +49,7 @@ pub(crate) enum Stmt {
Style(Box<Style>),
Media {
super_selector: Selector,
params: String,
query: String,
body: Vec<Stmt>,
},
UnknownAtRule {
@ -442,6 +443,22 @@ impl<'a> Parser<'a> {
})
}
pub fn parse_interpolation_as_string(&mut self) -> SassResult<Cow<'static, str>> {
let interpolation = self.parse_interpolation()?;
Ok(match interpolation.node {
Value::String(v, ..) => Cow::owned(v),
v => v.to_css_string(interpolation.span)?,
})
}
pub fn parse_value_as_string_from_vec(
&mut self,
toks: Vec<Token>,
) -> SassResult<Cow<'static, str>> {
let value = self.parse_value_from_vec(toks)?;
value.node.to_css_string(value.span)
}
pub fn whitespace(&mut self) -> bool {
let mut found_whitespace = false;
while let Some(tok) = self.toks.peek() {
@ -1000,8 +1017,7 @@ impl<'a> Parser<'a> {
if let Some(Token { kind: '{', pos }) = self.toks.peek() {
self.span_before = self.span_before.merge(*pos);
self.toks.next();
let interpolation = self.parse_interpolation()?;
params.push_str(&interpolation.node.to_css_string(interpolation.span)?);
params.push_str(&self.parse_interpolation_as_string()?);
} else {
params.push(tok.kind);
}
@ -1046,10 +1062,12 @@ impl<'a> Parser<'a> {
}
fn parse_media(&mut self) -> SassResult<Stmt> {
let params = self.parse_media_args()?;
let query = self.parse_media_query_list()?;
if params.is_empty() {
return Err(("Expected identifier.", self.span_before).into());
self.whitespace();
if !matches!(self.toks.next(), Some(Token { kind: '{', .. })) {
return Err(("expected \"{\".", self.span_before).into());
}
let raw_body = Parser {
@ -1091,7 +1109,7 @@ impl<'a> Parser<'a> {
Ok(Stmt::Media {
super_selector: Selector::new(self.span_before),
params: params.trim().to_owned(),
query,
body,
})
}