From e820395cc5e370f9f4e35a8292d0ce4a524d3aa1 Mon Sep 17 00:00:00 2001 From: ConnorSkees <39542938+ConnorSkees@users.noreply.github.com> Date: Sun, 19 Apr 2020 13:51:34 -0400 Subject: [PATCH] refactor printing and parsing of quoted strings --- src/utils.rs | 93 +++++++++++++++++----------------- src/value/mod.rs | 119 ++++++++++++++++++++++++++++++++++++-------- tests/str-escape.rs | 6 +++ tests/values.rs | 10 ---- 4 files changed, 153 insertions(+), 75 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 6feedc9..d475795 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -580,6 +580,16 @@ pub(crate) fn eat_comment>( }) } +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>( toks: &mut Peekable, scope: &Scope, @@ -587,7 +597,6 @@ pub(crate) fn parse_quoted_string>( super_selector: &Selector, ) -> SassResult> { 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>( 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>( } } '\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 { diff --git a/src/value/mod.rs b/src/value/mod.rs index ab9fe28..aa7eaf7 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -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 { 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(), diff --git a/tests/str-escape.rs b/tests/str-escape.rs index be1e5a9..81e3bd2 100644 --- a/tests/str-escape.rs +++ b/tests/str-escape.rs @@ -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" +); diff --git a/tests/values.rs b/tests/values.rs index 426dc53..2dfec0b 100644 --- a/tests/values.rs +++ b/tests/values.rs @@ -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",