cmark_writer_macros/
lib.rs

1extern crate proc_macro;
2use proc_macro::TokenStream;
3use quote::quote;
4use syn::{
5    parse::Parse, parse::ParseStream, parse_macro_input, DeriveInput, Ident, LitBool, Token,
6};
7
8/// Parse custom_node attribute parameters
9struct CustomNodeArgs {
10    is_block: Option<bool>,
11    html_impl: Option<bool>,
12}
13
14impl Parse for CustomNodeArgs {
15    fn parse(input: ParseStream) -> syn::Result<Self> {
16        let mut is_block = None;
17        let mut html_impl = None;
18
19        if input.is_empty() {
20            return Ok(CustomNodeArgs {
21                is_block,
22                html_impl,
23            });
24        }
25
26        loop {
27            if input.is_empty() {
28                break;
29            }
30
31            let ident: Ident = input.parse()?;
32
33            if ident == "block" {
34                let _: Token![=] = input.parse()?;
35                let value: LitBool = input.parse()?;
36                is_block = Some(value.value);
37            } else if ident == "html_impl" {
38                let _: Token![=] = input.parse()?;
39                let value: LitBool = input.parse()?;
40                html_impl = Some(value.value);
41            } else {
42                return Err(syn::Error::new_spanned(
43                    ident,
44                    "Unknown attribute parameter",
45                ));
46            }
47
48            // Handle optional comma separator
49            if input.peek(Token![,]) {
50                let _: Token![,] = input.parse()?;
51            }
52        }
53
54        Ok(CustomNodeArgs {
55            is_block,
56            html_impl,
57        })
58    }
59}
60
61/// Custom node attribute macro for implementing the CustomNode trait
62///
63/// This macro automatically implements the CustomNode trait. Users can specify
64/// whether the node is a block element using the `block` parameter and whether
65/// it implements HTML rendering with the `html_impl` parameter.
66///
67/// # Example
68///
69/// ```rust
70/// use cmark_writer_macros::custom_node;
71///
72/// // Specified as an inline element with both CommonMark and HTML implementations
73/// #[derive(Debug, Clone, PartialEq)]
74/// #[custom_node(block=false, html_impl=true)]
75/// struct HighlightNode {
76///     content: String,
77///     color: String,
78/// }
79///
80/// impl HighlightNode {
81///     // Required for CommonMark rendering
82///     fn write_custom(&self, writer: &mut CommonMarkWriter) -> WriteResult<()> {
83///         writer.write_str("<span style=\"background-color: ")?;
84///         writer.write_str(&self.color)?;
85///         writer.write_str("\">")?;
86///         writer.write_str(&self.content)?;
87///         writer.write_str("</span>")?;
88///         Ok(())
89///     }
90///     
91///     // Optional HTML rendering implementation
92///     fn write_html_custom(&self, writer: &mut HtmlWriter) -> HtmlWriteResult<()> {
93///         writer.start_tag("span")?;
94///         writer.attribute("style", &format!("background-color: {}", self.color))?;
95///         writer.finish_tag()?;
96///         writer.text(&self.content)?;
97///         writer.end_tag("span")?;
98///         Ok(())
99///     }
100/// }
101///
102/// // Only CommonMark implementation, default HTML implementation
103/// #[derive(Debug, Clone, PartialEq)]
104/// #[custom_node(block=true)]
105/// struct AlertNode {
106///     content: String,
107/// }
108///
109/// impl AlertNode {
110///     fn write_custom(&self, writer: &mut CommonMarkWriter) -> WriteResult<()> {
111///         writer.write_str("<div class=\"alert\">")?;
112///         writer.write_str(&self.content)?;
113///         writer.write_str("</div>")?;
114///         Ok(())
115///     }
116/// }
117/// ```
118#[proc_macro_attribute]
119pub fn custom_node(attr: TokenStream, item: TokenStream) -> TokenStream {
120    let args = syn::parse_macro_input!(attr as CustomNodeArgs);
121    let input = parse_macro_input!(item as DeriveInput);
122    let name = &input.ident;
123
124    // Configure is_block implementation
125    let is_block_impl = if let Some(is_block) = args.is_block {
126        quote! {
127            fn is_block(&self) -> bool {
128                #is_block
129            }
130        }
131    } else {
132        quote! {
133            fn is_block(&self) -> bool {
134                self.is_block_custom()
135            }
136        }
137    };
138
139    // Configure html_write implementation
140    let html_write_impl = if args.html_impl.unwrap_or(false) {
141        // When html_impl=true, expect user to implement write_html_custom method
142        quote! {
143            fn html_write(
144                &self,
145                writer: &mut ::cmark_writer::writer::HtmlWriter,
146            ) -> ::cmark_writer::writer::HtmlWriteResult<()> {
147                self.write_html_custom(writer)
148            }
149        }
150    } else {
151        // When html_impl is not set or false, use default implementation
152        quote! {
153            fn html_write(
154                &self,
155                writer: &mut ::cmark_writer::writer::HtmlWriter,
156            ) -> ::cmark_writer::writer::HtmlWriteResult<()> {
157                writer.raw_html(&format!(
158                    "<!-- HTML rendering not implemented for Custom Node: {} -->",
159                    self.type_name()
160                ))?;
161                Ok(())
162            }
163        }
164    };
165
166    let expanded = quote! {
167        #input
168
169        impl ::cmark_writer::ast::CustomNode for #name {
170            fn write(
171                &self,
172                writer: &mut ::cmark_writer::writer::CommonMarkWriter,
173            ) -> ::cmark_writer::error::WriteResult<()> {
174                self.write_custom(writer)
175            }
176
177            #html_write_impl
178
179            fn clone_box(&self) -> Box<dyn ::cmark_writer::ast::CustomNode> {
180                Box::new(self.clone())
181            }
182
183            fn eq_box(&self, other: &dyn ::cmark_writer::ast::CustomNode) -> bool {
184                if let Some(other) = other.as_any().downcast_ref::<Self>() {
185                    self == other
186                } else {
187                    false
188                }
189            }
190
191            #is_block_impl
192
193            fn as_any(&self) -> &dyn std::any::Any {
194                self
195            }
196
197            fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
198                self
199            }
200        }
201
202        impl #name {
203            pub fn matches(node: &dyn ::cmark_writer::ast::CustomNode) -> bool {
204                node.type_name() == std::any::type_name::<#name>() ||
205                    node.as_any().downcast_ref::<#name>().is_some()
206            }
207
208            pub fn extract(node: Box<dyn ::cmark_writer::ast::CustomNode>) -> Option<#name> {
209                node.as_any().downcast_ref::<#name>().map(|n| n.clone())
210            }
211        }
212    };
213
214    TokenStream::from(expanded)
215}
216
217/// Custom error attribute macro, replaces the struct form errors in the original define_custom_errors! macro
218///
219/// # Example
220///
221/// ```rust
222/// use cmark_writer_macros::structure_error;
223///
224/// #[structure_error(format = "Table column mismatch: {}")]
225/// struct TableColumnMismatchError(pub &'static str);
226/// ```
227#[proc_macro_attribute]
228pub fn structure_error(attr: TokenStream, item: TokenStream) -> TokenStream {
229    let attr_str = attr.to_string();
230    let input = parse_macro_input!(item as DeriveInput);
231    let name = &input.ident;
232
233    // Parse format attribute
234    let format = if attr_str.starts_with("format") {
235        let format_str = attr_str
236            .replace("format", "")
237            .replace("=", "")
238            .trim()
239            .trim_matches('"')
240            .to_string();
241        format_str
242    } else {
243        // Default error message if format not specified
244        "{}".to_string()
245    };
246
247    let expanded = quote! {
248        #input
249
250        impl #name {
251            pub fn new(message: &'static str) -> Self {
252                Self(message)
253            }
254
255            pub fn into_error(self) -> ::cmark_writer::error::WriteError {
256                let mut error_factory = ::cmark_writer::error::StructureError::new(#format);
257
258                let arg = self.0.to_string();
259                error_factory = error_factory.arg(arg);
260
261                <::cmark_writer::error::StructureError as ::cmark_writer::error::CustomErrorFactory>::create_error(&error_factory)
262            }
263        }
264
265        impl From<#name> for ::cmark_writer::error::WriteError {
266            fn from(factory: #name) -> Self {
267                factory.into_error()
268            }
269        }
270
271        impl ::cmark_writer::error::CustomErrorFactory for #name {
272            fn create_error(&self) -> ::cmark_writer::error::WriteError {
273                let mut error_factory = ::cmark_writer::error::StructureError::new(#format);
274
275                let arg = self.0.to_string();
276                error_factory = error_factory.arg(arg);
277
278                <::cmark_writer::error::StructureError as ::cmark_writer::error::CustomErrorFactory>::create_error(&error_factory)
279            }
280        }
281    };
282
283    TokenStream::from(expanded)
284}
285
286/// Custom coded error attribute macro, replaces the coded form errors in the original define_custom_errors! macro
287///
288/// # Example
289///
290/// ```rust
291/// use cmark_writer_macros::coded_error;
292///
293/// #[coded_error]
294/// struct MarkdownSyntaxError(pub &'static str, pub &'static str);
295/// ```
296#[proc_macro_attribute]
297pub fn coded_error(_attr: TokenStream, item: TokenStream) -> TokenStream {
298    let input = parse_macro_input!(item as DeriveInput);
299    let name = &input.ident;
300
301    let expanded = quote! {
302        #input
303
304        impl #name {
305            pub fn new(message: &str, code: &str) -> Self {
306                Self(message.to_string(), code.to_string())
307            }
308
309            pub fn into_error(self) -> ::cmark_writer::error::WriteError {
310                let coded_error = ::cmark_writer::error::CodedError::new(self.0, self.1);
311                <::cmark_writer::error::CodedError as ::cmark_writer::error::CustomErrorFactory>::create_error(&coded_error)
312            }
313        }
314
315        impl From<#name> for ::cmark_writer::error::WriteError {
316            fn from(factory: #name) -> Self {
317                factory.into_error()
318            }
319        }
320
321        impl ::cmark_writer::error::CustomErrorFactory for #name {
322            fn create_error(&self) -> ::cmark_writer::error::WriteError {
323                let coded_error = ::cmark_writer::error::CodedError::new(self.0.clone(), self.1.clone());
324                <::cmark_writer::error::CodedError as ::cmark_writer::error::CustomErrorFactory>::create_error(&coded_error)
325            }
326        }
327    };
328
329    TokenStream::from(expanded)
330}