rustev-core 0.2.0

Rustev's pure core: validation, value kinds, exact operators, compiler, calibration binding and selection policy.
Documentation
use super::*;
pub(super) fn name(s: &str) -> bool {
    (1..=64).contains(&s.len())
        && s.bytes().next().is_some_and(|b| b.is_ascii_lowercase())
        && s.bytes()
            .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
}
fn path(s: &str) -> Result<String, Error> {
    if s.split('.').all(name) {
        Ok(s.into())
    } else {
        Err(Error::Syntax)
    }
}
pub(super) fn parse(source: &str) -> Result<Vec<Node>, Error> {
    let mut remaining = source;
    body(&mut remaining, None, 0)
}
fn body(rest: &mut &str, close: Option<&str>, depth: usize) -> Result<Vec<Node>, Error> {
    let mut nodes = Vec::new();
    while !rest.is_empty() {
        let Some((literal, tail)) = rest.split_once("{{") else {
            nodes.push(Node::Text((*rest).into()));
            *rest = "";
            break;
        };
        if !literal.is_empty() {
            nodes.push(Node::Text(literal.into()));
        }
        let raw = tail.starts_with('{');
        let tail = if raw {
            tail.strip_prefix('{').ok_or(Error::Syntax)?
        } else {
            tail
        };
        let (tag, next) = tail
            .split_once(if raw { "}}}" } else { "}}" })
            .ok_or(Error::Syntax)?;
        *rest = next;
        if !raw && tag.starts_with('!') {
            continue;
        }
        if !raw && let Some(p) = tag.strip_prefix('/') {
            let p = path(p.trim_matches(' '))?;
            return if close == Some(p.as_str()) {
                Ok(nodes)
            } else {
                Err(Error::Syntax)
            };
        }
        if !raw && (tag.starts_with('#') || tag.starts_with('^')) {
            if depth == 4 {
                return Err(Error::Nesting);
            }
            let inverted = tag.starts_with('^');
            let p = path(tag.get(1..).ok_or(Error::Syntax)?.trim_matches(' '))?;
            let children = body(rest, Some(&p), depth + 1)?;
            nodes.push(Node::Section(p, inverted, children));
            continue;
        }
        let tag = tag.trim_matches(' ');
        if raw {
            nodes.push(Node::Emit(path(tag)?, Format::Raw));
            continue;
        }
        let words: Vec<_> = tag.split(' ').filter(|s| !s.is_empty()).collect();
        let first = *words.first().ok_or(Error::Syntax)?;
        if words.len() == 1 {
            nodes.push(Node::Emit(path(first)?, Format::Plain));
            continue;
        }
        let p = path(words.get(1).ok_or(Error::Syntax)?)?;
        let mut args = BTreeMap::new();
        for word in words.iter().skip(2) {
            let (k, v) = word.split_once('=').ok_or(Error::Syntax)?;
            if args.insert(k, v).is_some() {
                return Err(Error::Syntax);
            }
        }
        let scale = |args: &BTreeMap<&str, &str>| -> Result<u32, Error> {
            let s = args.get("scale").ok_or(Error::Syntax)?;
            if s.len() != 1 {
                return Err(Error::Syntax);
            }
            s.parse::<u32>()
                .ok()
                .filter(|n| *n <= 9)
                .ok_or(Error::Syntax)
        };
        let format = match first {
            "number" if args.len() == 1 => Format::Number(scale(&args)?),
            "money" if args.len() == 2 => {
                let currency = *args.get("currency").ok_or(Error::Syntax)?;
                let currency = if code(currency) {
                    Currency::Literal(currency.into())
                } else {
                    Currency::Path(path(currency)?)
                };
                Format::Money(scale(&args)?, currency)
            }
            "date" if args.len() == 1 => Format::Date(match args.get("style") {
                Some(&"iso") => true,
                Some(&"numeric") => false,
                _ => return Err(Error::Syntax),
            }),
            "list_join" if args.is_empty() && words.len() == 2 => Format::Join,
            _ => return Err(Error::Syntax),
        };
        nodes.push(Node::Emit(p, format));
    }
    if close.is_some() {
        Err(Error::Syntax)
    } else {
        Ok(nodes)
    }
}
pub(super) fn code(s: &str) -> bool {
    s.len() == 3 && s.bytes().all(|b| b.is_ascii_uppercase())
}