diff --git a/src/output.rs b/src/output.rs index 2d29575..5b1cdfa 100644 --- a/src/output.rs +++ b/src/output.rs @@ -15,7 +15,7 @@ enum Toplevel { body: Vec, }, Media { - params: String, + query: String, body: Vec, }, 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, diff --git a/src/parse/media.rs b/src/parse/media.rs new file mode 100644 index 0000000..e71b7d6 --- /dev/null +++ b/src/parse/media.rs @@ -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, + + /// The media type, for example "screen" or "print". + /// + /// This may be `None`. If so, `self.features` will not be empty. + media_type: Option, + + /// Feature queries, including parentheses. + features: Vec, +} + +#[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) -> 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 { + 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> { + 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 { + 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 { + 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 { + 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) + } +} diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 140407c..178df3c 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -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