properly handle url()
This commit is contained in:
parent
b2e3322f9a
commit
fc3facb80c
@ -2,6 +2,7 @@ pub(crate) use chars::*;
|
|||||||
pub(crate) use comment_whitespace::*;
|
pub(crate) use comment_whitespace::*;
|
||||||
pub(crate) use interpolation::*;
|
pub(crate) use interpolation::*;
|
||||||
pub(crate) use number::*;
|
pub(crate) use number::*;
|
||||||
|
pub(crate) use peek_until::*;
|
||||||
pub(crate) use read_until::*;
|
pub(crate) use read_until::*;
|
||||||
pub(crate) use strings::*;
|
pub(crate) use strings::*;
|
||||||
pub(crate) use variables::*;
|
pub(crate) use variables::*;
|
||||||
@ -10,6 +11,7 @@ mod chars;
|
|||||||
mod comment_whitespace;
|
mod comment_whitespace;
|
||||||
mod interpolation;
|
mod interpolation;
|
||||||
mod number;
|
mod number;
|
||||||
|
mod peek_until;
|
||||||
mod read_until;
|
mod read_until;
|
||||||
mod strings;
|
mod strings;
|
||||||
mod variables;
|
mod variables;
|
||||||
|
108
src/utils/peek_until.rs
Normal file
108
src/utils/peek_until.rs
Normal file
@ -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<I: Iterator<Item = Token>>(
|
||||||
|
toks: &mut PeekMoreIterator<I>,
|
||||||
|
) -> Vec<Token> {
|
||||||
|
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<I: Iterator<Item = Token>>(
|
||||||
|
toks: &mut PeekMoreIterator<I>,
|
||||||
|
q: char,
|
||||||
|
) -> Vec<Token> {
|
||||||
|
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<I: Iterator<Item = Token>>(toks: &mut PeekMoreIterator<I>) {
|
||||||
|
while let Some(tok) = toks.peek() {
|
||||||
|
if tok.kind == '\n' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
toks.move_forward(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peek_whitespace<I: Iterator<Item = W>, W: IsWhitespace>(s: &mut PeekMoreIterator<I>) -> 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
|
||||||
|
}
|
@ -68,6 +68,10 @@ pub(crate) fn read_until_closing_curly_brace<I: Iterator<Item = Token>>(
|
|||||||
};
|
};
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
'(' => {
|
||||||
|
t.push(toks.next().unwrap());
|
||||||
|
t.extend(read_until_closing_paren(toks));
|
||||||
|
}
|
||||||
_ => t.push(toks.next().unwrap()),
|
_ => t.push(toks.next().unwrap()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ use peekmore::PeekMoreIterator;
|
|||||||
use crate::error::SassResult;
|
use crate::error::SassResult;
|
||||||
use crate::scope::Scope;
|
use crate::scope::Scope;
|
||||||
use crate::selector::Selector;
|
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;
|
use crate::Token;
|
||||||
|
|
||||||
pub(crate) fn eat_calc_args<I: Iterator<Item = Token>>(
|
pub(crate) fn eat_calc_args<I: Iterator<Item = Token>>(
|
||||||
@ -78,3 +78,125 @@ pub(crate) fn eat_progid<I: Iterator<Item = Token>>(
|
|||||||
}
|
}
|
||||||
Ok(string)
|
Ok(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn try_eat_url<I: Iterator<Item = Token>>(
|
||||||
|
toks: &mut PeekMoreIterator<I>,
|
||||||
|
scope: &Scope,
|
||||||
|
super_selector: &Selector,
|
||||||
|
) -> SassResult<Option<String>> {
|
||||||
|
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<I: Iterator<Item = Token>>(toks: &mut PeekMoreIterator<I>) -> SassResult<String> {
|
||||||
|
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<I: Iterator<Item = Token>>(
|
||||||
|
toks: &mut PeekMoreIterator<I>,
|
||||||
|
scope: &Scope,
|
||||||
|
super_selector: &Selector,
|
||||||
|
) -> SassResult<(Spanned<Value>, 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,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
@ -10,7 +10,7 @@ use codemap::{Span, Spanned};
|
|||||||
|
|
||||||
use peekmore::{PeekMore, PeekMoreIterator};
|
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::args::eat_call_args;
|
||||||
use crate::builtin::GLOBAL_FUNCTIONS;
|
use crate::builtin::GLOBAL_FUNCTIONS;
|
||||||
@ -530,7 +530,13 @@ impl Value {
|
|||||||
}
|
}
|
||||||
// "min" => {}
|
// "min" => {}
|
||||||
// "max" => {}
|
// "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(
|
_ => s.push_str(
|
||||||
&eat_call_args(toks, scope, super_selector)?
|
&eat_call_args(toks, scope, super_selector)?
|
||||||
.to_css_string(scope, super_selector)?,
|
.to_css_string(scope, super_selector)?,
|
||||||
|
91
tests/url.rs
Normal file
91
tests/url.rs
Normal file
@ -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");
|
Loading…
x
Reference in New Issue
Block a user