Skip to main content

ass_editor/core/document/
constructors.rs

1//! Constructors, file IO, event-channel wiring, and ID generation
2//!
3//! Provides the various ways to build an `EditorDocument` (empty, from a
4//! string, from disk), the persistence helpers, and the private `emit`
5//! helper shared with the editing submodules.
6
7use super::EditorDocument;
8#[cfg(feature = "std")]
9use super::EventSender;
10use crate::core::errors::{EditorError, Result};
11use crate::core::history::UndoManager;
12use ass_core::parser::Script;
13
14#[cfg(feature = "std")]
15use crate::events::DocumentEvent;
16
17#[cfg(not(feature = "std"))]
18use alloc::{format, string::String};
19
20impl EditorDocument {
21    /// Create a new empty document
22    pub fn new() -> Self {
23        Self {
24            #[cfg(feature = "rope")]
25            text_rope: ropey::Rope::new(),
26            #[cfg(not(feature = "rope"))]
27            text_content: String::new(),
28            id: Self::generate_id(),
29            modified: false,
30            file_path: None,
31            #[cfg(feature = "plugins")]
32            registry_integration: None,
33            history: UndoManager::new(),
34            #[cfg(feature = "std")]
35            event_tx: None,
36            #[cfg(feature = "stream")]
37            incremental_parser: crate::core::incremental::IncrementalParser::new(),
38            validator: crate::utils::validator::LazyValidator::new(),
39        }
40    }
41
42    /// Create a new document with event channel
43    #[cfg(feature = "std")]
44    pub fn with_event_channel(event_tx: EventSender) -> Self {
45        let mut doc = Self::new();
46        doc.event_tx = Some(event_tx);
47        doc
48    }
49
50    /// Create document from file path
51    #[cfg(feature = "std")]
52    pub fn from_file(path: &str) -> Result<Self> {
53        use std::fs;
54        let content = fs::read_to_string(path).map_err(|e| EditorError::IoError(e.to_string()))?;
55        let mut doc = Self::from_content(&content)?;
56        doc.file_path = Some(path.to_string());
57        Ok(doc)
58    }
59
60    /// Save document to file
61    #[cfg(feature = "std")]
62    pub fn save(&mut self) -> Result<()> {
63        if let Some(path) = self.file_path.clone() {
64            self.save_to_file(&path)
65        } else {
66            Err(EditorError::IoError(
67                "No file path set for document".to_string(),
68            ))
69        }
70    }
71
72    /// Save document to specific file path
73    #[cfg(feature = "std")]
74    pub fn save_to_file(&mut self, path: &str) -> Result<()> {
75        use std::fs;
76        let content = self.text();
77        fs::write(path, content).map_err(|e| EditorError::IoError(e.to_string()))?;
78        self.modified = false;
79        self.file_path = Some(path.to_string());
80        Ok(())
81    }
82
83    /// Create document with specific ID
84    pub fn with_id(id: String) -> Self {
85        let mut doc = Self::new();
86        doc.id = id;
87        doc
88    }
89
90    /// Emit an event to the event channel
91    #[cfg(feature = "std")]
92    pub(super) fn emit(&mut self, event: DocumentEvent) {
93        if let Some(tx) = &mut self.event_tx {
94            let _ = tx.send(event);
95        }
96    }
97
98    /// Set the event channel for this document
99    #[cfg(feature = "std")]
100    pub fn set_event_channel(&mut self, event_tx: EventSender) {
101        self.event_tx = Some(event_tx);
102    }
103
104    /// Check if document has an event channel
105    #[cfg(feature = "std")]
106    pub fn has_event_channel(&self) -> bool {
107        self.event_tx.is_some()
108    }
109
110    /// Load document from string content
111    ///
112    /// Creates a new `EditorDocument` from ASS subtitle content. The content
113    /// is validated during creation to ensure it's parseable.
114    ///
115    /// # Examples
116    ///
117    /// ```
118    /// use ass_editor::EditorDocument;
119    ///
120    /// let content = r#"
121    /// [Script Info]
122    /// Title: My Subtitle
123    ///
124    /// [V4+ Styles]
125    /// Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
126    /// Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
127    ///
128    /// [Events]
129    /// Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
130    /// Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Hello World
131    /// "#;
132    ///
133    /// let doc = EditorDocument::from_content(content).unwrap();
134    /// assert!(doc.text().contains("Hello World"));
135    /// ```
136    ///
137    /// # Errors
138    ///
139    /// Returns `Err` if the content cannot be parsed as valid ASS format.
140    pub fn from_content(content: &str) -> Result<Self> {
141        // Validate that content can be parsed
142        let _ = Script::parse(content).map_err(EditorError::from)?;
143
144        #[cfg(feature = "stream")]
145        let mut incremental_parser = crate::core::incremental::IncrementalParser::new();
146        #[cfg(feature = "stream")]
147        incremental_parser.initialize_cache(content);
148
149        Ok(Self {
150            #[cfg(feature = "rope")]
151            text_rope: ropey::Rope::from_str(content),
152            #[cfg(not(feature = "rope"))]
153            text_content: content.to_string(),
154            id: Self::generate_id(),
155            modified: false,
156            file_path: None,
157            #[cfg(feature = "plugins")]
158            registry_integration: None,
159            history: UndoManager::new(),
160            #[cfg(feature = "std")]
161            event_tx: None,
162            #[cfg(feature = "stream")]
163            incremental_parser,
164            validator: crate::utils::validator::LazyValidator::new(),
165        })
166    }
167
168    /// Generate unique document ID
169    fn generate_id() -> String {
170        // Simple ID generation - in production might use UUID
171        #[cfg(feature = "std")]
172        {
173            use std::time::{SystemTime, UNIX_EPOCH};
174            let timestamp = SystemTime::now()
175                .duration_since(UNIX_EPOCH)
176                .unwrap_or_default()
177                .as_nanos();
178            format!("doc_{timestamp}")
179        }
180        #[cfg(not(feature = "std"))]
181        {
182            use core::sync::atomic::{AtomicU32, Ordering};
183            static COUNTER: AtomicU32 = AtomicU32::new(0);
184            let id = COUNTER.fetch_add(1, Ordering::Relaxed).wrapping_add(1);
185            format!("doc_{id}")
186        }
187    }
188}