dazzle-backend-rtf 0.4.6

RTF backend for Dazzle: Document formatting output engine
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
//! RTF backend for Dazzle document formatting
//!
//! This crate implements the `FotBuilder` trait for RTF (Rich Text Format) output,
//! which is used for document formatting and print output.
//!
//! ## Purpose
//!
//! The RTF backend generates formatted documents from DSSSL specifications,
//! supporting:
//!
//! - **Paragraphs**: Text blocks with formatting
//! - **Character properties**: Font, size, weight, posture, color
//! - **Page setup**: Margins, size, headers/footers
//! - **Document structure**: Sections, page sequences
//! - **Advanced features**: Links, tables, lists
//!
//! ## Architecture
//!
//! ```text
//! RtfBackend
//!   ├─ output: OutputByteStream (RTF file being written)
//!   ├─ font_table: FontTable (font declarations)
//!   ├─ color_table: ColorTable (color definitions)
//!   ├─ document_props: DocumentProperties (page setup, margins)
//!   └─ flow_stack: Vec<FlowObject> (nested flow object context)
//! ```
//!
//! ## RTF Format Overview
//!
//! RTF structure:
//! ```text
//! {\rtf1\ansi\deff0
//! {\fonttbl...}          # Font table
//! {\colortbl...}         # Color table
//! {\stylesheet...}       # Style definitions
//! \deflang1024           # Document properties
//! ...content...          # Paragraphs and text
//! }
//! ```
//!
//! ## Usage
//!
//! ```rust,ignore
//! use dazzle_backend_rtf::RtfBackend;
//! use dazzle_core::fot::FotBuilder;
//! use std::fs::File;
//!
//! let file = File::create("output.rtf")?;
//! let mut backend = RtfBackend::new(file)?;
//!
//! // Start a paragraph
//! backend.start_paragraph()?;
//! backend.literal("Hello, world!")?;
//! backend.end_paragraph()?;
//!
//! // Finalize and close
//! backend.finish()?;
//! ```

use dazzle_core::fot::FotBuilder;
use std::io::{Result, Write};

/// RTF backend for document formatting
///
/// Implements the `FotBuilder` trait with full support for DSSSL flow objects.
#[derive(Debug)]
pub struct RtfBackend<W: Write + std::fmt::Debug> {
    /// Output stream for RTF content
    output: W,

    /// Whether the document header has been written
    header_written: bool,

    /// Whether we're currently in a paragraph
    in_paragraph: bool,

    /// Current buffer for accumulating content before writing to file
    /// (used for entity flow object compatibility)
    current_buffer: String,
}

impl<W: Write + std::fmt::Debug> RtfBackend<W> {
    /// Get the inner writer by consuming self (for testing)
    #[cfg(test)]
    fn into_inner(self) -> W {
        use std::mem::ManuallyDrop;
        use std::ptr;

        // Prevent Drop from running
        let this = ManuallyDrop::new(self);

        // SAFETY: We're manually dropping and taking ownership of the field
        // This is safe because we're preventing Drop from running
        unsafe {
            ptr::read(&this.output)
        }
    }
    /// Create a new RTF backend
    ///
    /// # Arguments
    ///
    /// * `output` - Output stream to write RTF to
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// use std::fs::File;
    /// use dazzle_backend_rtf::RtfBackend;
    ///
    /// let file = File::create("output.rtf")?;
    /// let backend = RtfBackend::new(file)?;
    /// ```
    pub fn new(output: W) -> Result<Self> {
        Ok(RtfBackend {
            output,
            header_written: false,
            in_paragraph: false,
            current_buffer: String::new(),
        })
    }

