pipa-lang 1.0.0-alpha.1

A tiny template language
Documentation
// SPDX-FileCopyrightText: Copyright 2026 olav@occy.org
// SPDX-License-Identifier: MPL-2.0

use crate::value::Value;
use crate::value::bytes::Bytes;
use crate::value::list::List;
use crate::value::map::Map;
use crate::value::number::Number;
use bstr::ByteSlice;
use bstr::ByteVec;
use either::Either;
use itertools::Itertools;

pub(crate) trait Print {
    fn print_syntax(self) -> Vec<u8>;
    fn print_input(self, printer: Printer) -> Vec<u8>;
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Printer {
    Bytes,
    List,
    Map,
    Root,
}

impl Printer {
    pub(crate) fn narrow(self, other: Self) -> Self {
        match self {
            _ if other == Printer::Map => other,
            _ if self == Printer::Map => self,
            _ if other == Printer::List => other,
            _ if self == Printer::List => self,
            _ if other == Printer::Bytes => other,
            _ if self == Printer::Bytes => self,
            p => p,
        }
    }

    fn escape(self) -> &'static [u8] {
        match self {
            Printer::Bytes => b"",
            Printer::List => br#""', "#,
            Printer::Map => br#""',: "#,
            Printer::Root => b"",
        }
    }
}

impl Print for Value {
    fn print_syntax(self) -> Vec<u8> {
        match self {
            Value::Bytes(v) => v.print_syntax(),
            Value::List(v) => v.print_syntax(),
            Value::Map(v) => v.print_syntax(),
        }
    }

    fn print_input(self, printer: Printer) -> Vec<u8> {
        match self {
            Value::Bytes(v) => v.print_input(printer),
            Value::List(v) => v.print_input(printer),
            Value::Map(v) => v.print_input(printer),
        }
    }
}

impl Print for Bytes {
    fn print_syntax(self) -> Vec<u8> {
        number_or_quote_bytes(self.data)
    }

    fn print_input(self, printer: Printer) -> Vec<u8> {
        match find_unescaped_bytes(&self.data, printer.escape()) {
            Some(_) => number_or_quote_bytes(self.data),
            None => self.data,
        }
    }
}

impl Print for List {
    fn print_syntax(self) -> Vec<u8> {
        let iter = self.into_iter();
        let iter = iter.map(Value::print_syntax);
        let iter = Itertools::intersperse(iter, b" ".into());

        let prefix = core::iter::once(b"[".to_vec());
        let suffix = core::iter::once(b"]".to_vec());

        prefix.chain(iter).chain(suffix).flatten().collect()
    }

    fn print_input(self, printer: Printer) -> Vec<u8> {
        let printer = printer.narrow(Printer::List);
        let iter = self.into_iter().map(|v| v.print_input(printer));
        let iter = Itertools::intersperse(iter, b", ".into());

        iter.flatten().collect()
    }
}

impl Print for Map {
    fn print_syntax(self) -> Vec<u8> {
        let iter = self.into_iter();
        let iter = iter.flat_map(|(k, v)| [escape_map_key(k), v.print_syntax()]);
        let iter = Itertools::intersperse(iter, b" ".into());

        let prefix = core::iter::once(b"{".to_vec());
        let suffix = core::iter::once(b"}".to_vec());

        prefix.chain(iter).chain(suffix).flatten().collect()
    }

    fn print_input(self, printer: Printer) -> Vec<u8> {
        let printer = printer.narrow(Printer::Map);
        let iter = self.into_iter();
        let iter = iter.flat_map(|(k, v)| [escape_map_key(k), v.print_input(printer)]);
        let iter = Itertools::intersperse(iter, b": ".into());

        iter.flatten().collect()
    }
}

fn escape_map_key(data: Vec<u8>) -> Vec<u8> {
    let quote = br#"""#;

    let data = match escape(data, quote) {
        Either::Right(data) => return quote_unescaped(data, quote),
        Either::Left(data) => data,
    };

    if data.iter().any(u8::is_ascii_whitespace) {
        return quote_unescaped(data, quote);
    }

    if find_unescaped_bytes(&data, Printer::Map.escape()).is_some() {
        return quote_unescaped(data, quote);
    }

    data
}

fn escape(mut data: Vec<u8>, bytes: &[u8]) -> Either<Vec<u8>, Vec<u8>> {
    let Some(index) = find_unescaped_bytes(&data, bytes) else {
        return Either::Left(data);
    };

    let rest = data.split_off(index + 1);
    let byte = data.pop().unwrap_or_default();
    data.push_byte(b'\\');
    data.push_byte(byte);
    data.append(&mut escape(rest, bytes).into_inner());

    Either::Right(data)
}

fn find_unescaped_bytes(data: &[u8], bytes: &[u8]) -> Option<usize> {
    bytes.iter().find_map(|b| find_unescaped_byte(data, *b))
}

fn find_unescaped_byte(data: &[u8], byte: u8) -> Option<usize> {
    data.find_byte(byte)
        .filter(|i| *i == 0 || data.get(i - 1) != Some(&b'\\'))
}

