use crate::parser::errors::CustomError;
use crate::parser::trivia::{is_non_ascii, is_wschar, newline, ws, ws_newlines};
use crate::repr::InternalString;
use combine::error::{Commit, Info};
use combine::parser::char::char;
use combine::parser::range::{range, take, take_while};
use combine::stream::RangeStream;
use combine::*;
use std::char;
parse!(string() -> InternalString, {
choice((
ml_basic_string(),
basic_string(),
ml_literal_string(),
literal_string().map(|s: &'a str| s.into()),
))
});
parse!(basic_string() -> InternalString, {
between(char(QUOTATION_MARK), char(QUOTATION_MARK),
many(basic_char()))
.message("While parsing a Basic String")
});
const QUOTATION_MARK: char = '"';
parse!(basic_char() -> char, {
satisfy(|c| is_basic_unescaped(c) || c == ESCAPE)
.then(|c| parser(move |input| {
match c {
ESCAPE => escape().parse_stream(input).into_result(),
_ => Ok((c, Commit::Peek(()))),
}
}))
});
#[inline]
fn is_basic_unescaped(c: char) -> bool {
is_wschar(c)
| matches!(c, '\u{21}' | '\u{23}'..='\u{5B}' | '\u{5D}'..='\u{7E}')
| is_non_ascii(c)
}
const ESCAPE: char = '\\';
parse!(escape() -> char, {
satisfy(is_escape_seq_char)
.message("While parsing escape sequence")
.then(|c| {
parser(move |input| {
match c {
'b' => Ok(('\u{8}', Commit::Peek(()))),
'f' => Ok(('\u{c}', Commit::Peek(()))),
'n' => Ok(('\n', Commit::Peek(()))),
'r' => Ok(('\r', Commit::Peek(()))),
't' => Ok(('\t', Commit::Peek(()))),
'u' => hexescape(4).parse_stream(input).into_result(),
'U' => hexescape(8).parse_stream(input).into_result(),
_ => Ok((c, Commit::Peek(()))),
}
})
})
});
#[inline]
fn is_escape_seq_char(c: char) -> bool {
matches!(c, '"' | '\\' | 'b' | 'f' | 'n' | 'r' | 't' | 'u' | 'U')
}
parse!(hexescape(n: usize) -> char, {
take(*n)
.and_then(|s| u32::from_str_radix(s, 16))
.and_then(|h| char::from_u32(h).ok_or(CustomError::InvalidHexEscape(h)))
});
parse!(ml_basic_string() -> InternalString, {
between(range(ML_BASIC_STRING_DELIM),
range(ML_BASIC_STRING_DELIM),
ml_basic_body())
.message("While parsing a Multiline Basic String")
});
const ML_BASIC_STRING_DELIM: &str = "\"\"\"";
parse!(ml_basic_body() -> InternalString, {
optional(newline())
.skip(try_eat_escaped_newline())
.with(
many(
not_followed_by(range(ML_BASIC_STRING_DELIM).map(Info::Range))
.with(
choice((
newline(),
mlb_char(),
))
)
.skip(try_eat_escaped_newline())
)
)
});
parse!(mlb_char() -> char, {
satisfy(|c| is_mlb_unescaped(c) || c == ESCAPE)
.then(|c| parser(move |input| {
match c {
ESCAPE => escape().parse_stream(input).into_result(),
_ => Ok((c, Commit::Peek(()))),
}
}))
});
#[inline]
fn is_mlb_unescaped(c: char) -> bool {
is_wschar(c)
| matches!(c, '\u{21}' | '\u{23}'..='\u{5B}' | '\u{5D}'..='\u{7E}')
| is_non_ascii(c)
| (c == '\u{22}')
}
parse!(try_eat_escaped_newline() -> (), {
skip_many(attempt((
char(ESCAPE),
ws(),
ws_newlines(),
)))
});
parse!(literal_string() -> &'a str, {
between(char(APOSTROPHE), char(APOSTROPHE),
take_while(is_literal_char))
.message("While parsing a Literal String")
});
const APOSTROPHE: char = '\'';
#[inline]
fn is_literal_char(c: char) -> bool {
matches!(c, '\u{09}' | '\u{20}'..='\u{26}' | '\u{28}'..='\u{7E}') | is_non_ascii(c)
}
parse!(ml_literal_string() -> InternalString, {
between(range(ML_LITERAL_STRING_DELIM),
range(ML_LITERAL_STRING_DELIM),
ml_literal_body())
.message("While parsing a Multiline Literal String")
});
const ML_LITERAL_STRING_DELIM: &str = "'''";
parse!(ml_literal_body() -> InternalString, {
optional(newline())
.with(
many(
not_followed_by(range(ML_LITERAL_STRING_DELIM).map(Info::Range))
.with(
choice((
newline(),
satisfy(is_mll_char),
))
)
)
)
});
#[inline]
fn is_mll_char(c: char) -> bool {
matches!(c, '\u{09}' | '\u{20}'..='\u{26}' | '\u{28}'..='\u{7E}') | is_non_ascii(c)
| (c == '\u{27}')
}