Revision control
Copy as Markdown
Other Tools
use super::Path;
use core::fmt;
use quote::ToTokens;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use syn::parse::{self, Parse, ParseStream};
use syn::{Attribute, Ident, Meta, Token};
#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug, Default)]
pub struct Docs(String, Vec<RustLink>);
/// The type of markdown generated by [`Docs::to_markdown()`]
///
/// Note that this only controls markdown generated by this code. Existing markdown
/// in the Rust documentation will not be sanitized in any way.
#[derive(PartialEq, Eq, Clone, Debug)]
#[non_exhaustive]
pub enum MarkdownStyle {
/// Regular markdown with no specific extensions, compatible with most common flavors
Normal,
/// Markdown that can be losslessly converted to ReStructuredText
RstCompat,
}
impl Docs {
pub fn from_attrs(attrs: &[Attribute]) -> Self {
Self(Self::get_doc_lines(attrs), Self::get_rust_link(attrs))
}
fn get_doc_lines(attrs: &[Attribute]) -> String {
let mut lines: String = String::new();
attrs.iter().for_each(|attr| {
if let Meta::NameValue(ref nv) = attr.meta {
if nv.path.is_ident("doc") {
let node: syn::LitStr = syn::parse2(nv.value.to_token_stream()).unwrap();
let line = node.value().trim().to_string();
if !lines.is_empty() {
lines.push('\n');
}
lines.push_str(&line);
}
}
});
lines
}
fn get_rust_link(attrs: &[Attribute]) -> Vec<RustLink> {
attrs
.iter()
.filter(|i| i.path().to_token_stream().to_string() == "diplomat :: rust_link")
.map(|i| i.parse_args().expect("Malformed attribute"))
.collect()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty() && self.1.is_empty()
}
/// Convert to markdown
pub fn to_markdown(&self, docs_url_gen: &DocsUrlGenerator, style: MarkdownStyle) -> String {
use std::fmt::Write;
let mut lines = self.0.clone();
let mut has_compact = false;
let backtick = if style == MarkdownStyle::RstCompat {
""
} else {
"`"
};
for rust_link in &self.1 {
if rust_link.display == RustLinkDisplay::Compact {
has_compact = true;
} else if rust_link.display == RustLinkDisplay::Normal {
if !lines.is_empty() {
write!(lines, "\n\n").unwrap();
}
write!(
lines,
"See the [Rust documentation for {backtick}{name}{backtick}]({link}) for more information.",
name = rust_link.path.elements.last().unwrap(),
link = docs_url_gen.gen_for_rust_link(rust_link)
)
.unwrap();
}
}
if has_compact {
if !lines.is_empty() {
write!(lines, "\n\n").unwrap();
}
write!(lines, "Additional information: ").unwrap();
for (i, rust_link) in self
.1
.iter()
.filter(|r| r.display == RustLinkDisplay::Compact)
.enumerate()
{
if i != 0 {
write!(lines, ", ").unwrap();
}
write!(
lines,
"[{}]({})",
i + 1,
docs_url_gen.gen_for_rust_link(rust_link)
)
.unwrap();
}
}
lines
}
pub fn rust_links(&self) -> &[RustLink] {
&self.1
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[non_exhaustive]
pub enum RustLinkDisplay {
/// A nice expanded representation that includes the type name
///
/// e.g. "See the \[link to Rust documentation\] for more details"
Normal,
/// A compact representation that will fit multiple rust_link entries in one line
///
/// E.g. "For further information, see: 1, 2, 3, 4" (all links)
Compact,
/// Hidden. Useful for programmatically annotating an API as related without showing a link to the user
Hidden,
}
#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug, PartialOrd, Ord)]
#[non_exhaustive]
pub struct RustLink {
pub path: Path,
pub typ: DocType,
pub display: RustLinkDisplay,
}
impl Parse for RustLink {
fn parse(input: ParseStream<'_>) -> parse::Result<Self> {
let path = input.parse()?;
let path = Path::from_syn(&path);
let _comma: Token![,] = input.parse()?;
let ty_ident: Ident = input.parse()?;
let typ = match &*ty_ident.to_string() {
"Struct" => DocType::Struct,
"StructField" => DocType::StructField,
"Enum" => DocType::Enum,
"EnumVariant" => DocType::EnumVariant,
"EnumVariantField" => DocType::EnumVariantField,
"Trait" => DocType::Trait,
"FnInStruct" => DocType::FnInStruct,
"FnInEnum" => DocType::FnInEnum,
"FnInTrait" => DocType::FnInTrait,
"DefaultFnInTrait" => DocType::DefaultFnInTrait,
"Fn" => DocType::Fn,
"Mod" => DocType::Mod,
"Constant" => DocType::Constant,
"AssociatedConstantInEnum" => DocType::AssociatedConstantInEnum,
"AssociatedConstantInTrait" => DocType::AssociatedConstantInTrait,
"AssociatedConstantInStruct" => DocType::AssociatedConstantInStruct,
"Macro" => DocType::Macro,
"AssociatedTypeInEnum" => DocType::AssociatedTypeInEnum,
"AssociatedTypeInTrait" => DocType::AssociatedTypeInTrait,
"AssociatedTypeInStruct" => DocType::AssociatedTypeInStruct,
"Typedef" => DocType::Typedef,
_ => {
return Err(parse::Error::new(
ty_ident.span(),
"Unknown rust_link doc type",
))
}
};
let lookahead = input.lookahead1();
let display = if lookahead.peek(Token![,]) {
let _comma: Token![,] = input.parse()?;
let display_ident: Ident = input.parse()?;
match &*display_ident.to_string() {
"normal" => RustLinkDisplay::Normal,
"compact" => RustLinkDisplay::Compact,
"hidden" => RustLinkDisplay::Hidden,
_ => return Err(parse::Error::new(display_ident.span(), "Unknown rust_link display style: Must be must be `normal`, `compact`, or `hidden`.")),
}
} else {
RustLinkDisplay::Normal
};
Ok(RustLink { path, typ, display })
}
}
impl fmt::Display for RustLink {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}#{:?}", self.path, self.typ)
}
}
#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug, PartialOrd, Ord)]
#[non_exhaustive]
pub enum DocType {
Struct,
StructField,
Enum,
EnumVariant,
EnumVariantField,
Trait,
FnInStruct,
FnInEnum,
FnInTrait,
DefaultFnInTrait,
Fn,
Mod,
Constant,
AssociatedConstantInEnum,
AssociatedConstantInTrait,
AssociatedConstantInStruct,
Macro,
AssociatedTypeInEnum,
AssociatedTypeInTrait,
AssociatedTypeInStruct,
Typedef,
}
#[derive(Default)]
pub struct DocsUrlGenerator {
default_url: Option<String>,
base_urls: HashMap<String, String>,
}
impl DocsUrlGenerator {
pub fn with_base_urls(default_url: Option<String>, base_urls: HashMap<String, String>) -> Self {
Self {
default_url,
base_urls,
}
}
fn gen_for_rust_link(&self, rust_link: &RustLink) -> String {
use DocType::*;
let mut r = String::new();
let base = self
.base_urls
.get(rust_link.path.elements[0].as_str())
.map(String::as_str)
.or(self.default_url.as_deref())
r.push_str(base);
if !base.ends_with('/') {
r.push('/');
}
r.push_str(rust_link.path.elements[0].as_str());
r.push_str("/latest/");
}
let mut elements = rust_link.path.elements.iter().peekable();
let module_depth = rust_link.path.elements.len()
- match rust_link.typ {
Mod => 0,
Struct | Enum | Trait | Fn | Macro | Constant | Typedef => 1,
FnInEnum
| FnInStruct
| FnInTrait
| DefaultFnInTrait
| EnumVariant
| StructField
| AssociatedTypeInEnum
| AssociatedTypeInStruct
| AssociatedTypeInTrait
| AssociatedConstantInEnum
| AssociatedConstantInStruct
| AssociatedConstantInTrait => 2,
EnumVariantField => 3,
};
for _ in 0..module_depth {
r.push_str(elements.next().unwrap().as_str());
r.push('/');
}
if elements.peek().is_none() {
r.push_str("index.html");
return r;
}
r.push_str(match rust_link.typ {
Typedef => "type.",
Struct
| StructField
| FnInStruct
| AssociatedTypeInStruct
| AssociatedConstantInStruct => "struct.",
Enum
| EnumVariant
| EnumVariantField
| FnInEnum
| AssociatedTypeInEnum
| AssociatedConstantInEnum => "enum.",
Trait
| FnInTrait
| DefaultFnInTrait
| AssociatedTypeInTrait
| AssociatedConstantInTrait => "trait.",
Fn => "fn.",
Constant => "constant.",
Macro => "macro.",
Mod => unreachable!(),
});
r.push_str(elements.next().unwrap().as_str());
r.push_str(".html");
match rust_link.typ {
FnInStruct | FnInEnum | DefaultFnInTrait => {
r.push_str("#method.");
r.push_str(elements.next().unwrap().as_str());
}
AssociatedTypeInStruct | AssociatedTypeInEnum | AssociatedTypeInTrait => {
r.push_str("#associatedtype.");
r.push_str(elements.next().unwrap().as_str());
}
AssociatedConstantInStruct | AssociatedConstantInEnum | AssociatedConstantInTrait => {
r.push_str("#associatedconstant.");
r.push_str(elements.next().unwrap().as_str());
}
FnInTrait => {
r.push_str("#tymethod.");
r.push_str(elements.next().unwrap().as_str());
}
EnumVariant => {
r.push_str("#variant.");
r.push_str(elements.next().unwrap().as_str());
}
StructField => {
r.push_str("#structfield.");
r.push_str(elements.next().unwrap().as_str());
}
EnumVariantField => {
r.push_str("#variant.");
r.push_str(elements.next().unwrap().as_str());
r.push_str(".field.");
r.push_str(elements.next().unwrap().as_str());
}
_ => {}
}
r
}
}
#[test]
fn test_docs_url_generator() {
let test_cases = [
(
syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Struct)] },
),
(
syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, StructField)] },
),
(
syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Enum)] },
),
(
syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, EnumVariant)] },
),
(
syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, EnumVariantField)] },
),
(
syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Trait)] },
),
(
syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, FnInStruct)] },
),
(
syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, FnInEnum)] },
),
(
syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, FnInTrait)] },
),
(
syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, DefaultFnInTrait)] },
),
(
syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Fn)] },
),
(
syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Mod)] },
),
(
syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Constant)] },
),
(
syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Macro)] },
),
];
for (attr, expected) in test_cases.clone() {
assert_eq!(
DocsUrlGenerator::default().gen_for_rust_link(&Docs::from_attrs(&[attr]).1[0]),
expected
);
}
assert_eq!(
DocsUrlGenerator::with_base_urls(
None,
.into_iter()
.collect()
)
.gen_for_rust_link(&Docs::from_attrs(&[test_cases[0].0.clone()]).1[0]),
);
assert_eq!(
.gen_for_rust_link(&Docs::from_attrs(&[test_cases[0].0.clone()]).1[0]),
);
}