From fc3facb80ca1df9fc4e3b689d9e23252982661cf Mon Sep 17 00:00:00 2001 From: ConnorSkees <39542938+ConnorSkees@users.noreply.github.com> Date: Tue, 21 Apr 2020 04:20:35 -0400 Subject: [PATCH] properly handle `url()` --- src/utils/mod.rs | 2 + src/utils/peek_until.rs | 108 +++++++++++++++++++++++++++++++++ src/utils/read_until.rs | 4 ++ src/value/css_function.rs | 124 +++++++++++++++++++++++++++++++++++++- src/value/parse.rs | 10 ++- tests/url.rs | 91 ++++++++++++++++++++++++++++ 6 files changed, 336 insertions(+), 3 deletions(-) create mode 100644 src/utils/peek_until.rs create mode 100644 tests/url.rs diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 227e585..cee1e0b 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -2,6 +2,7 @@ pub(crate) use chars::*; pub(crate) use comment_whitespace::*; pub(crate) use interpolation::*; pub(crate) use number::*; +pub(crate) use peek_until::*; pub(crate) use read_until::*; pub(crate) use strings::*; pub(crate) use variables::*; @@ -10,6 +11,7 @@ mod chars; mod comment_whitespace; mod interpolation; mod number; +mod peek_until; mod read_until; mod strings; mod variables; diff --git a/src/utils/peek_until.rs b/src/utils/peek_until.rs new file mode 100644 index 0000000..0eef894 --- /dev/null +++ b/src/utils/peek_until.rs @@ -0,0 +1,108 @@ +use std::iter::Iterator; + +use peekmore::PeekMoreIterator; + +use crate::Token; + +use super::IsWhitespace; + +pub(crate) fn peek_until_closing_curly_brace>( + toks: &mut PeekMoreIterator, +) -> Vec { + let mut t = Vec::new(); + let mut nesting = 0; + while let Some(tok) = toks.peek() { + match tok.kind { + q @ '"' | q @ '\'' => { + t.push(*toks.peek().unwrap()); + toks.move_forward(1); + t.extend(peek_until_closing_quote(toks, q)); + } + '{' => { + nesting += 1; + t.push(*toks.peek().unwrap()); + toks.move_forward(1); + } + '}' => { + if nesting == 0 { + break; + } else { + nesting -= 1; + t.push(*toks.peek().unwrap()); + toks.move_forward(1); + } + } + '/' => { + let next = *toks.peek_forward(1).unwrap(); + match toks.peek().unwrap().kind { + '/' => peek_until_newline(toks), + _ => t.push(next), + }; + continue; + } + _ => { + t.push(*toks.peek().unwrap()); + toks.move_forward(1); + } + } + } + peek_whitespace(toks); + t +} + +fn peek_until_closing_quote>( + toks: &mut PeekMoreIterator, + q: char, +) -> Vec { + let mut t = Vec::new(); + while let Some(tok) = toks.peek() { + match tok.kind { + '"' if q == '"' => { + t.push(*tok); + toks.move_forward(1); + break; + } + '\'' if q == '\'' => { + t.push(*tok); + toks.move_forward(1); + break; + } + '\\' => { + t.push(*tok); + t.push(*toks.peek_forward(1).unwrap()); + } + '#' => { + t.push(*tok); + let next = toks.peek().unwrap(); + if next.kind == '{' { + t.push(*toks.peek_forward(1).unwrap()); + t.append(&mut peek_until_closing_curly_brace(toks)); + } + } + _ => t.push(*tok), + } + toks.move_forward(1); + } + t +} + +fn peek_until_newline>(toks: &mut PeekMoreIterator) { + while let Some(tok) = toks.peek() { + if tok.kind == '\n' { + break; + } + toks.move_forward(1); + } +} + +fn peek_whitespace, W: IsWhitespace>(s: &mut PeekMoreIterator) -> bool { + let mut found_whitespace = false; + while let Some(w) = s.peek() { + if !w.is_whitespace() { + break; + } + found_whitespace = true; + s.move_forward(1); + } + found_whitespace +} diff --git a/src/utils/read_until.rs b/src/utils/read_until.rs index 0bcbeed..94e5c1b 100644 --- a/src/utils/read_until.rs +++ b/src/utils/read_until.rs @@ -68,6 +68,10 @@ pub(crate) fn read_until_closing_curly_brace>( }; continue; } + '(' => { + t.push(toks.next().unwrap()); + t.extend(read_until_closing_paren(toks)); + } _ => t.push(toks.next().unwrap()), } } diff --git a/src/value/css_function.rs b/src/value/css_function.rs index 5832997..877de43 100644 --- a/src/value/css_function.rs +++ b/src/value/css_function.rs @@ -3,7 +3,7 @@ use peekmore::PeekMoreIterator; use crate::error::SassResult; use crate::scope::Scope; use crate::selector::Selector; -use crate::utils::{devour_whitespace, parse_interpolation}; +use crate::utils::{devour_whitespace, parse_interpolation, peek_until_closing_curly_brace}; use crate::Token; pub(crate) fn eat_calc_args>( @@ -78,3 +78,125 @@ pub(crate) fn eat_progid>( } Ok(string) } + +pub(crate) fn try_eat_url>( + toks: &mut PeekMoreIterator, + scope: &Scope, + super_selector: &Selector, +) -> SassResult> { + let mut buf = String::from("url("); + let mut peek_counter = 0; + while let Some(tok) = toks.peek() { + let kind = tok.kind; + toks.move_forward(1); + peek_counter += 1; + if kind == '!' + || kind == '%' + || kind == '&' + || (kind >= '*' && kind <= '~') + || kind as u32 >= 0x0080 + { + buf.push(kind); + } else if kind == '\\' { + buf.push_str(&peek_escape(toks)?); + } else if kind == '#' { + let next = toks.peek(); + if next.is_some() && next.unwrap().kind == '{' { + toks.move_forward(1); + peek_counter += 1; + let (interpolation, count) = peek_interpolation(toks, scope, super_selector)?; + peek_counter += count; + buf.push_str(&match interpolation.node { + Value::Ident(s, ..) => s, + v => v.to_css_string(interpolation.span)?, + }); + } else { + buf.push('#'); + } + } else if kind == ')' { + buf.push(')'); + for _ in 0..=peek_counter { + toks.next(); + } + return Ok(Some(buf)); + } else { + break; + } + } + toks.reset_view(); + 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; + +fn peek_interpolation>( + toks: &mut PeekMoreIterator, + scope: &Scope, + super_selector: &Selector, +) -> SassResult<(Spanned, usize)> { + let vec = peek_until_closing_curly_brace(toks); + let peek_counter = vec.len(); + toks.move_forward(1); + let val = Value::from_vec(vec, scope, super_selector)?; + Ok(( + Spanned { + node: val.node.eval(val.span)?.node.unquote(), + span: val.span, + }, + peek_counter, + )) +} diff --git a/src/value/parse.rs b/src/value/parse.rs index 6e94f3d..a85d7d9 100644 --- a/src/value/parse.rs +++ b/src/value/parse.rs @@ -10,7 +10,7 @@ use codemap::{Span, Spanned}; use peekmore::{PeekMore, PeekMoreIterator}; -use super::css_function::{eat_calc_args, eat_progid}; +use super::css_function::{eat_calc_args, eat_progid, try_eat_url}; use crate::args::eat_call_args; use crate::builtin::GLOBAL_FUNCTIONS; @@ -530,7 +530,13 @@ impl Value { } // "min" => {} // "max" => {} - // "url" => {} + "url" => match try_eat_url(toks, scope, super_selector)? { + Some(val) => s = val, + None => s.push_str( + &eat_call_args(toks, scope, super_selector)? + .to_css_string(scope, super_selector)?, + ), + }, _ => s.push_str( &eat_call_args(toks, scope, super_selector)? .to_css_string(scope, super_selector)?, diff --git a/tests/url.rs b/tests/url.rs new file mode 100644 index 0000000..c28dcfb --- /dev/null +++ b/tests/url.rs @@ -0,0 +1,91 @@ +#![cfg(test)] + +#[macro_use] +mod macros; + +test!( + arithmetic_both_space, + "a {\n color: url(1 + 2);\n}\n", + "a {\n color: url(3);\n}\n" +); +test!( + arithmetic_space_right, + "a {\n color: url(1+ 2);\n}\n", + "a {\n color: url(3);\n}\n" +); +test!( + arithmetic_space_left, + "a {\n color: url(1 +2);\n}\n", + "a {\n color: url(3);\n}\n" +); +test!( + arithmetic_no_space, + "a {\n color: url(1+2);\n}\n", + "a {\n color: url(1+2);\n}\n" +); +test!( + silent_comment, + "a {\n color: url(//some/absolute/path);\n}\n" +); +test!( + multiline_comment, + "a {\n color: url(/*looks-like-a*/comment);\n}\n" +); +test!(plain_css_function, "a {\n color: url(fn(\"s\"));\n}\n"); +test!( + builtin_function, + "a {\n color: url(if(true, \"red.png\", \"blue.png\"));\n}\n", + "a {\n color: url(\"red.png\");\n}\n" +); +test!( + user_defined_function, + "$file-1x: \"budge.png\";\n@function fudge($str) {\n @return \"assets/fudge/\"+$str;\n}\n\na {\n color: url(fudge(\"#{$file-1x}\"));\n}\n", + "a {\n color: url(\"assets/fudge/budge.png\");\n}\n" +); +test!( + unquoted_interpolation, + "a {\n color: url(hello-#{world}.png);\n}\n", + "a {\n color: url(hello-world.png);\n}\n" +); +test!( + quoted_interpolation, + "a {\n color: url(\"hello-#{world}.png\");\n}\n", + "a {\n color: url(\"hello-world.png\");\n}\n" +); +test!(simple_forward_slash, "a {\n color: url(foo/bar.css);\n}\n"); +test!(http_url, "a {\n color: url(http://foo.bar.com);\n}\n"); +test!( + google_fonts_url, + "a {\n color: url(http://fonts.googleapis.com/css?family=Karla:400,700,400italic|Anonymous+Pro:400,700,400italic);\n}\n" +); +test!( + interpolation_in_http_url, + "a {\n color: url(http://blah.com/bar-#{foo}.css);\n}\n", + "a {\n color: url(http://blah.com/bar-foo.css);\n}\n" +); +test!( + many_forward_slashes, + "a {\n color: url(http://box_////fudge.css);\n}\n" +); +test!( + url_whitespace, + "a {\n color: url( 1 );\n}\n", + "a {\n color: url(1);\n}\n" +); +test!( + url_newline, + "a {\n color: url(\n);\n}\n", + "a {\n color: url();\n}\n" +); +test!(url_comma_list, "a {\n color: url(1, 2, a, b, c);\n}\n"); +test!( + url_contains_only_interpolation, + "a {\n color: url(#{1 + 2});\n}\n", + "a {\n color: url(3);\n}\n" +); +test!( + url_begins_with_interpolation, + "a {\n color: url(#{http}://foo);\n}\n", + "a {\n color: url(http://foo);\n}\n" +); +test!(url_dot_dot, "a {\n color: url(../foo/bar/..baz/);\n}\n");