lunar-lib 0.9.0

Common utilities for lunar applications
Documentation
use std::fmt::{self, Write};

use crate::formatter::{
    ErrorKind, FormatTable, Render, TemplateError,
    block::{Block, BlockBuilder, BlockMode},
    condition::ConditionalToken,
    lexer::{Lex, lex_str},
    tag::Tag,
};

/// Ready-to-render arguments for [`crate::formatter::format()`]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Template<'a> {
    args: Vec<TemplateItem<'a>>,
}

impl<'a> Template<'a> {
    pub(super) fn from_lex(
        lexes: impl IntoIterator<Item = Lex<'a>>,
    ) -> Result<Template<'a>, TemplateError> {
        let mut args = Vec::new();
        let mut escaped = false;

        let mut stack: Vec<ParserEntry<'_>> = Vec::new();

        for lex in lexes {
            if escaped {
                push_arg(&mut stack, &mut args, TemplateItem::Text(lex.to_str()));
                escaped = false;
            } else {
                match lex {
                Lex::BlockStart => stack.push(ParserEntry::Block(BlockBuilder::default())),
                Lex::BlockEnd => end_block(&mut stack, &mut args)?,
                Lex::Variable => {
                    end_tag(&mut stack, &mut args);
                    stack.push(ParserEntry::Tag(Tag::default()));
                }
                Lex::Conditional | Lex::Prefix | Lex::Suffix | Lex::Fallback => block_mode_switch(
                    &mut stack,
                    &mut args,
                    lex.to_block_mode()
                        .expect("The lex was already matched on block modes. This should not fail"),
                )?,
                Lex::Or | Lex::And | Lex::Not => push_conditional_token(
                    &mut stack,
                    &mut args,
                    lex.to_condition_token().expect(
                        "The lex was already matched on conditional tokens. This should not fail",
                    ),
                ),
                Lex::Space => {
                    end_tag(&mut stack, &mut args);
                    push_arg(&mut stack, &mut args, TemplateItem::Text(lex.to_str()));
                }
                Lex::Escape => escaped = true,
                Lex::Text(str) => push_arg(&mut stack, &mut args, TemplateItem::Text(str)),
            }
            }
        }

        end_tag(&mut stack, &mut args);

        if !stack.is_empty() {
            return Err(TemplateError::new(ErrorKind::BadClosure(
                "Completed parsing all lexes, but the stack was not empty. This means a tag or block was left unclosed",
            )));
        }

        Ok(Template { args })
    }
}

impl<'a> TryFrom<&'a str> for Template<'a> {
    type Error = TemplateError;

    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
        let lexes = lex_str(value);
        Template::from_lex(lexes)
    }
}

impl Render for Template<'_> {
    fn render(&self, format_table: &FormatTable) -> String {
        self.args.render(format_table)
    }
}

impl fmt::Display for Template<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for arg in &self.args {
            arg.fmt(f)?;
        }
        Ok(())
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum TemplateItem<'a> {
    Text(&'a str),
    Tag(Tag<'a>),
    Block(Block<'a>),
}

impl Render for TemplateItem<'_> {
    fn render(&self, format_table: &super::FormatTable) -> String {
        match self {
            TemplateItem::Text(str) => str.to_string(),
            TemplateItem::Tag(tag) => tag.render(format_table),
            TemplateItem::Block(block) => block.render(format_table),
        }
    }
}

impl Render for [TemplateItem<'_>] {
    fn render(&self, format_table: &FormatTable) -> String {
        self.iter().map(|arg| arg.render(format_table)).collect()
    }
}

enum ParserEntry<'a> {
    Tag(Tag<'a>),
    Block(BlockBuilder<'a>),
}

#[inline(always)]
fn push_arg<'a>(
    stack: &mut Vec<ParserEntry<'a>>,
    args: &mut Vec<TemplateItem<'a>>,
    arg: TemplateItem<'a>,
) {
    match stack.last_mut() {
        Some(ParserEntry::Block(block_builder)) => {
            block_builder.push_arg(arg);
        }
        Some(ParserEntry::Tag(tag)) => tag.args.push(arg),
        None => args.push(arg),
    }
}

fn push_conditional_token<'a>(
    stack: &mut Vec<ParserEntry<'a>>,
    args: &mut Vec<TemplateItem<'a>>,
    token: ConditionalToken<'a>,
) {
    if let Some(ParserEntry::Block(block)) = stack.last_mut() {
        block.push_conditional_token(token);
    } else {
        push_arg(stack, args, TemplateItem::Text(token.to_str()));
    }
}

