typlite/
common.rs

1//! Common types and interfaces for the conversion system
2
3use cmark_writer::CommonMarkWriter;
4use cmark_writer::HtmlAttribute;
5use cmark_writer::HtmlElement;
6use cmark_writer::HtmlWriteResult;
7use cmark_writer::HtmlWriter;
8use cmark_writer::HtmlWriterOptions;
9use cmark_writer::WriteResult;
10use cmark_writer::WriterOptions;
11use cmark_writer::ast::Node;
12use cmark_writer::custom_node;
13use ecow::EcoString;
14use ecow::eco_format;
15use std::path::PathBuf;
16
17use crate::Result;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum ListState {
21    Ordered,
22    Unordered,
23}
24
25/// Valid formats for the conversion.
26#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Format {
28    #[default]
29    Md,
30    LaTeX,
31    Text,
32    #[cfg(feature = "docx")]
33    Docx,
34}
35
36/// Figure node implementation for all formats
37#[derive(Debug, PartialEq, Clone)]
38#[custom_node(block = true, html_impl = true)]
39pub struct FigureNode {
40    /// The main content of the figure, can be any block node
41    pub body: Box<Node>,
42    /// The caption text for the figure
43    pub caption: String,
44}
45
46impl FigureNode {
47    fn write_custom(&self, writer: &mut CommonMarkWriter) -> WriteResult<()> {
48        let mut temp_writer = CommonMarkWriter::with_options(writer.options.clone());
49        temp_writer.write(&self.body)?;
50        let content = temp_writer.into_string();
51        writer.write_str(&content)?;
52        writer.write_str("\n")?;
53        writer.write_str(&self.caption)?;
54        Ok(())
55    }
56
57    fn write_html_custom(&self, writer: &mut HtmlWriter) -> HtmlWriteResult<()> {
58        let body = self.body.clone();
59        let node = Node::HtmlElement(HtmlElement {
60            tag: EcoString::inline("figure"),
61            attributes: vec![HtmlAttribute {
62                name: EcoString::inline("class"),
63                value: EcoString::inline("figure"),
64            }],
65            children: vec![*body],
66            self_closing: false,
67        });
68        writer.write_node(&node)?;
69        Ok(())
70    }
71}
72
73/// External Frame node for handling frames stored as external files
74#[derive(Debug, PartialEq, Clone)]
75#[custom_node(block = true, html_impl = true)]
76pub struct ExternalFrameNode {
77    /// The path to the external file containing the frame
78    pub file_path: PathBuf,
79    /// Alternative text for the frame
80    pub alt_text: EcoString,
81    /// Original SVG data (needed for DOCX that still embeds images)
82    pub svg: String,
83}
84
85impl ExternalFrameNode {
86    fn write_custom(&self, writer: &mut CommonMarkWriter) -> WriteResult<()> {
87        // The actual handling is implemented in format-specific writers
88        writer.write_str(&format!(
89            "![{}]({})",
90            self.alt_text,
91            self.file_path.display()
92        ))?;
93        Ok(())
94    }
95
96    fn write_html_custom(&self, writer: &mut HtmlWriter) -> HtmlWriteResult<()> {
97        let node = Node::HtmlElement(HtmlElement {
98            tag: EcoString::inline("img"),
99            attributes: vec![
100                HtmlAttribute {
101                    name: EcoString::inline("src"),
102                    value: self.file_path.display().to_string().into(),
103                },
104                HtmlAttribute {
105                    name: EcoString::inline("alt"),
106                    value: self.alt_text.clone(),
107                },
108            ],
109            children: vec![],
110            self_closing: true,
111        });
112        writer.write_node(&node)?;
113        Ok(())
114    }
115}
116
117/// Highlight node for highlighted text
118#[derive(Debug, PartialEq, Clone)]
119#[custom_node(block = false, html_impl = true)]
120pub struct HighlightNode {
121    /// The content to be highlighted
122    pub content: Vec<Node>,
123}
124
125impl HighlightNode {
126    fn write_custom(&self, writer: &mut CommonMarkWriter) -> WriteResult<()> {
127        let mut temp_writer = CommonMarkWriter::with_options(writer.options.clone());
128        for node in &self.content {
129            temp_writer.write(node)?;
130        }
131        let content = temp_writer.into_string();
132        writer.write_str(&format!("=={content}=="))?;
133        Ok(())
134    }
135
136    fn write_html_custom(&self, writer: &mut HtmlWriter) -> HtmlWriteResult<()> {
137        let node = Node::HtmlElement(HtmlElement {
138            tag: EcoString::inline("mark"),
139            attributes: vec![],
140            children: self.content.clone(),
141            self_closing: false,
142        });
143        writer.write_node(&node)?;
144        Ok(())
145    }
146}
147
148/// Node for centered content
149#[derive(Debug, PartialEq, Clone)]
150#[custom_node(block = true, html_impl = true)]
151pub struct CenterNode {
152    /// The content to be centered
153    pub node: Node,
154}
155
156impl CenterNode {
157    pub fn new(children: Vec<Node>) -> Self {
158        CenterNode {
159            node: Node::HtmlElement(cmark_writer::ast::HtmlElement {
160                tag: EcoString::inline("p"),
161                attributes: vec![cmark_writer::ast::HtmlAttribute {
162                    name: EcoString::inline("align"),
163                    value: EcoString::inline("center"),
164                }],
165                children,
166                self_closing: false,
167            }),
168        }
169    }
170
171    fn write_custom(&self, writer: &mut CommonMarkWriter) -> WriteResult<()> {
172        let mut temp_writer = CommonMarkWriter::with_options(writer.options.clone());
173        temp_writer.write(&self.node)?;
174        let content = temp_writer.into_string();
175        writer.write_str(&content)?;
176        writer.write_str("\n")?;
177        Ok(())
178    }
179
180    fn write_html_custom(&self, writer: &mut HtmlWriter) -> HtmlWriteResult<()> {
181        let mut temp_writer = HtmlWriter::with_options(HtmlWriterOptions {
182            strict: false,
183            ..Default::default()
184        });
185        temp_writer.write_node(&self.node)?;
186        let content = temp_writer.into_string();
187        writer.write_str(&content)?;
188        Ok(())
189    }
190}
191
192/// Inline node for flattened inline content (useful for table cells)
193#[derive(Debug, PartialEq, Clone)]
194#[custom_node(block = false, html_impl = true)]
195pub struct InlineNode {
196    /// The inline content nodes
197    pub content: Vec<Node>,
198}
199
200impl InlineNode {
201    fn write_custom(&self, writer: &mut CommonMarkWriter) -> WriteResult<()> {
202        for node in &self.content {
203            writer.write(node)?;
204        }
205        Ok(())
206    }
207
208    fn write_html_custom(&self, writer: &mut HtmlWriter) -> HtmlWriteResult<()> {
209        for node in &self.content {
210            writer.write_node(node)?;
211        }
212        Ok(())
213    }
214}
215
216/// Verbatim node for raw text output
217#[derive(Debug, PartialEq, Clone)]
218#[custom_node(block = false, html_impl = false)]
219pub struct VerbatimNode {
220    /// The content to directly output
221    pub content: EcoString,
222}
223
224impl VerbatimNode {
225    fn write_custom(&self, writer: &mut CommonMarkWriter) -> WriteResult<()> {
226        writer.write_str(&self.content)?;
227        Ok(())
228    }
229}
230
231/// Alert node for alert messages
232#[derive(Debug, PartialEq, Clone)]
233#[custom_node(block = true, html_impl = false)]
234pub struct AlertNode {
235    /// The content of the alert
236    pub content: Vec<Node>,
237    /// The class of the alert
238    pub class: EcoString,
239}
240
241impl AlertNode {
242    fn write_custom(&self, writer: &mut CommonMarkWriter) -> WriteResult<()> {
243        let quote = Node::BlockQuote(vec![
244            Node::Paragraph(vec![Node::Text(eco_format!(
245                "[!{}]",
246                self.class.to_ascii_uppercase()
247            ))]),
248            Node::Paragraph(vec![Node::Text("".into())]),
249        ]);
250        let mut tmp_writer = CommonMarkWriter::with_options(WriterOptions {
251            escape_special_chars: false,
252            ..writer.options.clone()
253        });
254        tmp_writer.write(&quote)?;
255        let mut content = tmp_writer.into_string();
256        let quote = Node::BlockQuote(self.content.clone());
257        let mut tmp_writer = CommonMarkWriter::with_options(writer.options.clone());
258        tmp_writer.write(&quote)?;
259        content += tmp_writer.into_string();
260        writer.write_str(&content)?;
261        Ok(())
262    }
263}
264
265/// Common writer interface for different formats
266pub trait FormatWriter {
267    /// Write AST document to output format
268    fn write_eco(&mut self, document: &Node, output: &mut EcoString) -> Result<()>;
269
270    /// Write AST document to vector
271    fn write_vec(&mut self, document: &Node) -> Result<Vec<u8>>;
272}