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/// use ecow::EcoString;
72///
73/// // Specified as an inline element with both CommonMark and HTML implementations
74/// #[derive(Debug, Clone, PartialEq)]
75/// #[custom_node(block=false, html_impl=true)]
76/// struct HighlightNode {
77///     content: EcoString,
78///     color: EcoString,
79/// }
80///
81/// impl HighlightNode {
82///     // Required for CommonMark rendering
83///     fn write_custom(&self, writer: &mut CommonMarkWriter) -> WriteResult<()> {
84///         writer.write_str("<span style=\"background-color: ")?;
85///         writer.write_str(&self.color)?;
86///         writer.write_str("\">")?;
87///         writer.write_str(&self.content)?;
88///         writer.write_str("</span>")?;
89///         Ok(())
90///     }
91///     
92///     // Optional HTML rendering implementation
93///     fn write_html_custom(&self, writer: &mut HtmlWriter) -> HtmlWriteResult<()> {
94///         writer.start_tag("span")?;
95///         writer.attribute("style", &format!("background-color: {}", self.color))?;
96///         writer.finish_tag()?;
97///         writer.text(&self.content)?;
98///         writer.end_tag("span")?;
99///         Ok(())
100///     }
101/// }
102///
103/// // Only CommonMark implementation, default HTML implementation
104/// #[derive(Debug, Clone, PartialEq)]
105/// #[custom_node(block=true)]
106/// struct AlertNode {
107///     content: EcoString,
108/// }
109///
110/// impl AlertNode {
111///     fn write_custom(&self, writer: &mut CommonMarkWriter) -> WriteResult<()> {
112///         writer.write_str("<div class=\"alert\">")?;
113///         writer.write_str(&self.content)?;
114///         writer.write_str("</div>")?;
115///         Ok(())
116///     }
117/// }
118/// ```
119#[proc_macro_attribute]
120pub fn custom_node(attr: TokenStream, item: TokenStream) -> TokenStream {
121    let args = syn::parse_macro_input!(attr as CustomNodeArgs);
122    let input = parse_macro_input!(item as DeriveInput);
123    let name = &input.ident;
124
125    // Configure is_block implementation
126    let is_block_impl = if let Some(is_block) = args.is_block {
127        quote! {
128            fn is_block(&self) -> bool {
129                #is_block
130            }
131        }
132    } else {
133        quote! {
134            fn is_block(&self) -> bool {
135                self.is_block_custom()
136            }
137        }
138    };
139
140    // Configure html_write implementation
141    let html_write_impl = if args.html_impl.unwrap_or(false) {
142        // When html_impl=true, expect user to implement write_html_custom method
143        quote! {
144            fn html_write(
145                &self,
146                writer: &mut ::cmark_writer::writer::HtmlWriter,
147            ) -> ::cmark_writer::writer::HtmlWriteResult<()> {
148                self.write_html_custom(writer)
149            }
150        }
151    } else {
152        // When html_impl is not set or false, use default implementation
153        quote! {
154            fn html_write(
155                &self,
156                writer: &mut ::cmark_writer::writer::HtmlWriter,
157            ) -> ::cmark_writer::writer::HtmlWriteResult<()> {
158                writer.raw_html(&format!(
159                    "<!-- HTML rendering not implemented for Custom Node: {} -->",
160                    self.type_name()
161                ))?;
162                Ok(())
163            }
164        }
165    };
166
167    let expanded = quote! {
168        #input
169
170        impl ::cmark_writer::ast::CustomNode for #name {
171            fn write(
172                &self,
173                writer: &mut ::cmark_writer::writer::CommonMarkWriter,
174            ) -> ::cmark_writer::error::WriteResult<()> {
175                self.write_custom(writer)
176            }
177
178            #html_write_impl
179
180            fn clone_box(&self) -> Box<dyn ::cmark_writer::ast::CustomNode> {
181                Box::new(self.clone())
182            }
183
184            fn eq_box(&self, other: &dyn ::cmark_writer::ast::CustomNode) -> bool {
185                if let Some(other) = other.as_any().downcast_ref::<Self>() {
186                    self == other
187                } else {
188                    false
189                }
190            }
191
192            #is_block_impl
193
194            fn as_any(&self) -> &dyn std::any::Any {
195                self
196            }
197
198            fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
199                self
200            }
201        }
202
203        impl #name {
204            pub fn matches(node: &dyn ::cmark_writer::ast::CustomNode) -> bool {
205                node.type_name() == std::any::type_name::<#name>() ||
206                    node.as_any().downcast_ref::<#name>().is_some()
207            }
208
209            pub fn extract(node: Box<dyn ::cmark_writer::ast::CustomNode>) -> Option<#name> {
210                node.as_any().downcast_ref::<#name>().map(|n| n.clone())
211            }
212        }
213    };
214
215    TokenStream::from(expanded)
216}
217
218/// Custom error attribute macro, replaces the struct form errors in the original define_custom_errors! macro
219///
220/// # Example
221///
222/// ```rust
223/// use cmark_writer_macros::structure_error;
224///
225/// #[structure_error(format = "Table column mismatch: {}")]
226/// struct TableColumnMismatchError(pub &'static str);
227/// ```
228#[proc_macro_attribute]
229pub fn structure_error(attr: TokenStream, item: TokenStream) -> TokenStream {
230    let attr_str = attr.to_string();
231    let input = parse_macro_input!(item as DeriveInput);
232    let name = &input.ident;
233
234    // Parse format attribute
235    let format = if attr_str.starts_with("format") {
236        let format_str = attr_str
237            .replace("format", "")
238            .replace("=", "")
239            .trim()
240            .trim_matches('"')
241            .to_string();
242        format_str
243    } else {
244        // Default error message if format not specified
245        "{}".to_string()
246    };
247
248    let expanded = quote! {
249        #input
250
251        impl #name {
252            pub fn new(message: &'static str) -> Self {
253                Self(message)
254            }
255
256            pub fn into_error(self) -> ::cmark_writer::error::WriteError {
257                let mut error_factory = ::cmark_writer::error::StructureError::new(#format);
258
259                let arg = self.0.to_string();
260                error_factory = error_factory.arg(arg);
261
262                <::cmark_writer::error::StructureError as ::cmark_writer::error::CustomErrorFactory>::create_error(&error_factory)
263            }
264        }
265
266        impl From<#name> for ::cmark_writer::error::WriteError {
267            fn from(factory: #name) -> Self {
268                factory.into_error()
269            }
270        }
271
272        impl ::cmark_writer::error::CustomErrorFactory for #name {
273            fn create_error(&self) -> ::cmark_writer::error::WriteError {
274                let mut error_factory = ::cmark_writer::error::StructureError::new(#format);
275
276                let arg = self.0.to_string();
277                error_factory = error_factory.arg(arg);
278
279                <::cmark_writer::error::StructureError as ::cmark_writer::error::CustomErrorFactory>::create_error(&error_factory)
280            }
281        }
282    };
283
284    TokenStream::from(expanded)
285}
286
287/// Custom coded error attribute macro, replaces the coded form errors in the original define_custom_errors! macro
288///
289/// # Example
290///
291/// ```rust
292/// use cmark_writer_macros::coded_error;
293///
294/// #[coded_error]
295/// struct MarkdownSyntaxError(pub &'static str, pub &'static str);
296/// ```
297#[proc_macro_attribute]
298pub fn coded_error(_attr: TokenStream, item: TokenStream) -> TokenStream {
299    let input = parse_macro_input!(item as DeriveInput);
300    let name = &input.ident;
301
302    let expanded = quote! {
303        #input
304
305        impl #name {
306            pub fn new(message: &str, code: &str) -> Self {
307                Self(message.to_string(), code.to_string())
308            }
309
310            pub fn into_error(self) -> ::cmark_writer::error::WriteError {
311                let coded_error = ::cmark_writer::error::CodedError::new(self.0, self.1);
312                <::cmark_writer::error::CodedError as ::cmark_writer::error::CustomErrorFactory>::create_error(&coded_error)
313            }
314        }
315
316        impl From<#name> for ::cmark_writer::error::WriteError {
317            fn from(factory: #name) -> Self {
318                factory.into_error()
319            }
320        }
321
322        impl ::cmark_writer::error::CustomErrorFactory for #name {
323            fn create_error(&self) -> ::cmark_writer::error::WriteError {
324                let coded_error = ::cmark_writer::error::CodedError::new(self.0.clone(), self.1.clone());
325                <::cmark_writer::error::CodedError as ::cmark_writer::error::CustomErrorFactory>::create_error(&coded_error)
326            }
327        }
328    };
329
330    TokenStream::from(expanded)
331}