1use crate::audio::AudioChannel;
7use crate::error::{EdlError, EdlResult};
8use crate::event::{EditType, EdlEvent, TrackType};
9use crate::motion::MotionEffect;
10use crate::reel::ReelId;
11use crate::timecode::{EdlFrameRate, EdlTimecode};
12use crate::{Edl, EdlFormat};
13use nom::{
14 branch::alt,
15 bytes::complete::{tag, take_while1},
16 character::complete::{space0, space1},
17 combinator::{map_res, opt, value},
18 sequence::terminated,
19 IResult, Parser,
20};
21
22pub fn parse_edl(input: &str) -> EdlResult<Edl> {
28 let mut parser = EdlParser::new();
29 parser.parse(input)
30}
31
32#[derive(Debug)]
34pub struct EdlParser {
35 pub strict_mode: bool,
37 current_line: usize,
39}
40
41impl EdlParser {
42 #[must_use]
44 pub const fn new() -> Self {
45 Self {
46 strict_mode: false,
47 current_line: 0,
48 }
49 }
50
51 #[must_use]
53 pub const fn strict() -> Self {
54 Self {
55 strict_mode: true,
56 current_line: 0,
57 }
58 }
59
60 pub fn set_strict_mode(&mut self, strict: bool) {
62 self.strict_mode = strict;
63 }
64
65 #[allow(clippy::too_many_lines)]
71 pub fn parse(&mut self, input: &str) -> EdlResult<Edl> {
72 let mut edl = Edl::new(EdlFormat::Cmx3600);
73 let mut current_event: Option<EdlEvent> = None;
74
75 for (line_num, line) in input.lines().enumerate() {
76 self.current_line = line_num + 1;
77 let trimmed = line.trim();
78
79 if trimmed.is_empty() {
81 continue;
82 }
83
84 if trimmed.starts_with('*') {
86 if let Some(comment) = Self::parse_comment_line(trimmed) {
87 if comment.starts_with("FROM CLIP NAME:") {
89 if let Some(event) = &mut current_event {
90 let name = comment.trim_start_matches("FROM CLIP NAME:").trim();
91 event.set_clip_name(name.to_string());
92 }
93 } else if comment.starts_with("TO CLIP NAME:") {
94 if let Some(event) = &mut current_event {
95 let name = comment.trim_start_matches("TO CLIP NAME:").trim();
96 event.add_comment(format!("TO CLIP NAME: {name}"));
97 }
98 } else if comment.starts_with("M2") {
99 if let Some(event) = &mut current_event {
101 if let Ok(effect) = MotionEffect::from_m2_comment(&comment) {
102 event.set_motion_effect(effect);
103 }
104 }
105 } else if let Some(event) = &mut current_event {
106 event.add_comment(comment);
107 }
108 }
109 continue;
110 }
111
112 if trimmed.starts_with("TITLE:") {
114 let title = trimmed.trim_start_matches("TITLE:").trim();
115 edl.set_title(title.to_string());
116 continue;
117 }
118
119 if trimmed.starts_with("FCM:") {
120 let fcm = trimmed.trim_start_matches("FCM:").trim();
121 let fcm_upper = fcm.to_uppercase();
122 let frame_rate = if fcm_upper.contains("NON") {
123 EdlFrameRate::Fps2997NDF
124 } else if fcm_upper.contains("DROP") {
125 EdlFrameRate::Fps2997DF
126 } else {
127 EdlFrameRate::Fps2997NDF
128 };
129 edl.set_frame_rate(frame_rate);
130 continue;
131 }
132
133 if let Ok(event) = self.parse_event_line(trimmed, edl.frame_rate) {
135 if let Some(prev_event) = current_event.take() {
137 edl.add_event(prev_event)
138 .map_err(|e| EdlError::parse(self.current_line, format!("{e}")))?;
139 }
140 current_event = Some(event);
141 }
142 }
143
144 if let Some(event) = current_event {
146 edl.add_event(event)
147 .map_err(|e| EdlError::parse(self.current_line, format!("{e}")))?;
148 }
149
150 Ok(edl)
151 }
152
153 fn parse_comment_line(line: &str) -> Option<String> {
155 line.strip_prefix('*').map(|s| s.trim().to_string())
156 }
157
158 #[allow(clippy::too_many_lines)]
166 fn parse_event_line(&self, line: &str, frame_rate: EdlFrameRate) -> EdlResult<EdlEvent> {
167 let result = Self::event_line_parser(line, frame_rate);
168
169 match result {
170 Ok((_, event)) => Ok(event),
171 Err(e) => Err(EdlError::parse(
172 self.current_line,
173 format!("Failed to parse event line: {e}"),
174 )),
175 }
176 }
177
178 fn event_line_parser(input: &str, frame_rate: EdlFrameRate) -> IResult<&str, EdlEvent> {
180 let (input, _) = space0.parse(input)?;
181
182 let mut parse_num = map_res(take_while1(|c: char| c.is_ascii_digit()), |s: &str| {
184 s.parse::<u32>()
185 });
186 let (input, event_num) = parse_num.parse(input)?;
187
188 let (input, _) = space1.parse(input)?;
189
190 let (input, reel) = take_while1(|c: char| !c.is_whitespace()).parse(input)?;
192 let reel_id = ReelId::new(reel).map_err(|_| {
193 nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Tag))
194 })?;
195
196 let (input, _) = space1.parse(input)?;
197
198 let (input, track) = Self::track_type_parser(input)?;
200
201 let (input, _) = space1.parse(input)?;
202
203 let (input, edit_type) = Self::edit_type_parser(input)?;
205
206 let (input, _) = space0.parse(input)?;
207
208 let parse_duration = map_res(take_while1(|c: char| c.is_ascii_digit()), |s: &str| {
210 s.parse::<u32>()
211 });
212 let mut opt_duration = opt(terminated(parse_duration, space1));
213 let (input, transition_duration) = opt_duration.parse(input)?;
214
215 let (input, _) = space0.parse(input)?;
217
218 let (input, source_in) = Self::timecode_parser(input, frame_rate)?;
220 let (input, _) = space1.parse(input)?;
221 let (input, source_out) = Self::timecode_parser(input, frame_rate)?;
222 let (input, _) = space1.parse(input)?;
223 let (input, record_in) = Self::timecode_parser(input, frame_rate)?;
224 let (input, _) = space1.parse(input)?;
225 let (input, record_out) = Self::timecode_parser(input, frame_rate)?;
226
227 let mut event = EdlEvent::new(
228 event_num,
229 reel_id.to_string(),
230 track,
231 edit_type,
232 source_in,
233 source_out,
234 record_in,
235 record_out,
236 );
237
238 if let Some(duration) = transition_duration {
239 event.set_transition_duration(duration);
240 }
241
242 Ok((input, event))
243 }
244
245 fn track_type_parser(input: &str) -> IResult<&str, TrackType> {
247 alt((
248 value(TrackType::AudioPairWithVideo, tag("AA/V")),
249 value(TrackType::AudioWithVideo, tag("A/V")),
250 value(TrackType::AudioPair, tag("AA")),
251 value(TrackType::Audio(AudioChannel::A4), tag("A4")),
252 value(TrackType::Audio(AudioChannel::A3), tag("A3")),
253 value(TrackType::Audio(AudioChannel::A2), tag("A2")),
254 value(TrackType::Audio(AudioChannel::A1), tag("A")),
255 value(TrackType::Video, tag("V")),
256 ))
257 .parse(input)
258 }
259
260 fn edit_type_parser(input: &str) -> IResult<&str, EditType> {
262 alt((
263 value(EditType::Cut, tag("C")),
264 value(EditType::Dissolve, tag("D")),
265 value(EditType::Wipe, tag("W")),
266 value(EditType::Key, tag("K")),
267 ))
268 .parse(input)
269 }
270
271 fn timecode_parser(input: &str, frame_rate: EdlFrameRate) -> IResult<&str, EdlTimecode> {
273 let (input, tc_str) =
274 take_while1(|c: char| c.is_ascii_digit() || c == ':' || c == ';').parse(input)?;
275
276 let tc = EdlTimecode::parse(tc_str, frame_rate).map_err(|_| {
277 nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Tag))
278 })?;
279
280 Ok((input, tc))
281 }
282}
283
284impl Default for EdlParser {
285 fn default() -> Self {
286 Self::new()
287 }
288}
289
290#[derive(Debug, Clone)]
297pub struct EventHeader {
298 pub number: u32,
300 pub reel: String,
302 pub track_type_raw: String,
304 pub edit_type_raw: String,
306}
307
308#[derive(Debug, Clone)]
313pub struct EventDetail {
314 pub transition_duration: Option<u32>,
316 pub source_in_raw: String,
318 pub source_out_raw: String,
320 pub record_in_raw: String,
322 pub record_out_raw: String,
324 pub comments: Vec<String>,
326}
327
328pub struct LazyEvent {
336 pub header: EventHeader,
338 raw_detail: String,
340 detail: std::cell::RefCell<Option<EventDetail>>,
342 frame_rate: EdlFrameRate,
344}
345
346impl std::fmt::Debug for LazyEvent {
347 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
348 f.debug_struct("LazyEvent")
349 .field("header", &self.header)
350 .field("raw_detail", &self.raw_detail)
351 .field(
352 "detail",
353 if self.detail.borrow().is_some() {
354 &"Some(<resolved>)"
355 } else {
356 &"None"
357 },
358 )
359 .finish()
360 }
361}
362
363impl LazyEvent {
364 pub fn detail(&self) -> EdlResult<std::cell::Ref<'_, EventDetail>> {
370 if self.detail.borrow().is_none() {
372 let parsed = Self::parse_detail(&self.raw_detail, self.frame_rate)?;
373 *self.detail.borrow_mut() = Some(parsed);
374 }
375 Ok(std::cell::Ref::map(self.detail.borrow(), |opt| {
377 opt.as_ref().expect("detail was just populated")
378 }))
379 }
380
381 fn parse_detail(raw: &str, frame_rate: EdlFrameRate) -> EdlResult<EventDetail> {
383 let mut comments = Vec::new();
384 let mut event_line: Option<&str> = None;
385
386 for line in raw.lines() {
387 let trimmed = line.trim();
388 if trimmed.is_empty() {
389 continue;
390 }
391 if trimmed.starts_with('*') {
392 if let Some(c) = trimmed.strip_prefix('*') {
393 comments.push(c.trim().to_string());
394 }
395 } else if event_line.is_none() {
396 event_line = Some(trimmed);
397 }
398 }
399
400 let ev_line = event_line.ok_or_else(|| EdlError::parse(0, "no event line in detail"))?;
401
402 let tokens: Vec<&str> = ev_line.split_whitespace().collect();
406 if tokens.len() < 8 {
409 return Err(EdlError::parse(0, "insufficient tokens on event line"));
410 }
411
412 let mut idx = 4usize;
413
414 let transition_duration = if tokens.get(idx).map_or(false, |t| {
417 t.chars().all(|c| c.is_ascii_digit())
418 && t.len() <= 5
419 && !t.contains(':')
420 && !t.contains(';')
421 }) && tokens.len() >= 9
422 {
423 let dur = tokens[idx]
424 .parse::<u32>()
425 .map_err(|_| EdlError::parse(0, "invalid transition duration"))?;
426 idx += 1;
427 Some(dur)
428 } else {
429 None
430 };
431
432 if tokens.len() < idx + 4 {
434 return Err(EdlError::parse(0, "missing timecode tokens"));
435 }
436
437 for tc_tok in &tokens[idx..idx + 4] {
439 if !tc_tok.contains(':') && !tc_tok.contains(';') {
440 return Err(EdlError::parse(
441 0,
442 format!("token does not look like a timecode: {tc_tok}"),
443 ));
444 }
445 }
446
447 EdlTimecode::parse(tokens[idx], frame_rate)
449 .map_err(|e| EdlError::parse(0, format!("invalid source_in timecode: {e}")))?;
450
451 Ok(EventDetail {
452 transition_duration,
453 source_in_raw: tokens[idx].to_string(),
454 source_out_raw: tokens[idx + 1].to_string(),
455 record_in_raw: tokens[idx + 2].to_string(),
456 record_out_raw: tokens[idx + 3].to_string(),
457 comments,
458 })
459 }
460}
461
462pub fn parse_lazy(input: &str, frame_rate: EdlFrameRate) -> EdlResult<Vec<LazyEvent>> {
472 let mut events: Vec<LazyEvent> = Vec::new();
473 let mut current_header: Option<EventHeader> = None;
474 let mut current_raw_lines: Vec<&str> = Vec::new();
475
476 for line in input.lines() {
477 let trimmed = line.trim();
478
479 if trimmed.is_empty() {
480 continue;
481 }
482
483 if trimmed.starts_with("TITLE:") || trimmed.starts_with("FCM:") {
485 continue;
486 }
487
488 if trimmed.starts_with('*') {
490 current_raw_lines.push(line);
491 continue;
492 }
493
494 if trimmed.chars().next().map_or(false, |c| c.is_ascii_digit()) {
496 if let Some(header) = current_header.take() {
498 let raw_detail = current_raw_lines.join("\n");
499 current_raw_lines.clear();
500 events.push(LazyEvent {
501 header,
502 raw_detail,
503 detail: std::cell::RefCell::new(None),
504 frame_rate,
505 });
506 }
507
508 let tokens: Vec<&str> = trimmed.split_whitespace().collect();
510 if tokens.len() >= 4 {
511 let number = tokens[0]
512 .parse::<u32>()
513 .map_err(|_| EdlError::parse(0, "invalid event number"))?;
514 current_header = Some(EventHeader {
515 number,
516 reel: tokens[1].to_string(),
517 track_type_raw: tokens[2].to_string(),
518 edit_type_raw: tokens[3].to_string(),
519 });
520 current_raw_lines.push(line);
521 }
522 }
523 }
524
525 if let Some(header) = current_header {
527 let raw_detail = current_raw_lines.join("\n");
528 events.push(LazyEvent {
529 header,
530 raw_detail,
531 detail: std::cell::RefCell::new(None),
532 frame_rate,
533 });
534 }
535
536 Ok(events)
537}
538
539#[allow(dead_code)]
543fn parse_fcm(input: &str) -> EdlResult<EdlFrameRate> {
544 let upper = input.to_uppercase();
545 if upper.contains("NON") {
547 Ok(EdlFrameRate::Fps2997NDF)
548 } else if upper.contains("DROP") {
549 Ok(EdlFrameRate::Fps2997DF)
550 } else {
551 Ok(EdlFrameRate::Fps2997NDF)
552 }
553}
554
555#[cfg(test)]
556mod tests {
557 use super::*;
558
559 #[test]
560 fn test_parse_simple_edl() {
561 let edl_text = r#"TITLE: Test EDL
562FCM: DROP FRAME
563
564001 AX V C 01:00:00:00 01:00:05:00 01:00:00:00 01:00:05:00
565* FROM CLIP NAME: SHOT_001.MOV
566
567002 AX V D 030 01:00:05:00 01:00:10:00 01:00:05:00 01:00:10:00
568* FROM CLIP NAME: SHOT_002.MOV
569"#;
570
571 let mut parser = EdlParser::new();
572 let edl = parser.parse(edl_text).expect("failed to parse");
573
574 assert_eq!(edl.title, Some("Test EDL".to_string()));
575 assert_eq!(edl.events.len(), 2);
576 assert_eq!(edl.events[0].number, 1);
577 assert_eq!(edl.events[0].edit_type, EditType::Cut);
578 assert_eq!(edl.events[1].number, 2);
579 assert_eq!(edl.events[1].edit_type, EditType::Dissolve);
580 assert_eq!(edl.events[1].transition_duration, Some(30));
581 }
582
583 #[test]
584 fn test_parse_comment_line() {
585 let comment = EdlParser::parse_comment_line("* This is a comment");
586 assert_eq!(comment, Some("This is a comment".to_string()));
587 }
588
589 #[test]
590 fn test_timecode_parser() {
591 let (_, tc) = EdlParser::timecode_parser("01:02:03:04", EdlFrameRate::Fps25)
592 .expect("operation should succeed");
593 assert_eq!(tc.hours(), 1);
594 assert_eq!(tc.minutes(), 2);
595 assert_eq!(tc.seconds(), 3);
596 assert_eq!(tc.frames(), 4);
597 }
598
599 #[test]
600 fn test_track_type_parser() {
601 let (_, track) = EdlParser::track_type_parser("V").expect("operation should succeed");
602 assert_eq!(track, TrackType::Video);
603
604 let (_, track) = EdlParser::track_type_parser("A").expect("operation should succeed");
605 assert_eq!(track, TrackType::Audio(AudioChannel::A1));
606
607 let (_, track) = EdlParser::track_type_parser("AA/V").expect("operation should succeed");
608 assert_eq!(track, TrackType::AudioPairWithVideo);
609 }
610
611 #[test]
612 fn test_edit_type_parser() {
613 let (_, edit) = EdlParser::edit_type_parser("C").expect("operation should succeed");
614 assert_eq!(edit, EditType::Cut);
615
616 let (_, edit) = EdlParser::edit_type_parser("D").expect("operation should succeed");
617 assert_eq!(edit, EditType::Dissolve);
618 }
619
620 #[test]
621 fn test_event_line_parser() {
622 let line = "001 AX V C 01:00:00:00 01:00:05:00 01:00:00:00 01:00:05:00";
623 let (_, event) = EdlParser::event_line_parser(line, EdlFrameRate::Fps2997DF)
624 .expect("operation should succeed");
625
626 assert_eq!(event.number, 1);
627 assert_eq!(event.reel, "AX");
628 assert_eq!(event.track, TrackType::Video);
629 assert_eq!(event.edit_type, EditType::Cut);
630 }
631
632 #[test]
633 fn test_event_with_transition() {
634 let line = "002 AX V D 030 01:00:05:00 01:00:10:00 01:00:05:00 01:00:10:00";
635 let (_, event) = EdlParser::event_line_parser(line, EdlFrameRate::Fps2997DF)
636 .expect("operation should succeed");
637
638 assert_eq!(event.number, 2);
639 assert_eq!(event.edit_type, EditType::Dissolve);
640 assert_eq!(event.transition_duration, Some(30));
641 }
642
643 #[test]
644 fn test_parse_fcm() {
645 assert_eq!(
646 parse_fcm("DROP FRAME").expect("operation should succeed"),
647 EdlFrameRate::Fps2997DF
648 );
649 assert_eq!(
650 parse_fcm("NON-DROP FRAME").expect("operation should succeed"),
651 EdlFrameRate::Fps2997NDF
652 );
653 assert_eq!(
654 parse_fcm("NON DROP FRAME").expect("operation should succeed"),
655 EdlFrameRate::Fps2997NDF
656 );
657 }
658
659 #[test]
660 fn test_parse_clip_name_comment() {
661 let edl_text = r#"001 AX V C 01:00:00:00 01:00:05:00 01:00:00:00 01:00:05:00
662* FROM CLIP NAME: test_clip.mov"#;
663
664 let mut parser = EdlParser::new();
665 let edl = parser.parse(edl_text).expect("failed to parse");
666
667 assert_eq!(edl.events.len(), 1);
668 assert_eq!(edl.events[0].clip_name, Some("test_clip.mov".to_string()));
669 }
670
671 const LAZY_SAMPLE_EDL: &str = "\
672TITLE: Lazy Sample\n\
673FCM: DROP FRAME\n\
674\n\
675001 A001 V C 01:00:00;00 01:00:05;00 01:00:00;00 01:00:05;00\n\
676* FROM CLIP NAME: shot001.mov\n\
677\n\
678002 A002 V D 030 01:00:05;00 01:00:10;00 01:00:05;00 01:00:10;00\n\
679* FROM CLIP NAME: shot002.mov\n\
680* Generic comment\n\
681\n\
682003 B001 V C 01:00:10;00 01:00:15;00 01:00:10;00 01:00:15;00\n";
683
684 #[test]
687 fn test_lazy_parse_headers_only() {
688 let events = parse_lazy(LAZY_SAMPLE_EDL, EdlFrameRate::Fps2997DF)
689 .expect("parse_lazy should succeed");
690
691 assert_eq!(events.len(), 3);
692
693 for ev in &events {
695 let _ = ev.header.number;
696 let _ = &ev.header.reel;
697 let _ = &ev.header.track_type_raw;
698 let _ = &ev.header.edit_type_raw;
699 }
700
701 for ev in &events {
703 assert!(
704 ev.detail.borrow().is_none(),
705 "detail should not have been parsed when accessing only header fields"
706 );
707 }
708
709 assert_eq!(events[0].header.number, 1);
711 assert_eq!(events[0].header.reel, "A001");
712 assert_eq!(events[1].header.number, 2);
713 assert_eq!(events[1].header.reel, "A002");
714 assert_eq!(events[2].header.number, 3);
715 assert_eq!(events[2].header.reel, "B001");
716 }
717
718 #[test]
721 fn test_lazy_detail_resolves() {
722 let events = parse_lazy(LAZY_SAMPLE_EDL, EdlFrameRate::Fps2997DF)
723 .expect("parse_lazy should succeed");
724
725 assert_eq!(events.len(), 3);
726
727 assert!(events[0].detail.borrow().is_none());
729
730 {
732 let detail = events[0].detail().expect("detail should resolve");
733 assert_eq!(detail.source_in_raw, "01:00:00;00");
734 assert_eq!(detail.source_out_raw, "01:00:05;00");
735 assert_eq!(detail.record_in_raw, "01:00:00;00");
736 assert_eq!(detail.record_out_raw, "01:00:05;00");
737 assert!(detail.transition_duration.is_none());
738 assert_eq!(detail.comments.len(), 1);
739 assert!(detail.comments[0].contains("shot001"));
740 }
741
742 assert!(events[0].detail.borrow().is_some());
744
745 {
748 let detail2 = events[0]
749 .detail()
750 .expect("detail should resolve on second call");
751 assert_eq!(detail2.source_in_raw, "01:00:00;00");
752 }
753
754 {
756 let detail2 = events[1].detail().expect("event 2 detail should resolve");
757 assert_eq!(detail2.transition_duration, Some(30));
758 assert_eq!(detail2.comments.len(), 2);
759 }
760 }
761}