cmark_writer_macros/
lib.rs

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