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                // handle nested parens {{{i(things (inside things) other things)}}}
56                let mut paren_nesting = 0;
57
58                loop {
59                    // cursor.next() at the end
60                    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                                // ditch backslash
117                                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}