refactor printing and parsing of quoted strings
This commit is contained in:
parent
2f7391acda
commit
e820395cc5
91
src/utils.rs
91
src/utils.rs
@ -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;
|
||||
}
|
||||
n.push(c.kind);
|
||||
toks.next();
|
||||
}
|
||||
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' {
|
||||
'\\' => {
|
||||
let first = match toks.peek() {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
s.push('\u{FFFD}');
|
||||
} else {
|
||||
s.push(c);
|
||||
}
|
||||
is_escaped = false;
|
||||
continue;
|
||||
}
|
||||
_ if is_escaped => {
|
||||
is_escaped = false;
|
||||
};
|
||||
|
||||
if first.kind == '\n' {
|
||||
return Err(("Expected escape sequence.", first.pos()).into());
|
||||
}
|
||||
_ => {}
|
||||
|
||||
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;
|
||||
}
|
||||
if is_escaped && tok.kind != '\\' {
|
||||
is_escaped = false;
|
||||
value = (value << 4) + as_hex(toks.next().unwrap().kind as u32);
|
||||
}
|
||||
if tok.kind != '\\' {
|
||||
s.push_str(&tok.kind.to_string());
|
||||
|
||||
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(toks.next().unwrap().kind);
|
||||
}
|
||||
}
|
||||
_ => s.push(tok.kind),
|
||||
}
|
||||
}
|
||||
Ok(Spanned {
|
||||
|
115
src/value/mod.rs
115
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<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() {
|
||||
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 {
|
||||
'"' => {
|
||||
buf.push('\\');
|
||||
buf.push('"');
|
||||
'\n' => {
|
||||
buf.push(' ');
|
||||
after_newline = true;
|
||||
}
|
||||
' ' => {
|
||||
if !after_newline {
|
||||
buf.push(' ');
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
buf.push(c);
|
||||
after_newline = false;
|
||||
}
|
||||
v => buf.push(v),
|
||||
}
|
||||
}
|
||||
buf.push('"');
|
||||
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(),
|
||||
|
@ -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"
|
||||
);
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user