1use crate::ir::{Block, Line, PageLayout};
4
5pub trait Output {
7 fn render(&self, pages: &[PageLayout]) -> String;
8}
9
10pub struct Text;
14
15impl Output for Text {
16 fn render(&self, pages: &[PageLayout]) -> String {
17 let mut out = String::new();
18 for (page_index, page) in pages.iter().enumerate() {
19 if page_index > 0 {
20 out.push('\u{c}');
21 }
22 let mut written = 0usize;
23 for block in &page.blocks {
24 match block {
25 Block::Heading { lines, .. } | Block::Paragraph { lines, .. } => {
26 for line in lines {
27 open_line(&mut out, &mut written);
28 push_line(&mut out, line);
29 }
30 }
31 Block::List { items, .. } => {
32 for line in items.iter().flat_map(|item| &item.lines) {
33 open_line(&mut out, &mut written);
34 push_line(&mut out, line);
35 }
36 }
37 Block::Table { rows, .. } => {
38 for row in rows {
39 open_line(&mut out, &mut written);
40 push_row(&mut out, row);
41 }
42 }
43 }
44 }
45 }
46 out
47 }
48}
49
50fn open_line(out: &mut String, written: &mut usize) {
53 if *written > 0 {
54 out.push('\n');
55 }
56 *written += 1;
57}
58
59pub(crate) fn line_text(line: &Line) -> String {
61 let mut out = String::new();
62 push_line(&mut out, line);
63 out
64}
65
66fn push_line(out: &mut String, line: &Line) {
69 for inline in &line.inlines {
70 out.push_str(&inline.text);
71 }
72}
73
74fn push_row(out: &mut String, row: &[crate::ir::Cell]) {
79 let row_start = out.len();
80 for line in row.iter().filter_map(|cell| cell.line.as_ref()) {
81 let text = line_text(line);
82 let separated = out.len() == row_start
83 || out.ends_with(char::is_whitespace)
84 || text.starts_with(char::is_whitespace);
85 if !separated {
86 out.push(' ');
87 }
88 out.push_str(&text);
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use crate::ir::{BBox, Cell, Inline};
96
97 fn cell(text: &str) -> Cell {
98 Cell {
99 line: Some(Line {
100 inlines: vec![Inline {
101 text: text.to_string(),
102 bold: false,
103 italic: false,
104 }],
105 y: 700.0,
106 x: 72.0,
107 end_x: 100.0,
108 size: 10.0,
109 }),
110 colspan: 1,
111 rowspan: 1,
112 }
113 }
114
115 #[test]
118 fn a_table_row_joins_cells_with_one_space() {
119 let page = PageLayout {
120 blocks: vec![Block::Table {
121 rows: vec![vec![cell("0.933 "), cell("0.215"), cell(" 0.216")]],
122 bbox: BBox {
123 x0: 72.0,
124 y0: 700.0,
125 x1: 300.0,
126 y1: 710.0,
127 },
128 }],
129 };
130 assert_eq!(Text.render(&[page]), "0.933 0.215 0.216");
131 }
132}