Skip to main content

org_rust_parser/object/
org_macro.rs

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            // permitted characters
31            chr.is_ascii_alphanumeric()
32                || chr == HYPHEN
33                || chr == UNDERSCORE
34                // start params
35                && (chr != LPAREN
36                // macro end
37                || chr != RBRACE)
38        })?;
39
40        // A string starting with a alphabetic character followed by any number of
41        // alphanumeric characters, hyphens and underscores (-_).
42        cursor.index = name_match.end;
43
44        match cursor.curr() {
45            LPAREN => {
46                // used to check if we have {{{name()}}} (emtpy func call)
47                cursor.next();
48                let mut arg_vec: Vec<Cow<str>> = Vec::new();
49                let mut prev_ind = cursor.index;
50                // use join_prev solution to avoid duplicating source string
51                // unless escaped commas are used
52                // TODO: handle abc(1\\,) case (escaping backslash used to escape comam)
53                let mut join_prev = false;
54
55                loop {
56                    // cursor.next() at the end
57                    match cursor.try_curr()? {
58                        NEWLINE => {
59                            parse_opts.from_paragraph = true;
60                            parse_opts.list_line = false;
61                            parse_opts.from_object = false;
62                            match parse_element(parser, cursor.adv_copy(1), parent, parse_opts) {
63                                Ok(_) => return Err(MatchError::InvalidLogic),
64                                Err(MatchError::InvalidLogic) => {}
65                                ret @ Err(_) => return ret,
66                            }
67                        }
68                        RBRACE => {
69                            if cursor.word("}}}").is_ok() {
70                                return Err(MatchError::InvalidLogic);
71                            }
72                        }
73                        RPAREN => {
74                            // do this here before we (potentially) consume more text from the cursor
75                            let arg = cursor.clamp_backwards(prev_ind);
76
77                            // is the macro actually ending or is this a loose paren
78                            if !cursor.word(")}}}").is_ok() {
79                                cursor.next();
80                                continue;
81                            }
82
83                            if join_prev {
84                                if let Cow::Owned(a) = arg_vec.last_mut().unwrap() {
85                                    a.push_str(arg);
86                                }
87                            } else {
88                                arg_vec.push(Cow::Borrowed(arg));
89                            }
90
91                            return Ok(parser.alloc(
92                                MacroCall {
93                                    name: name_match.obj,
94                                    args: arg_vec,
95                                },
96                                start,
97                                cursor.index,
98                                parent,
99                            ));
100                        }
101                        COMMA => {
102                            if cursor.peek_rev(1)? != BACKSLASH {
103                                if join_prev {
104                                    if let Cow::Owned(a) = arg_vec.last_mut().unwrap() {
105                                        a.push_str(cursor.clamp_backwards(prev_ind));
106                                    }
107                                    join_prev = false;
108                                } else {
109                                    arg_vec.push(cursor.clamp_backwards(prev_ind).into());
110                                }
111                            } else {
112                                // ditch backslash
113                                let mut pushee =
114                                    cursor.clamp(prev_ind, cursor.index - 1).to_owned();
115                                pushee.push(COMMA as char);
116
117                                if join_prev {
118                                    if let Cow::Owned(a) = arg_vec.last_mut().unwrap() {
119                                        a.push_str(&pushee);
120                                    }
121                                } else {
122                                    arg_vec.push(pushee.into());
123                                }
124                                join_prev = true;
125                            }
126
127                            prev_ind = cursor.index + 1;
128                        }
129                        _ => {}
130                    }
131                    cursor.next();
132                }
133            }
134            RBRACE => {
135                cursor.word("}}}")?;
136                Ok(parser.alloc(
137                    MacroCall {
138                        name: name_match.obj,
139                        args: Vec::new(),
140                    },
141                    start,
142                    cursor.index,
143                    parent,
144                ))
145            }
146            _ => Err(MatchError::InvalidLogic),
147        }
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use std::borrow::Cow;
154
155    use pretty_assertions::assert_eq;
156
157    use crate::{
158        element::{ArgNumOrText, MacroDef},
159        expr_in_pool,
160        object::MacroCall,
161        parse_org,
162        types::Expr,
163    };
164
165    #[test]
166    fn basic_macro() {
167        let input = r"{{{abc}}}";
168        let parsed = parse_org(input);
169        let l = expr_in_pool!(parsed, Macro).unwrap();
170        assert_eq!(
171            l,
172            &MacroCall {
173                name: "abc",
174                args: Vec::new()
175            }
176        )
177    }
178
179    #[test]
180    fn macro_with_args() {
181        let input = r"{{{poem(cool, three)}}}";
182        let parsed = parse_org(input);
183        let l = expr_in_pool!(parsed, Macro).unwrap();
184        assert_eq!(
185            l,
186            &MacroCall {
187                name: "poem",
188                args: vec!["cool".into(), " three".into()]
189            }
190        )
191    }
192
193    #[test]
194    fn basic_macro_def() {
195        let input = r"#+macro: poem hiii $1 $2 text
196";
197        let parsed = parse_org(input);
198        let l = expr_in_pool!(parsed, MacroDef).unwrap();
199        assert_eq!(
200            l,
201            &MacroDef {
202                num_args: 2,
203                input: vec![
204                    ArgNumOrText::Text("hiii "),
205                    ArgNumOrText::ArgNum(1),
206                    ArgNumOrText::Text(" "),
207                    ArgNumOrText::ArgNum(2),
208                    ArgNumOrText::Text(" text")
209                ],
210                name: "poem"
211            }
212        )
213    }
214
215    #[test]
216    fn repeated_macro_def() {
217        let input = r"#+macro: poem $1 $1 text
218";
219        let parsed = parse_org(input);
220        let l = expr_in_pool!(parsed, MacroDef).unwrap();
221        assert_eq!(
222            l,
223            &MacroDef {
224                num_args: 1,
225                input: vec![
226                    ArgNumOrText::Text(""),
227                    ArgNumOrText::ArgNum(1),
228                    ArgNumOrText::Text(" "),
229                    ArgNumOrText::ArgNum(1),
230                    ArgNumOrText::Text(" text")
231                ],
232                name: "poem"
233            }
234        )
235    }
236
237    #[test]
238    fn combined_macros() {
239        let input = r"#+macro: poem hiii $1 $2 text
240
241{{{poem(cool, three)}}}
242";
243        let pool = parse_org(input);
244        pool.print_tree();
245    }
246
247    #[test]
248    fn macro_escape() {
249        let input = r"{{{poem(cool\, three)}}}";
250        let parsed = parse_org(input);
251        let l = expr_in_pool!(parsed, Macro).unwrap();
252        assert_eq!(
253            l,
254            &MacroCall {
255                name: "poem",
256                args: vec![Cow::Borrowed("cool, three"),]
257            }
258        )
259    }
260
261    #[test]
262    fn macro_multiple_escape() {
263        let input = r"{{{poem(cool\, \, \, \, three)}}}";
264        let parsed = parse_org(input);
265        let l = expr_in_pool!(parsed, Macro).unwrap();
266        assert_eq!(
267            l,
268            &MacroCall {
269                name: "poem",
270                args: vec![Cow::Borrowed("cool, , , , three"),]
271            }
272        )
273    }
274
275    #[test]
276    fn macro_nested_params() {
277        let input = r"{{{i(things (inside things) other things)}}}";
278        let parsed = parse_org(input);
279        let l = expr_in_pool!(parsed, Macro).unwrap();
280        assert_eq!(
281            l,
282            &MacroCall {
283                name: "i",
284                args: vec![Cow::Borrowed("things (inside things) other things"),]
285            }
286        )
287    }
288
289    #[test]
290    fn macro_almost_nested_params() {
291        let input = r"{{{i(things (inside things other things)}}}";
292        let parsed = parse_org(input);
293        let l = expr_in_pool!(parsed, Macro).unwrap();
294        assert_eq!(
295            l,
296            &MacroCall {
297                name: "i",
298                args: vec![Cow::Borrowed("things (inside things other things"),]
299            }
300        )
301    }
302}