fn number_or_quote_bytes(data: Vec<u8>) -> Vec<u8> {
    match Number::try_from(data.as_slice()).ok() {
        None => quote_escaped(data, br#"""#),
        Some(_) => data,
    }
}

pub(crate) fn quote_escaped(data: Vec<u8>, quote: &[u8]) -> Vec<u8> {
    quote_unescaped(escape(data, quote).into_inner(), quote)
}

fn quote_unescaped(mut data: Vec<u8>, quote: &[u8]) -> Vec<u8> {
    data.splice(0..0, quote.iter().copied());
    data.extend_from_slice(quote);
    data
}

#[cfg(test)]
mod test {
    use crate::value::Value;
    use crate::value::list::List;
    use crate::value::map::Map;
    use crate::value::print::Print;
    use crate::value::print::Printer;

    #[test]
    fn test_print_syntax_bytes() {
        assert_eq!(
            print_syntax(Value::from(r#"helloworld"#)),
            r#""helloworld""#
        );
        assert_eq!(
            print_syntax(Value::from(r#"hello world"#)),
            r#""hello world""#
        );
        assert_eq!(
            print_syntax(Value::from("hello 'a' world")),
            r#""hello 'a' world""#
        );
        assert_eq!(
            print_syntax(Value::from(r#"hello "b" world"#)),
            r#""hello \"b\" world""#
        );
    }

    #[test]
    fn test_print_syntax_list() {
        assert_eq!(
            print_syntax(list_from(&[r#"hello,world"#])),
            r#"["hello,world"]"#
        );
        assert_eq!(
            print_syntax(list_from(&[r#"hello, world"#])),
            r#"["hello, world"]"#
        );
        assert_eq!(
            print_syntax(list_from(&[r#"hello, "a", world"#])),
            r#"["hello, \"a\", world"]"#
        );
        assert_eq!(
            print_syntax(list_from(&[r#"hello, "b, world""#])),
            r#"["hello, \"b, world\""]"#
        );
        assert_eq!(
            print_syntax(list_from(&["hello", "world"])),
            r#"["hello" "world"]"#
        );
        assert_eq!(
            print_syntax(list_from(&[r#""a""#, r#""b""#])),
            r#"["\"a\"" "\"b\""]"#
        );
        assert_eq!(
            print_syntax(list_from(&[r#"a,"b""#, r#""c",d"#])),
            r#"["a,\"b\"" "\"c\",d"]"#
        );
    }

    #[test]
    fn test_print_syntax_map() {
        assert_eq!(
            print_syntax(Map::from((r#"ab"#, r#"cd"#)).into()),
            r#"{ab "cd"}"#
        );
        assert_eq!(
            print_syntax(Map::from((r#"a b"#, r#"c d"#)).into()),
            r#"{"a b" "c d"}"#
        );
        assert_eq!(
            print_syntax(Map::from((r#"a'b"#, r#"c'd"#)).into()),
            r#"{"a'b" "c'd"}"#
        );
        assert_eq!(
            print_syntax(Map::from((r#"a"b"#, r#"c"d"#)).into()),
            r#"{"a\"b" "c\"d"}"#
        );
        assert_eq!(
            print_syntax(Map::from((r#"a,b"#, r#"c:d"#)).into()),
            r#"{"a,b" "c:d"}"#
        );
        assert_eq!(
            print_syntax(Map::from((r#"a:b"#, r#"c,d"#)).into()),
            r#"{"a:b" "c,d"}"#
        );
    }

    #[test]
    fn test_print_input_bytes() {
        assert_eq!(print_input(Value::from(r#"a"#)), r#"a"#);
        assert_eq!(print_input(Value::from(r#"a b"#)), r#"a b"#);
        assert_eq!(print_input(Value::from("a 'b' c")), r#"a 'b' c"#);
        assert_eq!(print_input(Value::from(r#"a "b" c"#)), r#"a "b" c"#);
    }

    #[test]
    fn test_print_input_list() {
        assert_eq!(
            print_input(list_from(&[r#"hellosimpleworld"#])),
            "hellosimpleworld"
        );
        assert_eq!(
            print_input(list_from(&[r#"hello,world"#])),
            r#""hello,world""#
        );
        assert_eq!(
            print_input(list_from(&[r#"hello, world"#])),
            r#""hello, world""#
        );
        assert_eq!(
            print_input(list_from(&[r#"hello, "a", world"#])),
            r#""hello, \"a\", world""#
        );
        assert_eq!(
            print_input(list_from(&[r#"hello, "b, world""#])),
            r#""hello, \"b, world\"""#
        );
        assert_eq!(
            print_input(list_from(&["hello", "a", "world"])),
            "hello, a, world"
        );
        assert_eq!(
            print_input(list_from(&[r#""a""#, r#""b""#])),
            r#""\"a\"", "\"b\"""#
        );
        assert_eq!(
            print_input(list_from(&[r#"a,"b""#, r#""c",d"#])),
            r#""a,\"b\"", "\"c\",d""#
        );
    }

    #[test]
    fn test_print_input_map() {
        assert_eq!(
            print_input(Map::from((r#"ab"#, r#"cd"#)).into()),
            r#"ab: cd"#
        );
        assert_eq!(
            print_input(Map::from((r#"a b"#, r#"c d"#)).into()),
            r#""a b": "c d""#
        );
        assert_eq!(
            print_input(Map::from((r#"a'b"#, r#"c'd"#)).into()),
            r#""a'b": "c'd""#
        );
        assert_eq!(
            print_input(Map::from((r#"a"b"#, r#"c"d"#)).into()),
            r#""a\"b": "c\"d""#
        );
        assert_eq!(
            print_input(Map::from((r#"a,b"#, r#"c:d"#)).into()),
            r#""a,b": "c:d""#
        );
        assert_eq!(
            print_input(Map::from((r#"a:b"#, r#"c,d"#)).into()),
            r#""a:b": "c,d""#
        );
    }

    #[track_caller]
    fn list_from(items: &[&str]) -> Value {
        items.to_vec().into_iter().collect::<List>().into()
    }

    #[track_caller]
    fn print_syntax(value: Value) -> String {
        String::from_utf8_lossy(&value.print_syntax()).into_owned()
    }

    #[track_caller]
    fn print_input(value: Value) -> String {
        String::from_utf8_lossy(&value.print_input(Printer::Root)).into_owned()
    }
}