refactor printing and parsing of quoted strings
This commit is contained in:
parent
2f7391acda
commit
e820395cc5
93
src/utils.rs
93
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>>(
|
pub(crate) fn parse_quoted_string<I: Iterator<Item = Token>>(
|
||||||
toks: &mut Peekable<I>,
|
toks: &mut Peekable<I>,
|
||||||
scope: &Scope,
|
scope: &Scope,
|
||||||
@ -587,7 +597,6 @@ pub(crate) fn parse_quoted_string<I: Iterator<Item = Token>>(
|
|||||||
super_selector: &Selector,
|
super_selector: &Selector,
|
||||||
) -> SassResult<Spanned<Value>> {
|
) -> SassResult<Spanned<Value>> {
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
let mut is_escaped = false;
|
|
||||||
let mut span = if let Some(tok) = toks.peek() {
|
let mut span = if let Some(tok) = toks.peek() {
|
||||||
tok.pos()
|
tok.pos()
|
||||||
} else {
|
} else {
|
||||||
@ -596,25 +605,9 @@ pub(crate) fn parse_quoted_string<I: Iterator<Item = Token>>(
|
|||||||
while let Some(tok) = toks.next() {
|
while let Some(tok) = toks.next() {
|
||||||
span = span.merge(tok.pos());
|
span = span.merge(tok.pos());
|
||||||
match tok.kind {
|
match tok.kind {
|
||||||
'"' if !is_escaped && q == '"' => break,
|
'"' if q == '"' => break,
|
||||||
'"' if is_escaped => {
|
'\'' if q == '\'' => break,
|
||||||
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 toks.peek().unwrap().kind == '{' {
|
if toks.peek().unwrap().kind == '{' {
|
||||||
toks.next();
|
toks.next();
|
||||||
let interpolation = parse_interpolation(toks, scope, super_selector)?;
|
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()),
|
'\n' => return Err(("Expected \".", tok.pos()).into()),
|
||||||
v if v.is_ascii_hexdigit() && is_escaped => {
|
'\\' => {
|
||||||
let mut n = v.to_string();
|
let first = match toks.peek() {
|
||||||
while let Some(c) = toks.peek() {
|
Some(c) => c,
|
||||||
if !c.kind.is_ascii_hexdigit() || n.len() > 6 {
|
None => {
|
||||||
break;
|
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' {
|
if first.kind.is_ascii_hexdigit() {
|
||||||
s.push_str(&format!("\\{}", n.to_ascii_lowercase()));
|
let mut value = 0;
|
||||||
} else if c == '\0' {
|
for _ in 0..6 {
|
||||||
s.push('\u{FFFD}');
|
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 {
|
} else {
|
||||||
s.push(c);
|
s.push(toks.next().unwrap().kind);
|
||||||
}
|
}
|
||||||
is_escaped = false;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
_ if is_escaped => {
|
_ => s.push(tok.kind),
|
||||||
is_escaped = false;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
if is_escaped && tok.kind != '\\' {
|
|
||||||
is_escaped = false;
|
|
||||||
}
|
|
||||||
if tok.kind != '\\' {
|
|
||||||
s.push_str(&tok.kind.to_string());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Spanned {
|
Ok(Spanned {
|
||||||
|
119
src/value/mod.rs
119
src/value/mod.rs
@ -39,6 +39,83 @@ pub(crate) enum Value {
|
|||||||
Function(SassFunction),
|
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 {
|
impl Value {
|
||||||
pub fn is_null(&self, span: Span) -> SassResult<bool> {
|
pub fn is_null(&self, span: Span) -> SassResult<bool> {
|
||||||
match self {
|
match self {
|
||||||
@ -92,30 +169,32 @@ impl Value {
|
|||||||
format!("{}", self.clone().eval(span)?.to_css_string(span)?)
|
format!("{}", self.clone().eval(span)?.to_css_string(span)?)
|
||||||
}
|
}
|
||||||
Self::Paren(val) => format!("{}", val.to_css_string(span)?),
|
Self::Paren(val) => format!("{}", val.to_css_string(span)?),
|
||||||
Self::Ident(val, QuoteKind::None) => return Ok(val.clone()),
|
Self::Ident(string, QuoteKind::None) => {
|
||||||
Self::Ident(val, QuoteKind::Quoted) => {
|
let mut after_newline = false;
|
||||||
let has_single_quotes = val.contains(|x| x == '\'');
|
let mut buf = String::with_capacity(string.len());
|
||||||
let has_double_quotes = val.contains(|x| x == '"');
|
for c in string.chars() {
|
||||||
match (has_single_quotes, has_double_quotes) {
|
match c {
|
||||||
(true, false) => format!("\"{}\"", val),
|
'\n' => {
|
||||||
(false, true) => format!("'{}'", val),
|
buf.push(' ');
|
||||||
(false, false) => format!("\"{}\"", val),
|
after_newline = true;
|
||||||
(true, true) => {
|
}
|
||||||
let mut buf = String::with_capacity(val.len() + 2);
|
' ' => {
|
||||||
buf.push('"');
|
if !after_newline {
|
||||||
for c in val.chars() {
|
buf.push(' ');
|
||||||
match c {
|
|
||||||
'"' => {
|
|
||||||
buf.push('\\');
|
|
||||||
buf.push('"');
|
|
||||||
}
|
|
||||||
v => buf.push(v),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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::True => "true".to_string(),
|
||||||
Self::False => "false".to_string(),
|
Self::False => "false".to_string(),
|
||||||
|
@ -142,3 +142,9 @@ test!(
|
|||||||
// "a {\n color: quote(\\b);\n}\n",
|
// "a {\n color: quote(\\b);\n}\n",
|
||||||
// "a {\n color: \"\\\\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",
|
||||||
"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!(
|
test!(
|
||||||
color_equals_color,
|
color_equals_color,
|
||||||
"a {\n color: red == red;\n}\n",
|
"a {\n color: red == red;\n}\n",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user