doxygen_bindgen/
lib.rs

1use std::error::Error;
2use yap::{IntoTokens, Tokens};
3
4const SEPS: [char; 5] = [' ', '\t', '\r', '\n', '['];
5
6/// Formats a reference string as markdown.
7fn format_ref(str: String) -> String {
8    if str.contains("://") {
9        format!("[{str}]({str})")
10    } else {
11        format!("[`{str}`]")
12    }
13}
14
15/// Extracts the next word token.
16fn take_word(toks: &mut impl Tokens<Item = char>) -> String {
17    toks.take_while(|&c| !SEPS.into_iter().any(|s| c == s))
18        .collect::<String>()
19}
20
21/// Skips whitespace tokens.
22fn skip_whitespace(toks: &mut impl Tokens<Item = char>) {
23    toks.skip_while(|c| c.is_ascii_whitespace());
24}
25
26/// Emits a section header if it's not already emitted.
27fn emit_section_header(output: &mut Vec<String>, header: &str) {
28    if !output.iter().any(|line| line.trim() == header) {
29
30        // inspect previous token, transform single new line into two new lines
31        if let Some(last) = output.last_mut() {
32            if last == "\n" {
33                last.push('\n');
34            }
35        }
36
37        output.push(header.to_owned());
38        output.push("\n\n".to_owned());
39    }
40}
41
42/// Transforms Doxygen comments into markdown for Rustdoc.
43pub fn transform(str: &str) -> Result<String, Box<dyn Error>> {
44    let mut res: Vec<String> = vec![];
45    let mut toks = str.into_tokens();
46
47    skip_whitespace(&mut toks);
48    while let Some(tok) = toks.next() {
49        if "@\\".chars().any(|c| c == tok) {
50            let tag = take_word(&mut toks);
51            skip_whitespace(&mut toks);
52            match tag.as_str() {
53                "param" => {
54                    emit_section_header(&mut res, "# Arguments");
55                    let (mut argument, mut attributes) = (take_word(&mut toks), "".to_owned());
56                    if argument.is_empty() {
57                        if toks.next() != Some('[') {
58                            return Err("Expected opening '[' inside attribute list".into());
59                        }
60                        attributes = toks.take_while(|&c| c != ']').collect::<String>();
61                        if toks.next() != Some(']') {
62                            return Err("Expected closing ']' inside attribute list".into());
63                        }
64                        attributes = format!(" [{}] ", attributes);
65                        skip_whitespace(&mut toks);
66                        argument = take_word(&mut toks);
67                    }
68                    res.push(format!("* `{}`{} -", argument, attributes));
69                }
70                "c" | "p" => res.push(format!("`{}`", take_word(&mut toks))),
71                "ref" => res.push(format_ref(take_word(&mut toks))),
72                "see" | "sa" => {
73                    emit_section_header(&mut res, "# See also");
74                    res.push(format!("> {}", format_ref(take_word(&mut toks))));
75                }
76                "a" | "e" | "em" => res.push(format!("_{}_", take_word(&mut toks))),
77                "b" => res.push(format!("**{}**", take_word(&mut toks))),
78                "note" => res.push("> **Note** ".to_owned()),
79                "since" => res.push("> **Since** ".to_owned()),
80                "deprecated" => res.push("> **Deprecated** ".to_owned()),
81                "remark" | "remarks" => res.push("> ".to_owned()),
82                "li" => res.push("- ".to_owned()),
83                "par" => res.push("# ".to_owned()),
84                "returns" | "return" | "result" => emit_section_header(&mut res, "# Returns"),
85                "{" => { /* group start, not implemented  */ }
86                "}" => { /* group end, not implemented */ }
87                "brief" | "short" => {}
88                _ => res.push(format!("{tok}{tag} ")),
89            }
90        } else if tok == '\n' {
91            skip_whitespace(&mut toks);
92            res.push(format!("{tok}"));
93        } else {
94            res.push(format!("{tok}"));
95        }
96    }
97    Ok(res.join(""))
98}
99
100#[cfg(test)]
101mod tests {
102    #[test]
103    fn basic() {
104        const S: &str = "The FILE_BASIC_INFORMATION structure contains timestamps and basic attributes of a file.\n \\li If you specify a value of zero for any of the XxxTime members, the file system keeps a file's current value for that time.\n \\li If you specify a value of -1 for any of the XxxTime members, time stamp updates are disabled for I/O operations preformed on the file handle.\n\\li If you specify a value of -2 for any of the XxxTime members, time stamp updates are enabled for I/O operations preformed on the file handle.\n\\remarks To set the members of this structure, the caller must have FILE_WRITE_ATTRIBUTES access to the file.";
105        const S_: &str = "The FILE_BASIC_INFORMATION structure contains timestamps and basic attributes of a file.\n- If you specify a value of zero for any of the XxxTime members, the file system keeps a file's current value for that time.\n- If you specify a value of -1 for any of the XxxTime members, time stamp updates are disabled for I/O operations preformed on the file handle.\n- If you specify a value of -2 for any of the XxxTime members, time stamp updates are enabled for I/O operations preformed on the file handle.\n> To set the members of this structure, the caller must have FILE_WRITE_ATTRIBUTES access to the file.";
106        assert_eq!(crate::transform(S).unwrap(), S_);
107    }
108
109    #[test]
110    fn with_sections() {
111        const S: &str = " The NtDelayExecution routine suspends the current thread until the specified condition is met.\n\n @param Alertable The function returns when either the time-out period has elapsed or when the APC function is called.\n @param DelayInterval The time interval for which execution is to be suspended, in milliseconds.\n - A value of zero causes the thread to relinquish the remainder of its time slice to any other thread that is ready to run.\n - If there are no other threads ready to run, the function returns immediately, and the thread continues execution.\n - A value of INFINITE indicates that the suspension should not time out.\n @return NTSTATUS Successful or errant status. The return value is STATUS_USER_APC when Alertable is TRUE, and the function returned due to one or more I/O completion callback functions.\n @remarks Note that a ready thread is not guaranteed to run immediately. Consequently, the thread will not run until some arbitrary time after the sleep interval elapses,\n based upon the system \"tick\" frequency and the load factor from other processes.\n @see https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-sleepex";
112        const S_: &str = "The NtDelayExecution routine suspends the current thread until the specified condition is met.\n\n# Arguments\n\n* `Alertable` - The function returns when either the time-out period has elapsed or when the APC function is called.\n* `DelayInterval` - The time interval for which execution is to be suspended, in milliseconds.\n- A value of zero causes the thread to relinquish the remainder of its time slice to any other thread that is ready to run.\n- If there are no other threads ready to run, the function returns immediately, and the thread continues execution.\n- A value of INFINITE indicates that the suspension should not time out.\n\n# Returns\n\nNTSTATUS Successful or errant status. The return value is STATUS_USER_APC when Alertable is TRUE, and the function returned due to one or more I/O completion callback functions.\n> Note that a ready thread is not guaranteed to run immediately. Consequently, the thread will not run until some arbitrary time after the sleep interval elapses,\nbased upon the system \"tick\" frequency and the load factor from other processes.\n\n# See also\n\n> [https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-sleepex](https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-sleepex)";
113        assert_eq!(crate::transform(S).unwrap(), S_);
114    }
115
116    #[test]
117    fn with_attributes() {
118        const S: &str = "Creates a new registry key or opens an existing one, and it associates the key with a transaction.\n\n@param[out] KeyHandle A pointer to a handle that receives the key handle.\n @param[in] DesiredAccess The access mask that specifies the desired access rights.\n@param[in] ObjectAttributes A pointer to an OBJECT_ATTRIBUTES structure that specifies the object attributes.\n@param[in] TitleIndex Reserved.\n@param[in, optional] Class A pointer to a UNICODE_STRING structure that specifies the class of the key.\n @param[in] CreateOptions The options to use when creating the key.\n@param[in] TransactionHandle A handle to the transaction.\n @param[out, optional] Disposition A pointer to a variable that receives the disposition value.\n@return NTSTATUS Successful or errant status.\n";
119        const S_: &str = "Creates a new registry key or opens an existing one, and it associates the key with a transaction.\n\n# Arguments\n\n* `KeyHandle` [out]  - A pointer to a handle that receives the key handle.\n* `DesiredAccess` [in]  - The access mask that specifies the desired access rights.\n* `ObjectAttributes` [in]  - A pointer to an OBJECT_ATTRIBUTES structure that specifies the object attributes.\n* `TitleIndex` [in]  - Reserved.\n* `Class` [in, optional]  - A pointer to a UNICODE_STRING structure that specifies the class of the key.\n* `CreateOptions` [in]  - The options to use when creating the key.\n* `TransactionHandle` [in]  - A handle to the transaction.\n* `Disposition` [out, optional]  - A pointer to a variable that receives the disposition value.\n\n# Returns\n\nNTSTATUS Successful or errant status.\n";
120        assert_eq!(crate::transform(S).unwrap(), S_);
121    }
122
123    #[test]
124    fn new_paragraph_after_html() {
125        const S: &str =  "Set encoding parameters to default values:\n<ul>\n<li>Lossless</li>\n<li>1 tile\n</li>\n<li>etc...</li>\n</ul>\n@param parameters Compression parameters";
126        const S_: &str = "Set encoding parameters to default values:\n<ul>\n<li>Lossless</li>\n<li>1 tile\n</li>\n<li>etc...</li>\n</ul>\n\n# Arguments\n\n* `parameters` - Compression parameters";
127        assert_eq!(crate::transform(S).unwrap(), S_);
128    }
129}