at main 361 lines 11 kB view raw
1//! State structures for EditorWriter, grouped by concern. 2 3use std::collections::HashMap; 4use std::ops::Range; 5 6use markdown_weaver::Alignment; 7use smol_str::{SmolStr, ToSmolStr, format_smolstr}; 8 9use crate::offset_map::OffsetMapping; 10use crate::syntax::{SyntaxSpanInfo, SyntaxType}; 11 12/// Table rendering state. 13#[derive(Debug, Clone, Default)] 14pub struct TableContext { 15 pub state: TableState, 16 pub alignments: Vec<Alignment>, 17 pub cell_index: usize, 18 pub render_as_markdown: bool, 19 pub start_offset: Option<usize>, 20} 21 22#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] 23pub enum TableState { 24 #[default] 25 Head, 26 Body, 27} 28 29/// Code block buffering state. 30#[derive(Debug, Clone, Default)] 31pub struct CodeBlockContext { 32 /// (language, content) being buffered 33 pub buffer: Option<(Option<SmolStr>, String)>, 34 /// Byte range of buffered content 35 pub byte_range: Option<Range<usize>>, 36 /// Char range of buffered content 37 pub char_range: Option<Range<usize>>, 38 /// Char offset where code block started 39 pub block_start: Option<usize>, 40 /// Index of opening fence syntax span 41 pub opening_span_idx: Option<usize>, 42} 43 44impl CodeBlockContext { 45 pub fn is_active(&self) -> bool { 46 self.buffer.is_some() 47 } 48 49 pub fn clear(&mut self) { 50 *self = Self::default(); 51 } 52} 53 54/// Node ID generation for DOM element IDs. 55#[derive(Debug, Clone)] 56pub struct NodeIdGenerator { 57 /// Paragraph ID prefix (e.g., "p-0") 58 pub prefix: Option<SmolStr>, 59 /// Auto-increment base for paragraph prefixes 60 pub auto_increment_base: Option<usize>, 61 /// Override for specific paragraph index 62 pub static_override: Option<(usize, SmolStr)>, 63 /// Current paragraph index (0-indexed) 64 pub current_paragraph: usize, 65 /// Next node ID counter within paragraph 66 pub next_node_id: usize, 67 /// Next syntax span ID counter 68 pub next_syn_id: usize, 69} 70 71impl Default for NodeIdGenerator { 72 fn default() -> Self { 73 Self { 74 prefix: None, 75 auto_increment_base: None, 76 static_override: None, 77 current_paragraph: 0, 78 next_node_id: 0, 79 next_syn_id: 0, 80 } 81 } 82} 83 84impl NodeIdGenerator { 85 /// Get the current paragraph prefix. 86 pub fn current_prefix(&self) -> SmolStr { 87 if let Some((idx, ref prefix)) = self.static_override { 88 if idx == self.current_paragraph { 89 return prefix.clone(); 90 } 91 } 92 if let Some(base) = self.auto_increment_base { 93 return format_smolstr!("p-{}", base + self.current_paragraph); 94 } 95 self.prefix.clone().unwrap_or_else(|| "p-0".to_smolstr()) 96 } 97 98 /// Generate a node ID (e.g., "p-0-n3") 99 pub fn next_node(&mut self) -> SmolStr { 100 let id = if let Some(ref prefix) = self.prefix { 101 format_smolstr!("{}-n{}", prefix, self.next_node_id) 102 } else { 103 format_smolstr!("n{}", self.next_node_id) 104 }; 105 self.next_node_id += 1; 106 SmolStr::new(id) 107 } 108 109 /// Generate a syntax span ID (e.g., "s5") 110 pub fn next_syn(&mut self) -> SmolStr { 111 let id = format_smolstr!("s{}", self.next_syn_id); 112 self.next_syn_id += 1; 113 SmolStr::new(id) 114 } 115 116 /// Advance to next paragraph. 117 pub fn next_paragraph(&mut self) { 118 self.current_paragraph += 1; 119 self.next_node_id = 0; 120 121 // Update prefix for next paragraph 122 if let Some((override_idx, ref override_prefix)) = self.static_override { 123 if self.current_paragraph == override_idx { 124 self.prefix = Some(override_prefix.clone()); 125 } else if let Some(base) = self.auto_increment_base { 126 self.prefix = Some(format_smolstr!("p-{}", base + self.current_paragraph)); 127 } 128 } else if let Some(base) = self.auto_increment_base { 129 self.prefix = Some(format_smolstr!("p-{}", base + self.current_paragraph)); 130 } 131 } 132} 133 134/// Current DOM node tracking for offset mapping. 135#[derive(Debug, Clone, Default)] 136pub struct CurrentNodeState { 137 /// Node ID for current text container 138 pub id: Option<SmolStr>, 139 /// UTF-16 offset within current node 140 pub char_offset: usize, 141 /// Number of child elements in current container 142 pub child_count: usize, 143} 144 145impl CurrentNodeState { 146 pub fn begin(&mut self, id: SmolStr) { 147 self.id = Some(id); 148 self.char_offset = 0; 149 self.child_count = 0; 150 } 151 152 pub fn end(&mut self) { 153 self.id = None; 154 self.char_offset = 0; 155 self.child_count = 0; 156 } 157} 158 159/// Paragraph boundary tracking. 160#[derive(Debug, Clone, Default)] 161pub struct ParagraphTracker { 162 /// Completed paragraph ranges: (byte_range, char_range) 163 pub ranges: Vec<(Range<usize>, Range<usize>)>, 164 /// Start of current paragraph: (byte_offset, char_offset) 165 pub current_start: Option<(usize, usize)>, 166 /// Pre-gap position for paragraph start (captured before gap emission). 167 /// This ensures the paragraph's char_range includes leading whitespace. 168 pub pre_gap_start: Option<(usize, usize)>, 169 /// List nesting depth (suppress paragraph boundaries inside lists) 170 pub list_depth: usize, 171 /// In footnote definition (suppress inner paragraph boundaries) 172 pub in_footnote_def: bool, 173} 174 175impl ParagraphTracker { 176 pub fn start_paragraph(&mut self, byte_offset: usize, char_offset: usize) { 177 self.current_start = Some((byte_offset, char_offset)); 178 } 179 180 pub fn end_paragraph( 181 &mut self, 182 byte_offset: usize, 183 char_offset: usize, 184 ) -> Option<(Range<usize>, Range<usize>)> { 185 if let Some((start_byte, start_char)) = self.current_start.take() { 186 let ranges = (start_byte..byte_offset, start_char..char_offset); 187 self.ranges.push(ranges.clone()); 188 Some(ranges) 189 } else { 190 None 191 } 192 } 193 194 pub fn in_list(&self) -> bool { 195 self.list_depth > 0 196 } 197 198 pub fn should_track_boundaries(&self) -> bool { 199 self.list_depth == 0 && !self.in_footnote_def 200 } 201} 202 203/// Current paragraph build state (offset maps, syntax spans, refs). 204#[derive(Debug, Clone, Default)] 205pub struct ParagraphBuildState { 206 /// Offset mappings for current paragraph 207 pub offset_maps: Vec<OffsetMapping>, 208 /// Syntax spans for current paragraph 209 pub syntax_spans: Vec<SyntaxSpanInfo>, 210 /// Collected refs for current paragraph 211 pub collected_refs: Vec<weaver_common::ExtractedRef>, 212 /// Stack of pending inline formats: (syn_id, char_start) 213 pub pending_inline_formats: Vec<(SmolStr, usize)>, 214} 215 216impl ParagraphBuildState { 217 pub fn take_all( 218 &mut self, 219 ) -> ( 220 Vec<OffsetMapping>, 221 Vec<SyntaxSpanInfo>, 222 Vec<weaver_common::ExtractedRef>, 223 ) { 224 ( 225 std::mem::take(&mut self.offset_maps), 226 std::mem::take(&mut self.syntax_spans), 227 std::mem::take(&mut self.collected_refs), 228 ) 229 } 230 231 /// Finalize a paired inline format (Strong, Emphasis, Strikethrough). 232 pub fn finalize_paired_format(&mut self, last_char_offset: usize) { 233 if let Some((opening_syn_id, format_start)) = self.pending_inline_formats.pop() { 234 let formatted_range = format_start..last_char_offset; 235 236 // Update opening span 237 if let Some(span) = self 238 .syntax_spans 239 .iter_mut() 240 .find(|s| s.syn_id == opening_syn_id) 241 { 242 span.formatted_range = Some(formatted_range.clone()); 243 } 244 245 // Update closing span (most recent) 246 if let Some(closing) = self.syntax_spans.last_mut() { 247 if closing.syntax_type == SyntaxType::Inline { 248 closing.formatted_range = Some(formatted_range); 249 } 250 } 251 } 252 } 253} 254 255/// WeaverBlock prefix system state. 256#[derive(Debug, Clone, Default)] 257pub struct WeaverBlockContext { 258 /// Pending attrs to apply to next block element 259 pub pending_attrs: Option<markdown_weaver::WeaverAttributes<'static>>, 260 /// Type of wrapper element currently open 261 pub active_wrapper: Option<WrapperElement>, 262 /// Buffer for WeaverBlock text content 263 pub buffer: String, 264 /// Start char offset of current WeaverBlock 265 pub char_start: Option<usize>, 266} 267 268#[derive(Debug, Clone, Copy, PartialEq, Eq)] 269pub enum WrapperElement { 270 Aside, 271 Div, 272} 273 274/// Footnote reference/definition linking state. 275#[derive(Debug, Clone, Default)] 276pub struct FootnoteContext { 277 /// Maps footnote name -> (syntax_span_index, char_start) 278 pub ref_spans: HashMap<SmolStr, (usize, usize)>, 279 /// Current footnote def being processed: (name, span_idx, char_start) 280 pub current_def: Option<(SmolStr, usize, usize)>, 281} 282 283/// UTF-16 offset checkpoints for incremental tracking. 284#[derive(Debug, Clone, Default)] 285pub struct Utf16Tracker { 286 /// Checkpoints: (char_offset, utf16_offset) 287 pub checkpoints: Vec<(usize, usize)>, 288} 289 290impl Utf16Tracker { 291 pub fn new() -> Self { 292 Self { 293 checkpoints: vec![(0, 0)], 294 } 295 } 296 297 /// Add a checkpoint. 298 pub fn checkpoint(&mut self, char_offset: usize, utf16_offset: usize) { 299 if self.checkpoints.last().map(|(c, _)| *c) != Some(char_offset) { 300 self.checkpoints.push((char_offset, utf16_offset)); 301 } 302 } 303 304 /// Get the last checkpoint. 305 pub fn last(&self) -> (usize, usize) { 306 self.checkpoints.last().copied().unwrap_or((0, 0)) 307 } 308} 309 310#[cfg(test)] 311mod tests { 312 use super::*; 313 314 #[test] 315 fn test_node_id_generator() { 316 let mut generator = NodeIdGenerator::default(); 317 generator.prefix = Some("p-0".to_smolstr()); 318 319 assert_eq!(generator.next_node().as_str(), "p-0-n0"); 320 assert_eq!(generator.next_node().as_str(), "p-0-n1"); 321 assert_eq!(generator.next_syn().as_str(), "s0"); 322 assert_eq!(generator.next_syn().as_str(), "s1"); 323 } 324 325 #[test] 326 fn test_node_id_generator_auto_increment() { 327 let mut generator = NodeIdGenerator::default(); 328 generator.auto_increment_base = Some(0); 329 generator.prefix = Some("p-0".to_smolstr()); 330 331 assert_eq!(generator.next_node().as_str(), "p-0-n0"); 332 generator.next_paragraph(); 333 assert_eq!(generator.prefix, Some("p-1".to_smolstr())); 334 assert_eq!(generator.next_node().as_str(), "p-1-n0"); 335 } 336 337 #[test] 338 fn test_paragraph_tracker() { 339 let mut tracker = ParagraphTracker::default(); 340 341 tracker.start_paragraph(0, 0); 342 let ranges = tracker.end_paragraph(10, 10); 343 assert_eq!(ranges, Some((0..10, 0..10))); 344 345 tracker.list_depth = 1; 346 assert!(tracker.in_list()); 347 assert!(!tracker.should_track_boundaries()); 348 } 349 350 #[test] 351 fn test_code_block_context() { 352 let mut ctx = CodeBlockContext::default(); 353 assert!(!ctx.is_active()); 354 355 ctx.buffer = Some((Some("rust".to_smolstr()), String::new())); 356 assert!(ctx.is_active()); 357 358 ctx.clear(); 359 assert!(!ctx.is_active()); 360 } 361}