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        output.push(header.to_owned());
30        output.push("\n\n".to_owned());
31    }
32}
33
34/// Transforms Doxygen comments into markdown for Rustdoc.
35pub fn transform(str: &str) -> Result<String, Box<dyn Error>> {
36    let mut res: Vec<String> = vec![];
37    let mut toks = str.into_tokens();
38
39    skip_whitespace(&mut toks);
40    while let Some(tok) = toks.next() {
41        if "@\\".chars().any(|c| c == tok) {
42            let tag = take_word(&mut toks);
43            skip_whitespace(&mut toks);
44            match tag.as_str() {
45                "param" => {
46                    emit_section_header(&mut res, "# Arguments");
47                    let (mut argument, mut attributes) = (take_word(&mut toks), "".to_owned());
48                    if argument.is_empty() {
49                        if toks.next() != Some('[') {
50                            return Err("Expected opening '[' inside attribute list".into());
51                        }
52                        attributes = toks.take_while(|&c| c != ']').collect::<String>();
53                        if toks.next() != Some(']') {
54                            return Err("Expected closing ']' inside attribute list".into());
55                        }
56                        attributes = format!(" [{}] ", attributes);
57                        skip_whitespace(&mut toks);
58                        argument = take_word(&mut toks);
59                    }
60                    res.push(format!("* `{}`{} -", argument, attributes));
61                }
62                "c" | "p" => res.push(format!("`{}`", take_word(&mut toks))),
63                "ref" => res.push(format_ref(take_word(&mut toks))),
64                "see" | "sa" => {
65                    emit_section_header(&mut res, "# See also");
66                    res.push(format!("> {}", format_ref(take_word(&mut toks))));
67                }
68                "a" | "e" | "em" => res.push(format!("_{}_", take_word(&mut toks))),
69                "b" => res.push(format!("**{}**", take_word(&mut toks))),
70                "note" => res.push("> **Note** ".to_owned()),
71                "since" => res.push("> **Since** ".to_owned()),
72                "deprecated" => res.push("> **Deprecated** ".to_owned()),
73                "remark" | "remarks" => res.push("> ".to_owned()),
74                "li" => res.push("- ".to_owned()),
75                "par" => res.push("# ".to_owned()),
76                "returns" | "return" | "result" => emit_section_header(&mut res, "# Returns"),
77                "{" => { /* group start, not implemented  */ }
78                "}" => { /* group end, not implemented */ }
79                "brief" | "short" => {}
80                _ => res.push(format!("{tok}{tag} ")),
81            }
82        } else if tok == '\n' {
83            skip_whitespace(&mut toks);
84            res.push(format!("{tok}"));
85        } else {
86            res.push(format!("{tok}"));
87        }
88    }
89    Ok(res.join(""))
90}
91
92#[cfg(test)]
93mod tests {
94    #[test]
95    fn basic() {
96        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.";
97        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.";
98        assert_eq!(crate::transform(S).unwrap(), S_);
99    }
100
101    #[test]
102    fn with_sections() {
103        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";
104        const S_: &str = "The NtDelayExecution routine suspends the current thread until the specified condition is met.\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# 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# 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)";
105        assert_eq!(crate::transform(S).unwrap(), S_);
106    }
107
108    #[test]
109    fn with_attributes() {
110        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";
111        const S_: &str = "Creates a new registry key or opens an existing one, and it associates the key with a transaction.\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# Returns\n\nNTSTATUS Successful or errant status.\n";
112        assert_eq!(crate::transform(S).unwrap(), S_);
113    }
114}