cmark_writer/writer/cmark/
core.rs

1#[cfg(feature = "gfm")]
2use crate::ast::TableAlignment;
3use crate::ast::{CodeBlockType, CustomNode, HeadingType, ListItem, Node};
4use crate::error::{WriteError, WriteResult};
5use crate::options::WriterOptions;
6use crate::writer::runtime::diagnostics::{Diagnostic, DiagnosticSink, NullSink};
7use crate::writer::runtime::proxy::{BlockWriterProxy, InlineWriterProxy};
8use crate::writer::runtime::visitor::{walk_node, NodeHandler};
9use ecow::EcoString;
10use log;
11use std::fmt;
12
13use super::format::FormatPolicy;
14use super::utils::node_contains_newline;
15
16/// CommonMark writer responsible for serializing AST nodes to CommonMark text.
17pub struct CommonMarkWriter {
18    /// Writer options.
19    pub options: WriterOptions,
20    /// Buffer for storing the output text.
21    pub buffer: EcoString,
22    /// Formatting policy responsible for whitespace and newline management.
23    format: FormatPolicy,
24    /// Sink for collecting non-fatal diagnostics during rendering.
25    diagnostics: Box<dyn DiagnosticSink + 'static>,
26}
27
28impl fmt::Debug for CommonMarkWriter {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        f.debug_struct("CommonMarkWriter")
31            .field("options", &self.options)
32            .field("buffer", &self.buffer)
33            .field("format", &self.format)
34            .finish()
35    }
36}
37
38impl CommonMarkWriter {
39    /// Create a new CommonMark writer with default options.
40    pub fn new() -> Self {
41        Self::with_options(WriterOptions::default())
42    }
43
44    /// Create a new CommonMark writer with specified options.
45    pub fn with_options(options: WriterOptions) -> Self {
46        Self {
47            options,
48            buffer: EcoString::new(),
49            format: FormatPolicy,
50            diagnostics: Box::new(NullSink),
51        }
52    }
53
54    /// Replace the diagnostic sink used to capture non-fatal issues.
55    pub fn with_diagnostic_sink(mut self, sink: Box<dyn DiagnosticSink + 'static>) -> Self {
56        self.diagnostics = sink;
57        self
58    }
59
60    /// Swap the diagnostic sink on an existing writer.
61    pub fn set_diagnostic_sink(&mut self, sink: Box<dyn DiagnosticSink + 'static>) {
62        self.diagnostics = sink;
63    }
64
65    /// Get a mutable handle to the diagnostic sink.
66    pub fn diagnostic_sink(&mut self) -> &mut dyn DiagnosticSink {
67        self.diagnostics.as_mut()
68    }
69
70    /// Whether the writer is in strict mode.
71    pub(crate) fn is_strict_mode(&self) -> bool {
72        self.options.strict
73    }
74
75    /// Write an AST node as CommonMark format.
76    pub fn write(&mut self, node: &Node) -> WriteResult<()> {
77        walk_node(self, node)
78    }
79
80    /// Write a custom node using its implementation.
81    pub(crate) fn write_custom_node(&mut self, node: &dyn CustomNode) -> WriteResult<()> {
82        if node.is_block() {
83            let mut proxy = BlockWriterProxy::new(self);
84            node.write_block(&mut proxy)
85        } else {
86            let mut proxy = InlineWriterProxy::new(self);
87            node.write_inline(&mut proxy)
88        }
89    }
90
91    /// Check if the inline node contains a newline character and return an error if it does.
92    pub(crate) fn check_no_newline(&mut self, node: &Node, context: &str) -> WriteResult<()> {
93        if node_contains_newline(node) {
94            if self.is_strict_mode() {
95                return Err(WriteError::NewlineInInlineElement(
96                    context.to_string().into(),
97                ));
98            } else {
99                self.emit_warning(format!(
100                    "Newline character found in inline element '{context}', but non-strict mode allows it (output may be affected)."
101                ));
102            }
103        }
104        Ok(())
105    }
106
107    /// Run a write operation using an isolated buffer and return the generated content.
108    pub(crate) fn capture_with_buffer<F>(&mut self, f: F) -> WriteResult<EcoString>
109    where
110        F: FnOnce(&mut Self) -> WriteResult<()>,
111    {
112        let mut outer = EcoString::new();
113        std::mem::swap(&mut self.buffer, &mut outer);
114
115        let write_result = f(self);
116        let captured = std::mem::take(&mut self.buffer);
117        std::mem::swap(&mut self.buffer, &mut outer);
118
119        write_result?;
120        Ok(captured)
121    }
122
123    /// Get the generated CommonMark format text.
124    pub fn into_string(self) -> EcoString {
125        self.buffer
126    }
127
128    /// Write a string to the output buffer.
129    pub fn write_str(&mut self, s: &str) -> WriteResult<()> {
130        self.buffer.push_str(s);
131        Ok(())
132    }
133
134    /// Write a character to the output buffer.
135    pub fn write_char(&mut self, c: char) -> WriteResult<()> {
136        self.buffer.push(c);
137        Ok(())
138    }
139
140    /// Ensure content ends with a newline (for consistent handling at the end of block nodes).
141    pub(crate) fn ensure_trailing_newline(&mut self) -> WriteResult<()> {
142        self.format.ensure_trailing_newline(&mut self.buffer)
143    }
144
145    /// Ensure there is a blank line (two consecutive newlines) at the end of the buffer.
146    pub(crate) fn ensure_blank_line(&mut self) -> WriteResult<()> {
147        self.format.ensure_blank_line(&mut self.buffer)
148    }
149
150    /// Prepare spacing between consecutive nodes in a block context.
151    pub(crate) fn prepare_block_sequence(
152        &mut self,
153        previous_was_block: bool,
154        next_is_block: bool,
155    ) -> WriteResult<()> {
156        self.format
157            .prepare_block_sequence(&mut self.buffer, previous_was_block, next_is_block)
158    }
159
160    /// Emit a warning through the diagnostic sink and logger.
161    pub(crate) fn emit_warning<S: Into<EcoString>>(&mut self, message: S) {
162        let message = message.into();
163        self.diagnostics.emit(Diagnostic::warning(message.clone()));
164        log::warn!("{message}");
165    }
166
167    /// Emit an informational message through the diagnostic sink and logger.
168    pub(crate) fn emit_info<S: Into<EcoString>>(&mut self, message: S) {
169        let message = message.into();
170        self.diagnostics.emit(Diagnostic::info(message.clone()));
171        log::info!("{message}");
172    }
173}
174
175impl NodeHandler for CommonMarkWriter {
176    type Error = WriteError;
177
178    fn document(&mut self, children: &[Node]) -> WriteResult<()> {
179        self.write_document(children)
180    }
181
182    fn paragraph(&mut self, content: &[Node]) -> WriteResult<()> {
183        self.write_paragraph(content)
184    }
185
186    fn text(&mut self, text: &EcoString) -> WriteResult<()> {
187        self.write_text_content(text)
188    }
189
190    fn emphasis(&mut self, content: &[Node]) -> WriteResult<()> {
191        self.write_emphasis(content)
192    }
193
194    fn strong(&mut self, content: &[Node]) -> WriteResult<()> {
195        self.write_strong(content)
196    }
197
198    fn thematic_break(&mut self) -> WriteResult<()> {
199        self.write_thematic_break()
200    }
201
202    fn heading(
203        &mut self,
204        level: u8,
205        content: &[Node],
206        heading_type: &HeadingType,
207    ) -> WriteResult<()> {
208        self.write_heading(level, content, heading_type)
209    }
210
211    fn inline_code(&mut self, code: &EcoString) -> WriteResult<()> {
212        self.write_code_content(code)
213    }
214
215    fn code_block(
216        &mut self,
217        language: &Option<EcoString>,
218        content: &EcoString,
219        block_type: &CodeBlockType,
220    ) -> WriteResult<()> {
221        self.write_code_block(language, content, block_type)
222    }
223
224    fn html_block(&mut self, content: &EcoString) -> WriteResult<()> {
225        self.write_html_block(content)
226    }
227
228    fn html_element(&mut self, element: &crate::ast::HtmlElement) -> WriteResult<()> {
229        self.write_html_element(element)
230    }
231
232    fn block_quote(&mut self, content: &[Node]) -> WriteResult<()> {
233        self.write_blockquote(content)
234    }
235
236    fn unordered_list(&mut self, items: &[ListItem]) -> WriteResult<()> {
237        self.write_unordered_list(items)
238    }
239
240    fn ordered_list(&mut self, start: u32, items: &[ListItem]) -> WriteResult<()> {
241        self.write_ordered_list(start, items)
242    }
243
244    #[cfg(feature = "gfm")]
245    fn table(
246        &mut self,
247        headers: &[Node],
248        alignments: &[TableAlignment],
249        rows: &[Vec<Node>],
250    ) -> WriteResult<()> {
251        self.write_table_with_alignment(headers, alignments, rows)
252    }
253
254    #[cfg(not(feature = "gfm"))]
255    fn table(&mut self, headers: &[Node], rows: &[Vec<Node>]) -> WriteResult<()> {
256        self.write_table(headers, rows)
257    }
258
259    fn link(
260        &mut self,
261        url: &EcoString,
262        title: &Option<EcoString>,
263        content: &[Node],
264    ) -> WriteResult<()> {
265        self.write_link(url, title, content)
266    }
267
268    fn image(
269        &mut self,
270        url: &EcoString,
271        title: &Option<EcoString>,
272        alt: &[Node],
273    ) -> WriteResult<()> {
274        self.write_image(url, title, alt)
275    }
276
277    fn soft_break(&mut self) -> WriteResult<()> {
278        self.write_soft_break()
279    }
280
281    fn hard_break(&mut self) -> WriteResult<()> {
282        self.write_hard_break()
283    }
284
285    fn autolink(&mut self, url: &EcoString, is_email: bool) -> WriteResult<()> {
286        self.write_autolink(url, is_email)
287    }
288
289    #[cfg(feature = "gfm")]
290    fn extended_autolink(&mut self, url: &EcoString) -> WriteResult<()> {
291        self.write_extended_autolink(url)
292    }
293
294    fn link_reference_definition(
295        &mut self,
296        label: &EcoString,
297        destination: &EcoString,
298        title: &Option<EcoString>,
299    ) -> WriteResult<()> {
300        self.write_link_reference_definition(label, destination, title)
301    }
302
303    fn reference_link(&mut self, label: &EcoString, content: &[Node]) -> WriteResult<()> {
304        self.write_reference_link(label, content)
305    }
306
307    #[cfg(feature = "gfm")]
308    fn strikethrough(&mut self, content: &[Node]) -> WriteResult<()> {
309        self.write_strikethrough(content)
310    }
311
312    fn custom(&mut self, node: &dyn CustomNode) -> WriteResult<()> {
313        self.write_custom_node(node)
314    }
315
316    fn unsupported(&mut self, node: &Node) -> WriteResult<()> {
317        self.emit_warning(format!(
318            "Unsupported node type encountered and skipped: {node:?}"
319        ));
320        Ok(())
321    }
322}
323
324impl Default for CommonMarkWriter {
325    fn default() -> Self {
326        Self::new()
327    }
328}
329
330impl fmt::Display for Node {
331    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
332        let mut writer = CommonMarkWriter::new();
333        match writer.write(self) {
334            Ok(_) => write!(f, "{}", writer.into_string()),
335            Err(e) => write!(f, "Error writing Node: {e}"),
336        }
337    }
338}