doxygen_rs/
generator.rs

1use crate::emojis;
2use crate::parser::{parse, GrammarItem, ParseError};
3
4/// Creates a Rustdoc string from a Doxygen string.
5///
6/// # Errors
7///
8/// This function can error if there are missing parts of a given Doxygen annotation (like `@param`
9/// missing the variable name)
10pub fn rustdoc(input: String) -> Result<String, ParseError> {
11    let parsed = parse(input)?;
12    let mut result = String::new();
13    let mut already_added_params = false;
14    let mut already_added_returns = false;
15    let mut already_added_throws = false;
16    let mut group_started = false;
17
18    for item in parsed {
19        result += &match item {
20            GrammarItem::Notation { meta, params, tag } => {
21                let (str, (added_param, added_return, added_throws)) = generate_notation(
22                    tag,
23                    meta,
24                    params,
25                    (
26                        already_added_params,
27                        already_added_returns,
28                        already_added_throws,
29                    ),
30                );
31                if added_param {
32                    already_added_params = true;
33                }
34
35                if added_return {
36                    already_added_returns = true;
37                }
38
39                if added_throws {
40                    already_added_throws = true;
41                }
42
43                str
44            }
45            GrammarItem::Text(v) => if group_started {
46                v.replacen("*", "", 1)
47            } else {
48                v
49            },
50            // See <https://stackoverflow.com/a/40354789>
51            GrammarItem::GroupStart => {
52                group_started = true;
53                String::from("# ")
54            },
55            GrammarItem::GroupEnd => {
56                group_started = false;
57                continue
58            },
59        };
60    }
61
62    Ok(result)
63}
64
65fn generate_notation(
66    tag: String,
67    meta: Vec<String>,
68    params: Vec<String>,
69    (already_params, already_returns, already_throws): (bool, bool, bool),
70) -> (String, (bool, bool, bool)) {
71    let mut new_param = false;
72    let mut new_return = false;
73    let mut new_throw = false;
74
75    (
76        match tag.as_str() {
77            "param" => {
78                let param = params.get(0);
79                new_param = true;
80                let mut str = if !already_params {
81                    "# Arguments\n\n".into()
82                } else {
83                    String::new()
84                };
85
86                str += &if let Some(param) = param {
87                    if meta.is_empty() {
88                        format!("* `{param}` -")
89                    } else {
90                        if let Some(second) = meta.get(1) {
91                            format!(
92                                "* `{}` (direction {}, {}) -",
93                                param,
94                                meta.get(0).unwrap(),
95                                second
96                            )
97                        } else {
98                            format!("* `{}` (direction {}) -", param, meta.get(0).unwrap())
99                        }
100                    }
101                } else {
102                    String::new()
103                };
104
105                str
106            }
107            "a" | "e" | "em" => {
108                let word = params
109                    .get(0)
110                    .expect("@a/@e/@em doesn't contain a word to style");
111                format!("_{word}_")
112            }
113            "b" => {
114                let word = params.get(0).expect("@b doesn't contain a word to style");
115                format!("**{word}**")
116            }
117            "c" | "p" => {
118                let word = params
119                    .get(0)
120                    .expect("@c/@p doesn't contain a word to style");
121                format!("`{word}`")
122            }
123            "emoji" => {
124                let word = params.get(0).expect("@emoji doesn't contain an emoji");
125                emojis::EMOJIS
126                    .get(&word.replace(':', ""))
127                    .expect("invalid emoji")
128                    .to_string()
129            }
130            "sa" | "see" => {
131                let code_ref = params.get(0).expect("@sa/@see doesn't contain a reference");
132                format!("[`{code_ref}`]")
133            }
134            "retval" => {
135                let var = params.get(0).expect("@retval doesn't contain a parameter");
136                new_return = true;
137                let mut str = if !already_returns {
138                    "# Returns\n\n".into()
139                } else {
140                    String::new()
141                };
142
143                str += &format!("* `{var}` -");
144                str
145            }
146            "returns" | "return" | "result" => {
147                new_return = true;
148                if !already_returns {
149                    "# Returns\n\n".into()
150                } else {
151                    String::new()
152                }
153            }
154            "throw" | "throws" | "exception" => {
155                new_throw = true;
156                let exception = params.get(0).expect("@param doesn't contain a parameter");
157
158                let mut str = if !already_throws {
159                    "# Throws\n\n".into()
160                } else {
161                    String::new()
162                };
163
164                str += &format!("* [`{exception}`] -");
165                str
166            }
167            "note" => String::from("> **Note:** "),
168            "since" => String::from("> Available since: "),
169            "deprecated" => String::from("> **Deprecated** "),
170            "remark" | "remarks" => String::from("> "),
171            "par" => String::from("# "),
172            "details" | "pre" | "post" => String::from("\n\n"),
173            "brief" | "short" => String::new(),
174            _ => String::new(),
175        },
176        (new_param, new_return, new_throw),
177    )
178}
179
180#[cfg(test)]
181mod test {
182    use super::*;
183
184    macro_rules! test_rustdoc {
185        ($input:literal, $expected:literal) => {
186            let result = $crate::generator::rustdoc($input.into()).unwrap();
187            assert_eq!(result, $expected);
188        };
189    }
190
191    #[test]
192    fn unknown_annotation() {
193        test_rustdoc!("@thisdoesntexist Example doc", "Example doc");
194    }
195
196    #[test]
197    fn param_with_direction() {
198        test_rustdoc!(
199            "@param[in] example This insane thing.",
200            "# Arguments\n\n* `example` (direction in) - This insane thing."
201        );
202
203        test_rustdoc!(
204            "@param[in,out] example This insane thing.",
205            "# Arguments\n\n* `example` (direction in, out) - This insane thing."
206        );
207
208        test_rustdoc!(
209            "@param[out,in] example This insane thing.",
210            "# Arguments\n\n* `example` (direction in, out) - This insane thing."
211        );
212    }
213
214    #[test]
215    fn param_without_direction() {
216        test_rustdoc!(
217            "@param example This is definitively an example!",
218            "# Arguments\n\n* `example` - This is definitively an example!"
219        );
220    }
221
222    #[test]
223    fn multiple_params() {
224        test_rustdoc!(
225            "@param example1 This is the first example\n@param[out] example2 This is the second example\n@param[in] example3 This is the third example.",
226            "# Arguments\n\n* `example1` - This is the first example\n* `example2` (direction out) - This is the second example\n* `example3` (direction in) - This is the third example."
227        );
228    }
229
230    #[test]
231    fn italics() {
232        test_rustdoc!(
233            "This @a thing is without a doubt @e great. @em And you won't tell me otherwise.",
234            "This _thing_ is without a doubt _great._ _And_ you won't tell me otherwise."
235        );
236    }
237
238    #[test]
239    fn bold() {
240        test_rustdoc!("This is a @b bold claim.", "This is a **bold** claim.");
241    }
242
243    #[test]
244    fn code_inline() {
245        test_rustdoc!(
246            "@c u8 is not the same as @p u32",
247            "`u8` is not the same as `u32`"
248        );
249    }
250
251    #[test]
252    fn emoji() {
253        test_rustdoc!("@emoji :relieved: @emoji :ok_hand:", "😌 👌");
254    }
255
256    #[test]
257    fn text_styling() {
258        test_rustdoc!(
259            "This is from @a Italy. ( @b I @c hope @emoji :pray: )",
260            "This is from _Italy._ ( **I** `hope` 🙏 )"
261        );
262    }
263
264    #[test]
265    fn brief() {
266        test_rustdoc!(
267            "@brief This function does things.\n@short This function also does things.",
268            "This function does things.\nThis function also does things."
269        );
270    }
271
272    #[test]
273    fn see_also() {
274        test_rustdoc!(
275            "@sa random_thing @see random_thing_2",
276            "[`random_thing`] [`random_thing_2`]"
277        );
278    }
279
280    #[test]
281    fn deprecated() {
282        test_rustdoc!(
283            "@deprecated This function is deprecated!\n@param example_1 Example 1.",
284            "> **Deprecated** This function is deprecated!\n# Arguments\n\n* `example_1` - Example 1."
285        );
286    }
287
288    #[test]
289    fn details() {
290        test_rustdoc!(
291            "@brief This function is insane!\n@details This is an insane function because its functionality and performance is quite astonishing.",
292            "This function is insane!\n\n\nThis is an insane function because its functionality and performance is quite astonishing."
293        );
294    }
295
296    #[test]
297    fn paragraph() {
298        test_rustdoc!(
299            "@par Interesting fact about this function\nThis is a function.",
300            "# Interesting fact about this function\nThis is a function."
301        );
302    }
303
304    #[test]
305    fn remark() {
306        test_rustdoc!(
307            "@remark This things needs to be\n@remark remarked.",
308            "> This things needs to be\n> remarked."
309        );
310    }
311
312    #[test]
313    fn returns() {
314        test_rustdoc!(
315            "@returns A value that should be\n@return used with caution.\n@result And if it's @c -1 ... run.",
316            "# Returns\n\nA value that should be\nused with caution.\nAnd if it's `-1` ... run."
317        );
318    }
319
320    #[test]
321    fn return_value() {
322        test_rustdoc!(
323            "@retval example1 This return value is great!",
324            "# Returns\n\n* `example1` - This return value is great!"
325        );
326    }
327
328    #[test]
329    fn returns_and_return_value() {
330        test_rustdoc!(
331            "@returns Great values!\n@retval example1 Is this an example?\n@return Also maybe more things (?)",
332            "# Returns\n\nGreat values!\n* `example1` - Is this an example?\nAlso maybe more things (?)"
333        );
334
335        test_rustdoc!(
336            "@returns Great values!\n@return Also maybe more things (?)\n@retval example1 Is this an example?",
337            "# Returns\n\nGreat values!\nAlso maybe more things (?)\n* `example1` - Is this an example?"
338        );
339
340        test_rustdoc!(
341            "@retval example1 Is this an example?\n@returns Great values!\n@return Also maybe more things (?)",
342            "# Returns\n\n* `example1` - Is this an example?\nGreat values!\nAlso maybe more things (?)"
343        );
344    }
345
346    #[test]
347    fn since() {
348        test_rustdoc!(
349            "@since The bite of '87",
350            "> Available since: The bite of '87"
351        );
352    }
353
354    #[test]
355    fn throws() {
356        test_rustdoc!(
357            "@throw std::io::bonk This is thrown when INSANE things happen.\n@throws std::net::meow This is thrown when BAD things happen.\n@exception std::fs::no This is thrown when NEFARIOUS things happen.",
358            "# Throws\n\n* [`std::io::bonk`] - This is thrown when INSANE things happen.\n* [`std::net::meow`] - This is thrown when BAD things happen.\n* [`std::fs::no`] - This is thrown when NEFARIOUS things happen."
359        );
360    }
361
362    #[test]
363    fn can_parse_example() {
364        let example = include_str!("../tests/assets/example-bindgen.rs");
365        println!("{}", rustdoc(example.into()).unwrap());
366    }
367}