improve parsing of media queries
This commit is contained in:
parent
e5e3943e5c
commit
9512e9f39f
@ -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
248
src/parse/media.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user