1extern crate alloc;
7
8use alloc::format;
9use alloc::string::String;
10use alloc::vec;
11use alloc::vec::Vec;
12
13use crate::tokenizer::Token;
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
17#[non_exhaustive]
18pub enum TextStyle {
19 #[default]
21 Normal,
22 Bold,
24 Italic,
26 BoldItalic,
28}
29
30impl TextStyle {
31 pub fn is_bold(&self) -> bool {
33 matches!(self, TextStyle::Bold | TextStyle::BoldItalic)
34 }
35
36 pub fn is_italic(&self) -> bool {
38 matches!(self, TextStyle::Italic | TextStyle::BoldItalic)
39 }
40
41 pub fn with_bold(&self, bold: bool) -> Self {
43 match (bold, self.is_italic()) {
44 (true, true) => TextStyle::BoldItalic,
45 (true, false) => TextStyle::Bold,
46 (false, true) => TextStyle::Italic,
47 (false, false) => TextStyle::Normal,
48 }
49 }
50
51 pub fn with_italic(&self, italic: bool) -> Self {
53 match (self.is_bold(), italic) {
54 (true, true) => TextStyle::BoldItalic,
55 (true, false) => TextStyle::Bold,
56 (false, true) => TextStyle::Italic,
57 (false, false) => TextStyle::Normal,
58 }
59 }
60}
61
62#[derive(Clone, Debug, PartialEq, Eq)]
64pub struct TextSpan {
65 pub text: String,
67 pub style: TextStyle,
69}
70
71impl TextSpan {
72 pub fn new(text: String, style: TextStyle) -> Self {
74 Self { text, style }
75 }
76}
77
78#[derive(Clone, Debug, PartialEq)]
80pub struct Line {
81 pub spans: Vec<TextSpan>,
83 pub y: i32,
85}
86
87impl Line {
88 pub fn new(text: String, y: i32, style: TextStyle) -> Self {
90 Self {
91 spans: vec![TextSpan::new(text, style)],
92 y,
93 }
94 }
95
96 pub fn text(&self) -> String {
98 self.spans
99 .iter()
100 .map(|s| s.text.as_str())
101 .collect::<Vec<_>>()
102 .join("")
103 }
104
105 pub fn style(&self) -> TextStyle {
107 self.spans
108 .first()
109 .map(|s| s.style)
110 .unwrap_or(TextStyle::Normal)
111 }
112
113 pub fn is_empty(&self) -> bool {
115 self.spans.iter().all(|s| s.text.is_empty())
116 }
117
118 pub fn len(&self) -> usize {
120 self.spans.iter().map(|s| s.text.len()).sum()
121 }
122}
123
124#[derive(Clone, Debug, PartialEq)]
126pub struct Page {
127 pub lines: Vec<Line>,
129 pub page_number: usize,
131}
132
133impl Page {
134 pub fn new(page_number: usize) -> Self {
136 Self {
137 lines: Vec::with_capacity(0),
138 page_number,
139 }
140 }
141
142 pub fn add_line(&mut self, line: Line) {
144 self.lines.push(line);
145 }
146
147 pub fn is_empty(&self) -> bool {
149 self.lines.is_empty()
150 }
151
152 pub fn line_count(&self) -> usize {
154 self.lines.len()
155 }
156}
157
158#[derive(Clone, Debug)]
160pub struct FontMetrics {
161 pub char_width: f32,
163 pub char_height: f32,
165 pub bold_char_width: f32,
167 pub italic_char_width: f32,
169}
170
171impl Default for FontMetrics {
172 fn default() -> Self {
173 Self::font_10x20()
175 }
176}
177
178impl FontMetrics {
179 pub fn font_10x20() -> Self {
181 Self {
182 char_width: 10.0,
183 char_height: 20.0,
184 bold_char_width: 10.0,
185 italic_char_width: 10.0,
186 }
187 }
188
189 pub fn char_width_for_style(&self, style: TextStyle) -> f32 {
191 match style {
192 TextStyle::Normal | TextStyle::Italic => self.char_width,
193 TextStyle::Bold | TextStyle::BoldItalic => self.bold_char_width,
194 }
195 }
196
197 pub fn text_width(&self, text: &str, style: TextStyle) -> f32 {
199 text.chars().count() as f32 * self.char_width_for_style(style)
200 }
201}
202
203pub struct LayoutEngine {
205 page_width: f32,
207 line_height: f32,
209 font_metrics: FontMetrics,
211 left_margin: f32,
213 top_margin: f32,
215 current_spans: Vec<TextSpan>,
217 current_span_text: String,
219 current_span_style: TextStyle,
221 current_y: f32,
223 current_line_width: f32,
225 current_page_lines: Vec<Line>,
227 pages: Vec<Page>,
229 page_number: usize,
231 max_lines_per_page: usize,
233 current_line_count: usize,
235 list_depth: usize,
237 list_ordered_stack: Vec<bool>,
239 list_item_counters: Vec<usize>,
241}
242
243impl LayoutEngine {
244 pub const DISPLAY_WIDTH: f32 = 480.0;
246 pub const DISPLAY_HEIGHT: f32 = 800.0;
248 pub const DEFAULT_MARGIN: f32 = 32.0;
250 pub const DEFAULT_TOP_MARGIN: f32 = 0.0;
252 pub const DEFAULT_HEADER_HEIGHT: f32 = 45.0;
254 pub const DEFAULT_FOOTER_HEIGHT: f32 = 40.0;
256
257 pub fn new(page_width: f32, page_height: f32, line_height: f32) -> Self {
264 let font_metrics = FontMetrics::default();
265 let max_lines = ((page_height - line_height * 2.0) / line_height)
267 .floor()
268 .max(1.0) as usize;
269
270 Self {
271 page_width,
272 line_height,
273 font_metrics,
274 left_margin: Self::DEFAULT_MARGIN,
275 top_margin: Self::DEFAULT_TOP_MARGIN,
276 current_spans: Vec::with_capacity(0),
277 current_span_text: String::with_capacity(0),
278 current_span_style: TextStyle::Normal,
279 current_y: Self::DEFAULT_TOP_MARGIN,
280 current_line_width: 0.0,
281 current_page_lines: Vec::with_capacity(0),
282 pages: Vec::with_capacity(0),
283 page_number: 1,
284 max_lines_per_page: max_lines.max(1),
285 current_line_count: 0,
286 list_depth: 0,
287 list_ordered_stack: Vec::with_capacity(0),
288 list_item_counters: Vec::with_capacity(0),
289 }
290 }
291
292 pub fn with_defaults() -> Self {
297 LayoutConfig::default().create_engine()
298 }
299
300 pub fn with_font_metrics(mut self, metrics: FontMetrics) -> Self {
302 self.font_metrics = metrics;
303 self
304 }
305
306 pub fn with_margins(mut self, left: f32, top: f32) -> Self {
308 self.left_margin = left;
309 self.top_margin = top;
310 self.current_y = top;
311 self
312 }
313
314 pub fn layout_tokens(&mut self, tokens: &[Token]) -> Vec<Page> {
316 self.reset();
317
318 let mut bold_active = false;
319 let mut italic_active = false;
320 let mut heading_bold = false;
321
322 for token in tokens {
323 match token {
324 Token::Text(ref text) => {
325 let style =
326 self.current_style_from_flags(bold_active || heading_bold, italic_active);
327 self.add_text(text, style);
328 }
329 Token::ParagraphBreak => {
330 self.flush_line();
331 self.add_paragraph_space();
332 heading_bold = false;
333 }
334 Token::Heading(level) => {
335 self.flush_line();
336 if self.current_line_count > 0 {
338 let space_lines = if *level <= 2 { 2 } else { 1 };
340 for _ in 0..space_lines {
341 self.add_paragraph_space();
342 }
343 }
344 heading_bold = true;
346 }
349 Token::Emphasis(start) => {
350 self.flush_partial_word();
351 italic_active = *start;
352 self.current_span_style =
353 self.current_style_from_flags(bold_active || heading_bold, italic_active);
354 }
355 Token::Strong(start) => {
356 self.flush_partial_word();
357 bold_active = *start;
358 self.current_span_style =
359 self.current_style_from_flags(bold_active || heading_bold, italic_active);
360 }
361 Token::LineBreak => {
362 self.flush_line();
363 }
364 Token::ListStart(ordered) => {
366 self.flush_line();
367 self.list_depth += 1;
368 self.list_ordered_stack.push(*ordered);
369 self.list_item_counters.push(0);
370 }
371 Token::ListEnd => {
372 self.flush_line();
373 self.list_depth = self.list_depth.saturating_sub(1);
374 self.list_ordered_stack.pop();
375 self.list_item_counters.pop();
376 if self.list_depth == 0 {
377 self.add_paragraph_space();
378 }
379 }
380 Token::ListItemStart => {
381 self.flush_line();
382 if let Some(counter) = self.list_item_counters.last_mut() {
384 *counter += 1;
385 }
386 let indent = " ".repeat(self.list_depth.saturating_sub(1));
388 let is_ordered = self.list_ordered_stack.last().copied().unwrap_or(false);
389 let marker = if is_ordered {
390 let count = self.list_item_counters.last().copied().unwrap_or(1);
391 format!("{}{}.", indent, count)
392 } else {
393 format!("{}\u{2022}", indent) };
395 let marker_width = self.font_metrics.text_width(&marker, TextStyle::Normal);
396 self.current_span_text.push_str(&marker);
397 self.current_span_style = TextStyle::Normal;
398 self.current_line_width = marker_width;
399 }
400 Token::ListItemEnd => {
401 }
403 Token::LinkStart(_href) => {
405 }
408 Token::LinkEnd => {
409 }
411 Token::Image { src: _, ref alt } => {
413 self.flush_line();
414 let placeholder = if alt.is_empty() {
415 String::from("[Image]")
416 } else {
417 format!("[Image: {}]", alt)
418 };
419 let width = self
420 .font_metrics
421 .text_width(&placeholder, TextStyle::Normal);
422 self.current_span_text = placeholder;
423 self.current_span_style = TextStyle::Normal;
424 self.current_line_width = width;
425 self.flush_line();
426 self.add_paragraph_space();
427 }
428 }
429 }
430
431 self.flush_line();
433 self.finalize_page();
434
435 core::mem::take(&mut self.pages)
436 }
437
438 fn reset(&mut self) {
440 self.current_spans.clear();
441 self.current_span_text.clear();
442 self.current_span_style = TextStyle::Normal;
443 self.current_y = self.top_margin;
444 self.current_line_width = 0.0;
445 self.current_page_lines.clear();
446 self.pages.clear();
447 self.page_number = 1;
448 self.current_line_count = 0;
449 self.list_depth = 0;
450 self.list_ordered_stack.clear();
451 self.list_item_counters.clear();
452 }
453
454 fn current_style_from_flags(&self, bold: bool, italic: bool) -> TextStyle {
456 match (bold, italic) {
457 (true, true) => TextStyle::BoldItalic,
458 (true, false) => TextStyle::Bold,
459 (false, true) => TextStyle::Italic,
460 (false, false) => TextStyle::Normal,
461 }
462 }
463
464 fn add_text(&mut self, text: &str, style: TextStyle) {
466 for word in text.split_whitespace() {
468 self.add_word(word, style);
469 }
470 }
471
472 fn current_line_is_empty(&self) -> bool {
474 self.current_spans.is_empty() && self.current_span_text.is_empty()
475 }
476
477 fn add_word(&mut self, word: &str, style: TextStyle) {
479 let word_width = self.font_metrics.text_width(word, style);
480 let space_width = if self.current_line_is_empty() {
481 0.0
482 } else {
483 self.font_metrics.char_width_for_style(style)
484 };
485
486 let total_width = self.current_line_width + space_width + word_width;
487
488 if total_width <= self.page_width || self.current_line_is_empty() {
489 if style != self.current_span_style {
491 if !self.current_span_text.is_empty() {
492 self.current_spans.push(TextSpan::new(
493 core::mem::take(&mut self.current_span_text),
494 self.current_span_style,
495 ));
496 }
497 self.current_span_style = style;
498 }
499 if !self.current_line_is_empty() {
501 self.current_span_text.push(' ');
502 self.current_line_width += space_width;
503 }
504 self.current_span_text.push_str(word);
505 self.current_line_width += word_width;
506 } else {
507 self.flush_line();
509 self.current_span_style = style;
510 self.current_span_text.push_str(word);
511 self.current_line_width = word_width;
512 }
513 }
514
515 fn flush_partial_word(&mut self) {
517 if !self.current_span_text.is_empty() {
518 self.current_spans.push(TextSpan::new(
519 core::mem::take(&mut self.current_span_text),
520 self.current_span_style,
521 ));
522 }
523 }
524
525 fn flush_line(&mut self) {
527 if !self.current_span_text.is_empty() {
529 self.current_spans.push(TextSpan::new(
530 core::mem::take(&mut self.current_span_text),
531 self.current_span_style,
532 ));
533 }
534
535 if self.current_spans.is_empty() {
536 return;
537 }
538
539 if self.current_line_count >= self.max_lines_per_page {
541 self.finalize_page();
542 self.current_y = self.top_margin;
543 self.current_line_count = 0;
544 }
545
546 let line = Line {
548 spans: core::mem::take(&mut self.current_spans),
549 y: self.current_y as i32,
550 };
551
552 self.current_page_lines.push(line);
553 self.current_line_count += 1;
554 self.current_y += self.line_height;
555 self.current_line_width = 0.0;
556 }
557
558 fn add_paragraph_space(&mut self) {
560 if self.current_line_count >= self.max_lines_per_page {
562 self.finalize_page();
563 self.current_y = self.top_margin;
564 self.current_line_count = 0;
565 }
566
567 if self.current_line_count > 0 {
570 self.current_y += self.line_height * 0.5;
571 }
572 }
573
574 fn finalize_page(&mut self) {
576 if !self.current_page_lines.is_empty() {
577 let mut page = Page::new(self.page_number);
578 core::mem::swap(&mut page.lines, &mut self.current_page_lines);
579 self.pages.push(page);
580 self.page_number += 1;
581 }
582 }
583
584 pub fn into_pages(mut self) -> Vec<Page> {
586 self.finalize_page();
587 self.pages
588 }
589
590 pub fn current_page_number(&self) -> usize {
592 self.page_number
593 }
594
595 pub fn total_pages(&self) -> usize {
597 self.pages.len()
598 }
599
600 pub fn measure_text(&self, text: &str, style: TextStyle) -> f32 {
602 self.font_metrics.text_width(text, style)
603 }
604}
605
606#[derive(Clone, Debug)]
608pub struct LayoutConfig {
609 pub page_width: f32,
611 pub page_height: f32,
613 pub line_height: f32,
615 pub left_margin: f32,
617 pub top_margin: f32,
619 pub font_metrics: FontMetrics,
621}
622
623impl Default for LayoutConfig {
624 fn default() -> Self {
629 let content_width = LayoutEngine::DISPLAY_WIDTH - (LayoutEngine::DEFAULT_MARGIN * 2.0);
631 let content_height = LayoutEngine::DISPLAY_HEIGHT
632 - LayoutEngine::DEFAULT_HEADER_HEIGHT
633 - LayoutEngine::DEFAULT_FOOTER_HEIGHT;
634
635 Self {
636 page_width: content_width,
637 page_height: content_height,
638 line_height: 26.0, left_margin: LayoutEngine::DEFAULT_MARGIN,
640 top_margin: 0.0, font_metrics: FontMetrics::default(),
642 }
643 }
644}
645
646impl LayoutConfig {
647 pub fn create_engine(&self) -> LayoutEngine {
649 LayoutEngine::new(self.page_width, self.page_height, self.line_height)
650 .with_font_metrics(self.font_metrics.clone())
651 .with_margins(self.left_margin, self.top_margin)
652 }
653}
654
655#[cfg(test)]
656mod tests {
657 use super::*;
658
659 fn create_test_tokens() -> Vec<Token> {
660 vec![
661 Token::Text("This is ".to_string()),
662 Token::Emphasis(true),
663 Token::Text("italic".to_string()),
664 Token::Emphasis(false),
665 Token::Text(" and ".to_string()),
666 Token::Strong(true),
667 Token::Text("bold".to_string()),
668 Token::Strong(false),
669 Token::Text(" text.".to_string()),
670 Token::ParagraphBreak,
671 Token::Heading(1),
672 Token::Text("Chapter Title".to_string()),
673 Token::ParagraphBreak,
674 Token::Text("Another paragraph with more content here.".to_string()),
675 Token::ParagraphBreak,
676 ]
677 }
678
679 #[test]
680 fn test_layout_engine_new() {
681 let engine = LayoutEngine::new(460.0, 650.0, 20.0);
682 assert_eq!(engine.current_page_number(), 1);
683 assert_eq!(engine.total_pages(), 0);
684 }
685
686 #[test]
687 fn test_text_style() {
688 let mut style = TextStyle::Normal;
689 assert!(!style.is_bold());
690 assert!(!style.is_italic());
691
692 style = style.with_bold(true);
693 assert!(style.is_bold());
694 assert!(!style.is_italic());
695
696 style = style.with_italic(true);
697 assert!(style.is_bold());
698 assert!(style.is_italic());
699
700 style = style.with_bold(false);
701 assert!(!style.is_bold());
702 assert!(style.is_italic());
703 }
704
705 #[test]
706 fn test_layout_tokens_basic() {
707 let tokens = create_test_tokens();
708 let mut engine = LayoutEngine::new(460.0, 650.0, 20.0);
709 let pages = engine.layout_tokens(&tokens);
710
711 assert!(!pages.is_empty());
712 assert_eq!(pages[0].page_number, 1);
713
714 let total_lines: usize = pages.iter().map(|p| p.line_count()).sum();
716 assert!(total_lines > 0);
717 }
718
719 #[test]
720 fn test_pagination() {
721 let mut tokens = Vec::with_capacity(0);
723 for i in 0..50 {
724 tokens.push(Token::Text(format!(
725 "This is paragraph number {} with some content. ",
726 i
727 )));
728 tokens.push(Token::Text(
729 "Here is more text to fill the line. ".to_string(),
730 ));
731 tokens.push(Token::Text(
732 "And even more words here to make it long enough.".to_string(),
733 ));
734 tokens.push(Token::ParagraphBreak);
735 }
736
737 let mut engine = LayoutEngine::new(460.0, 200.0, 20.0); let pages = engine.layout_tokens(&tokens);
739
740 assert!(pages.len() > 1);
742
743 for (i, page) in pages.iter().enumerate() {
745 assert_eq!(page.page_number, i + 1);
746 }
747 }
748
749 #[test]
750 fn test_line_breaking() {
751 let tokens = vec![
753 Token::Text("This is a very long line of text that should definitely wrap to multiple lines because it is longer than the available width".to_string()),
754 Token::ParagraphBreak,
755 ];
756
757 let mut engine = LayoutEngine::new(100.0, 200.0, 20.0); let pages = engine.layout_tokens(&tokens);
759
760 assert!(!pages.is_empty());
761 assert!(pages[0].line_count() > 1);
763 }
764
765 #[test]
766 fn test_empty_input() {
767 let tokens: Vec<Token> = vec![];
768 let mut engine = LayoutEngine::new(460.0, 650.0, 20.0);
769 let pages = engine.layout_tokens(&tokens);
770
771 assert!(pages.is_empty());
773 }
774
775 #[test]
776 fn test_font_metrics() {
777 let metrics = FontMetrics::default(); assert_eq!(metrics.text_width("hello", TextStyle::Normal), 50.0); assert_eq!(metrics.text_width("hello", TextStyle::Bold), 50.0); let metrics_10x20 = FontMetrics::font_10x20();
782 assert_eq!(metrics_10x20.text_width("hello", TextStyle::Normal), 50.0);
783 }
784
785 #[test]
786 fn test_page_struct() {
787 let mut page = Page::new(1);
788 assert!(page.is_empty());
789 assert_eq!(page.line_count(), 0);
790
791 page.add_line(Line::new("Test".to_string(), 10, TextStyle::Normal));
792 assert!(!page.is_empty());
793 assert_eq!(page.line_count(), 1);
794 }
795
796 #[test]
797 fn test_line_struct() {
798 let line = Line::new("Hello".to_string(), 50, TextStyle::Bold);
799 assert_eq!(line.text(), "Hello");
800 assert_eq!(line.y, 50);
801 assert_eq!(line.style(), TextStyle::Bold);
802 assert!(!line.is_empty());
803 assert_eq!(line.len(), 5);
804 }
805
806 #[test]
807 fn test_layout_config() {
808 let config = LayoutConfig::default();
809 assert_eq!(config.page_width, 416.0);
811 assert_eq!(config.page_height, 715.0);
813 assert_eq!(config.line_height, 26.0);
815 assert_eq!(config.left_margin, 32.0);
817 assert_eq!(config.top_margin, 0.0);
818
819 let engine = config.create_engine();
820 assert_eq!(engine.current_page_number(), 1);
821 }
822
823 #[test]
824 fn test_with_defaults() {
825 let engine = LayoutEngine::with_defaults();
826 assert_eq!(engine.current_page_number(), 1);
827 }
830
831 fn collect_line_texts(pages: &[Page]) -> Vec<String> {
833 pages
834 .iter()
835 .flat_map(|p| p.lines.iter())
836 .map(|l| l.text())
837 .collect()
838 }
839
840 #[test]
841 fn test_unordered_list_layout() {
842 let tokens = vec![
843 Token::ListStart(false),
844 Token::ListItemStart,
845 Token::Text("First".to_string()),
846 Token::ListItemEnd,
847 Token::ListItemStart,
848 Token::Text("Second".to_string()),
849 Token::ListItemEnd,
850 Token::ListEnd,
851 ];
852
853 let mut engine = LayoutEngine::new(460.0, 650.0, 20.0);
854 let pages = engine.layout_tokens(&tokens);
855 let texts = collect_line_texts(&pages);
856
857 assert_eq!(texts.len(), 2);
858 assert_eq!(texts[0], "\u{2022} First");
859 assert_eq!(texts[1], "\u{2022} Second");
860 }
861
862 #[test]
863 fn test_ordered_list_layout() {
864 let tokens = vec![
865 Token::ListStart(true),
866 Token::ListItemStart,
867 Token::Text("Alpha".to_string()),
868 Token::ListItemEnd,
869 Token::ListItemStart,
870 Token::Text("Beta".to_string()),
871 Token::ListItemEnd,
872 Token::ListItemStart,
873 Token::Text("Gamma".to_string()),
874 Token::ListItemEnd,
875 Token::ListEnd,
876 ];
877
878 let mut engine = LayoutEngine::new(460.0, 650.0, 20.0);
879 let pages = engine.layout_tokens(&tokens);
880 let texts = collect_line_texts(&pages);
881
882 assert_eq!(texts.len(), 3);
883 assert_eq!(texts[0], "1. Alpha");
884 assert_eq!(texts[1], "2. Beta");
885 assert_eq!(texts[2], "3. Gamma");
886 }
887
888 #[test]
889 fn test_nested_list_layout() {
890 let tokens = vec![
891 Token::ListStart(false),
892 Token::ListItemStart,
893 Token::Text("Outer".to_string()),
894 Token::ListItemEnd,
895 Token::ListStart(true),
897 Token::ListItemStart,
898 Token::Text("Inner A".to_string()),
899 Token::ListItemEnd,
900 Token::ListItemStart,
901 Token::Text("Inner B".to_string()),
902 Token::ListItemEnd,
903 Token::ListEnd,
904 Token::ListItemStart,
905 Token::Text("Outer again".to_string()),
906 Token::ListItemEnd,
907 Token::ListEnd,
908 ];
909
910 let mut engine = LayoutEngine::new(460.0, 650.0, 20.0);
911 let pages = engine.layout_tokens(&tokens);
912 let texts = collect_line_texts(&pages);
913
914 assert_eq!(texts.len(), 4);
915 assert_eq!(texts[0], "\u{2022} Outer");
917 assert_eq!(texts[1], " 1. Inner A");
919 assert_eq!(texts[2], " 2. Inner B");
920 assert_eq!(texts[3], "\u{2022} Outer again");
922 }
923
924 #[test]
925 fn test_image_placeholder_with_alt() {
926 let tokens = vec![Token::Image {
927 src: "img/cover.jpg".to_string(),
928 alt: "Book cover".to_string(),
929 }];
930
931 let mut engine = LayoutEngine::new(460.0, 650.0, 20.0);
932 let pages = engine.layout_tokens(&tokens);
933 let texts = collect_line_texts(&pages);
934
935 assert_eq!(texts.len(), 1);
936 assert_eq!(texts[0], "[Image: Book cover]");
937 }
938
939 #[test]
940 fn test_image_placeholder_without_alt() {
941 let tokens = vec![Token::Image {
942 src: "img/photo.png".to_string(),
943 alt: String::with_capacity(0),
944 }];
945
946 let mut engine = LayoutEngine::new(460.0, 650.0, 20.0);
947 let pages = engine.layout_tokens(&tokens);
948 let texts = collect_line_texts(&pages);
949
950 assert_eq!(texts.len(), 1);
951 assert_eq!(texts[0], "[Image]");
952 }
953
954 #[test]
955 fn test_link_text_renders_normally() {
956 let tokens = vec![
957 Token::Text("Click ".to_string()),
958 Token::LinkStart("https://example.com".to_string()),
959 Token::Text("here".to_string()),
960 Token::LinkEnd,
961 Token::Text(" for info.".to_string()),
962 ];
963
964 let mut engine = LayoutEngine::new(460.0, 650.0, 20.0);
965 let pages = engine.layout_tokens(&tokens);
966 let texts = collect_line_texts(&pages);
967
968 assert_eq!(texts.len(), 1);
970 assert_eq!(texts[0], "Click here for info.");
971 }
972
973 #[test]
974 fn test_mixed_content() {
975 let tokens = vec![
976 Token::Heading(1),
978 Token::Text("My Chapter".to_string()),
979 Token::ParagraphBreak,
980 Token::Text("Some introductory text.".to_string()),
982 Token::ParagraphBreak,
983 Token::ListStart(false),
985 Token::ListItemStart,
986 Token::Text("Item one".to_string()),
987 Token::ListItemEnd,
988 Token::ListItemStart,
989 Token::Text("Item two".to_string()),
990 Token::ListItemEnd,
991 Token::ListEnd,
992 Token::Image {
994 src: "fig1.png".to_string(),
995 alt: "Figure 1".to_string(),
996 },
997 Token::Text("Visit ".to_string()),
999 Token::LinkStart("https://example.com".to_string()),
1000 Token::Text("example".to_string()),
1001 Token::LinkEnd,
1002 Token::Text(" site.".to_string()),
1003 Token::ParagraphBreak,
1004 ];
1005
1006 let mut engine = LayoutEngine::new(460.0, 650.0, 20.0);
1007 let pages = engine.layout_tokens(&tokens);
1008 let texts = collect_line_texts(&pages);
1009
1010 assert!(texts.len() >= 6);
1012 assert_eq!(texts[0], "My Chapter");
1013 assert_eq!(texts[1], "Some introductory text.");
1014 assert_eq!(texts[2], "\u{2022} Item one");
1015 assert_eq!(texts[3], "\u{2022} Item two");
1016 assert_eq!(texts[4], "[Image: Figure 1]");
1017 assert_eq!(texts[5], "Visit example site.");
1018 }
1019
1020 #[test]
1021 fn test_list_counters_reset_between_lists() {
1022 let tokens = vec![
1023 Token::ListStart(true),
1025 Token::ListItemStart,
1026 Token::Text("A".to_string()),
1027 Token::ListItemEnd,
1028 Token::ListItemStart,
1029 Token::Text("B".to_string()),
1030 Token::ListItemEnd,
1031 Token::ListEnd,
1032 Token::ListStart(true),
1034 Token::ListItemStart,
1035 Token::Text("X".to_string()),
1036 Token::ListItemEnd,
1037 Token::ListItemStart,
1038 Token::Text("Y".to_string()),
1039 Token::ListItemEnd,
1040 Token::ListEnd,
1041 ];
1042
1043 let mut engine = LayoutEngine::new(460.0, 650.0, 20.0);
1044 let pages = engine.layout_tokens(&tokens);
1045 let texts = collect_line_texts(&pages);
1046
1047 assert_eq!(texts.len(), 4);
1048 assert_eq!(texts[0], "1. A");
1049 assert_eq!(texts[1], "2. B");
1050 assert_eq!(texts[2], "1. X");
1052 assert_eq!(texts[3], "2. Y");
1053 }
1054
1055 #[test]
1058 fn test_layout_all_token_types_together() {
1059 let tokens = vec![
1061 Token::Heading(2),
1062 Token::Text("Title".to_string()),
1063 Token::ParagraphBreak,
1064 Token::Text("Normal ".to_string()),
1065 Token::Strong(true),
1066 Token::Text("bold".to_string()),
1067 Token::Strong(false),
1068 Token::Emphasis(true),
1069 Token::Text("italic".to_string()),
1070 Token::Emphasis(false),
1071 Token::ParagraphBreak,
1072 Token::ListStart(false),
1073 Token::ListItemStart,
1074 Token::Text("Bullet".to_string()),
1075 Token::ListItemEnd,
1076 Token::ListEnd,
1077 Token::ListStart(true),
1078 Token::ListItemStart,
1079 Token::Text("Numbered".to_string()),
1080 Token::ListItemEnd,
1081 Token::ListEnd,
1082 Token::LinkStart("http://example.com".to_string()),
1083 Token::Text("link text".to_string()),
1084 Token::LinkEnd,
1085 Token::ParagraphBreak,
1086 Token::Image {
1087 src: "img.png".to_string(),
1088 alt: "Alt text".to_string(),
1089 },
1090 Token::LineBreak,
1091 Token::Text("Final line.".to_string()),
1092 ];
1093
1094 let mut engine = LayoutEngine::new(460.0, 650.0, 20.0);
1095 let pages = engine.layout_tokens(&tokens);
1096
1097 assert!(!pages.is_empty());
1098 let total_lines: usize = pages.iter().map(|p| p.line_count()).sum();
1099 assert!(
1100 total_lines >= 5,
1101 "Expected at least 5 lines, got {}",
1102 total_lines
1103 );
1104 }
1105
1106 #[test]
1107 fn test_layout_only_headings_no_body() {
1108 let tokens = vec![
1109 Token::Heading(1),
1110 Token::Text("Chapter One".to_string()),
1111 Token::ParagraphBreak,
1112 Token::Heading(2),
1113 Token::Text("Section A".to_string()),
1114 Token::ParagraphBreak,
1115 Token::Heading(3),
1116 Token::Text("Subsection i".to_string()),
1117 ];
1118
1119 let mut engine = LayoutEngine::new(460.0, 650.0, 20.0);
1120 let pages = engine.layout_tokens(&tokens);
1121
1122 assert!(!pages.is_empty());
1123 let texts = collect_line_texts(&pages);
1124 assert!(texts.len() >= 3);
1125
1126 for page in &pages {
1128 for line in &page.lines {
1129 if !line.is_empty() {
1130 assert!(
1131 line.style().is_bold(),
1132 "Heading line '{}' should be bold, was {:?}",
1133 line.text(),
1134 line.style()
1135 );
1136 }
1137 }
1138 }
1139 }
1140
1141 #[test]
1142 fn test_layout_very_long_single_word() {
1143 let long_word = "superlongwordthatdoesnotfitinpagewidthatall";
1145 let tokens = vec![Token::Text(long_word.to_string()), Token::ParagraphBreak];
1146
1147 let mut engine = LayoutEngine::new(100.0, 400.0, 20.0);
1149 let pages = engine.layout_tokens(&tokens);
1150
1151 assert!(!pages.is_empty());
1152 let texts = collect_line_texts(&pages);
1155 assert!(texts.iter().any(|t| t == long_word));
1156 }
1157
1158 #[test]
1159 fn test_page_boundary_exact_fill() {
1160 let mut engine = LayoutEngine::new(400.0, 100.0, 20.0);
1163
1164 let tokens = vec![
1166 Token::Text("Line one text".to_string()),
1167 Token::LineBreak,
1168 Token::Text("Line two text".to_string()),
1169 Token::LineBreak,
1170 Token::Text("Line three text".to_string()),
1171 ];
1172
1173 let pages = engine.layout_tokens(&tokens);
1174 assert_eq!(pages.len(), 1);
1175 assert_eq!(pages[0].line_count(), 3);
1176 }
1177
1178 #[test]
1179 fn test_page_boundary_overflow_by_one() {
1180 let mut engine = LayoutEngine::new(400.0, 100.0, 20.0);
1182
1183 let tokens = vec![
1184 Token::Text("Line one".to_string()),
1185 Token::LineBreak,
1186 Token::Text("Line two".to_string()),
1187 Token::LineBreak,
1188 Token::Text("Line three".to_string()),
1189 Token::LineBreak,
1190 Token::Text("Line four overflow".to_string()),
1191 ];
1192
1193 let pages = engine.layout_tokens(&tokens);
1194 assert!(pages.len() >= 2, "Expected 2+ pages, got {}", pages.len());
1195 assert_eq!(pages[0].line_count(), 3);
1196 assert!(pages[1].line_count() >= 1);
1197 }
1198
1199 #[test]
1200 fn test_style_transitions_in_paragraph() {
1201 let tokens = vec![
1203 Token::Text("normal".to_string()),
1204 Token::Strong(true),
1205 Token::Text("bold".to_string()),
1206 Token::Strong(false),
1207 Token::Emphasis(true),
1208 Token::Text("italic".to_string()),
1209 Token::Strong(true),
1210 Token::Text("bolditalic".to_string()),
1211 Token::Strong(false),
1212 Token::Emphasis(false),
1213 Token::Text("normal_again".to_string()),
1214 ];
1215
1216 let mut engine = LayoutEngine::new(2000.0, 650.0, 20.0);
1218 let pages = engine.layout_tokens(&tokens);
1219
1220 assert!(!pages.is_empty());
1221 let texts = collect_line_texts(&pages);
1222 let joined = texts.join(" ");
1224 assert!(joined.contains("normal"));
1225 assert!(joined.contains("bold"));
1226 assert!(joined.contains("italic"));
1227 assert!(joined.contains("bolditalic"));
1228 assert!(joined.contains("normal_again"));
1229 }
1230
1231 #[test]
1232 fn test_multiple_paragraph_breaks_in_sequence() {
1233 let tokens = vec![
1234 Token::Text("First paragraph.".to_string()),
1235 Token::ParagraphBreak,
1236 Token::ParagraphBreak,
1237 Token::ParagraphBreak,
1238 Token::Text("After multiple breaks.".to_string()),
1239 ];
1240
1241 let mut engine = LayoutEngine::new(460.0, 650.0, 20.0);
1242 let pages = engine.layout_tokens(&tokens);
1243
1244 let texts = collect_line_texts(&pages);
1245 assert_eq!(texts.len(), 2);
1246 assert_eq!(texts[0], "First paragraph.");
1247 assert_eq!(texts[1], "After multiple breaks.");
1248 }
1249
1250 #[test]
1251 fn test_layout_with_custom_font_metrics() {
1252 let custom_metrics = FontMetrics {
1253 char_width: 8.0,
1254 char_height: 16.0,
1255 bold_char_width: 9.0,
1256 italic_char_width: 8.0,
1257 };
1258
1259 assert_eq!(custom_metrics.text_width("hello", TextStyle::Normal), 40.0);
1261 assert_eq!(custom_metrics.text_width("hello", TextStyle::Bold), 45.0);
1262 assert_eq!(custom_metrics.char_width_for_style(TextStyle::Italic), 8.0);
1263 assert_eq!(
1264 custom_metrics.char_width_for_style(TextStyle::BoldItalic),
1265 9.0
1266 );
1267
1268 let tokens = vec![
1270 Token::Text("Testing custom font metrics.".to_string()),
1271 Token::ParagraphBreak,
1272 ];
1273 let mut engine = LayoutEngine::new(200.0, 400.0, 20.0).with_font_metrics(custom_metrics);
1274 let pages = engine.layout_tokens(&tokens);
1275 assert!(!pages.is_empty());
1276 }
1277
1278 #[test]
1279 fn test_layout_config_create_engine_works() {
1280 let config = LayoutConfig {
1281 page_width: 300.0,
1282 page_height: 500.0,
1283 line_height: 18.0,
1284 left_margin: 15.0,
1285 top_margin: 20.0,
1286 font_metrics: FontMetrics {
1287 char_width: 8.0,
1288 char_height: 16.0,
1289 bold_char_width: 9.0,
1290 italic_char_width: 8.0,
1291 },
1292 };
1293
1294 let mut engine = config.create_engine();
1295 assert_eq!(engine.current_page_number(), 1);
1296
1297 let tokens = vec![
1298 Token::Text("Config engine test.".to_string()),
1299 Token::ParagraphBreak,
1300 Token::Text("Second paragraph.".to_string()),
1301 ];
1302 let pages = engine.layout_tokens(&tokens);
1303 assert!(!pages.is_empty());
1304 assert!(pages[0].line_count() >= 1);
1305 }
1306
1307 #[test]
1308 fn test_layout_default_config_create_engine() {
1309 let config = LayoutConfig::default();
1310 let mut engine = config.create_engine();
1311
1312 let tokens = vec![
1313 Token::Text("Default config test.".to_string()),
1314 Token::ParagraphBreak,
1315 ];
1316 let pages = engine.layout_tokens(&tokens);
1317 assert!(!pages.is_empty());
1318 }
1319
1320 #[test]
1321 fn test_layout_zero_length_text_tokens() {
1322 let tokens = vec![
1323 Token::Text(String::with_capacity(0)),
1324 Token::Text("visible".to_string()),
1325 Token::Text(String::with_capacity(0)),
1326 Token::ParagraphBreak,
1327 ];
1328
1329 let mut engine = LayoutEngine::new(460.0, 650.0, 20.0);
1330 let pages = engine.layout_tokens(&tokens);
1331
1332 let texts = collect_line_texts(&pages);
1333 assert_eq!(texts.len(), 1);
1334 assert_eq!(texts[0], "visible");
1335 }
1336
1337 #[test]
1338 fn test_heading_gets_extra_space() {
1339 let tokens = vec![
1341 Token::Text("Intro paragraph.".to_string()),
1342 Token::ParagraphBreak,
1343 Token::Heading(1),
1344 Token::Text("Chapter Title".to_string()),
1345 Token::ParagraphBreak,
1346 Token::Text("Body text.".to_string()),
1347 ];
1348
1349 let mut engine = LayoutEngine::new(460.0, 650.0, 20.0);
1350 let pages = engine.layout_tokens(&tokens);
1351
1352 assert!(!pages.is_empty());
1353 let intro_line = &pages[0].lines[0];
1354 let heading_line = pages[0]
1355 .lines
1356 .iter()
1357 .find(|l| l.text().contains("Chapter Title"))
1358 .expect("Heading line should exist");
1359
1360 assert!(heading_line.style().is_bold());
1362
1363 let gap = heading_line.y - intro_line.y;
1365 assert!(
1366 gap > 20,
1367 "Expected extra spacing before heading, gap was {}",
1368 gap
1369 );
1370 }
1371
1372 #[test]
1373 fn test_heading_level_spacing_difference() {
1374 let tokens_h1 = vec![
1376 Token::Text("Intro.".to_string()),
1377 Token::ParagraphBreak,
1378 Token::Heading(1),
1379 Token::Text("H1 Title".to_string()),
1380 ];
1381 let tokens_h4 = vec![
1382 Token::Text("Intro.".to_string()),
1383 Token::ParagraphBreak,
1384 Token::Heading(4),
1385 Token::Text("H4 Title".to_string()),
1386 ];
1387
1388 let mut engine1 = LayoutEngine::new(460.0, 650.0, 20.0);
1389 let pages_h1 = engine1.layout_tokens(&tokens_h1);
1390
1391 let mut engine4 = LayoutEngine::new(460.0, 650.0, 20.0);
1392 let pages_h4 = engine4.layout_tokens(&tokens_h4);
1393
1394 let h1_y = pages_h1[0]
1395 .lines
1396 .iter()
1397 .find(|l| l.text().contains("H1 Title"))
1398 .unwrap()
1399 .y;
1400 let h4_y = pages_h4[0]
1401 .lines
1402 .iter()
1403 .find(|l| l.text().contains("H4 Title"))
1404 .unwrap()
1405 .y;
1406
1407 assert!(
1409 h1_y > h4_y,
1410 "h1 spacing (y={}) should be greater than h4 spacing (y={})",
1411 h1_y,
1412 h4_y
1413 );
1414 }
1415
1416 #[test]
1417 fn test_layout_engine_reuse() {
1418 let mut engine = LayoutEngine::new(460.0, 650.0, 20.0);
1419
1420 let tokens1 = vec![Token::Text("First run.".to_string()), Token::ParagraphBreak];
1422 let pages1 = engine.layout_tokens(&tokens1);
1423 assert!(!pages1.is_empty());
1424
1425 let tokens2 = vec![
1427 Token::Text("Second run.".to_string()),
1428 Token::ParagraphBreak,
1429 ];
1430 let pages2 = engine.layout_tokens(&tokens2);
1431 assert!(!pages2.is_empty());
1432 assert_eq!(pages2[0].page_number, 1);
1433 assert_eq!(pages2[0].lines[0].text(), "Second run.");
1434 }
1435
1436 #[test]
1437 fn test_line_empty_and_len_edge_cases() {
1438 let empty_line = Line::new(String::with_capacity(0), 0, TextStyle::Normal);
1439 assert!(empty_line.is_empty());
1440 assert_eq!(empty_line.len(), 0);
1441
1442 let line = Line::new("Hello World!".to_string(), 100, TextStyle::Italic);
1443 assert!(!line.is_empty());
1444 assert_eq!(line.len(), 12);
1445 assert_eq!(line.style(), TextStyle::Italic);
1446 }
1447
1448 #[test]
1449 fn test_text_style_combinations() {
1450 assert_eq!(TextStyle::Normal.with_bold(true), TextStyle::Bold);
1452 assert_eq!(TextStyle::Normal.with_italic(true), TextStyle::Italic);
1454 assert_eq!(TextStyle::Bold.with_italic(true), TextStyle::BoldItalic);
1456 assert_eq!(TextStyle::BoldItalic.with_bold(false), TextStyle::Italic);
1458 assert_eq!(TextStyle::BoldItalic.with_italic(false), TextStyle::Bold);
1460 assert_eq!(TextStyle::Bold.with_bold(true), TextStyle::Bold);
1462 assert_eq!(TextStyle::Normal.with_bold(false), TextStyle::Normal);
1463 }
1464
1465 #[test]
1466 fn test_measure_text_various() {
1467 let engine = LayoutEngine::new(460.0, 650.0, 20.0);
1468 assert_eq!(engine.measure_text("hello", TextStyle::Normal), 50.0);
1470 assert_eq!(engine.measure_text("", TextStyle::Normal), 0.0);
1471 assert_eq!(engine.measure_text("a", TextStyle::Bold), 10.0);
1472 assert_eq!(engine.measure_text("test string", TextStyle::Italic), 110.0);
1473 }
1474
1475 #[test]
1476 fn test_with_margins_affects_layout() {
1477 let tokens = vec![
1478 Token::Text("Margin test.".to_string()),
1479 Token::ParagraphBreak,
1480 ];
1481
1482 let mut engine = LayoutEngine::new(460.0, 650.0, 20.0).with_margins(25.0, 40.0);
1483 let pages = engine.layout_tokens(&tokens);
1484
1485 assert!(!pages.is_empty());
1486 assert_eq!(pages[0].lines[0].y, 40);
1488 }
1489
1490 #[test]
1491 fn test_into_pages_empty_engine() {
1492 let engine = LayoutEngine::new(460.0, 650.0, 20.0);
1493 let pages = engine.into_pages();
1494 assert!(pages.is_empty());
1495 }
1496
1497 #[test]
1498 fn test_layout_whitespace_only_text() {
1499 let tokens = vec![
1500 Token::Text(" ".to_string()),
1501 Token::Text("visible".to_string()),
1502 Token::ParagraphBreak,
1503 ];
1504
1505 let mut engine = LayoutEngine::new(460.0, 650.0, 20.0);
1506 let pages = engine.layout_tokens(&tokens);
1507
1508 let texts = collect_line_texts(&pages);
1510 assert_eq!(texts.len(), 1);
1511 assert_eq!(texts[0], "visible");
1512 }
1513
1514 #[test]
1515 fn test_large_document_many_paragraphs() {
1516 let mut tokens = Vec::with_capacity(0);
1517 for i in 0..50 {
1518 tokens.push(Token::Text(alloc::format!(
1519 "Paragraph {} with enough text to be meaningful.",
1520 i
1521 )));
1522 tokens.push(Token::ParagraphBreak);
1523 }
1524
1525 let mut engine = LayoutEngine::new(460.0, 200.0, 20.0);
1526 let pages = engine.layout_tokens(&tokens);
1527
1528 assert!(pages.len() > 1);
1530
1531 for (i, page) in pages.iter().enumerate() {
1533 assert_eq!(page.page_number, i + 1);
1534 }
1535
1536 let total_lines: usize = pages.iter().map(|p| p.line_count()).sum();
1538 assert!(total_lines >= 50);
1539 }
1540}