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}