use crate::error::{ErrorKind, ParseError, Result};
use crate::types::{Ann, StringPart, Term, Token, Trivium, Whole};
use super::Parser;
impl Parser {
pub(super) fn parse_simple_string_literal(&mut self) -> Result<Ann<Vec<Vec<StringPart>>>> {
self.with_raw_ann(|p| {
let open_quote_pos = p.current.span;
if !matches!(p.current.value, Token::TDoubleQuote) {
return Err(ParseError::unexpected(
open_quote_pos,
vec!["'\"'".to_string()],
format!("'{}'", p.current.value.text()),
));
}
let _opening_quote = p.take_current();
let mut parts = Vec::new();
loop {
match p.lexer.peek() {
Some('"') => break,
None => {
return Err(ParseError::unclosed(
p.lexer.current_pos(),
'"',
open_quote_pos,
));
}
Some('$') if p.lexer.at("${") => {
let interp = p.parse_string_interpolation()?;
parts.push(interp);
}
_ => {
let text = p.parse_simple_string_part();
if !text.is_empty() {
parts.push(StringPart::TextPart(text.into_boxed_str()));
}
}
}
}
p.lexer.advance();
Ok(process_simple(parts))
})
}
pub(super) fn parse_simple_string(&mut self) -> Result<Term> {
let ann = self.parse_simple_string_literal()?;
Ok(Term::SimpleString(ann))
}
fn parse_simple_string_part(&mut self) -> String {
let mut text = String::new();
loop {
let run = self.lexer.scan_until3(b'"', b'\\', b'$');
if !run.is_empty() {
text.push_str(run);
}
match self.lexer.peek() {
Some('"') | None => break,
Some('$') if self.lexer.at("${") => break,
Some('\\') => {
self.lexer.advance();
match self.lexer.peek() {
Some('n') => {
text.push_str("\\n");
self.lexer.advance();
}
Some('r') => {
text.push_str("\\r");
self.lexer.advance();
}
Some('t') => {
text.push_str("\\t");
self.lexer.advance();
}
Some(ch) => {
text.push('\\');
text.push(ch);
self.lexer.advance();
}
None => break,
}
}
Some('$') if self.lexer.at("$$") => {
text.push_str("$$");
self.lexer.advance_by(2);
}
Some('$') => {
text.push('$');
self.lexer.advance();
}
Some(_) => unreachable!(),
}
}
text
}
pub(super) fn parse_string_interpolation(&mut self) -> Result<StringPart> {
self.lexer.advance_by(2);
self.current = self.lexer.lexeme()?;
if matches!(self.current.value, Token::TBraceClose) {
return Err(ParseError::invalid(
self.current.span,
"empty interpolation expression",
Some("string interpolations require an expression inside ${...}".to_string()),
));
}
let expr = match self.parse_expression() {
Ok(e) => e,
Err(err) => {
if let ErrorKind::UnclosedDelimiter {
delimiter: '"',
opening_span,
} = &err.kind
{
return Err(ParseError::invalid(
*opening_span,
"unclosed interpolation: expected '}', found '\"'",
Some("add '}' before the closing quote".to_string()),
));
}
return Err(err);
}
};
if !matches!(self.current.value, Token::TBraceClose) {
return Err(ParseError::invalid(
self.current.span,
format!(
"unclosed interpolation: expected '}}', found '{}'",
self.current.value.text()
),
Some("add '}' to close the interpolation".to_string()),
));
}
let trailing_trivia = std::mem::take(&mut self.current.pre_trivia);
self.lexer.rewind_trivia();
Ok(StringPart::Interpolation(Box::new(Whole {
value: expr,
trailing_trivia,
})))
}
pub(super) fn parse_indented_string(&mut self) -> Result<Term> {
let ann = self.with_raw_ann(|p| {
let open_quote_pos = p.current.span;
let _opening = p.take_current();
let mut lines = Vec::new();
lines.push(p.parse_indented_string_line()?);
while p.lexer.peek() == Some('\n') {
p.lexer.advance();
lines.push(p.parse_indented_string_line()?);
}
if !p.lexer.at("''") {
return Err(ParseError::unclosed(
p.lexer.current_pos(),
'\'',
open_quote_pos,
));
}
p.lexer.advance_by(2);
Ok(process_indented(lines))
})?;
Ok(classify_indented_string(ann))
}
fn parse_indented_string_line(&mut self) -> Result<Vec<StringPart>> {
let mut parts = Vec::new();
loop {
match self.lexer.peek() {
Some('\n') | None => break,
Some('\'')
if self.lexer.at("''")
&& !matches!(self.lexer.peek_ahead(2), Some('$' | '\'' | '\\')) =>
{
break;
}
Some('$') if self.lexer.at("${") => {
parts.push(self.parse_string_interpolation()?);
}
_ => {
let text = self.parse_indented_string_part();
if !text.is_empty() {
parts.push(StringPart::TextPart(text.into_boxed_str()));
}
}
}
}
Ok(parts)
}
fn parse_indented_string_part(&mut self) -> String {
let mut text = String::new();
loop {
text.push_str(self.lexer.scan_until3(b'\n', b'\'', b'$'));
match self.lexer.peek() {
None | Some('\n') => break,
_ if self.lexer.at("''$") => {
text.push_str("''$");
self.lexer.advance_by(3);
}
_ if self.lexer.at("'''") => {
text.push_str("'''");
self.lexer.advance_by(3);
}
_ if self.lexer.at("''\\") => {
text.push_str("''\\");
self.lexer.advance_by(3);
if let Some(ch) = self.lexer.peek().filter(|&c| c != '\n') {
text.push(ch);
self.lexer.advance();
}
}
Some('\'') if self.lexer.at("''") => break,
Some('\'') => {
text.push('\'');
self.lexer.advance();
}
Some('$') if self.lexer.at("${") => break,
Some('$') if self.lexer.at("$$") => {
text.push_str("$$");
self.lexer.advance_by(2);
}
Some('$') => {
text.push('$');
self.lexer.advance();
}
Some(_) => unreachable!(),
}
}
text
}
pub(super) fn parse_selector_interpolation(&mut self) -> Result<Ann<StringPart>> {
let mut open = self.take_current();
debug_assert!(matches!(open.value, Token::TInterOpen));
if let Some(tc) = open.trail_comment.take() {
self.lexer.trivia_buffer.insert(
0,
Trivium::LineComment(format!(" {}", tc.0).into_boxed_str()),
);
}
self.advance()?;
let expr = self.parse_expression()?;
let close = self.expect_token(Token::TBraceClose, "'}'")?;
Ok(Ann {
pre_trivia: open.pre_trivia,
span: open.span,
value: StringPart::Interpolation(Box::new(Whole {
value: expr,
trailing_trivia: close.pre_trivia,
})),
trail_comment: close.trail_comment,
})
}
}
fn process_simple(parts: Vec<StringPart>) -> Vec<Vec<StringPart>> {
split_on_newlines(parts)
.into_iter()
.map(merge_adjacent_text)
.collect()
}
fn classify_indented_string(ann: Ann<Vec<Vec<StringPart>>>) -> Term {
fn has_quote_or_backslash(part: &StringPart) -> bool {
match part {
StringPart::TextPart(t) => t.contains('"') || t.contains('\\'),
StringPart::Interpolation(_) => false,
}
}
fn convert_escapes(t: &str) -> String {
let mut out = String::with_capacity(t.len());
let bytes = t.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i..].starts_with(b"''$") {
out.push_str("\\$");
i += 3;
} else if bytes[i..].starts_with(b"'''") {
out.push_str("''");
i += 3;
} else {
let ch = t[i..].chars().next().unwrap();
out.push(ch);
i += ch.len_utf8();
}
}
out
}
let should_be_simple =
ann.value.len() <= 1 && !ann.value.iter().flatten().any(has_quote_or_backslash);
if !should_be_simple {
return Term::IndentedString(ann);
}
let value = ann
.value
.into_iter()
.map(|line| {
line.into_iter()
.map(|part| match part {
StringPart::TextPart(t) => {
StringPart::TextPart(convert_escapes(&t).into_boxed_str())
}
interp @ StringPart::Interpolation(_) => interp,
})
.collect()
})
.collect();
Term::SimpleString(Ann { value, ..ann })
}
fn process_indented(lines: Vec<Vec<StringPart>>) -> Vec<Vec<StringPart>> {
let lines = remove_empty_first_line(lines);
let lines = remove_empty_last_line(lines);
let lines = strip_common_indentation(lines);
lines
.into_iter()
.flat_map(split_on_newlines)
.map(merge_adjacent_text)
.collect()
}
fn split_on_newlines(parts: Vec<StringPart>) -> Vec<Vec<StringPart>> {
let mut result: Vec<Vec<StringPart>> = Vec::new();
let mut current: Vec<StringPart> = Vec::new();
for part in parts {
match part {
StringPart::TextPart(text) => {
let mut remaining: &str = &text;
loop {
if let Some(pos) = remaining.find('\n') {
let segment = &remaining[..pos];
if !segment.is_empty() {
current.push(StringPart::TextPart(segment.into()));
}
result.push(current);
current = Vec::new();
remaining = &remaining[pos + 1..];
} else {
if !remaining.is_empty() {
current.push(StringPart::TextPart(remaining.into()));
}
break;
}
}
}
other @ StringPart::Interpolation(_) => current.push(other),
}
}
result.push(current);
result
}
fn merge_adjacent_text(line: Vec<StringPart>) -> Vec<StringPart> {
let mut result: Vec<StringPart> = Vec::new();
for part in line {
match part {
StringPart::TextPart(text) => {
if text.is_empty() {
continue;
}
if let Some(StringPart::TextPart(existing)) = result.last_mut() {
let mut combined = String::from(std::mem::take(existing));
combined.push_str(&text);
*existing = combined.into_boxed_str();
} else {
result.push(StringPart::TextPart(text));
}
}
other @ StringPart::Interpolation(_) => result.push(other),
}
}
result
}
fn is_only_spaces(text: &str) -> bool {
text.bytes().all(|b| b == b' ')
}
fn is_empty_line(line: &[StringPart]) -> bool {
line.is_empty() || matches!(line, [StringPart::TextPart(text)] if is_only_spaces(text))
}
fn remove_empty_first_line(mut lines: Vec<Vec<StringPart>>) -> Vec<Vec<StringPart>> {
if let Some(first_line) = lines.first_mut() {
let first = merge_adjacent_text(std::mem::take(first_line));
if is_empty_line(&first) && lines.len() > 1 {
lines.remove(0);
} else {
lines[0] = first;
}
}
lines
}
fn remove_empty_last_line(mut lines: Vec<Vec<StringPart>>) -> Vec<Vec<StringPart>> {
match lines.len() {
0 => lines,
1 => {
let last = merge_adjacent_text(lines.pop().unwrap());
if is_empty_line(&last) {
vec![Vec::new()]
} else {
vec![last]
}
}
_ => {
let last_index = lines.len() - 1;
let last = merge_adjacent_text(std::mem::take(&mut lines[last_index]));
lines[last_index] = if is_empty_line(&last) {
Vec::new()
} else {
last
};
lines
}
}
}
fn line_prefix(line: &[StringPart]) -> Option<String> {
match line.first() {
None => None,
Some(StringPart::TextPart(text)) => {
if line.len() == 1 && is_only_spaces(text) {
None
} else {
Some(text.to_string())
}
}
Some(StringPart::Interpolation(_)) => Some(String::new()),
}
}
fn find_common_space_prefix(prefixes: &[String]) -> Option<String> {
prefixes
.iter()
.map(|p| p.bytes().take_while(|&b| b == b' ').count())
.min()
.map(|n| " ".repeat(n))
}
fn strip_prefix_from_line(prefix: &str, mut line: Vec<StringPart>) -> Vec<StringPart> {
if prefix.is_empty() {
return line;
}
let single = line.len() == 1;
if let Some(StringPart::TextPart(text)) = line.first_mut() {
if let Some(stripped) = text.strip_prefix(prefix) {
*text = stripped.into();
} else if single && is_only_spaces(text) {
return Vec::new();
}
}
line
}
fn strip_common_indentation(lines: Vec<Vec<StringPart>>) -> Vec<Vec<StringPart>> {
let prefixes: Vec<String> = lines.iter().filter_map(|line| line_prefix(line)).collect();
match find_common_space_prefix(&prefixes) {
None => lines.into_iter().map(|_| Vec::new()).collect(),
Some(prefix) => lines
.into_iter()
.map(|line| strip_prefix_from_line(&prefix, line))
.collect(),
}
}