use std::fmt::Write as _;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Expectation {
Literal(&'static str),
Named(&'static str),
EndOfInput,
}
impl Expectation {
fn describe(self) -> String {
match self {
Expectation::Literal(text) => format!("\"{}\"", escape(text)),
Expectation::Named(name) => name.to_string(),
Expectation::EndOfInput => "end of input".to_string(),
}
}
}
fn escape(text: &str) -> String {
let mut out = String::with_capacity(text.len());
for ch in text.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\0' => out.push_str("\\0"),
'\t' => out.push_str("\\t"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\u{1}'..='\u{f}' => {
let _ = write!(out, "\\x0{:X}", ch as u32);
}
'\u{10}'..='\u{1f}' | '\u{7f}'..='\u{9f}' => {
let _ = write!(out, "\\x{:X}", ch as u32);
}
_ => out.push(ch),
}
}
out
}
pub(crate) fn build_message(variants: &[Expectation], found: Option<char>) -> String {
let mut descriptions: Vec<String> = variants.iter().map(|e| e.describe()).collect();
descriptions.sort();
descriptions.dedup();
let expected = match descriptions.as_slice() {
[] => Expectation::EndOfInput.describe(),
[only] => only.clone(),
[first, second] => format!("{first} or {second}"),
[rest @ .., last] => format!("{}, or {last}", rest.join(", ")),
};
let found = match found {
Some(ch) => format!("\"{}\"", escape(&ch.to_string())),
None => "end of input".to_string(),
};
format!("Expected {expected} but {found} found.")
}
#[derive(Clone, Debug, thiserror::Error)]
#[error("{message}")]
pub struct TagError {
message: String,
start: usize,
end: usize,
}
impl TagError {
pub(crate) fn new(message: String, start: usize, end: usize) -> Self {
Self {
message,
start,
end,
}
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
#[must_use]
pub fn start(&self) -> usize {
self.start
}
#[must_use]
pub fn end(&self) -> usize {
self.end
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_expectation_reads_as_a_bare_description() {
let message = build_message(&[Expectation::EndOfInput], Some('['));
assert_eq!(message, "Expected end of input but \"[\" found.");
}
#[test]
fn two_expectations_are_joined_with_or_and_no_comma() {
let message = build_message(
&[Expectation::Literal("]"), Expectation::Named("whitespace")],
None,
);
assert_eq!(
message,
"Expected \"]\" or whitespace but end of input found."
);
}
#[test]
fn many_expectations_are_sorted_and_de_duplicated() {
let message = build_message(
&[
Expectation::Named("whitespace"),
Expectation::Literal("]"),
Expectation::Named("whitespace"),
Expectation::Literal(","),
],
Some('2'),
);
assert_eq!(
message,
"Expected \",\", \"]\", or whitespace but \"2\" found."
);
}
#[test]
fn control_characters_are_escaped_the_way_peggy_escapes_them() {
assert_eq!(escape("\u{1}"), "\\x01");
assert_eq!(escape("\u{1f}"), "\\x1F");
assert_eq!(escape("\u{7f}"), "\\x7F");
assert_eq!(escape("\0\t\n\r\\\""), "\\0\\t\\n\\r\\\\\\\"");
}
}