Skip to main content

dazzle_backend_rtf/
lib.rs

1//! RTF backend for Dazzle document formatting
2//!
3//! This crate implements the `FotBuilder` trait for RTF (Rich Text Format) output,
4//! which is used for document formatting and print output.
5//!
6//! ## Purpose
7//!
8//! The RTF backend generates formatted documents from DSSSL specifications,
9//! supporting:
10//!
11//! - **Paragraphs**: Text blocks with formatting
12//! - **Character properties**: Font, size, weight, posture, color
13//! - **Page setup**: Margins, size, headers/footers
14//! - **Document structure**: Sections, page sequences
15//! - **Advanced features**: Links, tables, lists
16//!
17//! ## Architecture
18//!
19//! ```text
20//! RtfBackend
21//!   ├─ output: OutputByteStream (RTF file being written)
22//!   ├─ font_table: FontTable (font declarations)
23//!   ├─ color_table: ColorTable (color definitions)
24//!   ├─ document_props: DocumentProperties (page setup, margins)
25//!   └─ flow_stack: Vec<FlowObject> (nested flow object context)
26//! ```
27//!
28//! ## RTF Format Overview
29//!
30//! RTF structure:
31//! ```text
32//! {\rtf1\ansi\deff0
33//! {\fonttbl...}          # Font table
34//! {\colortbl...}         # Color table
35//! {\stylesheet...}       # Style definitions
36//! \deflang1024           # Document properties
37//! ...content...          # Paragraphs and text
38//! }
39//! ```
40//!
41//! ## Usage
42//!
43//! ```rust,ignore
44//! use dazzle_backend_rtf::RtfBackend;
45//! use dazzle_core::fot::FotBuilder;
46//! use std::fs::File;
47//!
48//! let file = File::create("output.rtf")?;
49//! let mut backend = RtfBackend::new(file)?;
50//!
51//! // Start a paragraph
52//! backend.start_paragraph()?;
53//! backend.literal("Hello, world!")?;
54//! backend.end_paragraph()?;
55//!
56//! // Finalize and close
57//! backend.finish()?;
58//! ```
59
60use dazzle_core::fot::FotBuilder;
61use std::io::{Result, Write};
62
63/// RTF backend for document formatting
64///
65/// Implements the `FotBuilder` trait with full support for DSSSL flow objects.
66#[derive(Debug)]
67pub struct RtfBackend<W: Write + std::fmt::Debug> {
68    /// Output stream for RTF content
69    output: W,
70
71    /// Whether the document header has been written
72    header_written: bool,
73
74    /// Whether we're currently in a paragraph
75    in_paragraph: bool,
76
77    /// Current buffer for accumulating content before writing to file
78    /// (used for entity flow object compatibility)
79    current_buffer: String,
80}
81
82impl<W: Write + std::fmt::Debug> RtfBackend<W> {
83    /// Get the inner writer by consuming self (for testing)
84    #[cfg(test)]
85    fn into_inner(self) -> W {
86        use std::mem::ManuallyDrop;
87        use std::ptr;
88
89        // Prevent Drop from running
90        let this = ManuallyDrop::new(self);
91
92        // SAFETY: We're manually dropping and taking ownership of the field
93        // This is safe because we're preventing Drop from running
94        unsafe {
95            ptr::read(&this.output)
96        }
97    }
98    /// Create a new RTF backend
99    ///
100    /// # Arguments
101    ///
102    /// * `output` - Output stream to write RTF to
103    ///
104    /// # Example
105    ///
106    /// ```rust,ignore
107    /// use std::fs::File;
108    /// use dazzle_backend_rtf::RtfBackend;
109    ///
110    /// let file = File::create("output.rtf")?;
111    /// let backend = RtfBackend::new(file)?;
112    /// ```
113    pub fn new(output: W) -> Result<Self> {
114        Ok(RtfBackend {
115            output,
116            header_written: false,
117            in_paragraph: false,
118            current_buffer: String::new(),
119        })
120    }
121
122    /// Write the RTF document header
123    ///
124    /// This includes:
125    /// - RTF version and character set
126    /// - Font table
127    /// - Color table
128    /// - Stylesheet
129    /// - Default document properties
130    fn write_header(&mut self) -> Result<()> {
131        if self.header_written {
132            return Ok(());
133        }
134
135        // RTF header with basic setup
136        // Matches OpenJade's output structure
137        writeln!(self.output, "{{\\rtf1\\ansi\\deff0")?;
138
139        // Font table (basic fonts for now)
140        writeln!(self.output, "{{\\fonttbl{{\\f1\\fnil\\fcharset0 Helvetica;}}")?;
141        writeln!(self.output, "{{\\f6\\fnil\\fcharset161 Helvetica Greek;}}")?;
142        writeln!(self.output, "{{\\f3\\fnil\\fcharset2 Symbol;}}")?;
143        writeln!(self.output, "{{\\f2\\fnil\\fcharset0 Courier;}}")?;
144        writeln!(self.output, "{{\\f5\\fnil\\fcharset161 Courier Greek;}}")?;
145        writeln!(self.output, "{{\\f0\\fnil\\fcharset0 Times New Roman;}}")?;
146        writeln!(self.output, "{{\\f4\\fnil\\fcharset161 Times New Roman Greek;}}")?;
147        writeln!(self.output, "}}")?;
148
149        // Color table (empty for now)
150        writeln!(self.output, "{{\\colortbl;}}")?;
151
152        // Stylesheet (basic styles)
153        write!(self.output, "{{\\stylesheet")?;
154        write!(self.output, "{{\\s1 Heading 1;}}")?;
155        write!(self.output, "{{\\s2 Heading 2;}}")?;
156        write!(self.output, "{{\\s3 Heading 3;}}")?;
157        write!(self.output, "{{\\s4 Heading 4;}}")?;
158        write!(self.output, "{{\\s5 Heading 5;}}")?;
159        write!(self.output, "{{\\s6 Heading 6;}}")?;
160        write!(self.output, "{{\\s7 Heading 7;}}")?;
161        write!(self.output, "{{\\s8 Heading 8;}}")?;
162        write!(self.output, "{{\\s9 Heading 9;}}")?;
163        writeln!(self.output, "}}")?;
164
165        // Document defaults
166        writeln!(self.output, "\\deflang1024\\notabind\\facingp\\hyphauto1\\widowctrl")?;
167
168        self.header_written = true;
169        Ok(())
170    }
171
172    /// Finalize the RTF document
173    ///
174    /// Writes any pending content and closes the RTF structure.
175    pub fn finish(&mut self) -> Result<()> {
176        // Make sure header was written
177        self.write_header()?;
178
179        // Close any open paragraph
180        if self.in_paragraph {
181            writeln!(self.output, "\\par}}")?;
182            self.in_paragraph = false;
183        }
184
185        // Close the RTF document
186        writeln!(self.output, "}}")?;
187        self.output.flush()?;
188
189        Ok(())
190    }
191}
192
193impl<W: Write + std::fmt::Debug> FotBuilder for RtfBackend<W> {
194    // ============================================================================
195    // Code Generation Primitives (for compatibility with SGML backend)
196    // ============================================================================
197
198    fn entity(&mut self, system_id: &str, _content: &str) -> Result<()> {
199        // RTF backend doesn't support entity flow object (file writing)
200        // This is used by SGML backend for code generation, not document formatting
201        Err(std::io::Error::new(
202            std::io::ErrorKind::Unsupported,
203            format!("entity flow object not supported by RTF backend (attempted to write: {})", system_id),
204        ))
205    }
206
207    fn formatting_instruction(&mut self, data: &str) -> Result<()> {
208        // Append to buffer (used by SGML backend pattern)
209        // For RTF, we buffer until a paragraph or other flow object uses it
210        self.current_buffer.push_str(data);
211        Ok(())
212    }
213
214    fn directory(&mut self, _path: &str) -> Result<()> {
215        // RTF doesn't support directory creation
216        Err(std::io::Error::new(
217            std::io::ErrorKind::Unsupported,
218            "directory flow object not supported by RTF backend",
219        ))
220    }
221
222    fn current_output(&self) -> &str {
223        &self.current_buffer
224    }
225
226    fn clear_buffer(&mut self) {
227        self.current_buffer.clear();
228    }
229
230    // ============================================================================
231    // Document Formatting Primitives (RTF-specific)
232    // ============================================================================
233
234    fn start_paragraph(&mut self) -> Result<()> {
235        // Ensure header is written
236        self.write_header()?;
237
238        // If already in a paragraph, close it first (OpenJade compatibility)
239        // DSSSL can nest paragraphs, but RTF requires closing the previous one
240        if self.in_paragraph {
241            writeln!(self.output, "\\par")?;
242        }
243
244        // Start a new paragraph
245        write!(self.output, "\\pard")?;
246        self.in_paragraph = true;
247
248        Ok(())
249    }
250
251    fn end_paragraph(&mut self) -> Result<()> {
252        // If not in a paragraph, just ignore (OpenJade compatibility)
253        // This can happen with nested paragraph flow objects
254        if !self.in_paragraph {
255            return Ok(());
256        }
257
258        // End the paragraph with \par
259        writeln!(self.output, "\\par")?;
260        self.in_paragraph = false;
261
262        Ok(())
263    }
264
265    fn literal(&mut self, text: &str) -> Result<()> {
266        // Auto-start paragraph if needed
267        // DSSSL templates often use (literal "text") standalone without explicit paragraph flow objects
268        if !self.in_paragraph {
269            self.start_paragraph()?;
270        }
271
272        // Write text, escaping RTF special characters
273        for ch in text.chars() {
274            match ch {
275                '\\' => write!(self.output, "\\\\")?,
276                '{' => write!(self.output, "\\{{")?,
277                '}' => write!(self.output, "\\}}")?,
278                '\n' => write!(self.output, "\\line ")?,
279                '\t' => write!(self.output, "\\tab ")?,
280                // Unicode characters > 127
281                c if c as u32 > 127 => {
282                    write!(self.output, "\\u{}?", c as u32)?;
283                }
284                c => write!(self.output, "{}", c)?,
285            }
286        }
287
288        Ok(())
289    }
290
291    fn start_sequence(&mut self) -> Result<()> {
292        // Sequence is just a container, no RTF output needed
293        Ok(())
294    }
295
296    fn end_sequence(&mut self) -> Result<()> {
297        // Sequence is just a container, no RTF output needed
298        Ok(())
299    }
300
301    fn start_display_group(&mut self) -> Result<()> {
302        // Display group - no special RTF output needed yet
303        Ok(())
304    }
305
306    fn end_display_group(&mut self) -> Result<()> {
307        // Display group - no special RTF output needed yet
308        Ok(())
309    }
310
311    fn start_simple_page_sequence(&mut self) -> Result<()> {
312        // Ensure header is written
313        self.write_header()?;
314
315        // Simple page sequence is the main content container
316        // In RTF, this translates to a section with properties
317        // Match OpenJade's output: \sectd\plain followed by page properties
318        write!(self.output, "\\sectd\\plain")?;
319
320        // TODO: Parse and output page-width, page-height, margins from characteristics
321        // For now, use OpenJade's default page properties (8.5x11 inches, 1.5" left margin, 1" others)
322        // \pgwsxn12240 = page width (8.5 * 1440 twips/inch)
323        // \pghsxn15840 = page height (11 * 1440 twips/inch)
324        // \marglsxn2160 = left margin (1.5 * 1440)
325        // \margrsxn1440 = right margin (1 * 1440)
326        // \margtsxn1440 = top margin
327        // \margbsxn1920 = bottom margin (1.33 * 1440)
328        write!(self.output, "\\pgwsxn12240\\pghsxn15840\\marglsxn2160\\margrsxn1440\\margtsxn1440\\margbsxn1920")?;
329        write!(self.output, "\\headery0\\footery0\\pgndec")?;
330
331        Ok(())
332    }
333
334    fn end_simple_page_sequence(&mut self) -> Result<()> {
335        // End of page sequence - no special action needed for now
336        Ok(())
337    }
338
339    fn start_line_field(&mut self) -> Result<()> {
340        // Line-field is an inline container
341        // In RTF, this doesn't need special markup
342        Ok(())
343    }
344
345    fn end_line_field(&mut self) -> Result<()> {
346        // End of line-field
347        Ok(())
348    }
349}
350
351impl<W: Write + std::fmt::Debug> Drop for RtfBackend<W> {
352    fn drop(&mut self) {
353        // Attempt to finalize on drop (best effort)
354        let _ = self.finish();
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361    use std::io::Cursor;
362
363    #[test]
364    fn test_create_backend() {
365        let cursor = Cursor::new(Vec::new());
366        let backend = RtfBackend::new(cursor);
367        assert!(backend.is_ok());
368    }
369
370    #[test]
371    fn test_basic_paragraph() -> Result<()> {
372        let cursor = Cursor::new(Vec::new());
373        let mut backend = RtfBackend::new(cursor)?;
374
375        backend.start_paragraph()?;
376        backend.literal("Hello, world!")?;
377        backend.end_paragraph()?;
378        backend.finish()?;
379
380        let output = String::from_utf8(backend.into_inner().into_inner()).unwrap();
381
382        // Check that RTF header is present
383        assert!(output.contains("{\\rtf1\\ansi\\deff0"));
384
385        // Check that paragraph is present
386        assert!(output.contains("\\pard"));
387        assert!(output.contains("Hello, world!"));
388        assert!(output.contains("\\par"));
389
390        Ok(())
391    }
392
393    #[test]
394    fn test_character_escaping() -> Result<()> {
395        let cursor = Cursor::new(Vec::new());
396        let mut backend = RtfBackend::new(cursor)?;
397
398        backend.start_paragraph()?;
399        backend.literal("Test { } \\ special")?;
400        backend.end_paragraph()?;
401        backend.finish()?;
402
403        let output = String::from_utf8(backend.into_inner().into_inner()).unwrap();
404
405        // Check that special characters are escaped
406        assert!(output.contains("\\{"));
407        assert!(output.contains("\\}"));
408        assert!(output.contains("\\\\"));
409
410        Ok(())
411    }
412}