1#![warn(clippy::pedantic)]
2
3use std::collections::HashMap;
4
5use miette::miette;
6use pest::{Parser, iterators::Pairs};
7
8#[derive(pest_derive::Parser)]
9#[grammar = "grammar.pest"]
10struct MyParser;
11
12pub type RewriterFn = fn(&str, HashMap<&str, &str>) -> String;
13
14#[derive(Default)]
15pub struct Rewriter {
16 pub components: HashMap<String, RewriterFn>,
17}
18
19impl Rewriter {
20 pub fn add_component(&mut self, name: &str, func: RewriterFn) {
21 self.components.insert(name.to_string(), func);
22 }
23 pub fn rewrite(&self, input: &str) -> miette::Result<String> {
24 let mut parse =
25 MyParser::parse(Rule::document, input).map_err(pest::error::Error::into_miette)?;
26
27 let mut str_builder = String::new();
28
29 let doc = parse.next().unwrap();
30
31 for tag in doc.into_inner() {
32 match tag.as_rule() {
33 Rule::statement => {
34 let mut inner_tags: Pairs<_> = tag.into_inner();
35
36 let tag_open = inner_tags.next().unwrap();
37 assert_eq!(tag_open.as_rule(), Rule::tag_open);
38 let mut tag_open = tag_open.into_inner();
39
40 let tag_ident = tag_open.next().unwrap();
41 assert_eq!(tag_ident.as_rule(), Rule::tag_ident);
42 let tag_ident = tag_ident.as_str();
43
44 let attrs = tag_open.next();
45 let attrs: HashMap<&str, &str> = if let Some(attrs) = attrs {
46 assert_eq!(attrs.as_rule(), Rule::attr_list);
47 let attrs = attrs.into_inner();
48 attrs
49 .map(|attr| {
50 let mut elements = attr.into_inner();
51 let key = elements.next().unwrap();
52 let value = elements.next().unwrap();
53 (key.as_str(), value.as_str())
54 })
55 .collect()
56 } else {
57 HashMap::default()
58 };
59
60 let tag_text = inner_tags.next().unwrap();
61 assert_eq!(tag_text.as_rule(), Rule::text);
62 let tag_text = tag_text.as_str();
63
64 let component_fn = self
65 .components
66 .get(tag_ident)
67 .ok_or(miette!("Unregistered component"))?;
68 let resp = component_fn(tag_text, attrs);
69
70 str_builder.push_str(&resp);
71 }
72 Rule::text => {
73 str_builder.push_str(tag.as_span().as_str());
74 }
75 _ => {}
76 }
77 }
78
79 Ok(str_builder)
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 #[test]
88 fn simple_example() -> miette::Result<()> {
89 fn foo_rewriter(input: &str, _attrs: HashMap<&str, &str>) -> String {
90 String::from(format!("<span>Hello {input}</span>"))
91 }
92
93 const MARKUP: &str = r#"<body>
94 <h1>Hello!</h1>
95
96 <p>Lorem lorem</p>
97
98 <#hello>World</#hello>
99</body>"#;
100
101 const COMPILED_MARKUP: &str = r#"<body>
102 <h1>Hello!</h1>
103
104 <p>Lorem lorem</p>
105
106 <span>Hello World</span>
107</body>"#;
108
109 let mut writer = Rewriter::default();
110 writer.add_component("hello", foo_rewriter);
111
112 let rewrited = writer.rewrite(MARKUP)?;
113
114 assert_eq!(COMPILED_MARKUP, rewrited);
115
116 Ok(())
117 }
118
119 #[test]
120 fn attr_example() -> miette::Result<()> {
121 fn attr_rewriter(input: &str, attrs: HashMap<&str, &str>) -> String {
122 let el = attrs["element"];
123 String::from(format!("<{el}>{input}</{el}>"))
124 }
125
126 const MARKUP: &str = r#"<#foo element="span" test="Hello World. Foo bar.">Lorem</#foo>"#;
127 const COMPILED_MARKUP: &str = r#"<span>Lorem</span>"#;
128
129 let mut writer = Rewriter::default();
130 writer.add_component("foo", attr_rewriter);
131
132 let rewrited = writer.rewrite(MARKUP)?;
133
134 assert_eq!(COMPILED_MARKUP, rewrited);
135
136 Ok(())
137 }
138
139 #[test]
140 fn blog_example() -> miette::Result<()> {
141 const MARKUP: &str = include_str!("test.html");
142
143 let writer = Rewriter::default();
144 let rewrited = writer.rewrite(include_str!("test.html"))?;
145
146 assert_eq!(MARKUP, rewrited);
147
148 Ok(())
149 }
150}