sofie-engine 0.2.0

Screens: the template parser and renderer, and the component catalog.
Documentation
use std::fmt;

use super::cursor::Cursor;
use super::is_tag_name_byte;
use crate::syntax::{LocatedError, Position};

#[derive(Debug, PartialEq, Eq)]
pub(super) enum Closer {
    Tag(String),
    Else,
    EndIf,
    EndEach,
}

impl fmt::Display for Closer {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Tag(name) => write!(formatter, "`</{name}>`"),
            Self::Else => formatter.write_str("`{:else}`"),
            Self::EndIf => formatter.write_str("`{/if}`"),
            Self::EndEach => formatter.write_str("`{/each}`"),
        }
    }
}

pub(super) struct Opener {
    pub(super) description: String,
    pub(super) position: Position,
}

impl Opener {
    pub(super) fn new(description: impl Into<String>, position: Position) -> Self {
        Self {
            description: description.into(),
            position,
        }
    }
}

pub(super) fn is_at_closer(cursor: &Cursor<'_>) -> bool {
    ["</", "{:", "{/"]
        .iter()
        .any(|start| cursor.starts_with(start))
}

pub(super) fn close(
    cursor: &mut Cursor<'_>,
    opener: &Opener,
    accepted: &[Closer],
) -> Result<Closer, LocatedError> {
    let Some((closer, position)) = read_closer(cursor)? else {
        bail!(opener.position, "{} is never closed", opener.description);
    };
    if accepted.contains(&closer) {
        return Ok(closer);
    }
    bail!(
        position,
        "{closer} does not close {} opened at {}",
        opener.description,
        opener.position
    );
}

pub(super) fn read_closer(
    cursor: &mut Cursor<'_>,
) -> Result<Option<(Closer, Position)>, LocatedError> {
    let position = cursor.position();
    let closer = if cursor.eat("</") {
        let name = cursor.take_while(is_tag_name_byte);
        cursor.skip_whitespace();
        cursor.expect(">", &format!("to end `</{name}`"))?;
        Closer::Tag(name.to_owned())
    } else if cursor.eat("{:") {
        match read_clause_word(cursor)? {
            "else" => Closer::Else,
            word => return Err(unknown_clause(position, ":", word)),
        }
    } else if cursor.eat("{/") {
        match read_clause_word(cursor)? {
            "if" => Closer::EndIf,
            "each" => Closer::EndEach,
            word => return Err(unknown_clause(position, "/", word)),
        }
    } else {
        return Ok(None);
    };
    Ok(Some((closer, position)))
}

fn read_clause_word<'s>(cursor: &mut Cursor<'s>) -> Result<&'s str, LocatedError> {
    let word = cursor.take_identifier();
    cursor.skip_whitespace();
    cursor.expect("}", "to end the block tag")?;
    Ok(word)
}

fn unknown_clause(position: Position, sigil: &str, word: &str) -> LocatedError {
    LocatedError::new(
        position,
        format!(
            "unknown block tag `{{{sigil}{word}}}`; expected `{{:else}}`, `{{/if}}` or `{{/each}}`"
        ),
    )
}