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                    match cursor.try_curr()? {
57                        NEWLINE => {
58                            parse_opts.from_paragraph = true;
59                            parse_opts.list_line = false;
60                            parse_opts.from_object = false;
61                            match parse_element(parser, cursor.adv_copy(1), parent, parse_opts) {
62                                Ok(_) => return Err(MatchError::InvalidLogic),
63                                Err(MatchError::InvalidLogic) => {}
64                                ret @ Err(_) => return ret,
65                            }
66                        }
67                        RBRACE => {
68                            if cursor.word("}}}").is_ok() {
69                                return Err(MatchError::InvalidLogic);
70                            }
71                        }
72                        RPAREN => {
73                            if join_prev {
74                                if let Cow::Owned(a) = arg_vec.last_mut().unwrap() {
75                                    a.push_str(cursor.clamp_backwards(prev_ind));
76                                }
77                            } else {
78                                arg_vec.push(cursor.clamp_backwards(prev_ind).into());
79                            }
80
81                            cursor.word(")}}}")?;
82                            return Ok(parser.alloc(
83                                MacroCall {
84                                    name: name_match.obj,
85                                    args: arg_vec,
86                                },
87                                start,
88                                cursor.index,
89                                parent,
90                            ));
91                        }
92                        COMMA => {
93                            if cursor.peek_rev(1)? != BACKSLASH {
94                                if join_prev {
95                                    if let Cow::Owned(a) = arg_vec.last_mut().unwrap() {
96                                        a.push_str(cursor.clamp_backwards(prev_ind));
97                                    }
98                                    join_prev = false;
99                                } else {
100                                    arg_vec.push(cursor.clamp_backwards(prev_ind).into());
101                                }
102                            } else {
103                                // ditch backslash
104                                let mut pushee =
105                                    cursor.clamp(prev_ind, cursor.index - 1).to_owned();
106                                pushee.push(COMMA as char);
107
108                                if join_prev {
109                                    if let Cow::Owned(a) = arg_vec.last_mut().unwrap() {
110                                        a.push_str(&pushee);
111                                    }
112                                } else {
113                                    arg_vec.push(pushee.into());
114                                }
115                                join_prev = true;
116                            }
117
118                            prev_ind = cursor.index + 1;
119                        }
120                        _ => {}
121                    }
122                    cursor.next();
123                }
124            }
125            RBRACE => {
126                cursor.word("}}}")?;
127                Ok(parser.alloc(
128                    MacroCall {
129                        name: name_match.obj,
130                        args: Vec::new(),
131                    },
132                    start,
133                    cursor.index,
134                    parent,
135                ))
136            }
137            _ => Err(MatchError::InvalidLogic),
138        }
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use std::borrow::Cow;
145
146    use pretty_assertions::assert_eq;
147
148    use crate::{
149        element::{ArgNumOrText, MacroDef},
150        expr_in_pool,
151        object::MacroCall,
152        parse_org,
153        types::Expr,
154    };
155
156    #[test]
157    fn basic_macro() {
158        let input = r"{{{abc}}}";
159        let parsed = parse_org(input);
160        let l = expr_in_pool!(parsed, Macro).unwrap();
161        assert_eq!(
162            l,
163            &MacroCall {
164                name: "abc",
165                args: Vec::new()
166            }
167        )
168    }
169
170    #[test]
171    fn macro_with_args() {
172        let input = r"{{{poem(cool, three)}}}";
173        let parsed = parse_org(input);
174        let l = expr_in_pool!(parsed, Macro).unwrap();
175        assert_eq!(
176            l,
177            &MacroCall {
178                name: "poem",
179                args: vec!["cool".into(), " three".into()]
180            }
181        )
182    }
183
184    #[test]
185    fn basic_macro_def() {
186        let input = r"#+macro: poem hiii $1 $2 text
187";
188        let parsed = parse_org(input);
189        let l = expr_in_pool!(parsed, MacroDef).unwrap();
190        assert_eq!(
191            l,
192            &MacroDef {
193                num_args: 2,
194                input: vec![
195                    ArgNumOrText::Text("hiii "),
196                    ArgNumOrText::ArgNum(1),
197                    ArgNumOrText::Text(" "),
198                    ArgNumOrText::ArgNum(2),
199                    ArgNumOrText::Text(" text")
200                ],
201                name: "poem"
202            }
203        )
204    }
205
206    #[test]
207    fn repeated_macro_def() {
208        let input = r"#+macro: poem $1 $1 text
209";
210        let parsed = parse_org(input);
211        let l = expr_in_pool!(parsed, MacroDef).unwrap();
212        assert_eq!(
213            l,
214            &MacroDef {
215                num_args: 1,
216                input: vec![
217                    ArgNumOrText::Text(""),
218                    ArgNumOrText::ArgNum(1),
219                    ArgNumOrText::Text(" "),
220                    ArgNumOrText::ArgNum(1),
221                    ArgNumOrText::Text(" text")
222                ],
223                name: "poem"
224            }
225        )
226    }
227
228    #[test]
229    fn combined_macros() {
230        let input = r"#+macro: poem hiii $1 $2 text
231
232{{{poem(cool, three)}}}
233";
234        let pool = parse_org(input);
235        pool.print_tree();
236    }
237
238    #[test]
239    fn macro_escape() {
240        let input = r"{{{poem(cool\, three)}}}";
241        let parsed = parse_org(input);
242        let l = expr_in_pool!(parsed, Macro).unwrap();
243        assert_eq!(
244            l,
245            &MacroCall {
246                name: "poem",
247                args: vec![Cow::Borrowed("cool, three"), ]
248            }
249        )
250    }
251
252    #[test]
253    fn macro_multiple_escape() {
254        let input = r"{{{poem(cool\, \, \, \, three)}}}";
255        let parsed = parse_org(input);
256        let l = expr_in_pool!(parsed, Macro).unwrap();
257        assert_eq!(
258            l,
259            &MacroCall {
260                name: "poem",
261                args: vec![Cow::Borrowed("cool, , , , three"), ]
262            }
263        )
264    }
265}