sofie-engine 0.4.0

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

use super::component::each_expression;
use super::cursor::Cursor;
use super::expression::{parse_expression, parse_string};
use crate::language::{Argument, Call};
use crate::message::{is_argument_name, is_key};
use crate::syntax::{Expression, LocatedError, Op, Position, Written};
#[cfg(feature = "i18n")]
use crate::translations::Format;

pub(super) fn parse_call(
    cursor: &mut Cursor<'_>,
    scope: &[String],
) -> Result<Option<Expression>, LocatedError> {
    let mut ahead = cursor.clone();
    let start = ahead.position();
    let name = ahead.take_identifier();
    if ahead.peek() != Some(b'(') {
        return Ok(None);
    }
    *cursor = ahead;
    cursor.advance();
    cursor.skip_whitespace();
    let call = match name {
        "t" => parse_translation(cursor, scope, start)?,
        "date" => Call::date(parse_formatted(cursor, scope)?, start),
        #[cfg(feature = "i18n")]
        "number" => Call::Format {
            format: Format::Number,
            value: parse_formatted(cursor, scope)?,
            position: start,
        },
        #[cfg(feature = "i18n")]
        "money" => Call::Format {
            format: Format::Money,
            value: parse_formatted(cursor, scope)?,
            position: start,
        },
        #[cfg(not(feature = "i18n"))]
        "number" | "money" => {
            bail!(
                start,
                "`{name}(…)` formats for the viewer's language, which needs the `i18n` feature \
                 (`sofie/i18n`)"
            );
        }
        _ => {
            bail!(
                start,
                "unknown function `{name}`; functions are t, number, money and date"
            );
        }
    };
    cursor.skip_whitespace();
    cursor.expect(")", &format!("to end `{name}(…)`"))?;
    Ok(Some(Expression::Call(Box::new(call))))
}

fn parse_translation(
    cursor: &mut Cursor<'_>,
    scope: &[String],
    start: Position,
) -> Result<Call, LocatedError> {
    let key_position = cursor.position();
    if cursor.peek() != Some(b'"') {
        return Err(cursor.error("`t` takes its key first, quoted, such as `t(\"orders.title\")`"));
    }
    let key = parse_string(cursor)?;
    if !is_key(&key) {
        bail!(
            key_position,
            "`{key}` is not a translation key: lowercase names joined by `.`, such as `orders.title`"
        );
    }
    let mut arguments: Vec<Argument> = Vec::new();
    cursor.skip_whitespace();
    while cursor.eat(",") {
        cursor.skip_whitespace();
        let position = cursor.position();
        let name = cursor.take_identifier();
        if !is_argument_name(name) || arguments.iter().any(|argument| argument.name == name) {
            bail!(
                position,
                "expected a new lowercase argument name such as `count`, found `{name}`"
            );
        }
        cursor.skip_whitespace();
        cursor.expect("=", "after the argument name")?;
        let value = parse_expression(cursor, scope)?;
        arguments.push(Argument {
            name: name.to_owned(),
            value,
        });
        cursor.skip_whitespace();
    }
    Ok(Call::translate(key, arguments, start, None))
}

fn parse_formatted(cursor: &mut Cursor<'_>, scope: &[String]) -> Result<Expression, LocatedError> {
    let value_position = cursor.position();
    let value = parse_expression(cursor, scope)?;
    if matches!(value, Expression::Call(_)) {
        bail!(
            value_position,
            "a format takes a value such as `data.total`, not another call"
        );
    }
    Ok(value)
}

pub(crate) fn each_call(ops: &mut [Op<Written>], visit: &mut impl FnMut(&mut Call)) {
    let Ok(()) = each_expression::<Infallible>(ops, &mut |expression| {
        visit_calls(expression, visit);
        Ok(())
    });
}

fn visit_calls(expression: &mut Expression, visit: &mut impl FnMut(&mut Call)) {
    match expression {
        Expression::Interpolated(parts) => {
            for part in parts {
                visit_calls(part, visit);
            }
        }
        Expression::Call(call) => {
            for argument in call.arguments_mut() {
                visit_calls(argument, visit);
            }
            visit(call);
        }
        _ => {}
    }
}