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#[derive(PartialEq, Eq, Clone, Debug)]
17#[non_exhaustive]
18pub enum MarkdownStyle {
19 Normal,
21 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 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 Normal,
127 Compact,
131 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}