ryan 0.2.3

Ryan: a configuration language for the practical programmer
Documentation
use std::{fmt::Display, rc::Rc};

use indexmap::IndexMap;
use pest::iterators::Pairs;

use crate::rc_world;

use super::{value::TemplatedValue, ErrorLogger, Expression, Rule, State, Value};

#[derive(Debug, Clone, PartialEq)]
pub struct TemplateString {
    chunks: Vec<TemplateStringChunk>,
}

#[derive(Debug, Clone, PartialEq)]
pub enum TemplateStringChunk {
    Text(Rc<str>),
    Interpolation(Expression),
}

impl Display for TemplateString {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "`")?;

        for chunk in &self.chunks {
            match chunk {
                TemplateStringChunk::Interpolation(expr) => write!(f, "${{{expr}}}")?,
                TemplateStringChunk::Text(text) => {
                    for char in text.chars() {
                        match char {
                            '`' => write!(f, "\\`")?,
                            '$' => write!(f, "\\$")?,
                            ch => write!(f, "{ch}")?,
                        }
                    }
                }
            }
        }

        write!(f, "`")?;

        Ok(())
    }
}

impl TemplateString {
    pub(super) fn parse(logger: &mut ErrorLogger, pairs: Pairs<'_, Rule>) -> Self {
        let mut chunks = vec![];
        let mut chunk_builder = String::new();

        for pair in pairs {
            match pair.as_rule() {
                Rule::templateEscaped => {
                    if let Some(escaped) = pair.clone().into_inner().next() {
                        match escaped.as_rule() {
                            Rule::templateControlCode => match escaped.as_str() {
                                "`" => chunk_builder.push('`'),
                                "$" => chunk_builder.push('$'),
                                _ => unreachable!(),
                            },
                            Rule::interpolation => {
                                let chunk = rc_world::string_to_rc(chunk_builder);
                                chunk_builder = String::new();
                                chunks.push(TemplateStringChunk::Text(chunk));

                                let expression = Expression::parse(
                                    logger,
                                    pair.into_inner()
                                        .next()
                                        .expect("an interpolation always has an expression")
                                        .into_inner(),
                                );
                                chunks.push(TemplateStringChunk::Interpolation(expression));
                            }
                            _ => unreachable!(),
                        }
                    } else {
                        chunk_builder += pair.as_str();
                    }
                }
                r => panic!("{r:?}"),
            }
        }

        if !chunk_builder.is_empty() {
            let chunk = rc_world::string_to_rc(chunk_builder);
            chunks.push(TemplateStringChunk::Text(chunk));
        }

        TemplateString { chunks }
    }

    #[must_use]
    pub(super) fn capture(
        &self,
        state: &mut State<'_>,
        provided: &mut [Rc<str>],
        values: &mut IndexMap<Rc<str>, Value>,
    ) -> Option<()> {
        for chunk in &self.chunks {
            if let TemplateStringChunk::Interpolation(expr) = chunk {
                expr.capture(state, provided, values)?;
            }
        }

        Some(())
    }

    pub(super) fn eval(&self, state: &mut State<'_>) -> Option<Value> {
        let mut builder = String::new();
        for chunk in &self.chunks {
            match chunk {
                TemplateStringChunk::Text(text) => builder += text,
                TemplateStringChunk::Interpolation(expr) => {
                    let outcome = expr.eval(state)?;
                    builder += &TemplatedValue(outcome).to_string();
                }
            }
        }

        Some(Value::Text(rc_world::string_to_rc(builder)))
    }
}