1use crate::emojis;
2use crate::parser::{parse, GrammarItem, ParseError};
3
4pub 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 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}