1use super::super::range::{Position, Range};
12use super::super::traits::{AstNode, Container, Visitor, VisualStructure};
13use super::annotation::Annotation;
14use super::blank_line_group::BlankLineGroup;
15use super::definition::Definition;
16use super::list::{List, ListItem};
17use super::paragraph::{Paragraph, TextLine};
18use super::session::Session;
19use super::verbatim::Verbatim;
20use super::verbatim_line::VerbatimLine;
21use std::fmt;
22
23#[derive(Debug, Clone, PartialEq)]
25pub enum ContentItem {
26 Paragraph(Paragraph),
27 Session(Session),
28 List(List),
29 ListItem(ListItem),
30 TextLine(TextLine),
31 Definition(Definition),
32 Annotation(Annotation),
33 VerbatimBlock(Box<Verbatim>),
34 VerbatimLine(VerbatimLine),
35 BlankLineGroup(BlankLineGroup),
36}
37
38impl AstNode for ContentItem {
39 fn node_type(&self) -> &'static str {
40 match self {
41 ContentItem::Paragraph(p) => p.node_type(),
42 ContentItem::Session(s) => s.node_type(),
43 ContentItem::List(l) => l.node_type(),
44 ContentItem::ListItem(li) => li.node_type(),
45 ContentItem::TextLine(tl) => tl.node_type(),
46 ContentItem::Definition(d) => d.node_type(),
47 ContentItem::Annotation(a) => a.node_type(),
48 ContentItem::VerbatimBlock(fb) => fb.node_type(),
49 ContentItem::VerbatimLine(fl) => fl.node_type(),
50 ContentItem::BlankLineGroup(blg) => blg.node_type(),
51 }
52 }
53
54 fn display_label(&self) -> String {
55 match self {
56 ContentItem::Paragraph(p) => p.display_label(),
57 ContentItem::Session(s) => s.display_label(),
58 ContentItem::List(l) => l.display_label(),
59 ContentItem::ListItem(li) => li.display_label(),
60 ContentItem::TextLine(tl) => tl.display_label(),
61 ContentItem::Definition(d) => d.display_label(),
62 ContentItem::Annotation(a) => a.display_label(),
63 ContentItem::VerbatimBlock(fb) => fb.display_label(),
64 ContentItem::VerbatimLine(fl) => fl.display_label(),
65 ContentItem::BlankLineGroup(blg) => blg.display_label(),
66 }
67 }
68
69 fn range(&self) -> &Range {
70 match self {
71 ContentItem::Paragraph(p) => p.range(),
72 ContentItem::Session(s) => s.range(),
73 ContentItem::List(l) => l.range(),
74 ContentItem::ListItem(li) => li.range(),
75 ContentItem::TextLine(tl) => tl.range(),
76 ContentItem::Definition(d) => d.range(),
77 ContentItem::Annotation(a) => a.range(),
78 ContentItem::VerbatimBlock(fb) => fb.range(),
79 ContentItem::VerbatimLine(fl) => fl.range(),
80 ContentItem::BlankLineGroup(blg) => blg.range(),
81 }
82 }
83
84 fn accept(&self, visitor: &mut dyn Visitor) {
85 match self {
86 ContentItem::Paragraph(p) => p.accept(visitor),
87 ContentItem::Session(s) => s.accept(visitor),
88 ContentItem::List(l) => l.accept(visitor),
89 ContentItem::ListItem(li) => li.accept(visitor),
90 ContentItem::TextLine(tl) => tl.accept(visitor),
91 ContentItem::Definition(d) => d.accept(visitor),
92 ContentItem::Annotation(a) => a.accept(visitor),
93 ContentItem::VerbatimBlock(fb) => fb.accept(visitor),
94 ContentItem::VerbatimLine(fl) => fl.accept(visitor),
95 ContentItem::BlankLineGroup(blg) => blg.accept(visitor),
96 }
97 }
98}
99
100impl VisualStructure for ContentItem {
101 fn is_source_line_node(&self) -> bool {
102 match self {
103 ContentItem::Paragraph(p) => p.is_source_line_node(),
104 ContentItem::Session(s) => s.is_source_line_node(),
105 ContentItem::List(l) => l.is_source_line_node(),
106 ContentItem::ListItem(li) => li.is_source_line_node(),
107 ContentItem::TextLine(tl) => tl.is_source_line_node(),
108 ContentItem::Definition(d) => d.is_source_line_node(),
109 ContentItem::Annotation(a) => a.is_source_line_node(),
110 ContentItem::VerbatimBlock(fb) => fb.is_source_line_node(),
111 ContentItem::VerbatimLine(fl) => fl.is_source_line_node(),
112 ContentItem::BlankLineGroup(blg) => blg.is_source_line_node(),
113 }
114 }
115
116 fn has_visual_header(&self) -> bool {
117 match self {
118 ContentItem::Paragraph(p) => p.has_visual_header(),
119 ContentItem::Session(s) => s.has_visual_header(),
120 ContentItem::List(l) => l.has_visual_header(),
121 ContentItem::ListItem(li) => li.has_visual_header(),
122 ContentItem::TextLine(tl) => tl.has_visual_header(),
123 ContentItem::Definition(d) => d.has_visual_header(),
124 ContentItem::Annotation(a) => a.has_visual_header(),
125 ContentItem::VerbatimBlock(fb) => fb.has_visual_header(),
126 ContentItem::VerbatimLine(fl) => fl.has_visual_header(),
127 ContentItem::BlankLineGroup(blg) => blg.has_visual_header(),
128 }
129 }
130
131 fn collapses_with_children(&self) -> bool {
132 match self {
133 ContentItem::Paragraph(p) => p.collapses_with_children(),
134 ContentItem::Session(s) => s.collapses_with_children(),
135 ContentItem::List(l) => l.collapses_with_children(),
136 ContentItem::ListItem(li) => li.collapses_with_children(),
137 ContentItem::TextLine(tl) => tl.collapses_with_children(),
138 ContentItem::Definition(d) => d.collapses_with_children(),
139 ContentItem::Annotation(a) => a.collapses_with_children(),
140 ContentItem::VerbatimBlock(fb) => fb.collapses_with_children(),
141 ContentItem::VerbatimLine(fl) => fl.collapses_with_children(),
142 ContentItem::BlankLineGroup(blg) => blg.collapses_with_children(),
143 }
144 }
145}
146
147impl ContentItem {
148 pub fn label(&self) -> Option<&str> {
149 match self {
150 ContentItem::Session(s) => Some(s.label()),
151 ContentItem::Definition(d) => Some(d.label()),
152 ContentItem::Annotation(a) => Some(a.label()),
153 ContentItem::ListItem(li) => Some(li.label()),
154 ContentItem::VerbatimBlock(fb) => Some(fb.subject.as_string()),
155 _ => None,
156 }
157 }
158
159 pub fn children(&self) -> Option<&[ContentItem]> {
160 match self {
161 ContentItem::Session(s) => Some(&s.children),
162 ContentItem::Definition(d) => Some(&d.children),
163 ContentItem::Annotation(a) => Some(&a.children),
164 ContentItem::List(l) => Some(&l.items),
165 ContentItem::ListItem(li) => Some(&li.children),
166 ContentItem::Paragraph(p) => Some(&p.lines),
167 ContentItem::VerbatimBlock(fb) => Some(&fb.children),
168 ContentItem::TextLine(_) => None,
169 ContentItem::VerbatimLine(_) => None,
170 _ => None,
171 }
172 }
173
174 pub fn children_mut(&mut self) -> Option<&mut Vec<ContentItem>> {
175 match self {
176 ContentItem::Session(s) => Some(s.children.as_mut_vec()),
177 ContentItem::Definition(d) => Some(d.children.as_mut_vec()),
178 ContentItem::Annotation(a) => Some(a.children.as_mut_vec()),
179 ContentItem::List(l) => Some(l.items.as_mut_vec()),
180 ContentItem::ListItem(li) => Some(li.children.as_mut_vec()),
181 ContentItem::Paragraph(p) => Some(&mut p.lines),
182 ContentItem::VerbatimBlock(fb) => Some(fb.children.as_mut_vec()),
183 ContentItem::TextLine(_) => None,
184 ContentItem::VerbatimLine(_) => None,
185 _ => None,
186 }
187 }
188
189 pub fn text(&self) -> Option<String> {
190 match self {
191 ContentItem::Paragraph(p) => Some(p.text()),
192 _ => None,
193 }
194 }
195
196 pub fn is_paragraph(&self) -> bool {
197 matches!(self, ContentItem::Paragraph(_))
198 }
199 pub fn is_session(&self) -> bool {
200 matches!(self, ContentItem::Session(_))
201 }
202 pub fn is_list(&self) -> bool {
203 matches!(self, ContentItem::List(_))
204 }
205 pub fn is_list_item(&self) -> bool {
206 matches!(self, ContentItem::ListItem(_))
207 }
208 pub fn is_text_line(&self) -> bool {
209 matches!(self, ContentItem::TextLine(_))
210 }
211 pub fn is_definition(&self) -> bool {
212 matches!(self, ContentItem::Definition(_))
213 }
214 pub fn is_annotation(&self) -> bool {
215 matches!(self, ContentItem::Annotation(_))
216 }
217 pub fn is_verbatim_block(&self) -> bool {
218 matches!(self, ContentItem::VerbatimBlock(_))
219 }
220
221 pub fn is_verbatim_line(&self) -> bool {
222 matches!(self, ContentItem::VerbatimLine(_))
223 }
224
225 pub fn is_blank_line_group(&self) -> bool {
226 matches!(self, ContentItem::BlankLineGroup(_))
227 }
228
229 pub fn as_paragraph(&self) -> Option<&Paragraph> {
230 if let ContentItem::Paragraph(p) = self {
231 Some(p)
232 } else {
233 None
234 }
235 }
236 pub fn as_session(&self) -> Option<&Session> {
237 if let ContentItem::Session(s) = self {
238 Some(s)
239 } else {
240 None
241 }
242 }
243 pub fn as_list(&self) -> Option<&List> {
244 if let ContentItem::List(l) = self {
245 Some(l)
246 } else {
247 None
248 }
249 }
250 pub fn as_list_item(&self) -> Option<&ListItem> {
251 if let ContentItem::ListItem(li) = self {
252 Some(li)
253 } else {
254 None
255 }
256 }
257 pub fn as_definition(&self) -> Option<&Definition> {
258 if let ContentItem::Definition(d) = self {
259 Some(d)
260 } else {
261 None
262 }
263 }
264 pub fn as_annotation(&self) -> Option<&Annotation> {
265 if let ContentItem::Annotation(a) = self {
266 Some(a)
267 } else {
268 None
269 }
270 }
271 pub fn as_verbatim_block(&self) -> Option<&Verbatim> {
272 if let ContentItem::VerbatimBlock(fb) = self {
273 Some(fb)
274 } else {
275 None
276 }
277 }
278
279 pub fn as_verbatim_line(&self) -> Option<&VerbatimLine> {
280 if let ContentItem::VerbatimLine(fl) = self {
281 Some(fl)
282 } else {
283 None
284 }
285 }
286
287 pub fn as_blank_line_group(&self) -> Option<&BlankLineGroup> {
288 if let ContentItem::BlankLineGroup(blg) = self {
289 Some(blg)
290 } else {
291 None
292 }
293 }
294
295 pub fn as_paragraph_mut(&mut self) -> Option<&mut Paragraph> {
296 if let ContentItem::Paragraph(p) = self {
297 Some(p)
298 } else {
299 None
300 }
301 }
302 pub fn as_session_mut(&mut self) -> Option<&mut Session> {
303 if let ContentItem::Session(s) = self {
304 Some(s)
305 } else {
306 None
307 }
308 }
309 pub fn as_list_mut(&mut self) -> Option<&mut List> {
310 if let ContentItem::List(l) = self {
311 Some(l)
312 } else {
313 None
314 }
315 }
316 pub fn as_list_item_mut(&mut self) -> Option<&mut ListItem> {
317 if let ContentItem::ListItem(li) = self {
318 Some(li)
319 } else {
320 None
321 }
322 }
323 pub fn as_definition_mut(&mut self) -> Option<&mut Definition> {
324 if let ContentItem::Definition(d) = self {
325 Some(d)
326 } else {
327 None
328 }
329 }
330 pub fn as_annotation_mut(&mut self) -> Option<&mut Annotation> {
331 if let ContentItem::Annotation(a) = self {
332 Some(a)
333 } else {
334 None
335 }
336 }
337 pub fn as_verbatim_block_mut(&mut self) -> Option<&mut Verbatim> {
338 if let ContentItem::VerbatimBlock(fb) = self {
339 Some(fb)
340 } else {
341 None
342 }
343 }
344
345 pub fn as_verbatim_line_mut(&mut self) -> Option<&mut VerbatimLine> {
346 if let ContentItem::VerbatimLine(fl) = self {
347 Some(fl)
348 } else {
349 None
350 }
351 }
352
353 pub fn as_blank_line_group_mut(&mut self) -> Option<&mut BlankLineGroup> {
354 if let ContentItem::BlankLineGroup(blg) = self {
355 Some(blg)
356 } else {
357 None
358 }
359 }
360
361 pub fn element_at(&self, pos: Position) -> Option<&ContentItem> {
364 if let Some(children) = self.children() {
368 for child in children {
369 if let Some(result) = child.element_at(pos) {
370 return Some(result); }
372 }
373 }
374
375 if self.range().contains(pos) {
380 Some(self)
381 } else {
382 None
383 }
384 }
385
386 pub fn visual_line_at(&self, pos: Position) -> Option<&ContentItem> {
392 if let Some(children) = self.children() {
394 for child in children {
395 if let Some(result) = child.visual_line_at(pos) {
396 return Some(result);
397 }
398 }
399 }
400
401 let is_line_level = matches!(
404 self,
405 ContentItem::TextLine(_)
406 | ContentItem::ListItem(_)
407 | ContentItem::VerbatimLine(_)
408 | ContentItem::BlankLineGroup(_)
409 );
410
411 if is_line_level && self.range().contains(pos) {
412 Some(self)
413 } else {
414 None
415 }
416 }
417
418 pub fn block_element_at(&self, pos: Position) -> Option<&ContentItem> {
424 let is_block = matches!(
426 self,
427 ContentItem::Session(_)
428 | ContentItem::Definition(_)
429 | ContentItem::List(_)
430 | ContentItem::Paragraph(_)
431 | ContentItem::Annotation(_)
432 | ContentItem::VerbatimBlock(_)
433 );
434
435 if is_block && self.range().contains(pos) {
436 return Some(self);
437 }
438
439 if let Some(children) = self.children() {
441 for child in children {
442 if let Some(result) = child.block_element_at(pos) {
443 return Some(result);
444 }
445 }
446 }
447
448 None
449 }
450
451 pub fn node_path_at_position(&self, pos: Position) -> Vec<&ContentItem> {
454 if let Some(children) = self.children() {
456 for child in children {
457 let mut path = child.node_path_at_position(pos);
458 if !path.is_empty() {
459 path.insert(0, self);
460 return path;
461 }
462 }
463 }
464
465 if self.range().contains(pos) {
467 vec![self]
468 } else {
469 Vec::new()
470 }
471 }
472
473 pub fn descendants(&self) -> Box<dyn Iterator<Item = &ContentItem> + '_> {
476 if let Some(children) = self.children() {
477 Box::new(
478 children
479 .iter()
480 .flat_map(|child| std::iter::once(child).chain(child.descendants())),
481 )
482 } else {
483 Box::new(std::iter::empty())
484 }
485 }
486
487 pub fn descendants_with_depth(
490 &self,
491 start_depth: usize,
492 ) -> Box<dyn Iterator<Item = (&ContentItem, usize)> + '_> {
493 if let Some(children) = self.children() {
494 Box::new(children.iter().flat_map(move |child| {
495 std::iter::once((child, start_depth))
496 .chain(child.descendants_with_depth(start_depth + 1))
497 }))
498 } else {
499 Box::new(std::iter::empty())
500 }
501 }
502}
503
504impl fmt::Display for ContentItem {
505 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
506 match self {
507 ContentItem::Paragraph(p) => write!(f, "Paragraph({} lines)", p.lines.len()),
508 ContentItem::Session(s) => {
509 write!(
510 f,
511 "Session('{}', {} items)",
512 s.title.as_string(),
513 s.children.len()
514 )
515 }
516 ContentItem::List(l) => write!(f, "List({} items)", l.items.len()),
517 ContentItem::ListItem(li) => {
518 write!(f, "ListItem('{}', {} items)", li.text(), li.children.len())
519 }
520 ContentItem::TextLine(tl) => {
521 write!(f, "TextLine('{}')", tl.text())
522 }
523 ContentItem::Definition(d) => {
524 write!(
525 f,
526 "Definition('{}', {} items)",
527 d.subject.as_string(),
528 d.children.len()
529 )
530 }
531 ContentItem::Annotation(a) => write!(
532 f,
533 "Annotation('{}', {} params, {} items)",
534 a.data.label.value,
535 a.data.parameters.len(),
536 a.children.len()
537 ),
538 ContentItem::VerbatimBlock(fb) => {
539 write!(f, "VerbatimBlock('{}')", fb.subject.as_string())
540 }
541 ContentItem::VerbatimLine(fl) => {
542 write!(f, "VerbatimLine('{}')", fl.content.as_string())
543 }
544 ContentItem::BlankLineGroup(blg) => write!(f, "{blg}"),
545 }
546 }
547}
548
549#[cfg(test)]
550mod tests {
551 use super::super::super::range::{Position, Range};
552 use super::super::paragraph::Paragraph;
553 use super::*;
554 use crate::lex::ast::elements::typed_content;
555
556 #[test]
557 fn test_element_at_simple_paragraph() {
558 let para = Paragraph::from_line("Test".to_string()).at(Range::new(
559 0..0,
560 Position::new(0, 0),
561 Position::new(0, 4),
562 ));
563 let item = ContentItem::Paragraph(para);
564
565 let pos = Position::new(0, 2);
566 if let Some(result) = item.element_at(pos) {
567 assert!(result.is_text_line());
569 } else {
570 panic!("Expected to find element at position");
571 }
572 }
573
574 #[test]
575 fn test_element_at_position_outside_location() {
576 let para = Paragraph::from_line("Test".to_string()).at(Range::new(
577 0..0,
578 Position::new(0, 0),
579 Position::new(0, 4),
580 ));
581 let item = ContentItem::Paragraph(para);
582
583 let pos = Position::new(0, 10);
584 let result = item.element_at(pos);
585 assert!(result.is_none());
586 }
587
588 #[test]
589 fn test_element_at_no_location() {
590 let para = Paragraph::from_line("Test".to_string());
592 let item = ContentItem::Paragraph(para);
593
594 let pos = Position::new(5, 10);
595 assert!(item.element_at(pos).is_none());
596 }
597
598 #[test]
599 fn test_element_at_nested_session() {
600 let para = Paragraph::from_line("Nested".to_string()).at(Range::new(
601 0..0,
602 Position::new(1, 0),
603 Position::new(1, 6),
604 ));
605 let session = Session::new(
606 super::super::super::text_content::TextContent::from_string(
607 "Section".to_string(),
608 None,
609 ),
610 typed_content::into_session_contents(vec![ContentItem::Paragraph(para)]),
611 )
612 .at(Range::new(0..0, Position::new(0, 0), Position::new(2, 0)));
613 let item = ContentItem::Session(session);
614
615 let pos = Position::new(1, 3);
616 if let Some(result) = item.element_at(pos) {
617 assert!(result.is_text_line());
619 } else {
620 panic!("Expected to find deepest element");
621 }
622 }
623
624 #[test]
625 fn test_descendants_on_session_content_item() {
626 let mut inner_session = Session::with_title("Inner".to_string());
627 inner_session
628 .children
629 .push(ContentItem::Paragraph(Paragraph::from_line(
630 "Grandchild".to_string(),
631 )));
632
633 let mut session = Session::with_title("Outer".to_string());
634 session
635 .children
636 .push(ContentItem::Paragraph(Paragraph::from_line(
637 "Child".to_string(),
638 )));
639 session.children.push(ContentItem::Session(inner_session));
640
641 let item = ContentItem::Session(session);
642 let descendants: Vec<_> = item.descendants().collect();
643 assert_eq!(descendants.len(), 5);
644
645 let paragraphs: Vec<_> = item.descendants().filter(|d| d.is_paragraph()).collect();
646 assert_eq!(paragraphs.len(), 2);
647 }
648
649 #[test]
650 fn element_at_prefers_child_even_if_parent_range_is_tight() {
651 let paragraph = Paragraph::from_line("Child".to_string()).at(Range::new(
653 10..15,
654 Position::new(1, 0),
655 Position::new(1, 5),
656 ));
657
658 let mut session = Session::with_title("Header".to_string()).at(Range::new(
659 0..6,
660 Position::new(0, 0),
661 Position::new(0, 6),
662 ));
663 session.children.push(ContentItem::Paragraph(paragraph));
664
665 let pos = Position::new(1, 3);
666 let item = ContentItem::Session(session);
667 let result = item
668 .element_at(pos)
669 .expect("child paragraph should be discoverable");
670
671 assert!(result.is_text_line());
672 }
673
674 #[test]
675 fn descendants_with_depth_tracks_depths() {
676 let paragraph =
677 ContentItem::Paragraph(Paragraph::from_line("Para".to_string()).at(Range::new(
678 0..4,
679 Position::new(0, 0),
680 Position::new(0, 4),
681 )));
682
683 let list_item = ListItem::with_content("-".to_string(), "Item".to_string(), vec![])
684 .at(Range::new(5..9, Position::new(1, 0), Position::new(1, 4)));
685 let list = ContentItem::List(List::new(vec![list_item]));
686
687 let mut session = Session::with_title("Root".to_string());
688 session.children.push(paragraph.clone());
689 session.children.push(list.clone());
690
691 let depths: Vec<(&str, usize)> = ContentItem::Session(session)
692 .descendants_with_depth(0)
693 .map(|(item, depth)| (item.node_type(), depth))
694 .collect();
695
696 assert_eq!(
697 depths,
698 vec![
699 ("Paragraph", 0),
700 ("TextLine", 1),
701 ("List", 0),
702 ("ListItem", 1),
703 ]
704 );
705 }
706
707 #[test]
708 fn test_visual_line_at_finds_text_line() {
709 let para = Paragraph::from_line("Test line".to_string()).at(Range::new(
711 0..9,
712 Position::new(0, 0),
713 Position::new(0, 9),
714 ));
715 let item = ContentItem::Paragraph(para);
716
717 let pos = Position::new(0, 5);
718 let result = item.visual_line_at(pos);
719 assert!(result.is_some());
720 assert!(result.unwrap().is_text_line());
721 }
722
723 #[test]
724 fn test_visual_line_at_finds_list_item() {
725 let list_item = ListItem::with_content("-".to_string(), "Item text".to_string(), vec![])
726 .at(Range::new(0..10, Position::new(0, 0), Position::new(0, 10)));
727 let list = List::new(vec![list_item]).at(Range::new(
728 0..10,
729 Position::new(0, 0),
730 Position::new(0, 10),
731 ));
732 let item = ContentItem::List(list);
733
734 let pos = Position::new(0, 5);
735 let result = item.visual_line_at(pos);
736 assert!(result.is_some());
737 assert!(result.unwrap().is_list_item());
738 }
739
740 #[test]
741 fn test_visual_line_at_position_outside() {
742 let para = Paragraph::from_line("Test".to_string()).at(Range::new(
743 0..4,
744 Position::new(0, 0),
745 Position::new(0, 4),
746 ));
747 let item = ContentItem::Paragraph(para);
748
749 let pos = Position::new(10, 10);
750 let result = item.visual_line_at(pos);
751 assert!(result.is_none());
752 }
753
754 #[test]
755 fn test_block_element_at_finds_paragraph() {
756 let para = Paragraph::from_line("Test".to_string()).at(Range::new(
757 0..4,
758 Position::new(0, 0),
759 Position::new(0, 4),
760 ));
761 let item = ContentItem::Paragraph(para);
762
763 let pos = Position::new(0, 2);
764 let result = item.block_element_at(pos);
765 assert!(result.is_some());
766 assert!(result.unwrap().is_paragraph());
767 }
768
769 #[test]
770 fn test_block_element_at_finds_session() {
771 let mut session = Session::with_title("Section".to_string()).at(Range::new(
772 0..10,
773 Position::new(0, 0),
774 Position::new(2, 0),
775 ));
776 session
777 .children
778 .push(ContentItem::Paragraph(Paragraph::from_line(
779 "Content".to_string(),
780 )));
781 let item = ContentItem::Session(session);
782
783 let pos = Position::new(1, 0);
784 let result = item.block_element_at(pos);
785 assert!(result.is_some());
786 assert!(result.unwrap().is_session());
788 }
789
790 #[test]
791 fn test_block_element_at_skips_text_line() {
792 let para = Paragraph::from_line("Test".to_string()).at(Range::new(
794 0..4,
795 Position::new(0, 0),
796 Position::new(0, 4),
797 ));
798
799 let mut session = Session::with_title("Section".to_string()).at(Range::new(
800 0..10,
801 Position::new(0, 0),
802 Position::new(2, 0),
803 ));
804 session.children.push(ContentItem::Paragraph(para));
805 let item = ContentItem::Session(session);
806
807 let pos = Position::new(0, 2);
808 let result = item.block_element_at(pos);
809 assert!(result.is_some());
810 assert!(result.unwrap().is_session());
812 }
813
814 #[test]
815 fn test_block_element_at_position_outside() {
816 let para = Paragraph::from_line("Test".to_string()).at(Range::new(
817 0..4,
818 Position::new(0, 0),
819 Position::new(0, 4),
820 ));
821 let item = ContentItem::Paragraph(para);
822
823 let pos = Position::new(10, 10);
824 let result = item.block_element_at(pos);
825 assert!(result.is_none());
826 }
827
828 #[test]
829 fn test_comparison_element_at_vs_visual_line_at_vs_block_element_at() {
830 let para = Paragraph::from_line("Test line".to_string()).at(Range::new(
832 5..14,
833 Position::new(1, 0),
834 Position::new(1, 9),
835 ));
836
837 let mut session = Session::with_title("Title".to_string()).at(Range::new(
838 0..14,
839 Position::new(0, 0),
840 Position::new(1, 9),
841 ));
842 session.children.push(ContentItem::Paragraph(para));
843 let item = ContentItem::Session(session);
844
845 let pos = Position::new(1, 5);
846
847 let deepest = item.element_at(pos);
849 assert!(deepest.is_some());
850 assert!(deepest.unwrap().is_text_line());
851
852 let visual = item.visual_line_at(pos);
854 assert!(
855 visual.is_some(),
856 "visual_line_at should find a visual line element"
857 );
858 let visual_item = visual.unwrap();
859 assert!(
860 visual_item.is_text_line(),
861 "Expected TextLine but got: {:?}",
862 visual_item.node_type()
863 );
864
865 let block = item.block_element_at(pos);
867 assert!(block.is_some());
868 assert!(block.unwrap().is_session());
869 }
870}