fn end_tag<'a>(stack: &mut Vec<ParserEntry<'a>>, args: &mut Vec<TemplateItem<'a>>) {
    if let Some(ParserEntry::Tag(_)) = stack.last() {
        let tag = match stack.pop().unwrap() {
            ParserEntry::Tag(tag) => tag,
            _ => unreachable!(),
        };

        push_arg(stack, args, TemplateItem::Tag(tag));
    }
}

fn end_tag_if_in_block<'a>(stack: &mut Vec<ParserEntry<'a>>, args: &mut Vec<TemplateItem<'a>>) {
    if let Some(ParserEntry::Tag(_)) = stack.last()
        && let Some(ParserEntry::Block(_)) = stack.get(stack.len() - 2)
    {
        let tag = match stack.pop().unwrap() {
            ParserEntry::Tag(tag) => tag,
            _ => unreachable!(),
        };
        push_arg(stack, args, TemplateItem::Tag(tag));
    }
}

fn end_block<'a>(
    stack: &mut Vec<ParserEntry<'a>>,
    args: &mut Vec<TemplateItem<'a>>,
) -> Result<(), TemplateError> {
    end_tag(stack, args);

    let block = match stack.pop() {
        Some(ParserEntry::Block(block)) => block,
        Some(_) => {
            panic!("'}}' was found unescaped but it didn't close a block");
        }
        None => {
            return Err(TemplateError::new(ErrorKind::BadClosure(
                "'}' was found unescaped with nothing to close",
            )));
        }
    };

    push_arg(stack, args, TemplateItem::Block(block.build()));

    Ok(())
}

fn block_mode_switch<'a>(
    stack: &mut Vec<ParserEntry<'a>>,
    args: &mut Vec<TemplateItem<'a>>,
    mode: BlockMode,
) -> Result<(), TemplateError> {
    end_tag_if_in_block(stack, args);

    if let Some(ParserEntry::Block(block)) = stack.last_mut() {
        block.set_mode(mode)
    } else {
        args.push(TemplateItem::Text(mode.to_str()));
        Ok(())
    }
}

pub(super) fn write_escaped(f: &mut fmt::Formatter<'_>, s: &str, tag_context: bool) -> fmt::Result {
    for byte in s.bytes() {
        if byte == b' ' && !tag_context {
            f.write_char(' ')?;
            continue;
        }

        if matches!(
            byte,
            b'{' | b'}' | b'$' | b'@' | b'<' | b'>' | b'?' | b'|' | b'&' | b'!' | b'\\' | b' '
        ) {
            f.write_char('\\')?;
        }

        f.write_char(byte as char)?;
    }
    Ok(())
}

impl fmt::Display for TemplateItem<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            TemplateItem::Text(s) => write_escaped(f, s, false),
            TemplateItem::Tag(tag) => tag.fmt(f),
            TemplateItem::Block(block) => block.fmt(f),
        }
    }
}

#[derive(Debug)]
pub struct TemplateOwned {
    args: Template<'static>,
    string: Box<str>,
}

impl TemplateOwned {
    pub fn new(s: impl Into<String>) -> Result<Self, TemplateError> {
        let string: Box<str> = s.into().into_boxed_str();
        let ptr: *const str = &raw const *string;

        // SAFETY:
        // - 'string' is heap-allocated, moving 'ArgumentsOwned' never moves the string
        // - No mutable access is ever given to the 'string' field
        // - 'args' static lifetime is never exposed
        let args = unsafe {
            let str_ref: &'static str = &*ptr;
            let lexes = lex_str(str_ref);
            Template::from_lex(lexes)?
        };

        Ok(Self { args, string })
    }

    #[must_use]
    pub fn as_arguments(&self) -> &Template<'_> {
        &self.args
    }

    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.string
    }
}

impl Clone for TemplateOwned {
    fn clone(&self) -> Self {
        Self::new(self.string.clone()).expect("Previously valid string cannot suddenly fail")
    }
}

impl PartialEq for TemplateOwned {
    fn eq(&self, other: &Self) -> bool {
        self.args == other.args
    }
}

impl Eq for TemplateOwned {}

impl From<Template<'_>> for TemplateOwned {
    fn from(value: Template<'_>) -> Self {
        TemplateOwned::new(value.to_string()).expect("Arguments to_string are always valid")
    }
}

#[cfg(feature = "serde")]
impl serde::Serialize for TemplateOwned {
    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
        s.serialize_str(&self.string)
    }
}

#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for TemplateOwned {
    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
        let s = String::deserialize(d)?;
        TemplateOwned::new(s).map_err(serde::de::Error::custom)
    }
}

#[cfg(feature = "serde")]
impl serde::Serialize for Template<'_> {
    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
        s.serialize_str(&self.to_string())
    }
}