ass_core/parser/streaming/
processor.rs1use crate::Result;
7use alloc::string::ToString;
8
9use super::{
10 delta::DeltaBatch,
11 state::{ParserState, SectionKind, StreamingContext},
12};
13
14pub struct LineProcessor {
20 pub state: ParserState,
22 pub context: StreamingContext,
24}
25
26impl LineProcessor {
27 #[must_use]
29 pub const fn new() -> Self {
30 Self {
31 state: ParserState::ExpectingSection,
32 context: StreamingContext::new(),
33 }
34 }
35
36 pub fn process_line(&mut self, line: &str) -> Result<DeltaBatch<'static>> {
46 self.context.next_line();
47 let trimmed = line.trim();
48
49 if trimmed.is_empty() || trimmed.starts_with(';') || trimmed.starts_with("!:") {
51 return Ok(DeltaBatch::new());
52 }
53
54 if trimmed.starts_with('[') && trimmed.ends_with(']') {
56 return Ok(self.process_section_header(trimmed));
57 }
58
59 match &self.state {
61 ParserState::ExpectingSection => {
62 Ok(DeltaBatch::new())
64 }
65 ParserState::InSection(section_kind) => {
66 Ok(self.process_section_content(line, *section_kind))
67 }
68 ParserState::InEvent {
69 section,
70 fields_seen,
71 } => Ok(self.process_event_continuation(line, *section, *fields_seen)),
72 }
73 }
74
75 fn process_section_header(&mut self, line: &str) -> DeltaBatch<'static> {
77 let section_name = &line[1..line.len() - 1]; let section_kind = SectionKind::from_header(section_name);
79
80 self.state.enter_section(section_kind);
82 self.context.enter_section(section_kind);
83
84 if section_kind.expects_format() {
86 match section_kind {
87 SectionKind::Events => self.context.events_format = None,
88 SectionKind::Styles => self.context.styles_format = None,
89 _ => {}
90 }
91 }
92
93 DeltaBatch::new()
94 }
95
96 fn process_section_content(
98 &mut self,
99 line: &str,
100 section_kind: SectionKind,
101 ) -> DeltaBatch<'static> {
102 match section_kind {
103 SectionKind::ScriptInfo => Self::process_script_info_line(line),
104 SectionKind::Styles => self.process_styles_line(line),
105 SectionKind::Events => self.process_events_line(line),
106 SectionKind::Fonts | SectionKind::Graphics => Self::process_binary_line(line),
107 SectionKind::Unknown => {
108 DeltaBatch::new()
110 }
111 }
112 }
113
114 fn process_script_info_line(line: &str) -> DeltaBatch<'static> {
116 let trimmed = line.trim();
117
118 if let Some(colon_pos) = trimmed.find(':') {
119 let _key = trimmed[..colon_pos].trim();
120 let _value = trimmed[colon_pos + 1..].trim();
121 }
123
124 DeltaBatch::new()
125 }
126
127 fn process_styles_line(&mut self, line: &str) -> DeltaBatch<'static> {
129 let trimmed = line.trim();
130
131 if let Some(format_str) = trimmed.strip_prefix("Format:") {
132 let format_str = format_str.trim().to_string();
133 self.context.set_styles_format(format_str);
134 } else if trimmed.starts_with("Style:") {
135 }
137
138 DeltaBatch::new()
139 }
140
141 fn process_events_line(&mut self, line: &str) -> DeltaBatch<'static> {
143 let trimmed = line.trim();
144
145 if let Some(format_str) = trimmed.strip_prefix("Format:") {
146 let format_str = format_str.trim().to_string();
147 self.context.set_events_format(format_str);
148 return DeltaBatch::new();
149 }
150
151 if trimmed.starts_with("Dialogue:") || trimmed.starts_with("Comment:") {
152 self.state.enter_event(SectionKind::Events);
154 }
156
157 DeltaBatch::new()
158 }
159
160 fn process_binary_line(line: &str) -> DeltaBatch<'static> {
162 let trimmed = line.trim();
163
164 if trimmed.contains(':') {
165 } else {
167 }
169
170 DeltaBatch::new()
171 }
172
173 fn process_event_continuation(
175 &mut self,
176 line: &str,
177 section: SectionKind,
178 _fields_seen: usize,
179 ) -> DeltaBatch<'static> {
180 let trimmed = line.trim();
181
182 if !trimmed.is_empty() {
183 }
185
186 self.state = ParserState::InSection(section);
188 DeltaBatch::new()
189 }
190
191 pub fn reset(&mut self) {
193 self.state = ParserState::ExpectingSection;
194 self.context.reset();
195 }
196}
197
198impl Default for LineProcessor {
199 fn default() -> Self {
200 Self::new()
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 #[cfg(not(feature = "std"))]
208 #[test]
209 fn processor_creation() {
210 let processor = LineProcessor::new();
211 assert_eq!(processor.context.line_number, 0);
212 assert!(!processor.state.is_in_section());
213 }
214
215 #[test]
216 fn section_header_processing() {
217 let mut processor = LineProcessor::new();
218 let result = processor.process_line("[Script Info]").unwrap();
219 assert!(result.is_empty());
220 assert!(processor.state.is_in_section());
221 assert_eq!(
222 processor.state.current_section(),
223 Some(SectionKind::ScriptInfo)
224 );
225 }
226
227 #[test]
228 fn comment_line_skipping() {
229 let mut processor = LineProcessor::new();
230 let result = processor.process_line("; This is a comment").unwrap();
231 assert!(result.is_empty());
232 assert_eq!(processor.context.line_number, 1);
233 }
234
235 #[test]
236 fn format_line_processing() {
237 let mut processor = LineProcessor::new();
238 processor.state.enter_section(SectionKind::Events);
239 processor.context.enter_section(SectionKind::Events);
240
241 let result = processor
242 .process_line("Format: Layer, Start, End, Style, Text")
243 .unwrap();
244 assert!(result.is_empty());
245 assert!(processor.context.events_format.is_some());
246 }
247
248 #[test]
249 fn processor_reset() {
250 let mut processor = LineProcessor::new();
251 processor.state.enter_section(SectionKind::Events);
252 processor.context.next_line();
253
254 processor.reset();
255 assert!(!processor.state.is_in_section());
256 assert_eq!(processor.context.line_number, 0);
257 }
258
259 #[test]
260 fn processor_default() {
261 let processor = LineProcessor::default();
262 assert_eq!(processor.context.line_number, 0);
263 assert!(!processor.state.is_in_section());
264 }
265
266 #[test]
267 fn empty_line_processing() {
268 let mut processor = LineProcessor::new();
269 let result = processor.process_line("").unwrap();
270 assert!(result.is_empty());
271 assert_eq!(processor.context.line_number, 1);
272
273 let result = processor.process_line(" \t ").unwrap();
274 assert!(result.is_empty());
275 assert_eq!(processor.context.line_number, 2);
276 }
277
278 #[test]
279 fn different_comment_formats() {
280 let mut processor = LineProcessor::new();
281
282 let result = processor.process_line("; Standard comment").unwrap();
283 assert!(result.is_empty());
284
285 let result = processor.process_line("!: Aegisub comment").unwrap();
286 assert!(result.is_empty());
287
288 assert_eq!(processor.context.line_number, 2);
289 }
290
291 #[test]
292 fn all_section_headers() {
293 let mut processor = LineProcessor::new();
294
295 let sections = [
296 "[Script Info]",
297 "[V4+ Styles]",
298 "[Events]",
299 "[Fonts]",
300 "[Graphics]",
301 "[Unknown Section]",
302 ];
303
304 for section in §ions {
305 let result = processor.process_line(section).unwrap();
306 assert!(result.is_empty());
307 assert!(processor.state.is_in_section());
308 }
309 }
310
311 #[test]
312 fn script_info_line_processing() {
313 let mut processor = LineProcessor::new();
314 processor.state.enter_section(SectionKind::ScriptInfo);
315
316 let result = processor.process_line("Title: Test Script").unwrap();
317 assert!(result.is_empty());
318
319 let result = processor.process_line("Author: Test Author").unwrap();
320 assert!(result.is_empty());
321
322 let result = processor.process_line("ScriptType: v4.00+").unwrap();
323 assert!(result.is_empty());
324
325 let result = processor.process_line("Malformed line").unwrap();
327 assert!(result.is_empty());
328 }
329
330 #[test]
331 fn styles_line_processing() {
332 let mut processor = LineProcessor::new();
333 processor.state.enter_section(SectionKind::Styles);
334
335 let result = processor
336 .process_line("Format: Name, Fontname, Fontsize")
337 .unwrap();
338 assert!(result.is_empty());
339 assert!(processor.context.styles_format.is_some());
340
341 let result = processor.process_line("Style: Default,Arial,20").unwrap();
342 assert!(result.is_empty());
343 }
344
345 #[test]
346 fn events_line_processing() {
347 let mut processor = LineProcessor::new();
348 processor.state.enter_section(SectionKind::Events);
349
350 let result = processor
351 .process_line("Format: Layer, Start, End, Style, Text")
352 .unwrap();
353 assert!(result.is_empty());
354 assert!(processor.context.events_format.is_some());
355
356 let result = processor
357 .process_line("Dialogue: 0,0:00:00.00,0:00:05.00,Default,Hello")
358 .unwrap();
359 assert!(result.is_empty());
360
361 let result = processor
362 .process_line("Comment: 0,0:00:05.00,0:00:10.00,Default,Note")
363 .unwrap();
364 assert!(result.is_empty());
365 }
366
367 #[test]
368 fn binary_line_processing() {
369 let mut processor = LineProcessor::new();
370
371 processor.state.enter_section(SectionKind::Fonts);
373 let result = processor.process_line("fontname: Arial.ttf").unwrap();
374 assert!(result.is_empty());
375
376 let result = processor
377 .process_line("AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDD")
378 .unwrap();
379 assert!(result.is_empty());
380
381 processor.state.enter_section(SectionKind::Graphics);
383 let result = processor.process_line("graphic: logo.png").unwrap();
384 assert!(result.is_empty());
385
386 let result = processor
387 .process_line("0123456789ABCDEF0123456789ABCDEF")
388 .unwrap();
389 assert!(result.is_empty());
390 }
391
392 #[test]
393 fn event_continuation_processing() {
394 let mut processor = LineProcessor::new();
395 processor.state.enter_event(SectionKind::Events);
396
397 let result = processor.process_line(" continuation data").unwrap();
398 assert!(result.is_empty());
399 assert!(processor.state.is_in_section());
401 assert_eq!(processor.state.current_section(), Some(SectionKind::Events));
402
403 processor.state.enter_event(SectionKind::Events);
405 let result = processor.process_line("").unwrap();
406 assert!(result.is_empty());
407 }
408
409 #[test]
410 fn content_outside_sections() {
411 let mut processor = LineProcessor::new();
412 assert!(!processor.state.is_in_section());
414
415 let result = processor
416 .process_line("Random content outside sections")
417 .unwrap();
418 assert!(result.is_empty());
419 assert!(!processor.state.is_in_section());
421 }
422
423 #[test]
424 fn section_header_edge_cases() {
425 let mut processor = LineProcessor::new();
426
427 let result = processor.process_line("[ Script Info ]").unwrap();
429 assert!(result.is_empty());
430 assert!(processor.state.is_in_section());
431
432 let result = processor.process_line("[]").unwrap();
434 assert!(result.is_empty());
435
436 let result = processor.process_line("[Unclosed section").unwrap();
438 assert!(result.is_empty());
439
440 let result = processor.process_line("Unclosed section]").unwrap();
441 assert!(result.is_empty());
442 }
443
444 #[test]
445 fn unknown_section_processing() {
446 let mut processor = LineProcessor::new();
447 processor.state.enter_section(SectionKind::Unknown);
448
449 let result = processor
450 .process_line("Any content in unknown section")
451 .unwrap();
452 assert!(result.is_empty());
453
454 let result = processor.process_line("Key: Value").unwrap();
455 assert!(result.is_empty());
456 }
457
458 #[test]
459 fn line_counter_increments() {
460 let mut processor = LineProcessor::new();
461 assert_eq!(processor.context.line_number, 0);
462
463 processor.process_line("Line 1").unwrap();
464 assert_eq!(processor.context.line_number, 1);
465
466 processor.process_line("Line 2").unwrap();
467 assert_eq!(processor.context.line_number, 2);
468
469 processor.process_line("").unwrap();
470 assert_eq!(processor.context.line_number, 3);
471 }
472
473 #[test]
474 fn format_context_updates() {
475 let mut processor = LineProcessor::new();
476
477 processor.state.enter_section(SectionKind::Styles);
479 processor.context.enter_section(SectionKind::Styles);
480
481 assert!(processor.context.styles_format.is_none());
482 processor
483 .process_line("Format: Name, Fontname, Fontsize, Bold")
484 .unwrap();
485 assert!(processor.context.styles_format.is_some());
486
487 processor.state.enter_section(SectionKind::Events);
489 processor.context.enter_section(SectionKind::Events);
490
491 assert!(processor.context.events_format.is_none());
492 processor
493 .process_line("Format: Layer, Start, End, Style, Text")
494 .unwrap();
495 assert!(processor.context.events_format.is_some());
496 }
497
498 #[test]
499 fn complex_processing_sequence() {
500 let mut processor = LineProcessor::new();
501
502 let lines = [
504 "[Script Info]",
505 "Title: Test",
506 "Author: Tester",
507 "",
508 "[V4+ Styles]",
509 "Format: Name, Fontname, Fontsize",
510 "Style: Default,Arial,20",
511 "",
512 "[Events]",
513 "Format: Layer, Start, End, Style, Text",
514 "Dialogue: 0,0:00:00.00,0:00:05.00,Default,Hello World",
515 "; End of script",
516 ];
517
518 for line in &lines {
519 let result = processor.process_line(line).unwrap();
520 assert!(result.is_empty());
521 }
522
523 assert_eq!(processor.context.line_number, lines.len());
524 assert!(processor.context.events_format.is_some());
525 assert!(processor.context.styles_format.is_some());
526 }
527
528 #[test]
529 fn whitespace_handling() {
530 let mut processor = LineProcessor::new();
531
532 processor.process_line(" [Script Info] ").unwrap();
534 assert!(processor.state.is_in_section());
535
536 processor.process_line("\t\tTitle: Test\t\t").unwrap();
537
538 processor
539 .process_line(" ; Comment with spaces ")
540 .unwrap();
541
542 processor.process_line("\t\n").unwrap(); }
544}