fleuron/session/
output.rs1use std::ops::Range;
5
6use crate::LayoutOutput;
7use crate::content::NodeId;
8use crate::fonts::FontRegistry;
9use crate::images::Assets;
10use crate::layout::font_table;
11use crate::pages::{DrawItem, Folios, Navigation};
12use crate::pdf::{self, PdfError};
13
14use super::Session;
15
16impl Session<'_> {
17 pub fn preview(&mut self) -> &LayoutOutput {
19 self.update();
20 self.output.as_ref().expect("an update leaves an output")
21 }
22
23 pub fn export(&mut self) -> Result<Vec<u8>, PdfError> {
26 self.update();
27 let output = self.output.as_ref().expect("an update leaves an output");
28 pdf::write(
29 output,
30 self.registry.get(),
31 self.assets.get(),
32 &self.book.metadata,
33 )
34 }
35
36 pub fn folios(&mut self, nodes: &[NodeId]) -> Vec<Option<Folios>> {
50 let held: Vec<Option<Range<u32>>> =
51 nodes.iter().map(|node| self.book.subtree(*node)).collect();
52 self.update();
53 let output = self.output.as_ref().expect("an update leaves an output");
54 let mut answers: Vec<Option<Folios>> = vec![None; nodes.len()];
55 for (at, page) in output.pages.iter().enumerate() {
56 let at = at as u32;
57 let mut reached = |node: NodeId| {
58 for (answer, held) in answers.iter_mut().zip(&held) {
59 if !held.as_ref().is_some_and(|held| held.contains(&node.get())) {
60 continue;
61 }
62 *answer = Some(match *answer {
63 Some(folios) => Folios {
64 last: page.number,
65 count: at - folios.at + 1,
66 ..folios
67 },
68 None => Folios {
69 first: page.number,
70 last: page.number,
71 at,
72 count: 1,
73 },
74 });
75 }
76 };
77 for section in &page.sections {
81 reached(*section);
82 }
83 for item in &page.items {
84 if let DrawItem::Text {
85 origin: Some(origin),
86 ..
87 } = item
88 {
89 reached(origin.node.element());
90 }
91 }
92 }
93 answers
94 }
95
96 pub fn into_output(mut self) -> LayoutOutput {
98 self.update();
99 self.output.take().expect("an update leaves an output")
100 }
101}
102
103pub(super) fn blank_output(registry: &FontRegistry, assets: &Assets) -> LayoutOutput {
106 LayoutOutput {
107 pages: Vec::new(),
108 fonts: font_table(registry),
109 assets: assets.assets().to_vec(),
110 warnings: Vec::new(),
111 navigation: Navigation::default(),
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use crate::content::NodeId;
119 use crate::session::Session;
120 use crate::session::testing::{
121 book, heading, named_on, prose, registry, section, sheets, three_chapters,
122 };
123
124 #[test]
127 fn a_node_answers_the_first_and_last_folio_it_is_set_on() {
128 let mut session = three_chapters();
129 let chapters: Vec<NodeId> = session.book().sections.iter().map(|s| s.id).collect();
130 let settled = session.stages();
131 let folios = session.folios(&chapters);
132 assert_eq!(
133 session.stages(),
134 settled,
135 "answering ran a stage over pages that were already placed"
136 );
137 let output = session.preview();
138
139 for (chapter, folios) in chapters.iter().zip(&folios) {
140 let named = named_on(output, *chapter);
141 let at = *named.first().expect("a chapter of prose reaches a page");
142 let last = *named.last().expect("a chapter of prose reaches a page");
143 assert_eq!(
144 *folios,
145 Some(Folios {
146 first: output.pages[at].number,
147 last: output.pages[last].number,
148 at: at as u32,
149 count: (last - at + 1) as u32,
150 }),
151 "chapter {} is set on the pages {named:?}",
152 chapter.get()
153 );
154 }
155 assert!(
156 folios
157 .iter()
158 .any(|folios| folios.is_some_and(|folios| folios.first < folios.last)),
159 "no chapter of {} pages ran across two of them",
160 output.pages.len()
161 );
162 }
163
164 #[test]
169 fn a_restarted_counter_leaves_the_folio_and_the_page_apart() {
170 let mut session = Session::new(registry());
171 session.set_content(book(vec![
172 section("front.md", prose("alpha", 4)),
173 section("body.md", prose("beta", 8)),
174 ]));
175 session.set_style(sheets("section:last-child { counter-reset: page 1 }"));
176 let body = session.book().sections[1].id;
177 let folios = session.folios(&[body])[0].expect("the chapter reaches a page");
178 let output = session.preview();
179
180 assert_eq!(folios.first, 1, "the restarted chapter opens at folio 1");
181 assert!(
182 folios.at > 0,
183 "the restarted chapter is not the first page of the book"
184 );
185 assert_eq!(
186 output.pages[folios.at as usize].number, folios.first,
187 "`at` is not the page the folio is printed on"
188 );
189 assert_eq!(
190 output.pages[(folios.at + folios.count - 1) as usize].number,
191 folios.last,
192 "`at` and `count` do not reach the folio it ends on"
193 );
194 }
195
196 #[test]
199 fn several_nodes_are_answered_in_one_call() {
200 let mut session = three_chapters();
201 let mut asked: Vec<NodeId> = session.book().sections.iter().map(|s| s.id).collect();
202 asked.reverse();
203 asked.push(NodeId::new(u32::MAX));
204
205 let together = session.folios(&asked);
206 let apart: Vec<Option<Folios>> = asked
207 .iter()
208 .map(|node| session.folios(std::slice::from_ref(node))[0])
209 .collect();
210 assert_eq!(together, apart);
211 assert_eq!(together.len(), asked.len());
212 }
213
214 #[test]
218 fn a_node_the_book_does_not_hold_answers_with_nothing() {
219 let mut session = three_chapters();
220 let past = NodeId::new(u32::MAX);
221 assert_eq!(
222 session.folios(&[past, NodeId::UNASSIGNED, past]),
223 vec![None, None, None]
224 );
225 }
226
227 #[test]
231 fn a_node_no_run_names_answers_with_the_page_its_content_is_on() {
232 let mut session = Session::new(registry());
233 session.set_content(book(vec![section(
234 "one.md",
235 [vec![heading("Chapter One")], prose("alpha", 8)].concat(),
236 )]));
237 let node = crate::content::block_id(&session.book().sections[0].blocks[0]);
238 let folios = session.folios(&[node]);
239 let output = session.preview();
240
241 assert!(
242 named_on(output, node).is_empty(),
243 "a run named the heading, so this proves nothing"
244 );
245 let first = output.pages.first().expect("the book has pages").number;
246 assert_eq!(
247 folios,
248 vec![Some(Folios {
249 first,
250 last: first,
251 at: 0,
252 count: 1,
253 })]
254 );
255 }
256
257 #[test]
260 fn export_paints_from_the_stages_the_preview_used() {
261 let mut session = three_chapters();
262 let before = session.stages();
263 let bytes = session.export().expect("the fixture book writes PDF");
264 assert_eq!(session.stages(), before, "an export re-ran a stage");
265 assert!(bytes.starts_with(b"%PDF"), "that is not a PDF");
266 }
267}