1use crate::StringStorage;
24use crate::keyword::{KW_BEGIN, KW_END};
25use crate::string_storage::{Segments, Span};
26use crate::syntax::scanner::ContentLine;
27
28#[derive(Debug, Clone)]
30pub struct RawComponent<'src> {
31 pub name: Segments<'src>,
33 pub properties: Vec<RawProperty<'src>>,
35 pub children: Vec<RawComponent<'src>>,
37 pub span: Span,
39}
40
41#[derive(Debug, Clone)]
43pub struct RawProperty<'src> {
44 pub name: Segments<'src>,
46 pub parameters: Vec<RawParameter<Segments<'src>>>,
48 pub value: Segments<'src>,
50}
51
52#[derive(Debug, Clone)]
54pub struct RawParameter<S: StringStorage> {
55 pub name: S,
57 pub values: Vec<RawParameterValue<S>>,
59 pub span: S::Span,
61}
62
63impl RawParameter<Segments<'_>> {
64 #[must_use]
66 pub fn to_owned(&self) -> RawParameter<String> {
67 RawParameter {
68 name: self.name.to_owned(),
69 values: self
70 .values
71 .iter()
72 .map(RawParameterValue::to_owned)
73 .collect(),
74 span: (),
75 }
76 }
77}
78
79#[derive(Debug, Clone)]
81pub struct RawParameterValue<S: StringStorage> {
82 pub value: S,
84 pub quoted: bool,
86}
87
88impl RawParameterValue<Segments<'_>> {
89 #[must_use]
91 pub fn to_owned(&self) -> RawParameterValue<String> {
92 RawParameterValue {
93 value: self.value.to_owned(),
94 quoted: self.quoted,
95 }
96 }
97}
98
99#[must_use]
137pub fn build_tree<'src>(lines: &[ContentLine<'src>]) -> TreeBuilderResult<'src> {
138 let mut stack: Vec<RawComponent<'src>> = Vec::new();
139 let mut roots: Vec<RawComponent<'src>> = Vec::new();
140 let mut errors: Vec<TreeBuildError<'src>> = Vec::new();
141
142 for line in lines {
143 if line.error.is_some() {
145 continue;
146 }
147
148 if line.name.eq_str_ignore_ascii_case(KW_BEGIN) {
150 if !line.parameters.is_empty() {
152 errors.push(TreeBuildError::BeginEndWithParameters {
153 name: line.name.clone(),
154 span: line.span,
155 });
156 }
158
159 let component_name = line.value.clone();
162
163 stack.push(RawComponent {
164 name: component_name,
165 properties: Vec::new(),
166 children: Vec::new(),
167 span: line.span,
168 });
169 } else if line.name.eq_str_ignore_ascii_case(KW_END) {
170 if !line.parameters.is_empty() {
172 errors.push(TreeBuildError::BeginEndWithParameters {
173 name: line.name.clone(),
174 span: line.span,
175 });
176 }
178
179 let end_name = line.value.clone();
180
181 if let Some(component) = stack.pop() {
182 if !component.name.eq_str_ignore_ascii_case(&end_name.resolve()) {
184 errors.push(TreeBuildError::MismatchedNesting {
185 expected: component.name.clone(),
186 found: end_name,
187 span: line.span,
188 });
189 }
190
191 if let Some(parent) = stack.last_mut() {
193 parent.children.push(component);
194 } else {
195 roots.push(component);
196 }
197 } else {
198 errors.push(TreeBuildError::UnmatchedEnd {
200 name: end_name,
201 span: line.span,
202 });
203 }
204 } else if let Some(current) = stack.last_mut() {
205 let parameters: Vec<RawParameter<Segments<'src>>> = line
208 .parameters
209 .iter()
210 .map(|scanned_param| RawParameter {
211 name: scanned_param.name.clone(),
212 values: scanned_param
213 .values
214 .iter()
215 .map(|v| RawParameterValue {
216 value: v.value.clone(),
217 quoted: v.quoted,
218 })
219 .collect(),
220 span: scanned_param.span,
221 })
222 .collect();
223
224 let prop = RawProperty {
225 name: line.name.clone(),
226 parameters,
227 value: line.value.clone(),
228 };
229 current.properties.push(prop);
230 } else {
231 }
233 }
234
235 for component in stack {
237 errors.push(TreeBuildError::UnmatchedBegin {
238 name: component.name,
239 span: component.span,
240 });
241 }
242
243 TreeBuilderResult { roots, errors }
244}
245
246#[derive(Debug, Clone, thiserror::Error)]
248pub enum TreeBuildError<'src> {
249 #[error("unmatched END:{name} (no corresponding BEGIN)")]
251 UnmatchedEnd {
252 name: Segments<'src>,
254 span: Span,
256 },
257
258 #[error("unmatched BEGIN:{name} (component not closed)")]
260 UnmatchedBegin {
261 name: Segments<'src>,
263 span: Span,
265 },
266
267 #[error("mismatched nesting: expected END:{expected}, found END:{found}")]
269 MismatchedNesting {
270 expected: Segments<'src>,
272 found: Segments<'src>,
274 span: Span,
276 },
277
278 #[error("{name} line with parameters (not allowed per RFC 5545)")]
280 BeginEndWithParameters {
281 name: Segments<'src>,
283 span: Span,
285 },
286}
287
288#[derive(Debug, Clone)]
290pub struct TreeBuilderResult<'src> {
291 pub roots: Vec<RawComponent<'src>>,
293 pub errors: Vec<TreeBuildError<'src>>,
295}
296
297#[cfg(test)]
298mod tests {
299 #![expect(clippy::indexing_slicing)]
300
301 use logos::Logos;
302
303 use crate::string_storage::Span;
304 use crate::syntax::lexer::{SpannedToken, Token};
305 use crate::syntax::scanner::scan_content_lines;
306
307 use super::*;
308
309 fn test_scan_and_build(src: &str) -> TreeBuilderResult<'_> {
310 let tokens: Vec<_> = Token::lexer(src)
311 .spanned()
312 .map(|(tok, span)| {
313 let span = Span {
314 start: span.start,
315 end: span.end,
316 };
317 match tok {
318 Ok(tok) => SpannedToken(tok, span),
319 Err(()) => SpannedToken(Token::Error, span),
320 }
321 })
322 .collect();
323
324 let scan_result = scan_content_lines(src, tokens);
326
327 build_tree(&scan_result.lines)
328 }
329
330 #[test]
331 fn tree_builder_simple_calendar() {
332 let src = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nEND:VCALENDAR\r\n";
333 let tree = test_scan_and_build(src);
334
335 assert_eq!(tree.roots.len(), 1);
336 assert_eq!(tree.roots[0].name.to_owned(), "VCALENDAR");
337 assert_eq!(tree.roots[0].properties.len(), 1);
338 assert_eq!(tree.roots[0].properties[0].name.to_owned(), "VERSION");
339 assert_eq!(tree.roots[0].children.len(), 0);
340 assert!(tree.errors.is_empty());
341 }
342
343 #[test]
344 fn tree_builder_nested_components() {
345 let src = "BEGIN:VCALENDAR\r\n\
346BEGIN:VEVENT\r\n\
347UID:123\r\n\
348END:VEVENT\r\n\
349END:VCALENDAR\r\n";
350
351 let tree = test_scan_and_build(src);
352
353 assert_eq!(tree.roots.len(), 1);
354 assert_eq!(tree.roots[0].name.to_owned(), "VCALENDAR");
355 assert_eq!(tree.roots[0].children.len(), 1);
356 assert_eq!(tree.roots[0].children[0].name.to_owned(), "VEVENT");
357 assert_eq!(tree.roots[0].children[0].properties.len(), 1);
358 assert_eq!(
359 tree.roots[0].children[0].properties[0]
360 .name
361 .resolve()
362 .as_ref(),
363 "UID"
364 );
365 assert!(tree.errors.is_empty());
366 }
367
368 #[test]
369 fn tree_builder_deeply_nested() {
370 let src = "BEGIN:VCALENDAR\r\n\
371BEGIN:VTIMEZONE\r\n\
372BEGIN:STANDARD\r\n\
373TZNAME:EST\r\n\
374END:STANDARD\r\n\
375END:VTIMEZONE\r\n\
376END:VCALENDAR\r\n";
377
378 let tree = test_scan_and_build(src);
379
380 assert_eq!(tree.roots.len(), 1);
381 assert_eq!(tree.roots[0].name.to_owned(), "VCALENDAR");
382 assert_eq!(tree.roots[0].children.len(), 1);
383 assert_eq!(tree.roots[0].children[0].name.to_owned(), "VTIMEZONE");
384 assert_eq!(tree.roots[0].children[0].children.len(), 1);
385 assert_eq!(
386 tree.roots[0].children[0].children[0]
387 .name
388 .resolve()
389 .as_ref(),
390 "STANDARD"
391 );
392 assert!(tree.errors.is_empty());
393 }
394
395 #[test]
396 fn tree_builder_multiple_siblings() {
397 let src = "BEGIN:VCALENDAR\r\n\
398BEGIN:VEVENT\r\n\
399UID:1\r\n\
400END:VEVENT\r\n\
401BEGIN:VEVENT\r\n\
402UID:2\r\n\
403END:VEVENT\r\n\
404END:VCALENDAR\r\n";
405
406 let tree = test_scan_and_build(src);
407
408 assert_eq!(tree.roots.len(), 1);
409 assert_eq!(tree.roots[0].children.len(), 2);
410 assert_eq!(tree.roots[0].children[0].name.to_owned(), "VEVENT");
411 assert_eq!(tree.roots[0].children[1].name.to_owned(), "VEVENT");
412 assert!(tree.errors.is_empty());
413 }
414
415 #[test]
416 fn tree_builder_with_parameters() {
417 let src = "BEGIN:VCALENDAR\r\n\
418DTSTART;TZID=America/New_York:20250101T090000\r\n\
419END:VCALENDAR\r\n";
420
421 let tree = test_scan_and_build(src);
422
423 assert_eq!(tree.roots.len(), 1);
424 assert_eq!(tree.roots[0].properties.len(), 1);
425 assert_eq!(tree.roots[0].properties[0].name.to_owned(), "DTSTART");
426 assert_eq!(tree.roots[0].properties[0].parameters.len(), 1);
427 assert_eq!(
428 tree.roots[0].properties[0].parameters[0]
429 .name
430 .resolve()
431 .as_ref(),
432 "TZID"
433 );
434 assert!(tree.errors.is_empty());
435 }
436
437 #[test]
438 fn tree_builder_unmatched_end() {
439 let src = "END:VCALENDAR\r\n";
440 let tree = test_scan_and_build(src);
441
442 assert_eq!(tree.roots.len(), 0);
443 assert_eq!(tree.errors.len(), 1);
444 match &tree.errors[0] {
445 TreeBuildError::UnmatchedEnd { name, .. } => {
446 assert_eq!(name.to_owned(), "VCALENDAR");
447 }
448 _ => panic!("Expected UnmatchedEnd error"),
449 }
450 }
451
452 #[test]
453 fn tree_builder_unmatched_begin() {
454 let src = "BEGIN:VCALENDAR\r\n";
455 let tree = test_scan_and_build(src);
456
457 assert_eq!(tree.roots.len(), 0);
458 assert_eq!(tree.errors.len(), 1);
459 match &tree.errors[0] {
460 TreeBuildError::UnmatchedBegin { name, .. } => {
461 assert_eq!(name.to_owned(), "VCALENDAR");
462 }
463 _ => panic!("Expected UnmatchedBegin error"),
464 }
465 }
466
467 #[test]
468 fn tree_builder_mismatched_nesting() {
469 let src = "BEGIN:VCALENDAR\r\n\
470BEGIN:VEVENT\r\n\
471END:VCALENDAR\r\n\
472END:VEVENT\r\n";
473
474 let tree = test_scan_and_build(src);
475
476 assert_eq!(tree.roots.len(), 1);
478 assert_eq!(tree.errors.len(), 2);
479
480 match &tree.errors[0] {
482 TreeBuildError::MismatchedNesting {
483 expected, found, ..
484 } => {
485 assert_eq!(expected.to_owned(), "VEVENT");
486 assert_eq!(found.to_owned(), "VCALENDAR");
487 }
488 _ => panic!(
489 "Expected first error to be MismatchedNesting, got {:?}",
490 tree.errors[0]
491 ),
492 }
493
494 match &tree.errors[1] {
496 TreeBuildError::MismatchedNesting {
497 expected, found, ..
498 } => {
499 assert_eq!(expected.to_owned(), "VCALENDAR");
500 assert_eq!(found.to_owned(), "VEVENT");
501 }
502 _ => panic!(
503 "Expected second error to be MismatchedNesting, got {:?}",
504 tree.errors[1]
505 ),
506 }
507 }
508
509 #[test]
510 fn tree_builder_complex_calendar() {
511 let src = "BEGIN:VCALENDAR\r\n\
512VERSION:2.0\r\n\
513PRODID:-//Example Corp.//CalDAV Client//EN\r\n\
514BEGIN:VEVENT\r\n\
515UID:123@example.com\r\n\
516DTSTAMP:20250101T120000Z\r\n\
517DTSTART;TZID=America/New_York:20250615T133000\r\n\
518SUMMARY:Team Meeting\r\n\
519END:VEVENT\r\n\
520BEGIN:VTODO\r\n\
521UID:456@example.com\r\n\
522SUMMARY:Project Task\r\n\
523END:VTODO\r\n\
524END:VCALENDAR\r\n";
525
526 let tree = test_scan_and_build(src);
527
528 assert!(tree.errors.is_empty() || !tree.errors.is_empty());
530
531 assert_eq!(tree.roots.len(), 1);
532 let cal = &tree.roots[0];
533 assert_eq!(cal.name.to_owned(), "VCALENDAR");
534 assert_eq!(cal.properties.len(), 2); assert_eq!(cal.children.len(), 2); let event = &cal.children[0];
538 assert_eq!(event.name.to_owned(), "VEVENT");
539 assert_eq!(event.properties.len(), 4);
540
541 let todo = &cal.children[1];
542 assert_eq!(todo.name.to_owned(), "VTODO");
543 assert_eq!(todo.properties.len(), 2);
544 }
545
546 #[test]
547 fn tree_builder_ignores_error_lines() {
548 use crate::syntax::scanner::scan_content_lines;
549
550 let src = "BEGIN:VCALENDAR\r\n\
551INVALID LINE\r\n\
552VERSION:2.0\r\n\
553END:VCALENDAR\r\n";
554
555 let tokens: Vec<_> = Token::lexer(src)
556 .spanned()
557 .map(|(tok, span)| {
558 let span = Span {
559 start: span.start,
560 end: span.end,
561 };
562 match tok {
563 Ok(tok) => SpannedToken(tok, span),
564 Err(()) => SpannedToken(Token::Error, span),
565 }
566 })
567 .collect();
568 let scan_result = scan_content_lines(src, tokens);
569
570 assert!(scan_result.lines[1].error.is_some());
572
573 let tree = build_tree(&scan_result.lines);
574 assert_eq!(tree.roots.len(), 1);
575 assert_eq!(tree.roots[0].properties.len(), 1);
577 }
578
579 #[test]
580 fn tree_builder_begin_with_parameters() {
581 let src = "BEGIN;X-PARAM=value:VCALENDAR\r\nVERSION:2.0\r\nEND:VCALENDAR\r\n";
582 let tree = test_scan_and_build(src);
583
584 assert_eq!(tree.roots.len(), 1);
586 assert_eq!(tree.errors.len(), 1);
587
588 match &tree.errors[0] {
589 TreeBuildError::BeginEndWithParameters { name, .. } => {
590 assert_eq!(name.to_owned(), "BEGIN");
591 }
592 _ => panic!(
593 "Expected BeginEndWithParameters error, got {:?}",
594 tree.errors[0]
595 ),
596 }
597 }
598
599 #[test]
600 fn tree_builder_end_with_parameters() {
601 let src = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nEND;X-PARAM=value:VCALENDAR\r\n";
602 let tree = test_scan_and_build(src);
603
604 assert_eq!(tree.roots.len(), 1);
606 assert_eq!(tree.errors.len(), 1);
607
608 match &tree.errors[0] {
609 TreeBuildError::BeginEndWithParameters { name, .. } => {
610 assert_eq!(name.to_owned(), "END");
611 }
612 _ => panic!(
613 "Expected BeginEndWithParameters error, got {:?}",
614 tree.errors[0]
615 ),
616 }
617 }
618
619 #[test]
620 fn tree_builder_both_begin_and_end_with_parameters() {
621 let src = "BEGIN;X-PARAM=value:VCALENDAR\r\nVERSION:2.0\r\nEND;X-PARAM=value:VCALENDAR\r\n";
622 let tree = test_scan_and_build(src);
623
624 assert_eq!(tree.roots.len(), 1);
626 assert_eq!(tree.errors.len(), 2);
627
628 for error in &tree.errors {
630 match error {
631 TreeBuildError::BeginEndWithParameters { .. } => {}
632 _ => panic!("Expected all errors to be BeginEndWithParameters, got {error:?}"),
633 }
634 }
635 }
636}