dazzle_backend_sgml/
lib.rs

1//! SGML backend for Dazzle code generation
2//!
3//! This crate implements the `FotBuilder` trait for SGML-style output,
4//! which is used for code generation rather than document formatting.
5//!
6//! ## Purpose
7//!
8//! Unlike OpenJade's document formatting backends (RTF, TeX, MIF, HTML),
9//! the SGML backend is designed for **code generation**:
10//!
11//! - **`entity`**: Creates output files (e.g., generated Java classes)
12//! - **`formatting-instruction`**: Appends text to the current output buffer
13//!
14//! This maps perfectly to code generation use cases where you want to
15//! generate multiple source files from XML input.
16//!
17//! ## Architecture
18//!
19//! ```text
20//! SgmlBackend
21//!   ├─ output_dir: PathBuf (where to write files)
22//!   ├─ current_buffer: String (collecting text for current file)
23//!   └─ written_files: HashSet<PathBuf> (track what's been written)
24//! ```
25//!
26//! ## Usage
27//!
28//! ```rust,ignore
29//! use dazzle_backend_sgml::SgmlBackend;
30//! use dazzle_core::fot::FotBuilder;
31//!
32//! let mut backend = SgmlBackend::new("output");
33//!
34//! // Append text to buffer
35//! backend.formatting_instruction("public class Foo {\n")?;
36//! backend.formatting_instruction("  // generated code\n")?;
37//! backend.formatting_instruction("}\n")?;
38//!
39//! // Write buffer to file
40//! backend.entity("src/Foo.java", &backend.current_output())?;
41//! ```
42
43use dazzle_core::fot::FotBuilder;
44use std::collections::HashSet;
45use std::fs;
46use std::io::{Result, Write};
47use std::path::{Path, PathBuf};
48
49/// SGML backend for code generation
50///
51/// Implements the `FotBuilder` trait with support for:
52/// - Creating output files (`entity`)
53/// - Appending text to output buffer (`formatting_instruction`)
54/// - Creating directories and setting the current directory context
55#[derive(Debug)]
56pub struct SgmlBackend {
57    /// Output directory (where files are written)
58    output_dir: PathBuf,
59
60    /// Current directory context (for relative paths)
61    /// When set, entity paths are resolved relative to this directory
62    current_dir: Option<PathBuf>,
63
64    /// Current output buffer (text being accumulated)
65    current_buffer: String,
66
67    /// Set of files that have been written (to detect duplicates)
68    written_files: HashSet<PathBuf>,
69}
70
71impl SgmlBackend {
72    /// Create a new SGML backend
73    ///
74    /// # Arguments
75    ///
76    /// * `output_dir` - Directory where generated files will be written
77    ///
78    /// # Example
79    ///
80    /// ```rust
81    /// use dazzle_backend_sgml::SgmlBackend;
82    ///
83    /// let backend = SgmlBackend::new("output");
84    /// ```
85    pub fn new<P: AsRef<Path>>(output_dir: P) -> Self {
86        SgmlBackend {
87            output_dir: output_dir.as_ref().to_path_buf(),
88            current_dir: None,
89            current_buffer: String::new(),
90            written_files: HashSet::new(),
91        }
92    }
93
94    /// Get the list of files that have been written
95    pub fn written_files(&self) -> &HashSet<PathBuf> {
96        &self.written_files
97    }
98}
99
100impl FotBuilder for SgmlBackend {
101    /// Create an external entity (output file)
102    ///
103    /// Writes the provided content to a file in the output directory.
104    /// If a current directory is set (via `directory` flow object), relative paths
105    /// are resolved against it.
106    ///
107    /// # Arguments
108    ///
109    /// * `system_id` - Relative path for the output file (e.g., "src/Foo.java" or "Foo.java")
110    /// * `content` - File contents to write
111    ///
112    /// # Example
113    ///
114    /// ```rust,no_run
115    /// use dazzle_backend_sgml::SgmlBackend;
116    /// use dazzle_core::fot::FotBuilder;
117    ///
118    /// let mut backend = SgmlBackend::new("output");
119    /// backend.entity("Foo.java", "public class Foo { }")?;
120    /// # Ok::<(), std::io::Error>(())
121    /// ```
122    fn entity(&mut self, system_id: &str, content: &str) -> Result<()> {
123        // Resolve path relative to current directory if set
124        let relative_path = if let Some(ref current_dir) = self.current_dir {
125            current_dir.join(system_id)
126        } else {
127            PathBuf::from(system_id)
128        };
129
130        // Construct full output path
131        let output_path = self.output_dir.join(relative_path);
132
133        // Create parent directories if needed
134        if let Some(parent) = output_path.parent() {
135            fs::create_dir_all(parent)?;
136        }
137
138        // Write file
139        let mut file = fs::File::create(&output_path)?;
140        file.write_all(content.as_bytes())?;
141
142        // Track written file
143        self.written_files.insert(output_path);
144
145        Ok(())
146    }
147
148    /// Insert backend-specific formatting instruction
149    ///
150    /// Appends text to the current output buffer. This text can later
151    /// be written to a file using `entity()`.
152    ///
153    /// # Arguments
154    ///
155    /// * `data` - Text to append
156    ///
157    /// # Example
158    ///
159    /// ```rust
160    /// use dazzle_backend_sgml::SgmlBackend;
161    /// use dazzle_core::fot::FotBuilder;
162    ///
163    /// let mut backend = SgmlBackend::new("output");
164    /// backend.formatting_instruction("line 1\n")?;
165    /// backend.formatting_instruction("line 2\n")?;
166    ///
167    /// assert_eq!(backend.current_output(), "line 1\nline 2\n");
168    /// # Ok::<(), std::io::Error>(())
169    /// ```
170    fn formatting_instruction(&mut self, data: &str) -> Result<()> {
171        self.current_buffer.push_str(data);
172        Ok(())
173    }
174
175    /// Get the current output buffer contents
176    ///
177    /// This returns the accumulated text from `formatting_instruction` calls.
178    fn current_output(&self) -> &str {
179        &self.current_buffer
180    }
181
182    /// Clear the current output buffer
183    ///
184    /// Typically called after writing a file with `entity()`.
185    fn clear_buffer(&mut self) {
186        self.current_buffer.clear();
187    }
188
189    fn literal(&mut self, text: &str) -> Result<()> {
190        // For SGML backend, literal is the same as formatting-instruction
191        self.formatting_instruction(text)
192    }
193
194    /// Create a directory
195    ///
196    /// Creates the directory and all necessary parent directories.
197    /// Also sets this directory as the current directory context for subsequent
198    /// entity and directory operations.
199    ///
200    /// If a current directory is set, relative paths are resolved against it.
201    ///
202    /// # Arguments
203    ///
204    /// * `path` - Directory path to create (relative to current_dir or output_dir)
205    ///
206    /// # Example
207    ///
208    /// ```rust,no_run
209    /// use dazzle_backend_sgml::SgmlBackend;
210    /// use dazzle_core::fot::FotBuilder;
211    ///
212    /// let mut backend = SgmlBackend::new("output");
213    /// backend.directory("src/generated")?;
214    /// backend.directory("models")?;  // Creates src/generated/models
215    /// # Ok::<(), std::io::Error>(())
216    /// ```
217    fn directory(&mut self, path: &str) -> Result<()> {
218        // Resolve path relative to current directory if set
219        let relative_path = if let Some(ref current_dir) = self.current_dir {
220            current_dir.join(path)
221        } else {
222            PathBuf::from(path)
223        };
224
225        // Construct full directory path
226        let dir_path = self.output_dir.join(&relative_path);
227        fs::create_dir_all(&dir_path)
228            .map_err(|e| std::io::Error::new(
229                e.kind(),
230                format!("Failed to create directory {}: {}", dir_path.display(), e)
231            ))?;
232
233        // Normalize the path to handle .. and .
234        // We do this by canonicalizing relative to output_dir
235        let canonical = dir_path.canonicalize()
236            .map_err(|e| std::io::Error::new(
237                e.kind(),
238                format!("Failed to canonicalize directory {}: {}", dir_path.display(), e)
239            ))?;
240        let output_canonical = self.output_dir.canonicalize()
241            .map_err(|e| std::io::Error::new(
242                e.kind(),
243                format!("Failed to canonicalize output_dir {}: {}", self.output_dir.display(), e)
244            ))?;
245        let normalized_relative = canonical.strip_prefix(&output_canonical)
246            .map(|p| p.to_path_buf())
247            .unwrap_or_else(|_| relative_path.clone());
248
249        // Set this directory as the current directory context (use the normalized path)
250        self.set_current_directory(Some(normalized_relative.to_string_lossy().to_string()));
251
252        Ok(())
253    }
254
255    /// Get the current directory context
256    fn current_directory(&self) -> Option<&str> {
257        self.current_dir.as_ref().and_then(|p| p.to_str())
258    }
259
260    /// Set the current directory context
261    fn set_current_directory(&mut self, path: Option<String>) {
262        self.current_dir = path.map(PathBuf::from);
263    }
264
265    /// Start a simple page sequence (stub for SGML backend)
266    ///
267    /// The SGML backend is for code generation, not document formatting.
268    /// Page layout flow objects are silently ignored.
269    fn start_simple_page_sequence(&mut self) -> Result<()> {
270        // Stub: SGML backend ignores page layout
271        Ok(())
272    }
273
274    /// End a simple page sequence (stub for SGML backend)
275    fn end_simple_page_sequence(&mut self) -> Result<()> {
276        // Stub: SGML backend ignores page layout
277        Ok(())
278    }
279
280    /// Start a sequence (stub for SGML backend)
281    fn start_sequence(&mut self) -> Result<()> {
282        // Stub: SGML backend ignores formatting flow objects
283        Ok(())
284    }
285
286    /// End a sequence (stub for SGML backend)
287    fn end_sequence(&mut self) -> Result<()> {
288        // Stub: SGML backend ignores formatting flow objects
289        Ok(())
290    }
291
292    /// Start a paragraph (stub for SGML backend)
293    fn start_paragraph(&mut self) -> Result<()> {
294        // Stub: SGML backend ignores formatting flow objects
295        Ok(())
296    }
297
298    /// End a paragraph (stub for SGML backend)
299    fn end_paragraph(&mut self) -> Result<()> {
300        // Stub: SGML backend ignores formatting flow objects
301        Ok(())
302    }
303
304    /// Start a display group (stub for SGML backend)
305    fn start_display_group(&mut self) -> Result<()> {
306        // Stub: SGML backend ignores formatting flow objects
307        Ok(())
308    }
309
310    /// End a display group (stub for SGML backend)
311    fn end_display_group(&mut self) -> Result<()> {
312        // Stub: SGML backend ignores formatting flow objects
313        Ok(())
314    }
315
316    /// Start a line field (stub for SGML backend)
317    fn start_line_field(&mut self) -> Result<()> {
318        // Stub: SGML backend ignores formatting flow objects
319        Ok(())
320    }
321
322    /// End a line field (stub for SGML backend)
323    fn end_line_field(&mut self) -> Result<()> {
324        // Stub: SGML backend ignores formatting flow objects
325        Ok(())
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332    use std::fs;
333    use tempfile::TempDir;
334
335    #[test]
336    fn test_new_backend() {
337        let backend = SgmlBackend::new("output");
338        assert_eq!(backend.current_output(), "");
339        assert_eq!(backend.written_files().len(), 0);
340    }
341
342    #[test]
343    fn test_formatting_instruction() {
344        let mut backend = SgmlBackend::new("output");
345
346        backend.formatting_instruction("Hello ").unwrap();
347        backend.formatting_instruction("World").unwrap();
348
349        assert_eq!(backend.current_output(), "Hello World");
350    }
351
352    #[test]
353    fn test_clear_buffer() {
354        let mut backend = SgmlBackend::new("output");
355
356        backend.formatting_instruction("Some text").unwrap();
357        assert_eq!(backend.current_output(), "Some text");
358
359        backend.clear_buffer();
360        assert_eq!(backend.current_output(), "");
361    }
362
363    #[test]
364    fn test_entity_creates_file() {
365        let temp_dir = TempDir::new().unwrap();
366        let mut backend = SgmlBackend::new(temp_dir.path());
367
368        backend
369            .entity("test.txt", "Hello, World!")
370            .unwrap();
371
372        let file_path = temp_dir.path().join("test.txt");
373        assert!(file_path.exists());
374
375        let content = fs::read_to_string(&file_path).unwrap();
376        assert_eq!(content, "Hello, World!");
377    }
378
379    #[test]
380    fn test_entity_creates_nested_directories() {
381        let temp_dir = TempDir::new().unwrap();
382        let mut backend = SgmlBackend::new(temp_dir.path());
383
384        backend
385            .entity("src/main/java/Foo.java", "public class Foo {}")
386            .unwrap();
387
388        let file_path = temp_dir.path().join("src/main/java/Foo.java");
389        assert!(file_path.exists());
390    }
391
392    #[test]
393    fn test_entity_tracks_written_files() {
394        let temp_dir = TempDir::new().unwrap();
395        let mut backend = SgmlBackend::new(temp_dir.path());
396
397        backend.entity("file1.txt", "content1").unwrap();
398        backend.entity("file2.txt", "content2").unwrap();
399
400        assert_eq!(backend.written_files().len(), 2);
401        assert!(backend
402            .written_files()
403            .contains(&temp_dir.path().join("file1.txt")));
404        assert!(backend
405            .written_files()
406            .contains(&temp_dir.path().join("file2.txt")));
407    }
408
409    #[test]
410    fn test_combined_workflow() {
411        let temp_dir = TempDir::new().unwrap();
412        let mut backend = SgmlBackend::new(temp_dir.path());
413
414        // Build up content in buffer
415        backend.formatting_instruction("public class Foo {\n").unwrap();
416        backend.formatting_instruction("  // generated\n").unwrap();
417        backend.formatting_instruction("}\n").unwrap();
418
419        // Write buffer to file
420        let content = backend.current_output().to_string();
421        backend.entity("Foo.java", &content).unwrap();
422
423        // Clear buffer for next file
424        backend.clear_buffer();
425
426        // Verify file was written
427        let file_path = temp_dir.path().join("Foo.java");
428        let content = fs::read_to_string(&file_path).unwrap();
429        assert_eq!(content, "public class Foo {\n  // generated\n}\n");
430    }
431
432    #[test]
433    fn test_directory_creates_dir() {
434        let temp_dir = TempDir::new().unwrap();
435        let mut backend = SgmlBackend::new(temp_dir.path());
436
437        backend.directory("src/models").unwrap();
438
439        let dir_path = temp_dir.path().join("src/models");
440        assert!(dir_path.exists());
441        assert!(dir_path.is_dir());
442    }
443
444    #[test]
445    fn test_directory_creates_nested_dirs() {
446        let temp_dir = TempDir::new().unwrap();
447        let mut backend = SgmlBackend::new(temp_dir.path());
448
449        backend.directory("src/main/java/models").unwrap();
450
451        let dir_path = temp_dir.path().join("src/main/java/models");
452        assert!(dir_path.exists());
453        assert!(dir_path.is_dir());
454    }
455
456    #[test]
457    fn test_directory_idempotent() {
458        let temp_dir = TempDir::new().unwrap();
459        let mut backend = SgmlBackend::new(temp_dir.path());
460
461        // Creating the same directory twice should not fail
462        backend.directory("src/models").unwrap();
463        backend.directory("src/models").unwrap();
464
465        let dir_path = temp_dir.path().join("src/models");
466        assert!(dir_path.exists());
467        assert!(dir_path.is_dir());
468    }
469
470    #[test]
471    fn test_directory_sets_current_directory() {
472        let temp_dir = TempDir::new().unwrap();
473        let mut backend = SgmlBackend::new(temp_dir.path());
474
475        assert!(backend.current_directory().is_none());
476
477        backend.directory("src/models").unwrap();
478        assert_eq!(backend.current_directory(), Some("src/models"));
479
480        // Reset to None before creating a new absolute path
481        backend.set_current_directory(None);
482        backend.directory("src/controllers").unwrap();
483        assert_eq!(backend.current_directory(), Some("src/controllers"));
484    }
485
486    #[test]
487    fn test_entity_with_relative_path() {
488        let temp_dir = TempDir::new().unwrap();
489        let mut backend = SgmlBackend::new(temp_dir.path());
490
491        // Set current directory
492        backend.directory("generated/models").unwrap();
493
494        // Create file with relative path
495        backend.entity("User.rs", "pub struct User {}").unwrap();
496
497        // Verify file was created in the current directory
498        let file_path = temp_dir.path().join("generated/models/User.rs");
499        assert!(file_path.exists());
500        let content = fs::read_to_string(&file_path).unwrap();
501        assert_eq!(content, "pub struct User {}");
502    }
503
504    #[test]
505    fn test_entity_with_relative_path_multiple_files() {
506        let temp_dir = TempDir::new().unwrap();
507        let mut backend = SgmlBackend::new(temp_dir.path());
508
509        // Set current directory
510        backend.directory("generated/models").unwrap();
511
512        // Create multiple files with relative paths
513        backend.entity("User.rs", "pub struct User {}").unwrap();
514        backend.entity("Post.rs", "pub struct Post {}").unwrap();
515        backend.entity("Comment.rs", "pub struct Comment {}").unwrap();
516
517        // Verify all files were created in the current directory
518        assert!(temp_dir.path().join("generated/models/User.rs").exists());
519        assert!(temp_dir.path().join("generated/models/Post.rs").exists());
520        assert!(temp_dir.path().join("generated/models/Comment.rs").exists());
521    }
522
523    #[test]
524    fn test_directory_switching() {
525        let temp_dir = TempDir::new().unwrap();
526        let mut backend = SgmlBackend::new(temp_dir.path());
527
528        // Create first directory and file
529        backend.directory("src/models").unwrap();
530        backend.entity("User.rs", "pub struct User {}").unwrap();
531
532        // Reset and switch to another directory
533        backend.set_current_directory(None);
534        backend.directory("src/controllers").unwrap();
535        backend.entity("UserController.rs", "pub struct UserController {}").unwrap();
536
537        // Verify files in correct locations
538        assert!(temp_dir.path().join("src/models/User.rs").exists());
539        assert!(temp_dir.path().join("src/controllers/UserController.rs").exists());
540    }
541
542    #[test]
543    fn test_reset_current_directory() {
544        let temp_dir = TempDir::new().unwrap();
545        let mut backend = SgmlBackend::new(temp_dir.path());
546
547        // Set current directory
548        backend.directory("src/models").unwrap();
549        assert_eq!(backend.current_directory(), Some("src/models"));
550
551        // Reset to None
552        backend.set_current_directory(None);
553        assert!(backend.current_directory().is_none());
554
555        // File should now be created relative to output_dir
556        backend.entity("root.txt", "content").unwrap();
557        assert!(temp_dir.path().join("root.txt").exists());
558    }
559
560    #[test]
561    fn test_directory_with_relative_path() {
562        let temp_dir = TempDir::new().unwrap();
563        let mut backend = SgmlBackend::new(temp_dir.path());
564
565        // Create base directory
566        backend.directory("generated").unwrap();
567        assert_eq!(backend.current_directory(), Some("generated"));
568
569        // Create subdirectory with relative path
570        backend.directory("models").unwrap();
571        assert_eq!(backend.current_directory(), Some("generated/models"));
572
573        // Verify the full path was created
574        let dir_path = temp_dir.path().join("generated/models");
575        assert!(dir_path.exists());
576        assert!(dir_path.is_dir());
577    }
578
579    #[test]
580    fn test_directory_nested_relative_paths() {
581        let temp_dir = TempDir::new().unwrap();
582        let mut backend = SgmlBackend::new(temp_dir.path());
583
584        // Create nested directories using relative paths
585        backend.directory("src").unwrap();
586        backend.directory("main").unwrap();
587        backend.directory("java").unwrap();
588        backend.directory("models").unwrap();
589
590        // Current directory should be the full path
591        assert_eq!(backend.current_directory(), Some("src/main/java/models"));
592
593        // Verify the full nested path exists
594        let dir_path = temp_dir.path().join("src/main/java/models");
595        assert!(dir_path.exists());
596        assert!(dir_path.is_dir());
597    }
598
599    #[test]
600    fn test_directory_relative_then_entity() {
601        let temp_dir = TempDir::new().unwrap();
602        let mut backend = SgmlBackend::new(temp_dir.path());
603
604        // Create directory structure using relative paths
605        backend.directory("generated").unwrap();
606        backend.directory("models").unwrap();
607
608        // Create file with relative path
609        backend.entity("User.rs", "pub struct User {}").unwrap();
610
611        // Verify file is in the correct nested location
612        let file_path = temp_dir.path().join("generated/models/User.rs");
613        assert!(file_path.exists());
614        let content = fs::read_to_string(&file_path).unwrap();
615        assert_eq!(content, "pub struct User {}");
616    }
617
618    #[test]
619    fn test_directory_absolute_path_resets() {
620        let temp_dir = TempDir::new().unwrap();
621        let mut backend = SgmlBackend::new(temp_dir.path());
622
623        // Create nested directory
624        backend.directory("src").unwrap();
625        backend.directory("models").unwrap();
626        assert_eq!(backend.current_directory(), Some("src/models"));
627
628        // Reset to None and create new absolute path
629        backend.set_current_directory(None);
630        backend.directory("tests").unwrap();
631        assert_eq!(backend.current_directory(), Some("tests"));
632
633        // Verify both directories exist
634        assert!(temp_dir.path().join("src/models").exists());
635        assert!(temp_dir.path().join("tests").exists());
636    }
637
638    #[test]
639    fn test_directory_parent_navigation() {
640        let temp_dir = TempDir::new().unwrap();
641        let mut backend = SgmlBackend::new(temp_dir.path());
642
643        // Create: src/models
644        backend.directory("src").unwrap();
645        backend.directory("models").unwrap();
646        assert_eq!(backend.current_directory(), Some("src/models"));
647
648        // Navigate to parent and create sibling: src/controllers
649        backend.directory("..").unwrap();
650        assert_eq!(backend.current_directory(), Some("src"));
651        backend.directory("controllers").unwrap();
652        assert_eq!(backend.current_directory(), Some("src/controllers"));
653
654        // Verify both directories exist
655        assert!(temp_dir.path().join("src/models").exists());
656        assert!(temp_dir.path().join("src/controllers").exists());
657
658        // Create file in controllers
659        backend.entity("UserController.rs", "pub struct UserController {}").unwrap();
660        assert!(temp_dir.path().join("src/controllers/UserController.rs").exists());
661    }
662
663    #[test]
664    fn test_directory_natural_nesting() {
665        let temp_dir = TempDir::new().unwrap();
666        let mut backend = SgmlBackend::new(temp_dir.path());
667
668        // Natural nesting pattern
669        backend.directory("generated").unwrap();
670
671        // Create models subdirectory
672        backend.directory("models").unwrap();
673        backend.entity("User.rs", "pub struct User {}").unwrap();
674        backend.entity("Post.rs", "pub struct Post {}").unwrap();
675
676        // Go back to parent and create controllers subdirectory
677        backend.directory("..").unwrap();
678        backend.directory("controllers").unwrap();
679        backend.entity("UserController.rs", "pub struct UserController {}").unwrap();
680
681        // Go back to parent and create views subdirectory
682        backend.directory("..").unwrap();
683        backend.directory("views").unwrap();
684        backend.entity("user.html", "<div>User</div>").unwrap();
685
686        // Verify structure
687        assert!(temp_dir.path().join("generated/models/User.rs").exists());
688        assert!(temp_dir.path().join("generated/models/Post.rs").exists());
689        assert!(temp_dir.path().join("generated/controllers/UserController.rs").exists());
690        assert!(temp_dir.path().join("generated/views/user.html").exists());
691    }
692}