1use crate::ast::Node;
6use crate::error::{WriteError, WriteResult};
7use crate::traits::{
8 BlockNodeProcessor, ConfigurableProcessor, InlineNodeProcessor, NodeProcessor, Writer,
9};
10
11#[derive(Debug, Clone)]
13pub struct BlockProcessorConfig {
14 pub ensure_trailing_newlines: bool,
16 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#[derive(Debug, Clone)]
31pub struct InlineProcessorConfig {
32 pub strict_validation: bool,
34 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#[derive(Debug)]
49pub struct EnhancedBlockProcessor {
50 config: BlockProcessorConfig,
51}
52
53impl EnhancedBlockProcessor {
54 pub fn new() -> Self {
56 Self {
57 config: BlockProcessorConfig::default(),
58 }
59 }
60
61 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 custom_node.render_commonmark(writer)
138 }
139 _ => Err(WriteError::UnsupportedNodeType),
140 }?;
141
142 if self.config.ensure_trailing_newlines {
143 }
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#[derive(Debug)]
178pub struct EnhancedInlineProcessor {
179 config: InlineProcessorConfig,
180}
181
182impl EnhancedInlineProcessor {
183 pub fn new() -> Self {
185 Self {
186 config: InlineProcessorConfig::default(),
187 }
188 }
189
190 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 fn check_for_newlines(node: &Node) -> Result<(), String> {
272 match node {
273 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 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 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 for child in content {
321 check_for_newlines(child)?;
322 }
323 }
324 Node::ReferenceLink { content, label, .. } => {
325 if label.contains('\n') {
327 return Err(format!("Reference link label: {}", label));
328 }
329 for child in content {
331 check_for_newlines(child)?;
332 }
333 }
334 Node::Image {
335 alt, url, title, ..
336 } => {
337 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 for child in alt {
348 check_for_newlines(child)?;
349 }
350 }
351
352 Node::HtmlElement(_) => {
354 }
356
357 Node::Custom(_) => {
359 }
361
362 Node::SoftBreak | Node::HardBreak => {}
364
365 _ => {}
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#[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 }
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 custom_node.html_render(writer)
424 }
425 _ => Err(WriteError::UnsupportedNodeType),
426 }
427 }
428
429 fn priority(&self) -> u32 {
430 200 }
432}