diplomat_core/ast/
docs.rs

1use super::Path;
2use core::fmt;
3use quote::ToTokens;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use syn::parse::{self, Parse, ParseStream};
7use syn::{Attribute, Ident, Meta, Token};
8
9#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug, Default)]
10pub struct Docs(String, Vec<RustLink>);
11
12/// The type of markdown generated by [`Docs::to_markdown()`]
13///
14/// Note that this only controls markdown generated by this code. Existing markdown
15/// in the Rust documentation will not be sanitized in any way.
16#[derive(PartialEq, Eq, Clone, Debug)]
17#[non_exhaustive]
18pub enum MarkdownStyle {
19    /// Regular markdown with no specific extensions, compatible with most common flavors
20    Normal,
21    /// Markdown that can be losslessly converted to ReStructuredText
22    RstCompat,
23}
24
25impl Docs {
26    pub fn from_attrs(attrs: &[Attribute]) -> Self {
27        Self(Self::get_doc_lines(attrs), Self::get_rust_link(attrs))
28    }
29
30    fn get_doc_lines(attrs: &[Attribute]) -> String {
31        let mut lines: String = String::new();
32
33        attrs.iter().for_each(|attr| {
34            if let Meta::NameValue(ref nv) = attr.meta {
35                if nv.path.is_ident("doc") {
36                    let node: syn::LitStr = syn::parse2(nv.value.to_token_stream()).unwrap();
37                    let line = node.value().trim().to_string();
38
39                    if !lines.is_empty() {
40                        lines.push('\n');
41                    }
42
43                    lines.push_str(&line);
44                }
45            }
46        });
47
48        lines
49    }
50
51    fn get_rust_link(attrs: &[Attribute]) -> Vec<RustLink> {
52        attrs
53            .iter()
54            .filter(|i| i.path().to_token_stream().to_string() == "diplomat :: rust_link")
55            .map(|i| i.parse_args().expect("Malformed attribute"))
56            .collect()
57    }
58
59    pub fn is_empty(&self) -> bool {
60        self.0.is_empty() && self.1.is_empty()
61    }
62
63    /// Convert to markdown
64    pub fn to_markdown(&self, docs_url_gen: &DocsUrlGenerator, style: MarkdownStyle) -> String {
65        use std::fmt::Write;
66        let mut lines = self.0.clone();
67        let mut has_compact = false;
68        let backtick = if style == MarkdownStyle::RstCompat {
69            ""
70        } else {
71            "`"
72        };
73        for rust_link in &self.1 {
74            if rust_link.display == RustLinkDisplay::Compact {
75                has_compact = true;
76            } else if rust_link.display == RustLinkDisplay::Normal {
77                if !lines.is_empty() {
78                    write!(lines, "\n\n").unwrap();
79                }
80                write!(
81                    lines,
82                    "See the [Rust documentation for {backtick}{name}{backtick}]({link}) for more information.",
83                    name = rust_link.path.elements.last().unwrap(),
84                    link = docs_url_gen.gen_for_rust_link(rust_link)
85                )
86                .unwrap();
87            }
88        }
89        if has_compact {
90            if !lines.is_empty() {
91                write!(lines, "\n\n").unwrap();
92            }
93            write!(lines, "Additional information: ").unwrap();
94            for (i, rust_link) in self
95                .1
96                .iter()
97                .filter(|r| r.display == RustLinkDisplay::Compact)
98                .enumerate()
99            {
100                if i != 0 {
101                    write!(lines, ", ").unwrap();
102                }
103                write!(
104                    lines,
105                    "[{}]({})",
106                    i + 1,
107                    docs_url_gen.gen_for_rust_link(rust_link)
108                )
109                .unwrap();
110            }
111        }
112        lines
113    }
114
115    pub fn rust_links(&self) -> &[RustLink] {
116        &self.1
117    }
118}
119
120#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
121#[non_exhaustive]
122pub enum RustLinkDisplay {
123    /// A nice expanded representation that includes the type name
124    ///
125    /// e.g. "See the \[link to Rust documentation\] for more details"
126    Normal,
127    /// A compact representation that will fit multiple rust_link entries in one line
128    ///
129    /// E.g. "For further information, see: 1, 2, 3, 4" (all links)
130    Compact,
131    /// Hidden. Useful for programmatically annotating an API as related without showing a link to the user
132    Hidden,
133}
134
135#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug, PartialOrd, Ord)]
136#[non_exhaustive]
137pub struct RustLink {
138    pub path: Path,
139    pub typ: DocType,
140    pub display: RustLinkDisplay,
141}
142
143impl Parse for RustLink {
144    fn parse(input: ParseStream<'_>) -> parse::Result<Self> {
145        let path = input.parse()?;
146        let path = Path::from_syn(&path);
147        let _comma: Token![,] = input.parse()?;
148        let ty_ident: Ident = input.parse()?;
149        let typ = match &*ty_ident.to_string() {
150            "Struct" => DocType::Struct,
151            "StructField" => DocType::StructField,
152            "Enum" => DocType::Enum,
153            "EnumVariant" => DocType::EnumVariant,
154            "EnumVariantField" => DocType::EnumVariantField,
155            "Trait" => DocType::Trait,
156            "FnInStruct" => DocType::FnInStruct,
157            "FnInEnum" => DocType::FnInEnum,
158            "FnInTrait" => DocType::FnInTrait,
159            "DefaultFnInTrait" => DocType::DefaultFnInTrait,
160            "Fn" => DocType::Fn,
161            "Mod" => DocType::Mod,
162            "Constant" => DocType::Constant,
163            "AssociatedConstantInEnum" => DocType::AssociatedConstantInEnum,
164            "AssociatedConstantInTrait" => DocType::AssociatedConstantInTrait,
165            "AssociatedConstantInStruct" => DocType::AssociatedConstantInStruct,
166            "Macro" => DocType::Macro,
167            "AssociatedTypeInEnum" => DocType::AssociatedTypeInEnum,
168            "AssociatedTypeInTrait" => DocType::AssociatedTypeInTrait,
169            "AssociatedTypeInStruct" => DocType::AssociatedTypeInStruct,
170            "Typedef" => DocType::Typedef,
171            _ => {
172                return Err(parse::Error::new(
173                    ty_ident.span(),
174                    "Unknown rust_link doc type",
175                ))
176            }
177        };
178        let lookahead = input.lookahead1();
179        let display = if lookahead.peek(Token![,]) {
180            let _comma: Token![,] = input.parse()?;
181            let display_ident: Ident = input.parse()?;
182            match &*display_ident.to_string() {
183                "normal" => RustLinkDisplay::Normal,
184                "compact" => RustLinkDisplay::Compact,
185                "hidden" => RustLinkDisplay::Hidden,
186                _ => return Err(parse::Error::new(display_ident.span(), "Unknown rust_link display style: Must be must be `normal`, `compact`, or `hidden`.")),
187            }
188        } else {
189            RustLinkDisplay::Normal
190        };
191        Ok(RustLink { path, typ, display })
192    }
193}
194impl fmt::Display for RustLink {
195    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
196        write!(f, "{}#{:?}", self.path, self.typ)
197    }
198}
199
200#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug, PartialOrd, Ord)]
201#[non_exhaustive]
202pub enum DocType {
203    Struct,
204    StructField,
205    Enum,
206    EnumVariant,
207    EnumVariantField,
208    Trait,
209    FnInStruct,
210    FnInEnum,
211    FnInTrait,
212    DefaultFnInTrait,
213    Fn,
214    Mod,
215    Constant,
216    AssociatedConstantInEnum,
217    AssociatedConstantInTrait,
218    AssociatedConstantInStruct,
219    Macro,
220    AssociatedTypeInEnum,
221    AssociatedTypeInTrait,
222    AssociatedTypeInStruct,
223    Typedef,
224}
225
226#[derive(Default)]
227pub struct DocsUrlGenerator {
228    default_url: Option<String>,
229    base_urls: HashMap<String, String>,
230}
231
232impl DocsUrlGenerator {
233    pub fn with_base_urls(default_url: Option<String>, base_urls: HashMap<String, String>) -> Self {
234        Self {
235            default_url,
236            base_urls,
237        }
238    }
239
240    fn gen_for_rust_link(&self, rust_link: &RustLink) -> String {
241        use DocType::*;
242
243        let mut r = String::new();
244
245        let base = self
246            .base_urls
247            .get(rust_link.path.elements[0].as_str())
248            .map(String::as_str)
249            .or(self.default_url.as_deref())
250            .unwrap_or("https://docs.rs/");
251
252        r.push_str(base);
253        if !base.ends_with('/') {
254            r.push('/');
255        }
256        if r == "https://docs.rs/" {
257            r.push_str(rust_link.path.elements[0].as_str());
258            r.push_str("/latest/");
259        }
260
261        let mut elements = rust_link.path.elements.iter().peekable();
262
263        let module_depth = rust_link.path.elements.len()
264            - match rust_link.typ {
265                Mod => 0,
266                Struct | Enum | Trait | Fn | Macro | Constant | Typedef => 1,
267                FnInEnum
268                | FnInStruct
269                | FnInTrait
270                | DefaultFnInTrait
271                | EnumVariant
272                | StructField
273                | AssociatedTypeInEnum
274                | AssociatedTypeInStruct
275                | AssociatedTypeInTrait
276                | AssociatedConstantInEnum
277                | AssociatedConstantInStruct
278                | AssociatedConstantInTrait => 2,
279                EnumVariantField => 3,
280            };
281
282        for _ in 0..module_depth {
283            r.push_str(elements.next().unwrap().as_str());
284            r.push('/');
285        }
286
287        if elements.peek().is_none() {
288            r.push_str("index.html");
289            return r;
290        }
291
292        r.push_str(match rust_link.typ {
293            Typedef => "type.",
294            Struct
295            | StructField
296            | FnInStruct
297            | AssociatedTypeInStruct
298            | AssociatedConstantInStruct => "struct.",
299            Enum
300            | EnumVariant
301            | EnumVariantField
302            | FnInEnum
303            | AssociatedTypeInEnum
304            | AssociatedConstantInEnum => "enum.",
305            Trait
306            | FnInTrait
307            | DefaultFnInTrait
308            | AssociatedTypeInTrait
309            | AssociatedConstantInTrait => "trait.",
310            Fn => "fn.",
311            Constant => "constant.",
312            Macro => "macro.",
313            Mod => unreachable!(),
314        });
315
316        r.push_str(elements.next().unwrap().as_str());
317
318        r.push_str(".html");
319
320        match rust_link.typ {
321            FnInStruct | FnInEnum | DefaultFnInTrait => {
322                r.push_str("#method.");
323                r.push_str(elements.next().unwrap().as_str());
324            }
325            AssociatedTypeInStruct | AssociatedTypeInEnum | AssociatedTypeInTrait => {
326                r.push_str("#associatedtype.");
327                r.push_str(elements.next().unwrap().as_str());
328            }
329            AssociatedConstantInStruct | AssociatedConstantInEnum | AssociatedConstantInTrait => {
330                r.push_str("#associatedconstant.");
331                r.push_str(elements.next().unwrap().as_str());
332            }
333            FnInTrait => {
334                r.push_str("#tymethod.");
335                r.push_str(elements.next().unwrap().as_str());
336            }
337            EnumVariant => {
338                r.push_str("#variant.");
339                r.push_str(elements.next().unwrap().as_str());
340            }
341            StructField => {
342                r.push_str("#structfield.");
343                r.push_str(elements.next().unwrap().as_str());
344            }
345            EnumVariantField => {
346                r.push_str("#variant.");
347                r.push_str(elements.next().unwrap().as_str());
348                r.push_str(".field.");
349                r.push_str(elements.next().unwrap().as_str());
350            }
351            _ => {}
352        }
353        r
354    }
355}
356
357#[test]
358fn test_docs_url_generator() {
359    let test_cases = [
360        (
361            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Struct)] },
362            "https://docs.rs/std/latest/std/foo/bar/struct.batz.html",
363        ),
364        (
365            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, StructField)] },
366            "https://docs.rs/std/latest/std/foo/struct.bar.html#structfield.batz",
367        ),
368        (
369            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Enum)] },
370            "https://docs.rs/std/latest/std/foo/bar/enum.batz.html",
371        ),
372        (
373            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, EnumVariant)] },
374            "https://docs.rs/std/latest/std/foo/enum.bar.html#variant.batz",
375        ),
376        (
377            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, EnumVariantField)] },
378            "https://docs.rs/std/latest/std/enum.foo.html#variant.bar.field.batz",
379        ),
380        (
381            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Trait)] },
382            "https://docs.rs/std/latest/std/foo/bar/trait.batz.html",
383        ),
384        (
385            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, FnInStruct)] },
386            "https://docs.rs/std/latest/std/foo/struct.bar.html#method.batz",
387        ),
388        (
389            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, FnInEnum)] },
390            "https://docs.rs/std/latest/std/foo/enum.bar.html#method.batz",
391        ),
392        (
393            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, FnInTrait)] },
394            "https://docs.rs/std/latest/std/foo/trait.bar.html#tymethod.batz",
395        ),
396        (
397            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, DefaultFnInTrait)] },
398            "https://docs.rs/std/latest/std/foo/trait.bar.html#method.batz",
399        ),
400        (
401            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Fn)] },
402            "https://docs.rs/std/latest/std/foo/bar/fn.batz.html",
403        ),
404        (
405            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Mod)] },
406            "https://docs.rs/std/latest/std/foo/bar/batz/index.html",
407        ),
408        (
409            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Constant)] },
410            "https://docs.rs/std/latest/std/foo/bar/constant.batz.html",
411        ),
412        (
413            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Macro)] },
414            "https://docs.rs/std/latest/std/foo/bar/macro.batz.html",
415        ),
416    ];
417
418    for (attr, expected) in test_cases.clone() {
419        assert_eq!(
420            DocsUrlGenerator::default().gen_for_rust_link(&Docs::from_attrs(&[attr]).1[0]),
421            expected
422        );
423    }
424
425    assert_eq!(
426        DocsUrlGenerator::with_base_urls(
427            None,
428            [("std".to_string(), "http://std-docs.biz/".to_string())]
429                .into_iter()
430                .collect()
431        )
432        .gen_for_rust_link(&Docs::from_attrs(&[test_cases[0].0.clone()]).1[0]),
433        "http://std-docs.biz/std/foo/bar/struct.batz.html"
434    );
435
436    assert_eq!(
437        DocsUrlGenerator::with_base_urls(Some("http://std-docs.biz/".to_string()), HashMap::new())
438            .gen_for_rust_link(&Docs::from_attrs(&[test_cases[0].0.clone()]).1[0]),
439        "http://std-docs.biz/std/foo/bar/struct.batz.html"
440    );
441}