cmark_writer/writer/
processors.rs

1//! New node processor implementation
2//!
3//! Processor system rewritten with new trait architecture
4
5use crate::ast::Node;
6use crate::error::{WriteError, WriteResult};
7use crate::traits::{
8    BlockNodeProcessor, ConfigurableProcessor, InlineNodeProcessor, NodeProcessor, Writer,
9};
10
11/// Block processor configuration
12#[derive(Debug, Clone)]
13pub struct BlockProcessorConfig {
14    /// Whether to ensure trailing newlines
15    pub ensure_trailing_newlines: bool,
16    /// Block separator
17    pub block_separator: String,
18}
19
20impl Default for BlockProcessorConfig {
21    fn default() -> Self {
22        Self {
23            ensure_trailing_newlines: true,
24            block_separator: "\n\n".to_string(),
25        }
26    }
27}
28
29/// Inline processor configuration
30#[derive(Debug, Clone)]
31pub struct InlineProcessorConfig {
32    /// Enable strict validation mode
33    pub strict_validation: bool,
34    /// Allow newlines in inline elements
35    pub allow_newlines: bool,
36}
37
38impl Default for InlineProcessorConfig {
39    fn default() -> Self {
40        Self {
41            strict_validation: true,
42            allow_newlines: false,
43        }
44    }
45}
46
47/// Enhanced block node processor
48#[derive(Debug)]
49pub struct EnhancedBlockProcessor {
50    config: BlockProcessorConfig,
51}
52
53impl EnhancedBlockProcessor {
54    /// Create a new block processor
55    pub fn new() -> Self {
56        Self {
57            config: BlockProcessorConfig::default(),
58        }
59    }
60
61    /// Create with custom configuration
62    pub fn with_config(config: BlockProcessorConfig) -> Self {
63        Self { config }
64    }
65}
66
67impl Default for EnhancedBlockProcessor {
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73impl NodeProcessor for EnhancedBlockProcessor {
74    fn can_process(&self, node: &Node) -> bool {
75        matches!(
76            node,
77            Node::Document(_)
78                | Node::Heading { .. }
79                | Node::Paragraph(_)
80                | Node::BlockQuote(_)
81                | Node::CodeBlock { .. }
82                | Node::UnorderedList(_)
83                | Node::OrderedList { .. }
84                | Node::ThematicBreak
85                | Node::Table { .. }
86                | Node::HtmlBlock(_)
87                | Node::LinkReferenceDefinition { .. }
88        ) || matches!(node, Node::Custom(custom) if custom.is_block())
89    }
90
91    fn process_commonmark(
92        &self,
93        writer: &mut crate::writer::CommonMarkWriter,
94        node: &Node,
95    ) -> WriteResult<()> {
96        match node {
97            Node::Document(children) => {
98                for (i, child) in children.iter().enumerate() {
99                    if i > 0 {
100                        writer.write_str("\n\n")?;
101                    }
102                    writer.write_node(child)?;
103                }
104                Ok(())
105            }
106            Node::Heading {
107                level,
108                content,
109                heading_type,
110            } => writer.write_heading(*level, content, heading_type),
111            Node::Paragraph(content) => writer.write_paragraph(content),
112            Node::BlockQuote(content) => writer.write_blockquote(content),
113            Node::CodeBlock {
114                language,
115                content,
116                block_type,
117            } => writer.write_code_block(language, content, block_type),
118            Node::UnorderedList(items) => writer.write_unordered_list(items),
119            Node::OrderedList { start, items } => writer.write_ordered_list(items, *start, true),
120            Node::ThematicBreak => writer.write_thematic_break(),
121            #[cfg(feature = "gfm")]
122            Node::Table {
123                headers,
124                alignments,
125                rows,
126            } => writer.write_table_with_alignment(headers, alignments, rows),
127            #[cfg(not(feature = "gfm"))]
128            Node::Table { headers, rows, .. } => writer.write_table(headers, rows),
129            Node::HtmlBlock(content) => writer.write_html_block(content),
130            Node::LinkReferenceDefinition {
131                label,
132                destination,
133                title,
134            } => writer.write_link_reference_definition(label, destination, title),
135            Node::Custom(custom_node) if custom_node.is_block() => {
136                // CustomNode implements CommonMarkRenderable
137                custom_node.render_commonmark(writer)
138            }
139            _ => Err(WriteError::UnsupportedNodeType),
140        }?;
141
142        if self.config.ensure_trailing_newlines {
143            // Newline handling is now context-aware and automatic
144        }
145
146        Ok(())
147    }
148
149    fn process_html(&self, writer: &mut crate::writer::HtmlWriter, node: &Node) -> WriteResult<()> {
150        writer.write_node_internal(node).map_err(WriteError::from)
151    }
152
153    fn priority(&self) -> u32 {
154        100
155    }
156}
157
158impl BlockNodeProcessor for EnhancedBlockProcessor {
159    fn ensure_block_separation(&self, writer: &mut dyn Writer) -> WriteResult<()> {
160        writer.write_str(&self.config.block_separator)
161    }
162}
163
164impl ConfigurableProcessor for EnhancedBlockProcessor {
165    type Config = BlockProcessorConfig;
166
167    fn configure(&mut self, config: Self::Config) {
168        self.config = config;
169    }
170
171    fn config(&self) -> &Self::Config {
172        &self.config
173    }
174}
175
176/// Enhanced inline node processor
177#[derive(Debug)]
178pub struct EnhancedInlineProcessor {
179    config: InlineProcessorConfig,
180}
181
182impl EnhancedInlineProcessor {
183    /// Create a new inline processor
184    pub fn new() -> Self {
185        Self {
186            config: InlineProcessorConfig::default(),
187        }
188    }
189
190    /// Create with custom configuration
191    pub fn with_config(config: InlineProcessorConfig) -> Self {
192        Self { config }
193    }
194}
195
196impl Default for EnhancedInlineProcessor {
197    fn default() -> Self {
198        Self::new()
199    }
200}
201
202impl NodeProcessor for EnhancedInlineProcessor {
203    fn can_process(&self, node: &Node) -> bool {
204        matches!(
205            node,
206            Node::Text(_)
207                | Node::Emphasis(_)
208                | Node::Strong(_)
209                | Node::InlineCode(_)
210                | Node::Link { .. }
211                | Node::Image { .. }
212                | Node::Autolink { .. }
213                | Node::ReferenceLink { .. }
214                | Node::HtmlElement(_)
215                | Node::SoftBreak
216                | Node::HardBreak
217        ) || matches!(node, Node::Custom(custom) if !custom.is_block())
218            || (cfg!(feature = "gfm")
219                && matches!(node, Node::Strikethrough(_) | Node::ExtendedAutolink(_)))
220    }
221
222    fn process_commonmark(
223        &self,
224        writer: &mut crate::writer::CommonMarkWriter,
225        node: &Node,
226    ) -> WriteResult<()> {
227        if self.config.strict_validation {
228            self.validate_inline_content(node)?;
229        }
230
231        match node {
232            Node::Text(content) => writer.write_text_content(content),
233            Node::Emphasis(content) => writer.write_emphasis(content),
234            Node::Strong(content) => writer.write_strong(content),
235            #[cfg(feature = "gfm")]
236            Node::Strikethrough(content) => writer.write_strikethrough(content),
237            Node::InlineCode(content) => writer.write_code_content(content),
238            Node::Link {
239                url,
240                title,
241                content,
242            } => writer.write_link(url, title, content),
243            Node::Image { url, title, alt } => writer.write_image(url, title, alt),
244            Node::Autolink { url, is_email } => writer.write_autolink(url, *is_email),
245            #[cfg(feature = "gfm")]
246            Node::ExtendedAutolink(url) => writer.write_extended_autolink(url),
247            Node::ReferenceLink { label, content } => writer.write_reference_link(label, content),
248            Node::HtmlElement(element) => writer.write_html_element(element),
249            Node::SoftBreak => writer.write_soft_break(),
250            Node::HardBreak => writer.write_hard_break(),
251            Node::Custom(custom_node) if !custom_node.is_block() => {
252                custom_node.render_commonmark(writer)
253            }
254            _ => Err(WriteError::UnsupportedNodeType),
255        }
256    }
257
258    fn process_html(&self, writer: &mut crate::writer::HtmlWriter, node: &Node) -> WriteResult<()> {
259        writer.write_node_internal(node).map_err(WriteError::from)
260    }
261
262    fn priority(&self) -> u32 {
263        50
264    }
265}
266
267impl InlineNodeProcessor for EnhancedInlineProcessor {
268    fn validate_inline_content(&self, node: &Node) -> WriteResult<()> {
269        if !self.config.allow_newlines && !matches!(node, Node::SoftBreak | Node::HardBreak) {
270            // Recursive function to check for newlines in text content
271            fn check_for_newlines(node: &Node) -> Result<(), String> {
272                match node {
273                    // Direct text content nodes
274                    Node::Text(content) => {
275                        if content.contains('\n') {
276                            return Err(format!("Text node: {}", content));
277                        }
278                    }
279                    Node::InlineCode(content) => {
280                        if content.contains('\n') {
281                            return Err(format!("Inline code: {}", content));
282                        }
283                    }
284                    Node::Autolink { url, .. } => {
285                        if url.contains('\n') {
286                            return Err(format!("Autolink URL: {}", url));
287                        }
288                    }
289                    #[cfg(feature = "gfm")]
290                    Node::ExtendedAutolink(url) => {
291                        if url.contains('\n') {
292                            return Err(format!("Extended autolink URL: {}", url));
293                        }
294                    }
295
296                    // Nodes with child content that needs recursive checking
297                    Node::Emphasis(children)
298                    | Node::Strong(children)
299                    | Node::Strikethrough(children) => {
300                        for child in children {
301                            check_for_newlines(child)?;
302                        }
303                    }
304                    Node::Link {
305                        content,
306                        url,
307                        title,
308                        ..
309                    } => {
310                        // Check URL and title for newlines
311                        if url.contains('\n') {
312                            return Err(format!("Link URL: {}", url));
313                        }
314                        if let Some(title_text) = title {
315                            if title_text.contains('\n') {
316                                return Err(format!("Link title: {}", title_text));
317                            }
318                        }
319                        // Check content recursively
320                        for child in content {
321                            check_for_newlines(child)?;
322                        }
323                    }
324                    Node::ReferenceLink { content, label, .. } => {
325                        // Check label for newlines
326                        if label.contains('\n') {
327                            return Err(format!("Reference link label: {}", label));
328                        }
329                        // Check content recursively
330                        for child in content {
331                            check_for_newlines(child)?;
332                        }
333                    }
334                    Node::Image {
335                        alt, url, title, ..
336                    } => {
337                        // Check URL and title for newlines
338                        if url.contains('\n') {
339                            return Err(format!("Image URL: {}", url));
340                        }
341                        if let Some(title_text) = title {
342                            if title_text.contains('\n') {
343                                return Err(format!("Image title: {}", title_text));
344                            }
345                        }
346                        // Check alt text recursively
347                        for child in alt {
348                            check_for_newlines(child)?;
349                        }
350                    }
351
352                    // HTML elements might contain text, but we allow them for now
353                    Node::HtmlElement(_) => {
354                        // HTML elements are allowed to contain newlines as they might be formatted
355                    }
356
357                    // Custom nodes - delegate validation to the custom node implementation
358                    Node::Custom(_) => {
359                        // Custom nodes should handle their own validation
360                    }
361
362                    // Break nodes are explicitly allowed
363                    Node::SoftBreak | Node::HardBreak => {}
364
365                    // Other nodes shouldn't appear in inline context, but we don't error here
366                    _ => {}
367                }
368                Ok(())
369            }
370
371            if let Err(error_msg) = check_for_newlines(node) {
372                return Err(WriteError::NewlineInInlineElement(error_msg.into()));
373            }
374        }
375        Ok(())
376    }
377}
378
379impl ConfigurableProcessor for EnhancedInlineProcessor {
380    type Config = InlineProcessorConfig;
381
382    fn configure(&mut self, config: Self::Config) {
383        self.config = config;
384    }
385
386    fn config(&self) -> &Self::Config {
387        &self.config
388    }
389}
390
391/// Custom node processor
392#[derive(Debug, Default)]
393pub struct CustomNodeProcessor;
394
395impl NodeProcessor for CustomNodeProcessor {
396    fn can_process(&self, node: &Node) -> bool {
397        matches!(node, Node::Custom(_))
398    }
399
400    fn process_commonmark(
401        &self,
402        writer: &mut crate::writer::CommonMarkWriter,
403        node: &Node,
404    ) -> WriteResult<()> {
405        match node {
406            Node::Custom(custom_node) => {
407                custom_node.render_commonmark(writer)?;
408
409                if custom_node.is_block() {
410                    // Newline handling is now context-aware and automatic
411                }
412
413                Ok(())
414            }
415            _ => Err(WriteError::UnsupportedNodeType),
416        }
417    }
418
419    fn process_html(&self, writer: &mut crate::writer::HtmlWriter, node: &Node) -> WriteResult<()> {
420        match node {
421            Node::Custom(custom_node) => {
422                // Use the html_render method from CustomNode trait
423                custom_node.html_render(writer)
424            }
425            _ => Err(WriteError::UnsupportedNodeType),
426        }
427    }
428
429    fn priority(&self) -> u32 {
430        200 // High priority for custom node processing
431    }
432}