    /// Write the RTF document header
    ///
    /// This includes:
    /// - RTF version and character set
    /// - Font table
    /// - Color table
    /// - Stylesheet
    /// - Default document properties
    fn write_header(&mut self) -> Result<()> {
        if self.header_written {
            return Ok(());
        }

        // RTF header with basic setup
        // Matches OpenJade's output structure
        writeln!(self.output, "{{\\rtf1\\ansi\\deff0")?;

        // Font table (basic fonts for now)
        writeln!(self.output, "{{\\fonttbl{{\\f1\\fnil\\fcharset0 Helvetica;}}")?;
        writeln!(self.output, "{{\\f6\\fnil\\fcharset161 Helvetica Greek;}}")?;
        writeln!(self.output, "{{\\f3\\fnil\\fcharset2 Symbol;}}")?;
        writeln!(self.output, "{{\\f2\\fnil\\fcharset0 Courier;}}")?;
        writeln!(self.output, "{{\\f5\\fnil\\fcharset161 Courier Greek;}}")?;
        writeln!(self.output, "{{\\f0\\fnil\\fcharset0 Times New Roman;}}")?;
        writeln!(self.output, "{{\\f4\\fnil\\fcharset161 Times New Roman Greek;}}")?;
        writeln!(self.output, "}}")?;

        // Color table (empty for now)
        writeln!(self.output, "{{\\colortbl;}}")?;

        // Stylesheet (basic styles)
        write!(self.output, "{{\\stylesheet")?;
        write!(self.output, "{{\\s1 Heading 1;}}")?;
        write!(self.output, "{{\\s2 Heading 2;}}")?;
        write!(self.output, "{{\\s3 Heading 3;}}")?;
        write!(self.output, "{{\\s4 Heading 4;}}")?;
        write!(self.output, "{{\\s5 Heading 5;}}")?;
        write!(self.output, "{{\\s6 Heading 6;}}")?;
        write!(self.output, "{{\\s7 Heading 7;}}")?;
        write!(self.output, "{{\\s8 Heading 8;}}")?;
        write!(self.output, "{{\\s9 Heading 9;}}")?;
        writeln!(self.output, "}}")?;

        // Document defaults
        writeln!(self.output, "\\deflang1024\\notabind\\facingp\\hyphauto1\\widowctrl")?;

        self.header_written = true;
        Ok(())
    }

    /// Finalize the RTF document
    ///
    /// Writes any pending content and closes the RTF structure.
    pub fn finish(&mut self) -> Result<()> {
        // Make sure header was written
        self.write_header()?;

        // Close any open paragraph
        if self.in_paragraph {
            writeln!(self.output, "\\par}}")?;
            self.in_paragraph = false;
        }

        // Close the RTF document
        writeln!(self.output, "}}")?;
        self.output.flush()?;

        Ok(())
    }
}

impl<W: Write + std::fmt::Debug> FotBuilder for RtfBackend<W> {
    // ============================================================================
    // Code Generation Primitives (for compatibility with SGML backend)
    // ============================================================================

    fn entity(&mut self, system_id: &str, _content: &str) -> Result<()> {
        // RTF backend doesn't support entity flow object (file writing)
        // This is used by SGML backend for code generation, not document formatting
        Err(std::io::Error::new(
            std::io::ErrorKind::Unsupported,
            format!("entity flow object not supported by RTF backend (attempted to write: {})", system_id),
        ))
    }

    fn formatting_instruction(&mut self, data: &str) -> Result<()> {
        // Append to buffer (used by SGML backend pattern)
        // For RTF, we buffer until a paragraph or other flow object uses it
        self.current_buffer.push_str(data);
        Ok(())
    }

    fn directory(&mut self, _path: &str) -> Result<()> {
        // RTF doesn't support directory creation
        Err(std::io::Error::new(
            std::io::ErrorKind::Unsupported,
            "directory flow object not supported by RTF backend",
        ))
    }

    fn current_output(&self) -> &str {
        &self.current_buffer
    }

    fn clear_buffer(&mut self) {
        self.current_buffer.clear();
    }

    // ============================================================================
    // Document Formatting Primitives (RTF-specific)
    // ============================================================================

    fn start_paragraph(&mut self) -> Result<()> {
        // Ensure header is written
        self.write_header()?;

        // If already in a paragraph, close it first (OpenJade compatibility)
        // DSSSL can nest paragraphs, but RTF requires closing the previous one
        if self.in_paragraph {
            writeln!(self.output, "\\par")?;
        }

        // Start a new paragraph
        write!(self.output, "\\pard")?;
        self.in_paragraph = true;

        Ok(())
    }

    fn end_paragraph(&mut self) -> Result<()> {
        // If not in a paragraph, just ignore (OpenJade compatibility)
        // This can happen with nested paragraph flow objects
        if !self.in_paragraph {
            return Ok(());
        }

        // End the paragraph with \par
        writeln!(self.output, "\\par")?;
        self.in_paragraph = false;

        Ok(())
    }

