1use std::borrow::Cow;
2
3use crate::constants::{BACKSLASH, COMMA, HYPHEN, LPAREN, NEWLINE, RBRACE, RPAREN, UNDERSCORE};
4use crate::node_pool::NodeID;
5use crate::parse::parse_element;
6use crate::types::{Cursor, MatchError, ParseOpts, Parseable, Parser, Result};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct MacroCall<'a> {
10 pub name: &'a str,
11 pub args: Vec<Cow<'a, str>>,
12}
13
14impl<'a> Parseable<'a> for MacroCall<'a> {
15 fn parse(
16 parser: &mut Parser<'a>,
17 mut cursor: Cursor<'a>,
18 parent: Option<NodeID>,
19 mut parse_opts: ParseOpts,
20 ) -> Result<NodeID> {
21 let start = cursor.index;
22
23 cursor.word("{{{")?;
24
25 if !cursor.curr().is_ascii_alphabetic() {
26 return Err(MatchError::InvalidLogic);
27 }
28
29 let name_match = cursor.fn_while(|chr: u8| {
30 chr.is_ascii_alphanumeric()
32 || chr == HYPHEN
33 || chr == UNDERSCORE
34 && (chr != LPAREN
36 || chr != RBRACE)
38 })?;
39
40 cursor.index = name_match.end;
43
44 match cursor.curr() {
45 LPAREN => {
46 cursor.next();
48 let mut arg_vec: Vec<Cow<str>> = Vec::new();
49 let mut prev_ind = cursor.index;
50 let mut join_prev = false;
54
55 let mut paren_nesting = 0;
57
58 loop {
59 match cursor.try_curr()? {
61 NEWLINE => {
62 parse_opts.from_paragraph = true;
63 parse_opts.list_line = false;
64 parse_opts.from_object = false;
65 match parse_element(parser, cursor.adv_copy(1), parent, parse_opts) {
66 Ok(_) => return Err(MatchError::InvalidLogic),
67 Err(MatchError::InvalidLogic) => {}
68 ret @ Err(_) => return ret,
69 }
70 }
71 RBRACE => {
72 if cursor.word("}}}").is_ok() {
73 return Err(MatchError::InvalidLogic);
74 }
75 }
76 LPAREN => {
77 paren_nesting += 1;
78 }
79 RPAREN => {
80 if paren_nesting > 0 {
81 paren_nesting -= 1;
82 cursor.next();
83 continue;
84 }
85
86 if join_prev {
87 if let Cow::Owned(a) = arg_vec.last_mut().unwrap() {
88 a.push_str(cursor.clamp_backwards(prev_ind));
89 }
90 } else {
91 arg_vec.push(cursor.clamp_backwards(prev_ind).into());
92 }
93
94 cursor.word(")}}}")?;
95 return Ok(parser.alloc(
96 MacroCall {
97 name: name_match.obj,
98 args: arg_vec,
99 },
100 start,
101 cursor.index,
102 parent,
103 ));
104 }
105 COMMA => {
106 if cursor.peek_rev(1)? != BACKSLASH {
107 if join_prev {
108 if let Cow::Owned(a) = arg_vec.last_mut().unwrap() {
109 a.push_str(cursor.clamp_backwards(prev_ind));
110 }
111 join_prev = false;
112 } else {
113 arg_vec.push(cursor.clamp_backwards(prev_ind).into());
114 }
115 } else {
116 let mut pushee =
118 cursor.clamp(prev_ind, cursor.index - 1).to_owned();
119 pushee.push(COMMA as char);
120
121 if join_prev {
122 if let Cow::Owned(a) = arg_vec.last_mut().unwrap() {
123 a.push_str(&pushee);
124 }
125 } else {
126 arg_vec.push(pushee.into());
127 }
128 join_prev = true;
129 }
130
131 prev_ind = cursor.index + 1;
132 }
133 _ => {}
134 }
135 cursor.next();
136 }
137 }
138 RBRACE => {
139 cursor.word("}}}")?;
140 Ok(parser.alloc(
141 MacroCall {
142 name: name_match.obj,
143 args: Vec::new(),
144 },
145 start,
146 cursor.index,
147 parent,
148 ))
149 }
150 _ => Err(MatchError::InvalidLogic),
151 }
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use std::borrow::Cow;
158
159 use pretty_assertions::assert_eq;
160
161 use crate::{
162 element::{ArgNumOrText, MacroDef},
163 expr_in_pool,
164 object::MacroCall,
165 parse_org,
166 types::Expr,
167 };
168
169 #[test]
170 fn basic_macro() {
171 let input = r"{{{abc}}}";
172 let parsed = parse_org(input);
173 let l = expr_in_pool!(parsed, Macro).unwrap();
174 assert_eq!(
175 l,
176 &MacroCall {
177 name: "abc",
178 args: Vec::new()
179 }
180 )
181 }
182
183 #[test]
184 fn macro_with_args() {
185 let input = r"{{{poem(cool, three)}}}";
186 let parsed = parse_org(input);
187 let l = expr_in_pool!(parsed, Macro).unwrap();
188 assert_eq!(
189 l,
190 &MacroCall {
191 name: "poem",
192 args: vec!["cool".into(), " three".into()]
193 }
194 )
195 }
196
197 #[test]
198 fn basic_macro_def() {
199 let input = r"#+macro: poem hiii $1 $2 text
200";
201 let parsed = parse_org(input);
202 let l = expr_in_pool!(parsed, MacroDef).unwrap();
203 assert_eq!(
204 l,
205 &MacroDef {
206 num_args: 2,
207 input: vec![
208 ArgNumOrText::Text("hiii "),
209 ArgNumOrText::ArgNum(1),
210 ArgNumOrText::Text(" "),
211 ArgNumOrText::ArgNum(2),
212 ArgNumOrText::Text(" text")
213 ],
214 name: "poem"
215 }
216 )
217 }
218
219 #[test]
220 fn repeated_macro_def() {
221 let input = r"#+macro: poem $1 $1 text
222";
223 let parsed = parse_org(input);
224 let l = expr_in_pool!(parsed, MacroDef).unwrap();
225 assert_eq!(
226 l,
227 &MacroDef {
228 num_args: 1,
229 input: vec![
230 ArgNumOrText::Text(""),
231 ArgNumOrText::ArgNum(1),
232 ArgNumOrText::Text(" "),
233 ArgNumOrText::ArgNum(1),
234 ArgNumOrText::Text(" text")
235 ],
236 name: "poem"
237 }
238 )
239 }
240
241 #[test]
242 fn combined_macros() {
243 let input = r"#+macro: poem hiii $1 $2 text
244
245{{{poem(cool, three)}}}
246";
247 let pool = parse_org(input);
248 pool.print_tree();
249 }
250
251 #[test]
252 fn macro_escape() {
253 let input = r"{{{poem(cool\, three)}}}";
254 let parsed = parse_org(input);
255 let l = expr_in_pool!(parsed, Macro).unwrap();
256 assert_eq!(
257 l,
258 &MacroCall {
259 name: "poem",
260 args: vec![Cow::Borrowed("cool, three"),]
261 }
262 )
263 }
264
265 #[test]
266 fn macro_multiple_escape() {
267 let input = r"{{{poem(cool\, \, \, \, three)}}}";
268 let parsed = parse_org(input);
269 let l = expr_in_pool!(parsed, Macro).unwrap();
270 assert_eq!(
271 l,
272 &MacroCall {
273 name: "poem",
274 args: vec![Cow::Borrowed("cool, , , , three"),]
275 }
276 )
277 }
278
279 #[test]
280 fn macro_nested_params() {
281 let input = r"{{{i(things (inside things) other things)}}}";
282 let parsed = parse_org(input);
283 dbg!(&parsed);
284 let l = expr_in_pool!(parsed, Macro).unwrap();
285 assert_eq!(
286 l,
287 &MacroCall {
288 name: "i",
289 args: vec![Cow::Borrowed("things (inside things) other things"),]
290 }
291 )
292 }
293}