use std::collections::HashMap;
use crate::{
CatResult, Moo,
parser::{
escape_codes::proc_str,
markup::{parse_header, parse_markup},
tags::parse_tag,
},
prelude::*,
};
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 contents: Vec<AstNode<'input>>,
pub module: bool,
}
#[derive(Debug, PartialEq, Clone)]
pub enum AstNode<'input> {
Space,
Word(Moo<'input>),
Tag(Tag<'input>),
ContentBlock(TeaCatAst<'input>),
Markup {
of: MarkupType,
inner: TeaCatAst<'input>,
},
Define {
name: Moo<'input>,
content: Box<AstNode<'input>>,
},
With {
path: ModuleName,
imports: Vec<Moo<'input>>,
},
Variable(Moo<'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 mut pairs = TeaCatGrammar::parse(Rule::teacat, src)?
.next()
.expect("Parsing should output something")
.into_inner();
let is_module = if let Some(pair) = pairs.peek()
&& pair.as_rule() == Rule::module
{
pairs.next();
true
} else {
false
};
let pair = pairs
.next()
.expect("Rule teacat must contain line_sequence!");
assert_eq!(pair.as_rule(), Rule::line_sequence);
let mut ast = parse_line_sequence(pair);
ast.module = is_module;
Ok(ast)
}
}
impl<'input> From<Vec<AstNode<'input>>> for TeaCatAst<'input> {
fn from(contents: Vec<AstNode<'input>>) -> Self {
Self {
contents,
module: false,
}
}
}
impl TeaCatAst<'_> {
#[must_use]
pub fn owned(&self) -> TeaCatAst<'static> {
TeaCatAst {
contents: self.contents.iter().map(AstNode::owned).collect(),
module: self.module,
}
}
}
impl<'i> IntoIterator for TeaCatAst<'i> {
type Item = AstNode<'i>;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.contents.into_iter()
}
}
impl AstNode<'_> {
#[must_use]
pub fn owned(&self) -> AstNode<'static> {
match self {
Self::Space => AstNode::Space,
Self::Word(moo) => AstNode::Word(Moo::Owned(moo.clone().into_owned())),
Self::Tag(tag) => AstNode::Tag(tag.owned()),
Self::ContentBlock(ast) => AstNode::ContentBlock(ast.owned()),
Self::Markup { of, inner } => AstNode::Markup {
of: *of,
inner: inner.owned(),
},
Self::Define { name, content } => AstNode::Define {
name: Moo::Owned(name.clone().into_owned()),
content: content.owned().into(),
},
Self::With { path, imports } => AstNode::With {
path: path.clone(),
imports: imports
.iter()
.map(Moo::clone)
.map(Moo::into_owned)
.map(Moo::Owned)
.collect(),
},
Self::Variable(moo) => AstNode::Variable(Moo::Owned(moo.clone().into_owned())),
}
}
}
impl Tag<'_> {
#[must_use]
pub fn owned(&self) -> Tag<'static> {
Tag {
name: self.name.clone().into_owned().into(),
attrs: Attributes(
self.attrs
.0
.iter()
.map(|(k, v)| (k.clone().into_owned().into(), v.clone().into_owned().into()))
.collect(),
),
content: self.content.owned(),
}
}
}
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();
inner.into()
}
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::def => vec![parse_def(pair)],
Rule::with => vec![parse_with(pair)],
Rule::tag_raw | Rule::tag_sameline | Rule::tag_multiline => {
vec![parse_tag(pair.into_inner())]
}
Rule::content_block => vec![parse_content_block(pair)],
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::var => AstNode::Variable(pair.as_str()[1..].into()),
Rule::tag_inline => parse_tag(pair.into_inner()),
Rule::content_block_inline => parse_content_block(pair),
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()
}
fn parse_def(pair: TeaPair) -> AstNode {
assert_eq!(pair.as_rule(), Rule::def);
let mut pairs = pair.into_inner();
let name = pairs.next().expect("Rule def must contain ident!");
let content = pairs
.next()
.expect("Rule def must contain def_content!")
.into_inner()
.next()
.expect("Rule def_content must contain content!");
let content = match content.as_rule() {
Rule::content_block => parse_content_block(content),
Rule::tag_multiline | Rule::tag_raw => parse_tag(content.into_inner()),
other => panic!("Unexpected rule in def_content: {other:?}"),
};
AstNode::Define {
name: name.as_str().into(),
content: Box::new(content),
}
}
fn parse_with(pair: TeaPair) -> AstNode {
assert_eq!(pair.as_rule(), Rule::with);
let mut pairs = pair.into_inner();
let mut path = vec![];
let mut imports = vec![];
while let Some(pair) = pairs.peek()
&& pair.as_rule() == Rule::ident
{
pairs.next();
path.push(pair.as_str().to_owned());
}
let mut pairs = pairs
.next()
.expect("Rule with must contain with_content!")
.into_inner();
while let Some(pair) = pairs.peek()
&& pair.as_rule() == Rule::ident
{
pairs.next();
imports.push(pair.as_str().into());
}
AstNode::With {
path: ModuleName(path),
imports,
}
}
fn parse_content_block(pair: TeaPair) -> AstNode {
let fun = if let Rule::content_block_inline = pair.as_rule() {
|pair| parse_line_content(pair).into()
} else {
parse_line_sequence
};
AstNode::ContentBlock(fun(pair
.into_inner()
.next()
.expect("Rule content_block must contain content")))
}
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 { contents: vec![$($elem),*], module: false } };
}
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)
}
}