From a86d717f267c8aab7603d46a6eae708d2fac0b1c Mon Sep 17 00:00:00 2001 From: ConnorSkees <39542938+ConnorSkees@users.noreply.github.com> Date: Thu, 23 Apr 2020 21:30:25 -0400 Subject: [PATCH] properly parse variable flags --- src/utils/chars.rs | 13 ++-- src/utils/peek_until.rs | 133 +++++++++++++++++++++++++++++++++++++- src/utils/strings.rs | 4 +- src/utils/variables.rs | 117 ++++++++++++++++++++++++--------- src/value/css_function.rs | 57 +--------------- tests/variables.rs | 22 +++++++ 6 files changed, 250 insertions(+), 96 deletions(-) diff --git a/src/utils/chars.rs b/src/utils/chars.rs index 08d6a67..9d3c1b7 100644 --- a/src/utils/chars.rs +++ b/src/utils/chars.rs @@ -53,12 +53,11 @@ pub(crate) fn is_name_start(c: char) -> bool { c == '_' || c.is_alphanumeric() || c as u32 >= 0x0080 } -pub(crate) fn as_hex(c: u32) -> u32 { - if c <= '9' as u32 { - c - '0' as u32 - } else if c <= 'F' as u32 { - 10 + c - 'A' as u32 - } else { - 10 + c - 'a' as u32 +pub(crate) fn as_hex(c: char) -> u32 { + match c { + '0'..='9' => c as u32 - '0' as u32, + 'A'..='F' => 10 + c as u32 - 'A' as u32, + 'a'..='f' => 10 + c as u32 - 'a' as u32, + _ => panic!(), } } diff --git a/src/utils/peek_until.rs b/src/utils/peek_until.rs index 0eef894..935d083 100644 --- a/src/utils/peek_until.rs +++ b/src/utils/peek_until.rs @@ -1,10 +1,13 @@ use std::iter::Iterator; +use codemap::{Span, Spanned}; + use peekmore::PeekMoreIterator; +use crate::error::SassResult; use crate::Token; -use super::IsWhitespace; +use super::{as_hex, hex_char_for, is_name, is_name_start, IsWhitespace}; pub(crate) fn peek_until_closing_curly_brace>( toks: &mut PeekMoreIterator, @@ -106,3 +109,131 @@ fn peek_whitespace, W: IsWhitespace>(s: &mut PeekMoreItera } found_whitespace } + +pub(crate) fn peek_escape>( + toks: &mut PeekMoreIterator, +) -> SassResult { + let mut value = 0; + let first = match toks.peek() { + Some(t) => t, + None => return Ok(String::new()), + }; + if first.kind == '\n' { + return Err(("Expected escape sequence.", first.pos()).into()); + } else if first.kind.is_ascii_hexdigit() { + for _ in 0..6 { + let next = match toks.peek() { + Some(t) => t, + None => break, + }; + if !next.kind.is_ascii_hexdigit() { + break; + } + value *= 16; + value += as_hex(next.kind); + toks.peek_forward(1); + } + if toks.peek().is_some() && toks.peek().unwrap().kind.is_whitespace() { + toks.peek_forward(1); + } + } else { + value = toks.peek_forward(1).unwrap().kind as u32; + } + + // tabs are emitted literally + // TODO: figure out where this check is done + // in the source dart + if value == 0x9 { + return Ok("\\\t".to_string()); + } + + let c = std::char::from_u32(value).unwrap(); + if is_name(c) { + Ok(c.to_string()) + } else if value <= 0x1F || value == 0x7F { + let mut buf = String::with_capacity(4); + buf.push('\\'); + if value > 0xF { + buf.push(hex_char_for(value >> 4)); + } + buf.push(hex_char_for(value & 0xF)); + buf.push(' '); + Ok(buf) + } else { + Ok(format!("\\{}", c)) + } +} + +pub(crate) fn peek_ident_no_interpolation>( + toks: &mut PeekMoreIterator, + unit: bool, +) -> SassResult> { + let mut span = toks.peek().unwrap().pos(); + let mut text = String::new(); + if toks.peek().unwrap().kind == '-' { + toks.peek_forward(1); + text.push('-'); + if toks.peek().unwrap().kind == '-' { + toks.peek_forward(1); + text.push('-'); + text.push_str(&peek_ident_body_no_interpolation(toks, unit, span)?.node); + return Ok(Spanned { node: text, span }); + } + } + + let first = match toks.peek() { + Some(v) => v, + None => return Err(("Expected identifier.", span).into()), + }; + + if is_name_start(first.kind) { + text.push(first.kind); + toks.peek_forward(1); + } else if first.kind == '\\' { + toks.peek_forward(1); + text.push_str(&peek_escape(toks)?); + } else { + return Err(("Expected identifier.", first.pos()).into()); + } + + let body = peek_ident_body_no_interpolation(toks, unit, span)?; + span = span.merge(body.span); + text.push_str(&body.node); + Ok(Spanned { node: text, span }) +} + +fn peek_ident_body_no_interpolation>( + toks: &mut PeekMoreIterator, + unit: bool, + mut span: Span, +) -> SassResult> { + let mut text = String::new(); + while let Some(tok) = toks.peek() { + span = span.merge(tok.pos()); + if unit && tok.kind == '-' { + // Disallow `-` followed by a dot or a digit digit in units. + let second = match toks.peek_forward(1) { + Some(v) => *v, + None => break, + }; + + toks.peek_backward(1).unwrap(); + + if second.kind == '.' || second.kind.is_ascii_digit() { + break; + } + toks.peek_forward(1); + text.push('-'); + text.push(toks.peek_forward(1).unwrap().kind); + } else if is_name(tok.kind) { + text.push(tok.kind); + toks.peek_forward(1); + } else if tok.kind == '\\' { + toks.peek_forward(1); + text.push_str(&peek_escape(toks)?); + } else { + break; + } + } + Ok(Spanned { node: text, span }) +} diff --git a/src/utils/strings.rs b/src/utils/strings.rs index 46651f8..594f87b 100644 --- a/src/utils/strings.rs +++ b/src/utils/strings.rs @@ -102,7 +102,7 @@ fn escape>( break; } value *= 16; - value += as_hex(toks.next().unwrap().kind as u32) + value += as_hex(toks.next().unwrap().kind) } if toks.peek().is_some() && toks.peek().unwrap().kind.is_whitespace() { toks.next(); @@ -287,7 +287,7 @@ pub(crate) fn parse_quoted_string>( if !next.kind.is_ascii_hexdigit() { break; } - value = (value << 4) + as_hex(toks.next().unwrap().kind as u32); + value = (value << 4) + as_hex(toks.next().unwrap().kind); } if toks.peek().is_some() && toks.peek().unwrap().kind.is_ascii_whitespace() { diff --git a/src/utils/variables.rs b/src/utils/variables.rs index 7c6db2c..b29b2a0 100644 --- a/src/utils/variables.rs +++ b/src/utils/variables.rs @@ -2,14 +2,17 @@ use std::iter::Iterator; use codemap::Spanned; -use peekmore::{PeekMore, PeekMoreIterator}; +use peekmore::PeekMoreIterator; use crate::error::SassResult; use crate::selector::Selector; use crate::value::Value; use crate::{Scope, Token}; -use super::{devour_whitespace, eat_ident, read_until_semicolon_or_closing_curly_brace}; +use super::{ + devour_whitespace, peek_ident_no_interpolation, read_until_closing_paren, + read_until_closing_quote, read_until_newline, +}; pub(crate) struct VariableDecl { pub val: Spanned, @@ -35,43 +38,93 @@ pub(crate) fn eat_variable_value>( devour_whitespace(toks); let mut default = false; let mut global = false; - let mut raw = read_until_semicolon_or_closing_curly_brace(toks) - .into_iter() - .peekmore(); - if toks.peek().is_some() && toks.peek().unwrap().kind == ';' { - toks.next(); - } + let mut val_toks = Vec::new(); - while let Some(tok) = raw.next() { + let mut nesting = 0; + while let Some(tok) = toks.peek() { match tok.kind { - '!' => { - let next = raw.next().unwrap(); - match next.kind { - 'i' => todo!("!important"), - 'g' => { - let s = eat_ident(&mut raw, scope, super_selector)?; - if s.node.to_ascii_lowercase().as_str() == "lobal" { - global = true; - } else { - return Err(("Invalid flag name.", s.span).into()); - } - } - 'd' => { - let s = eat_ident(&mut raw, scope, super_selector)?; - if s.to_ascii_lowercase().as_str() == "efault" { - default = true; - } else { - return Err(("Invalid flag name.", s.span).into()); - } - } - _ => return Err(("Invalid flag name.", next.pos()).into()), + ';' => { + toks.next(); + break; + } + '\\' => { + val_toks.push(toks.next().unwrap()); + if toks.peek().is_some() { + val_toks.push(toks.next().unwrap()); } } - _ => val_toks.push(tok), + '"' | '\'' => { + let quote = toks.next().unwrap(); + val_toks.push(quote); + val_toks.extend(read_until_closing_quote(toks, quote.kind)); + } + '#' => { + val_toks.push(toks.next().unwrap()); + match toks.peek().unwrap().kind { + '{' => nesting += 1, + ';' => break, + '}' => { + if nesting == 0 { + break; + } else { + nesting -= 1; + } + } + _ => {} + } + val_toks.push(toks.next().unwrap()); + } + '{' => break, + '}' => { + if nesting == 0 { + break; + } else { + nesting -= 1; + val_toks.push(toks.next().unwrap()); + } + } + '/' => { + let next = toks.next().unwrap(); + match toks.peek().unwrap().kind { + '/' => read_until_newline(toks), + _ => val_toks.push(next), + }; + continue; + } + '(' => { + val_toks.push(toks.next().unwrap()); + val_toks.extend(read_until_closing_paren(toks)); + } + '!' => { + let pos = tok.pos(); + if toks.peek_forward(1).is_none() { + return Err(("Expected identifier.", pos).into()); + } + // todo: it should not be possible to declare the same flag more than once + let ident = peek_ident_no_interpolation(toks, false)?; + match ident.node.to_ascii_lowercase().as_str() { + "global" => { + toks.take(7).for_each(drop); + global = true; + } + "default" => { + toks.take(8).for_each(drop); + default = true; + } + "important" => { + toks.reset_view(); + val_toks.push(toks.next().unwrap()); + continue; + } + _ => { + return Err(("Invalid flag name.", ident.span).into()); + } + } + } + _ => val_toks.push(toks.next().unwrap()), } } devour_whitespace(toks); - let val = Value::from_vec(val_toks, scope, super_selector)?; Ok(VariableDecl::new(val, default, global)) } diff --git a/src/value/css_function.rs b/src/value/css_function.rs index 877de43..b479a7c 100644 --- a/src/value/css_function.rs +++ b/src/value/css_function.rs @@ -3,7 +3,9 @@ use peekmore::PeekMoreIterator; use crate::error::SassResult; use crate::scope::Scope; use crate::selector::Selector; -use crate::utils::{devour_whitespace, parse_interpolation, peek_until_closing_curly_brace}; +use crate::utils::{ + devour_whitespace, parse_interpolation, peek_escape, peek_until_closing_curly_brace, +}; use crate::Token; pub(crate) fn eat_calc_args>( @@ -127,59 +129,6 @@ pub(crate) fn try_eat_url>( Ok(None) } -use crate::utils::{as_hex, hex_char_for, is_name}; - -fn peek_escape>(toks: &mut PeekMoreIterator) -> SassResult { - let mut value = 0; - let first = match toks.peek() { - Some(t) => t, - None => return Ok(String::new()), - }; - if first.kind == '\n' { - return Err(("Expected escape sequence.", first.pos()).into()); - } else if first.kind.is_ascii_hexdigit() { - for _ in 0..6 { - let next = match toks.peek_forward(1) { - Some(t) => t, - None => break, - }; - if !next.kind.is_ascii_hexdigit() { - break; - } - value *= 16; - value += as_hex(toks.next().unwrap().kind as u32) - } - if toks.peek().is_some() && toks.peek().unwrap().kind.is_whitespace() { - toks.peek_forward(1); - } - } else { - value = toks.peek_forward(1).unwrap().kind as u32; - } - - // tabs are emitted literally - // TODO: figure out where this check is done - // in the source dart - if value == 0x9 { - return Ok("\\\t".to_string()); - } - - let c = std::char::from_u32(value).unwrap(); - if is_name(c) { - Ok(c.to_string()) - } else if value <= 0x1F || value == 0x7F { - let mut buf = String::with_capacity(4); - buf.push('\\'); - if value > 0xF { - buf.push(hex_char_for(value >> 4)); - } - buf.push(hex_char_for(value & 0xF)); - buf.push(' '); - Ok(buf) - } else { - Ok(format!("\\{}", c)) - } -} - use crate::value::Value; use codemap::Spanned; diff --git a/tests/variables.rs b/tests/variables.rs index 1f73ff4..ede07db 100644 --- a/tests/variables.rs +++ b/tests/variables.rs @@ -113,3 +113,25 @@ test!( "a {\n $x : true;\n color: $x;\n}\n", "a {\n color: true;\n}\n" ); +test!( + important_in_variable, + "$a: !important;\n\na {\n color: $a;\n}\n", + "a {\n color: !important;\n}\n" +); +test!( + important_in_variable_casing, + "$a: !ImPoRtAnT;\n\na {\n color: $a;\n}\n", + "a {\n color: !important;\n}\n" +); +test!( + exclamation_in_quoted_string, + "$a: \"big bang!\";\n\na {\n color: $a;\n}\n", + "a {\n color: \"big bang!\";\n}\n" +); +test!( + flag_uses_escape_sequence, + "$a: red;\n\na {\n $a: green !\\67 lobal;\n}\n\na {\n color: $a;\n}\n", + "a {\n color: green;\n}\n" +); +error!(ends_with_bang, "$a: red !;", "Error: Expected identifier."); +error!(unknown_flag, "$a: red !foo;", "Error: Invalid flag name.");