refactor printing and parsing of quoted strings

This commit is contained in:
ConnorSkees 2020-04-19 13:51:34 -04:00
parent 2f7391acda
commit e820395cc5
4 changed files with 153 additions and 75 deletions

View File

@ -580,6 +580,16 @@ pub(crate) fn eat_comment<I: Iterator<Item = Token>>(
})
}
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 parse_quoted_string<I: Iterator<Item = Token>>(
toks: &mut Peekable<I>,
scope: &Scope,
@ -587,7 +597,6 @@ pub(crate) fn parse_quoted_string<I: Iterator<Item = Token>>(
super_selector: &Selector,
) -> SassResult<Spanned<Value>> {
let mut s = String::new();
let mut is_escaped = false;
let mut span = if let Some(tok) = toks.peek() {
tok.pos()
} else {
@ -596,25 +605,9 @@ pub(crate) fn parse_quoted_string<I: Iterator<Item = Token>>(
while let Some(tok) = toks.next() {
span = span.merge(tok.pos());
match tok.kind {
'"' if !is_escaped && q == '"' => break,
'"' if is_escaped => {
s.push('"');
is_escaped = false;
continue;
}
'\'' if !is_escaped && q == '\'' => break,
'\'' if is_escaped => {
s.push('\'');
is_escaped = false;
continue;
}
'\\' if !is_escaped => is_escaped = true,
'\\' => {
is_escaped = false;
s.push('\\');
continue;
}
'#' if !is_escaped => {
'"' if q == '"' => break,
'\'' if q == '\'' => break,
'#' => {
if toks.peek().unwrap().kind == '{' {
toks.next();
let interpolation = parse_interpolation(toks, scope, super_selector)?;
@ -626,36 +619,46 @@ pub(crate) fn parse_quoted_string<I: Iterator<Item = Token>>(
}
}
'\n' => return Err(("Expected \".", tok.pos()).into()),
v if v.is_ascii_hexdigit() && is_escaped => {
let mut n = v.to_string();
while let Some(c) = toks.peek() {
if !c.kind.is_ascii_hexdigit() || n.len() > 6 {
break;
'\\' => {
let first = match toks.peek() {
Some(c) => c,
None => {
s.push('\u{FFFD}');
continue;
}
n.push(c.kind);
toks.next();
};
if first.kind == '\n' {
return Err(("Expected escape sequence.", first.pos()).into());
}
let c = std::char::from_u32(u32::from_str_radix(&n, 16).unwrap()).unwrap();
if c.is_control() && c != '\t' && c != '\0' {
s.push_str(&format!("\\{}", n.to_ascii_lowercase()));
} else if c == '\0' {
s.push('\u{FFFD}');
if first.kind.is_ascii_hexdigit() {
let mut value = 0;
for _ in 0..6 {
let next = match toks.peek() {
Some(c) => c,
None => break,
};
if !next.kind.is_ascii_hexdigit() {
break;
}
value = (value << 4) + as_hex(toks.next().unwrap().kind as u32);
}
if toks.peek().is_some() && toks.peek().unwrap().kind.is_ascii_whitespace() {
toks.next();
}
if value == 0 || (value >= 0xD800 && value <= 0xDFFF) || value >= 0x10FFFF {
s.push('\u{FFFD}');
} else {
s.push(dbg!(std::char::from_u32(value).unwrap()));
}
} else {
s.push(c);
s.push(toks.next().unwrap().kind);
}
is_escaped = false;
continue;
}
_ if is_escaped => {
is_escaped = false;
}
_ => {}
}
if is_escaped && tok.kind != '\\' {
is_escaped = false;
}
if tok.kind != '\\' {
s.push_str(&tok.kind.to_string());
_ => s.push(tok.kind),
}
}
Ok(Spanned {

View File

@ -39,6 +39,83 @@ pub(crate) enum Value {
Function(SassFunction),
}
fn hex_char_for(number: u32) -> char {
assert!(number < 0x10);
std::char::from_u32(if number < 0xA {
0x30 + number
} else {
0x61 - 0xA + number
})
.unwrap()
}
fn visit_quoted_string(buf: &mut String, force_double_quote: bool, string: &str) -> SassResult<()> {
let mut has_single_quote = false;
let mut has_double_quote = false;
let mut buffer = String::new();
if force_double_quote {
buffer.push('"');
}
let mut iter = string.chars().peekable();
while let Some(c) = iter.next() {
match c {
'\'' => {
if force_double_quote {
buffer.push('\'');
} else if has_double_quote {
return visit_quoted_string(buf, true, string);
} else {
has_single_quote = true;
buffer.push('\'');
}
}
'"' => {
if force_double_quote {
buffer.push('\\');
buffer.push('"');
} else if has_single_quote {
return visit_quoted_string(buf, true, string);
} else {
has_double_quote = true;
buffer.push('"');
}
}
'\x00'..='\x08' | '\x0A'..='\x1F' => {
buffer.push('\\');
if c as u32 > 0xF {
buffer.push(hex_char_for(c as u32 >> 4))
}
buffer.push(hex_char_for(c as u32 & 0xF));
if iter.peek().is_none() {
break;
}
let next = iter.peek().unwrap();
if next.is_ascii_hexdigit() || next == &' ' || next == &'\t' {
buffer.push(' ');
}
}
'\\' => {
buffer.push('\\');
buffer.push('\\');
}
_ => buffer.push(c),
}
}
if force_double_quote {
buffer.push('"');
} else {
let quote = if has_double_quote { '\'' } else { '"' };
buffer = format!("{}{}{}", quote, buffer, quote);
}
buf.push_str(&buffer);
Ok(())
}
impl Value {
pub fn is_null(&self, span: Span) -> SassResult<bool> {
match self {
@ -92,30 +169,32 @@ impl Value {
format!("{}", self.clone().eval(span)?.to_css_string(span)?)
}
Self::Paren(val) => format!("{}", val.to_css_string(span)?),
Self::Ident(val, QuoteKind::None) => return Ok(val.clone()),
Self::Ident(val, QuoteKind::Quoted) => {
let has_single_quotes = val.contains(|x| x == '\'');
let has_double_quotes = val.contains(|x| x == '"');
match (has_single_quotes, has_double_quotes) {
(true, false) => format!("\"{}\"", val),
(false, true) => format!("'{}'", val),
(false, false) => format!("\"{}\"", val),
(true, true) => {
let mut buf = String::with_capacity(val.len() + 2);
buf.push('"');
for c in val.chars() {
match c {
'"' => {
buf.push('\\');
buf.push('"');
}
v => buf.push(v),
Self::Ident(string, QuoteKind::None) => {
let mut after_newline = false;
let mut buf = String::with_capacity(string.len());
for c in string.chars() {
match c {
'\n' => {
buf.push(' ');
after_newline = true;
}
' ' => {
if !after_newline {
buf.push(' ');
}
}
buf.push('"');
buf
_ => {
buf.push(c);
after_newline = false;
}
}
}
buf
}
Self::Ident(string, QuoteKind::Quoted) => {
let mut buf = String::with_capacity(string.len());
visit_quoted_string(&mut buf, false, string)?;
buf
}
Self::True => "true".to_string(),
Self::False => "false".to_string(),

View File

@ -142,3 +142,9 @@ test!(
// "a {\n color: quote(\\b);\n}\n",
// "a {\n color: \"\\\\b \";\n}\n"
// );
test!(escaped_backslash, "a {\n color: \"\\\\\";\n}\n");
test!(
double_quotes_when_containing_single_quote,
"a {\n color: '\\\'';\n}\n",
"a {\n color: \"'\";\n}\n"
);

View File

@ -70,16 +70,6 @@ test!(
"a {\n color: \"f\"foo;\n}\n",
"a {\n color: \"f\" foo;\n}\n"
);
test!(
escaped_backslash,
"a {\n color: \"\\\\\";\n}\n",
"a {\n color: \"\\\";\n}\n"
);
test!(
double_quotes_when_containing_single_quote,
"a {\n color: '\\\'';\n}\n",
"a {\n color: \"'\";\n}\n"
);
test!(
color_equals_color,
"a {\n color: red == red;\n}\n",