1use crate::parser::{
7 ast::{Event, EventType, Section, Span},
8 errors::{IssueCategory, IssueSeverity, ParseError, ParseIssue},
9 position_tracker::PositionTracker,
10 sections::SectionParseResult,
11 ParseResult,
12};
13use alloc::{format, vec::Vec};
14
15pub struct EventsParser<'a> {
26 tracker: PositionTracker<'a>,
28 issues: Vec<ParseIssue>,
30 format: Option<Vec<&'a str>>,
32}
33
34impl<'a> EventsParser<'a> {
35 pub fn parse_event_line(
55 line: &'a str,
56 format: &[&'a str],
57 line_number: u32,
58 ) -> core::result::Result<Event<'a>, ParseError> {
59 let (event_type, data) = if let Some(data) = line.strip_prefix("Dialogue:") {
61 (EventType::Dialogue, data)
62 } else if let Some(data) = line.strip_prefix("Comment:") {
63 (EventType::Comment, data)
64 } else if let Some(data) = line.strip_prefix("Picture:") {
65 (EventType::Picture, data)
66 } else if let Some(data) = line.strip_prefix("Sound:") {
67 (EventType::Sound, data)
68 } else if let Some(data) = line.strip_prefix("Movie:") {
69 (EventType::Movie, data)
70 } else if let Some(data) = line.strip_prefix("Command:") {
71 (EventType::Command, data)
72 } else {
73 return Err(ParseError::InvalidEventType {
74 line: line_number as usize,
75 });
76 };
77
78 Self::parse_event_data_static(event_type, data.trim(), format, line_number)
80 }
81
82 fn parse_event_data_static(
84 event_type: EventType,
85 data: &'a str,
86 format: &[&'a str],
87 line_number: u32,
88 ) -> core::result::Result<Event<'a>, ParseError> {
89 let format = if format.is_empty() {
90 &[
91 "Layer", "Start", "End", "Style", "Name", "MarginL", "MarginR", "MarginV",
92 "Effect", "Text",
93 ]
94 } else {
95 format
96 };
97
98 let has_text_field = format
100 .iter()
101 .any(|&field| field.eq_ignore_ascii_case("Text"));
102
103 let parts: Vec<&str> = if has_text_field {
105 data.splitn(format.len(), ',').collect()
107 } else {
108 data.splitn(10, ',').collect()
110 };
111
112 if parts.len() < format.len() {
113 return Err(ParseError::InsufficientFields {
114 expected: format.len(),
115 found: parts.len(),
116 line: line_number as usize,
117 });
118 }
119
120 let get_field = |name: &str| -> &'a str {
121 format
122 .iter()
123 .position(|&field| field.eq_ignore_ascii_case(name))
124 .and_then(|idx| parts.get(idx))
125 .map_or("", |s| s.trim())
126 };
127
128 let span = Span::new(0, 0, line_number, 1);
130
131 Ok(Event {
132 event_type,
133 layer: get_field("Layer"),
134 start: get_field("Start"),
135 end: get_field("End"),
136 style: get_field("Style"),
137 name: get_field("Name"),
138 margin_l: get_field("MarginL"),
139 margin_r: get_field("MarginR"),
140 margin_v: get_field("MarginV"),
141 margin_t: format
142 .iter()
143 .any(|&f| f.eq_ignore_ascii_case("MarginT"))
144 .then(|| get_field("MarginT")),
145 margin_b: format
146 .iter()
147 .any(|&f| f.eq_ignore_ascii_case("MarginB"))
148 .then(|| get_field("MarginB")),
149 effect: get_field("Effect"),
150 text: get_field("Text"),
151 span,
152 })
153 }
154 #[must_use]
162 #[allow(clippy::missing_const_for_fn)] pub fn new(source: &'a str, start_position: usize, start_line: usize) -> Self {
164 Self {
165 tracker: PositionTracker::new_at(
166 source,
167 start_position,
168 u32::try_from(start_line).unwrap_or(u32::MAX),
169 1,
170 ),
171 issues: Vec::new(),
172 format: None,
173 }
174 }
175
176 #[must_use]
178 pub fn with_format(
179 source: &'a str,
180 format: &[&'a str],
181 start_position: usize,
182 start_line: u32,
183 ) -> Self {
184 Self {
185 tracker: PositionTracker::new_at(source, start_position, start_line, 1),
186 issues: Vec::new(),
187 format: Some(format.to_vec()),
188 }
189 }
190
191 pub fn parse(mut self) -> ParseResult<SectionParseResult<'a>> {
205 let mut events = Vec::new();
206
207 while !self.tracker.is_at_end() && !self.at_next_section() {
208 self.skip_whitespace_and_comments();
209
210 if self.tracker.is_at_end() || self.at_next_section() {
211 break;
212 }
213
214 let line_start = self.tracker.checkpoint();
215 let line = self.current_line().trim();
216
217 if line.is_empty() {
218 self.tracker.skip_line();
219 continue;
220 }
221
222 if line.starts_with("Format:") {
223 self.parse_format_line(line);
224 } else if let Some(event) = self.parse_event_line_internal(line, &line_start) {
225 events.push(event);
226 }
227
228 self.tracker.skip_line();
229 }
230
231 Ok((
232 Section::Events(events),
233 self.format,
234 self.issues,
235 self.tracker.offset(),
236 self.tracker.line() as usize,
237 ))
238 }
239
240 fn parse_format_line(&mut self, line: &'a str) {
242 if let Some(format_data) = line.strip_prefix("Format:") {
243 let fields: Vec<&'a str> = format_data.split(',').map(str::trim).collect();
244 self.format = Some(fields);
245 }
246 }
247
248 fn parse_event_line_internal(
250 &mut self,
251 line: &'a str,
252 line_start: &PositionTracker<'a>,
253 ) -> Option<Event<'a>> {
254 let (event_type, data) = if let Some(data) = line.strip_prefix("Dialogue:") {
255 (EventType::Dialogue, data)
256 } else if let Some(data) = line.strip_prefix("Comment:") {
257 (EventType::Comment, data)
258 } else if let Some(data) = line.strip_prefix("Picture:") {
259 (EventType::Picture, data)
260 } else if let Some(data) = line.strip_prefix("Sound:") {
261 (EventType::Sound, data)
262 } else if let Some(data) = line.strip_prefix("Movie:") {
263 (EventType::Movie, data)
264 } else if let Some(data) = line.strip_prefix("Command:") {
265 (EventType::Command, data)
266 } else {
267 return None;
268 };
269
270 self.parse_event_data(event_type, data.trim(), line_start)
271 }
272
273 fn parse_event_data(
275 &mut self,
276 event_type: EventType,
277 data: &'a str,
278 line_start: &PositionTracker<'a>,
279 ) -> Option<Event<'a>> {
280 let format = self.format.as_deref().unwrap_or(&[
281 "Layer", "Start", "End", "Style", "Name", "MarginL", "MarginR", "MarginV", "Effect",
282 "Text",
283 ]);
284
285 let has_text_field = format
287 .iter()
288 .any(|&field| field.eq_ignore_ascii_case("Text"));
289
290 let parts: Vec<&str> = if has_text_field {
292 data.splitn(format.len(), ',').collect()
294 } else {
295 data.splitn(10, ',').collect()
297 };
298
299 if parts.len() < format.len() {
300 self.issues.push(ParseIssue::new(
301 IssueSeverity::Warning,
302 IssueCategory::Format,
303 format!(
304 "Event line has {} fields, expected at least {}",
305 parts.len(),
306 format.len()
307 ),
308 line_start.line() as usize,
309 ));
310 return None;
311 }
312
313 let get_field = |name: &str| -> &'a str {
314 format
315 .iter()
316 .position(|&field| field.eq_ignore_ascii_case(name))
317 .and_then(|idx| parts.get(idx))
318 .map_or("", |s| s.trim())
319 };
320
321 let text = get_field("Text");
322
323 let full_line = self.current_line();
327 let span = line_start.span_for(full_line.len());
328
329 Some(Event {
330 event_type,
331 layer: get_field("Layer"),
332 start: get_field("Start"),
333 end: get_field("End"),
334 style: get_field("Style"),
335 name: get_field("Name"),
336 margin_l: get_field("MarginL"),
337 margin_r: get_field("MarginR"),
338 margin_v: get_field("MarginV"),
339 margin_t: format
340 .iter()
341 .any(|&f| f.eq_ignore_ascii_case("MarginT"))
342 .then(|| get_field("MarginT")),
343 margin_b: format
344 .iter()
345 .any(|&f| f.eq_ignore_ascii_case("MarginB"))
346 .then(|| get_field("MarginB")),
347 effect: get_field("Effect"),
348 text,
349 span,
350 })
351 }
352
353 fn current_line(&self) -> &'a str {
355 let remaining = self.tracker.remaining();
356 let end = remaining.find('\n').unwrap_or(remaining.len());
357 &remaining[..end]
358 }
359
360 #[must_use]
362 fn at_next_section(&self) -> bool {
363 self.tracker.remaining().trim_start().starts_with('[')
364 }
365
366 fn skip_whitespace_and_comments(&mut self) {
368 loop {
369 self.tracker.skip_whitespace();
370
371 let remaining = self.tracker.remaining();
372 if remaining.is_empty() {
373 break;
374 }
375
376 if remaining.starts_with(';') || remaining.starts_with('#') {
377 self.tracker.skip_line();
378 continue;
379 }
380
381 if remaining.starts_with('\n') {
383 self.tracker.advance(1);
384 continue;
385 }
386
387 break;
388 }
389 }
390
391 #[must_use]
393 pub fn issues(self) -> Vec<ParseIssue> {
394 self.issues
395 }
396
397 #[must_use]
399 pub const fn format(&self) -> Option<&Vec<&'a str>> {
400 self.format.as_ref()
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407 #[cfg(not(feature = "std"))]
408 use alloc::vec;
409
410 #[test]
411 fn parse_empty_section() {
412 let parser = EventsParser::new("", 0, 1);
413 let result = parser.parse();
414 assert!(result.is_ok());
415
416 let (section, ..) = result.unwrap();
417 if let Section::Events(events) = section {
418 assert!(events.is_empty());
419 } else {
420 panic!("Expected Events section");
421 }
422 }
423
424 #[test]
425 fn parse_with_format_and_dialogue() {
426 let content = "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Hello World!\n";
427 let parser = EventsParser::new(content, 0, 1);
428 let result = parser.parse();
429 assert!(result.is_ok());
430
431 let (section, ..) = result.unwrap();
432 if let Section::Events(events) = section {
433 assert_eq!(events.len(), 1);
434 let event = &events[0];
435 assert!(matches!(event.event_type, EventType::Dialogue));
436 assert_eq!(event.start, "0:00:00.00");
437 assert_eq!(event.end, "0:00:05.00");
438 assert_eq!(event.style, "Default");
439 assert_eq!(event.text, "Hello World!");
440 assert!(event.span.start > 0);
442 assert!(event.span.end > event.span.start);
443 } else {
444 panic!("Expected Events section");
445 }
446 }
447
448 #[test]
449 fn parse_different_event_types() {
450 let content = "Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Dialogue\nComment: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Comment\n";
451 let parser = EventsParser::new(content, 0, 1);
452 let result = parser.parse();
453 assert!(result.is_ok());
454
455 let (section, ..) = result.unwrap();
456 if let Section::Events(events) = section {
457 assert_eq!(events.len(), 2);
458 assert!(matches!(events[0].event_type, EventType::Dialogue));
459 assert!(matches!(events[1].event_type, EventType::Comment));
460 assert_eq!(events[0].text, "Dialogue");
461 assert_eq!(events[1].text, "Comment");
462 } else {
463 panic!("Expected Events section");
464 }
465 }
466
467 #[test]
468 fn handle_text_with_commas() {
469 let content =
470 "Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Hello, world, with commas!\n";
471 let parser = EventsParser::new(content, 0, 1);
472 let result = parser.parse();
473 assert!(result.is_ok());
474
475 let (section, ..) = result.unwrap();
476 if let Section::Events(events) = section {
477 assert_eq!(events.len(), 1);
478 assert_eq!(events[0].text, "Hello, world, with commas!");
479 } else {
480 panic!("Expected Events section");
481 }
482 }
483
484 #[test]
485 fn skip_comments_and_whitespace() {
486 let content = "; Comment\n# Another comment\n\nDialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Test\n";
487 let parser = EventsParser::new(content, 0, 1);
488 let result = parser.parse();
489 assert!(result.is_ok());
490
491 let (section, ..) = result.unwrap();
492 if let Section::Events(events) = section {
493 assert_eq!(events.len(), 1);
494 assert_eq!(events[0].text, "Test");
495 } else {
496 panic!("Expected Events section");
497 }
498 }
499
500 #[test]
501 fn parse_with_position_tracking() {
502 let prefix = "a".repeat(200); let section_content = "Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Test Event\n";
505 let full_content = format!("{prefix}{section_content}");
506
507 let parser = EventsParser::new(&full_content, 200, 15);
509 let result = parser.parse();
510 assert!(result.is_ok());
511
512 let (section, _, _, final_pos, final_line) = result.unwrap();
513 if let Section::Events(events) = section {
514 assert_eq!(events.len(), 1);
515 let event = &events[0];
516 assert_eq!(event.span.start, 200);
517 assert_eq!(event.span.line, 15);
518 assert_eq!(event.text, "Test Event");
519 } else {
520 panic!("Expected Events section");
521 }
522
523 assert_eq!(final_pos, 200 + section_content.len());
524 assert_eq!(final_line, 16);
525 }
526
527 #[test]
528 fn parse_without_format_line() {
529 let content = "Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,No format line\n";
530 let parser = EventsParser::new(content, 0, 1);
531 let result = parser.parse();
532 assert!(result.is_ok());
533
534 let (section, format, ..) = result.unwrap();
535 if let Section::Events(events) = section {
536 assert_eq!(events.len(), 1);
537 assert_eq!(events[0].text, "No format line");
538 } else {
539 panic!("Expected Events section");
540 }
541 assert!(format.is_none());
542 }
543
544 #[test]
545 fn test_public_parse_event_line() {
546 let format = vec![
547 "Layer", "Start", "End", "Style", "Name", "MarginL", "MarginR", "MarginV", "Effect",
548 "Text",
549 ];
550 let line = "Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Test text";
551
552 let result = EventsParser::parse_event_line(line, &format, 1);
553 assert!(result.is_ok());
554
555 let event = result.unwrap();
556 assert!(matches!(event.event_type, EventType::Dialogue));
557 assert_eq!(event.start, "0:00:00.00");
558 assert_eq!(event.end, "0:00:05.00");
559 assert_eq!(event.style, "Default");
560 assert_eq!(event.text, "Test text");
561 }
562
563 #[test]
564 fn test_parse_event_line_invalid_type() {
565 let format = vec!["Layer", "Start", "End", "Style", "Text"];
566 let line = "Invalid: 0,0:00:00.00,0:00:05.00,Default,Test";
567
568 let result = EventsParser::parse_event_line(line, &format, 1);
569 assert!(result.is_err());
570
571 if let Err(e) = result {
572 assert!(matches!(e, ParseError::InvalidEventType { .. }));
573 }
574 }
575
576 #[test]
577 fn test_parse_event_line_insufficient_fields() {
578 let format = vec![
579 "Layer", "Start", "End", "Style", "Name", "MarginL", "MarginR", "MarginV", "Effect",
580 "Text",
581 ];
582 let line = "Dialogue: 0,0:00:00.00,0:00:05.00"; let result = EventsParser::parse_event_line(line, &format, 1);
585 assert!(result.is_err());
586
587 if let Err(e) = result {
588 assert!(matches!(e, ParseError::InsufficientFields { .. }));
589 }
590 }
591
592 #[test]
593 fn test_parse_event_line_with_commas_in_text() {
594 let format = vec![
595 "Layer", "Start", "End", "Style", "Name", "MarginL", "MarginR", "MarginV", "Effect",
596 "Text",
597 ];
598 let line = "Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Hello, world, with commas!";
599
600 let result = EventsParser::parse_event_line(line, &format, 1);
601 assert!(result.is_ok());
602
603 let event = result.unwrap();
604 assert_eq!(event.text, "Hello, world, with commas!");
605 }
606}