use std::collections::HashMap;
use crate::{
CatResult, Moo,
error::TeaCatErr,
parser::{
escape_codes::proc_str,
markup::{parse_header, parse_markup},
tags::parse_tag,
},
};
use grammar::{Rule, TeaCatGrammar};
use pest::{
Parser,
iterators::{Pair, Pairs},
};
mod escape_codes;
mod markup;
mod tags;
type TeaPair<'i> = Pair<'i, Rule>;
type TeaPairs<'i> = Pairs<'i, Rule>;
#[derive(Debug, PartialEq, Clone)]
pub struct TeaCatAst<'input>(pub Vec<AstNode<'input>>);
#[derive(Debug, PartialEq, Clone)]
pub enum AstNode<'input> {
Space,
Word(Moo<'input>),
Tag(Tag<'input>),
Markup {
of: MarkupType,
inner: TeaCatAst<'input>,
},
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum MarkupType {
Bold,
Italic,
Strikethrough,
Underline,
Header(u8),
}
#[derive(Debug, PartialEq, Clone, Default)]
pub struct Attributes<'input>(pub HashMap<Moo<'input>, Moo<'input>>);
#[derive(Debug, PartialEq, Clone)]
pub struct Tag<'input> {
pub name: Moo<'input>,
pub attrs: Attributes<'input>,
pub content: TeaCatAst<'input>,
}
impl<'input> TryFrom<&'input str> for TeaCatAst<'input> {
type Error = TeaCatErr;
fn try_from(src: &'input str) -> CatResult<Self> {
let pair = TeaCatGrammar::parse(Rule::teacat, src)?
.next()
.expect("Parsing should output something")
.into_inner()
.next()
.expect("Rule teacat must not be empty!");
assert_eq!(pair.as_rule(), Rule::line_sequence);
Ok(parse_line_sequence(pair))
}
}
fn parse_line_sequence(pair: TeaPair) -> TeaCatAst {
assert_eq!(pair.as_rule(), Rule::line_sequence);
let mut inner = pair
.into_inner()
.map(parse_line)
.filter(|line| !matches!(line[..], [] | [AstNode::Space]))
.flat_map(|mut line| {
if line.last().is_some_and(|item| *item != AstNode::Space) {
line.push(AstNode::Space);
}
line
})
.collect::<Vec<_>>();
inner.pop();
TeaCatAst(inner)
}
fn parse_line(pair: TeaPair) -> Vec<AstNode> {
assert_eq!(pair.as_rule(), Rule::line);
let pair = pair.into_inner().next().expect("Line must contain rule!");
match pair.as_rule() {
Rule::tag_raw | Rule::tag_sameline | Rule::tag_multiline => {
vec![parse_tag(pair.into_inner())]
}
Rule::header => vec![parse_header(pair)],
Rule::line_content => parse_line_content(pair),
other => panic!("Unexpected rule in line: {other:?}"),
}
}
fn parse_line_content(pair: TeaPair) -> Vec<AstNode> {
assert_eq!(pair.as_rule(), Rule::line_content);
pair.into_inner()
.map(|pair| match pair.as_rule() {
Rule::tag_inline => parse_tag(pair.into_inner()),
Rule::markup => parse_markup(pair),
Rule::word => AstNode::Word(proc_str(pair)),
Rule::space => AstNode::Space,
other => panic!("Unexpected rule in line_content: {other:?}"),
})
.collect()
}
pub(crate) mod grammar {
use pest_derive::Parser;
#[derive(Parser)]
#[grammar = "teacat.pest"]
pub struct TeaCatGrammar;
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use crate::parser::{Attributes, MarkupType, Tag};
use super::{AstNode, TeaCatAst, grammar::*};
use pest::Parser;
macro_rules! tc_ast {
($($elem:expr),*) => { TeaCatAst(vec![$($elem),*]) };
}
macro_rules! tc_tag {
($tagname:ident ( $($argname:ident $content:literal),* ) $(,$item:expr)* $(,)?) => {
AstNode::Tag(Tag {
name: stringify!($tagname).into(),
attrs: Attributes(HashMap::from([
$((
stringify!($argname).into(),
$content.into()
)),*
])),
content: tc_ast!($($item),*),
})
};
}
#[test]
fn text() {
parse("hello chat").unwrap();
}
#[test]
fn single_line_tag() {
parse(":test test\n").unwrap();
}
#[test]
fn inline_tag() {
parse(":test[test]").unwrap();
}
#[test]
fn multi_line_tag() {
parse(":test [\ntest\n]\n").unwrap();
}
#[test]
fn pain() {
assert!(parse(":a :a[\n]testing").is_err());
}
#[test]
fn escape_codes() {
parse(r":test \t\n\s\:3\u[1F431]").unwrap();
}
#[test]
fn markup() {
let ast =
TeaCatAst::try_from("+bold+ and *italic* and ~strikethrough~ and _underline_").unwrap();
let and = AstNode::Word("and".into());
assert_eq!(
ast,
tc_ast![
AstNode::Markup {
of: MarkupType::Bold,
inner: tc_ast![AstNode::Word("bold".into())]
},
AstNode::Space,
and.clone(),
AstNode::Space,
AstNode::Markup {
of: MarkupType::Italic,
inner: tc_ast![AstNode::Word("italic".into())]
},
AstNode::Space,
and.clone(),
AstNode::Space,
AstNode::Markup {
of: MarkupType::Strikethrough,
inner: tc_ast![AstNode::Word("strikethrough".into())]
},
AstNode::Space,
and.clone(),
AstNode::Space,
AstNode::Markup {
of: MarkupType::Underline,
inner: tc_ast![AstNode::Word("underline".into())]
}
]
);
}
#[test]
fn markup_precedence() {
let ast = TeaCatAst::try_from("+a*b+c*").unwrap();
assert_eq!(
ast,
tc_ast![
AstNode::Word("+a".into()),
AstNode::Markup {
of: MarkupType::Italic,
inner: tc_ast![AstNode::Word("b+c".into())]
}
]
);
}
#[test]
fn escape_codes2() {
let ast = TeaCatAst::try_from(r"\t\[\u[1f431]").unwrap();
assert_eq!(ast, tc_ast![AstNode::Word("\t[🐱".into())]);
}
#[test]
fn args() {
let ast = TeaCatAst::try_from(r#":t(a "haii", b "chat")[]"#).unwrap();
assert_eq!(ast, tc_ast![tc_tag![t(a "haii", b "chat")]]);
}
fn parse(input: &str) -> Result<pest::iterators::Pairs<'_, Rule>, pest::error::Error<Rule>> {
TeaCatGrammar::parse(Rule::teacat, input)
}
}