svgbobdoc/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(rust_2018_idioms)]
3use proc_macro2::{Span, TokenStream};
4use quote::{quote, ToTokens};
5use syn::{
6    self,
7    parse::{Parse, ParseStream},
8    parse_macro_input, AttrStyle, Attribute, Error, Lit, LitStr, Meta, MetaNameValue, Result,
9};
10
11mod textproc;
12
13/// An `Attribute`, recognized as a doc comment or not.
14#[derive(Clone)]
15enum MaybeDocAttr {
16    /// A doc comment attribute.
17    ///
18    /// The first `Attribute` only specifies the surround tokens.
19    ///
20    /// `MetaNameValue::lit` must be a `Lit::Str(_)`.
21    Doc(Attribute, MetaNameValue),
22    /// An unrecognized attribute that we don't care.
23    Other(Attribute),
24}
25
26impl MaybeDocAttr {
27    fn from_attribute(attr: Attribute) -> Result<Self> {
28        if attr.path.is_ident("doc") {
29            let meta = attr.parse_meta()?;
30
31            if let Meta::NameValue(nv) = meta {
32                if let Lit::Str(_) = nv.lit {
33                    Ok(MaybeDocAttr::Doc(attr, nv))
34                } else {
35                    Err(Error::new(nv.lit.span(), "doc comment must be a string"))
36                }
37            } else {
38                // Ignore unrecognized form
39                Ok(MaybeDocAttr::Other(attr))
40            }
41        } else {
42            Ok(MaybeDocAttr::Other(attr))
43        }
44    }
45}
46
47impl ToTokens for MaybeDocAttr {
48    fn to_tokens(&self, tokens: &mut TokenStream) {
49        match self {
50            MaybeDocAttr::Doc(attr, nv) => {
51                attr.pound_token.to_tokens(tokens);
52                if let AttrStyle::Inner(ref b) = attr.style {
53                    b.to_tokens(tokens);
54                }
55                attr.bracket_token.surround(tokens, |tokens| {
56                    nv.to_tokens(tokens);
57                });
58            }
59            MaybeDocAttr::Other(attr) => attr.to_tokens(tokens),
60        }
61    }
62}
63
64impl Into<Attribute> for MaybeDocAttr {
65    /// The mostly-lossless conversion to `Attribute`.
66    fn into(self) -> Attribute {
67        match self {
68            MaybeDocAttr::Doc(mut attr, nv) => {
69                let lit = nv.lit;
70                attr.tokens = quote! { = #lit };
71                attr
72            }
73            MaybeDocAttr::Other(attr) => attr,
74        }
75    }
76}
77
78enum StrOrDocAttrs {
79    Str(LitStr),
80    Attrs(Vec<syn::Attribute>),
81}
82
83impl Parse for StrOrDocAttrs {
84    fn parse(input: ParseStream<'_>) -> Result<Self> {
85        if let Ok(lit_str) = input.parse() {
86            Ok(Self::Str(lit_str))
87        } else {
88            // `#[doc = ...]` sequence
89            let mut attrs = Attribute::parse_inner(input)?;
90            attrs.extend(Attribute::parse_outer(input)?);
91            Ok(Self::Attrs(attrs))
92        }
93    }
94}
95
96/// Render ASCII-diagram code blocks in a Markdown-formatted string literal or
97/// zero or more `#[doc = ...]` attributes as SVG images.
98///
99/// See [the module-level documentation](../index.html) for more.
100#[proc_macro]
101pub fn transform(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
102    let input: StrOrDocAttrs = parse_macro_input!(tokens);
103    let (mut iter1, mut iter2);
104    let iter: &mut dyn Iterator<Item = Result<LitStr>> = match input {
105        StrOrDocAttrs::Str(s) => {
106            iter1 = std::iter::once(Ok(s));
107            &mut iter1
108        }
109        StrOrDocAttrs::Attrs(attrs) => {
110            iter2 = attrs
111                .into_iter()
112                .map(|attr| match MaybeDocAttr::from_attribute(attr)? {
113                    MaybeDocAttr::Doc(
114                        _,
115                        syn::MetaNameValue {
116                            lit: syn::Lit::Str(s),
117                            ..
118                        },
119                    ) => Ok(s),
120                    MaybeDocAttr::Doc(attr, _) | MaybeDocAttr::Other(attr) => {
121                        Err(Error::new_spanned(
122                            &attr,
123                            "only `#[doc = ...]` attributes or a string literal are allowed here",
124                        ))
125                    }
126                });
127            &mut iter2
128        }
129    };
130
131    handle_error(|| {
132        let mut output = String::new();
133        use textproc::{TextProcOutput, TextProcState};
134        let mut text_proc = TextProcState::new();
135        for lit_str in iter {
136            let lit_str = lit_str?;
137            let st = lit_str.value();
138            match text_proc.step(&st, lit_str.span()) {
139                TextProcOutput::Passthrough => output.push_str(&st),
140                TextProcOutput::Fragment(fr) => output.push_str(&fr),
141                TextProcOutput::Empty => {}
142            }
143            output.push_str("\n");
144        }
145        text_proc.finalize()?;
146
147        Ok(LitStr::new(&output, Span::call_site())
148            .into_token_stream()
149            .into())
150    })
151}
152
153fn handle_error(cb: impl FnOnce() -> Result<proc_macro::TokenStream>) -> proc_macro::TokenStream {
154    match cb() {
155        Ok(tokens) => tokens,
156        Err(e) => e.to_compile_error().into(),
157    }
158}