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