    fn literal(&mut self, text: &str) -> Result<()> {
        // Auto-start paragraph if needed
        // DSSSL templates often use (literal "text") standalone without explicit paragraph flow objects
        if !self.in_paragraph {
            self.start_paragraph()?;
        }

        // Write text, escaping RTF special characters
        for ch in text.chars() {
            match ch {
                '\\' => write!(self.output, "\\\\")?,
                '{' => write!(self.output, "\\{{")?,
                '}' => write!(self.output, "\\}}")?,
                '\n' => write!(self.output, "\\line ")?,
                '\t' => write!(self.output, "\\tab ")?,
                // Unicode characters > 127
                c if c as u32 > 127 => {
                    write!(self.output, "\\u{}?", c as u32)?;
                }
                c => write!(self.output, "{}", c)?,
            }
        }

        Ok(())
    }

    fn start_sequence(&mut self) -> Result<()> {
        // Sequence is just a container, no RTF output needed
        Ok(())
    }

    fn end_sequence(&mut self) -> Result<()> {
        // Sequence is just a container, no RTF output needed
        Ok(())
    }

    fn start_display_group(&mut self) -> Result<()> {
        // Display group - no special RTF output needed yet
        Ok(())
    }

    fn end_display_group(&mut self) -> Result<()> {
        // Display group - no special RTF output needed yet
        Ok(())
    }

    fn start_simple_page_sequence(&mut self) -> Result<()> {
        // Ensure header is written
        self.write_header()?;

        // Simple page sequence is the main content container
        // In RTF, this translates to a section with properties
        // Match OpenJade's output: \sectd\plain followed by page properties
        write!(self.output, "\\sectd\\plain")?;

        // TODO: Parse and output page-width, page-height, margins from characteristics
        // For now, use OpenJade's default page properties (8.5x11 inches, 1.5" left margin, 1" others)
        // \pgwsxn12240 = page width (8.5 * 1440 twips/inch)
        // \pghsxn15840 = page height (11 * 1440 twips/inch)
        // \marglsxn2160 = left margin (1.5 * 1440)
        // \margrsxn1440 = right margin (1 * 1440)
        // \margtsxn1440 = top margin
        // \margbsxn1920 = bottom margin (1.33 * 1440)
        write!(self.output, "\\pgwsxn12240\\pghsxn15840\\marglsxn2160\\margrsxn1440\\margtsxn1440\\margbsxn1920")?;
        write!(self.output, "\\headery0\\footery0\\pgndec")?;

        Ok(())
    }

    fn end_simple_page_sequence(&mut self) -> Result<()> {
        // End of page sequence - no special action needed for now
        Ok(())
    }

    fn start_line_field(&mut self) -> Result<()> {
        // Line-field is an inline container
        // In RTF, this doesn't need special markup
        Ok(())
    }

    fn end_line_field(&mut self) -> Result<()> {
        // End of line-field
        Ok(())
    }
}

impl<W: Write + std::fmt::Debug> Drop for RtfBackend<W> {
    fn drop(&mut self) {
        // Attempt to finalize on drop (best effort)
        let _ = self.finish();
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Cursor;

    #[test]
    fn test_create_backend() {
        let cursor = Cursor::new(Vec::new());
        let backend = RtfBackend::new(cursor);
        assert!(backend.is_ok());
    }

    #[test]
    fn test_basic_paragraph() -> Result<()> {
        let cursor = Cursor::new(Vec::new());
        let mut backend = RtfBackend::new(cursor)?;

        backend.start_paragraph()?;
        backend.literal("Hello, world!")?;
        backend.end_paragraph()?;
        backend.finish()?;

        let output = String::from_utf8(backend.into_inner().into_inner()).unwrap();

        // Check that RTF header is present
        assert!(output.contains("{\\rtf1\\ansi\\deff0"));

        // Check that paragraph is present
        assert!(output.contains("\\pard"));
        assert!(output.contains("Hello, world!"));
        assert!(output.contains("\\par"));

        Ok(())
    }

    #[test]
    fn test_character_escaping() -> Result<()> {
        let cursor = Cursor::new(Vec::new());
        let mut backend = RtfBackend::new(cursor)?;

        backend.start_paragraph()?;
        backend.literal("Test { } \\ special")?;
        backend.end_paragraph()?;
        backend.finish()?;

        let output = String::from_utf8(backend.into_inner().into_inner()).unwrap();

        // Check that special characters are escaped
        assert!(output.contains("\\{"));
        assert!(output.contains("\\}"));
        assert!(output.contains("\\\\"));

        Ok(())
    }
}