mod links;
mod text;
pub use links::{
link_substitution_effect, parse_link_substitution_query, render_link,
render_link_substitution_query, LinkPattern, LinkRewriteOutcome, LinkRewriteProgram,
LinkRewriteRule, LinkRewriteStep, Slot,
};
pub use text::{parse_substitution_query, render_substitution_query, substitution_effect};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubstitutionQueryError {
pub message: String,
}
impl std::fmt::Display for SubstitutionQueryError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "{}", self.message)
}
}
impl std::error::Error for SubstitutionQueryError {}
fn err(message: impl Into<String>) -> SubstitutionQueryError {
SubstitutionQueryError {
message: message.into(),
}
}
const TWO_SIDES: &str =
"a substitution query needs two sides: (matching pattern) (substitution pattern)";
const MUST_QUOTE: &str = "operands must be quoted, as in (\"text\")";
fn escape(text: &str) -> String {
let mut escaped = String::with_capacity(text.len());
for character in text.chars() {
match character {
'\\' => escaped.push_str("\\\\"),
'"' => escaped.push_str("\\\""),
'\n' => escaped.push_str("\\n"),
'\r' => escaped.push_str("\\r"),
'\t' => escaped.push_str("\\t"),
_ => escaped.push(character),
}
}
escaped
}
struct Parser<'a> {
input: &'a str,
cursor: usize,
}
impl<'a> Parser<'a> {
const fn new(input: &'a str) -> Self {
Self { input, cursor: 0 }
}
fn rest(&self) -> &'a str {
&self.input[self.cursor..]
}
fn peek(&self) -> Option<char> {
self.rest().chars().next()
}
fn bump(&mut self) -> Option<char> {
let character = self.peek()?;
self.cursor += character.len_utf8();
Some(character)
}
fn eat(&mut self, expected: char) -> bool {
let found = self.peek() == Some(expected);
if found {
self.cursor += expected.len_utf8();
}
found
}
fn skip_whitespace(&mut self) {
while self.peek().is_some_and(char::is_whitespace) {
self.bump();
}
}
const fn at_end(&self) -> bool {
self.cursor >= self.input.len()
}
fn parse_string(&mut self) -> Result<String, SubstitutionQueryError> {
if !self.eat('"') {
return Err(err(MUST_QUOTE));
}
let mut text = String::new();
loop {
match self.bump() {
None => {
return Err(err(
"unbalanced quotes: a quoted operand is missing its closing `\"`",
));
}
Some('"') => return Ok(text),
Some('\\') => text.push(self.parse_escape()?),
Some(character) => text.push(character),
}
}
}
fn parse_escape(&mut self) -> Result<char, SubstitutionQueryError> {
match self.bump() {
Some('\\') => Ok('\\'),
Some('"') => Ok('"'),
Some('n') => Ok('\n'),
Some('r') => Ok('\r'),
Some('t') => Ok('\t'),
Some(unknown) => Err(err(format!(
"unknown escape `\\{unknown}` in a quoted operand"
))),
None => Err(err(
"unbalanced quotes: a quoted operand is missing its closing `\"`",
)),
}
}
}