Skip to main content

fleuron/session/
output.rs

1//! What a host asks for: the pages, a PDF, and the folios one node
2//! was set on.
3
4use 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    /// The display structure, brought up to date.
18    pub fn preview(&mut self) -> &LayoutOutput {
19        self.update();
20        self.output.as_ref().expect("an update leaves an output")
21    }
22
23    /// The same, as PDF bytes. The stages above the painter are the
24    /// ones the preview used, so an export cannot contradict it.
25    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    /// Where each of these nodes' content is set, answered in the
37    /// order they were asked about: the folios it runs between, and
38    /// the pages of the book those folios are.
39    ///
40    /// A node covers itself and everything under it, so a heading
41    /// answers with the page its own text is on, and a chapter with
42    /// the pages it runs across. Nothing for a node the book does
43    /// not hold, and nothing for one whose content reaches no page:
44    /// a node the engine synthesized, or a scene break, whose
45    /// ornament the engine wrote itself.
46    ///
47    /// The answer is a walk over the pages the session already
48    /// holds. It runs a stage only when an edit has left one to run.
49    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            // A page names the sections it carries, and each run of
78            // text names the node it was shaped from. Between them
79            // they name every node the page took content from.
80            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    /// The display structure by value, consuming the session.
97    pub fn into_output(mut self) -> LayoutOutput {
98        self.update();
99        self.output.take().expect("an update leaves an output")
100    }
101}
102
103/// An output with the font and asset tables filled in and nothing
104/// painted yet.
105pub(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    /// A chapter's content runs across pages, and the question
125    /// answers the folio it opens on and the folio it ends on.
126    #[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    /// A folio is what a page has printed on it, and a book whose
165    /// counter restarts prints a number that is not the page's place
166    /// in the book. Both are answered, so a host names one and
167    /// fetches by the other.
168    #[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    /// One call, one answer per node asked about, in the order they
197    /// were asked about.
198    #[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    /// A node the book does not hold, and the id the engine writes
215    /// its own text under, are both answered with nothing rather
216    /// than with a folio or an error.
217    #[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    /// A heading's runs are shaped from the text inside it, so no run
228    /// names the heading itself. It answers with the page that text
229    /// is on all the same.
230    #[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    /// An export costs nothing the preview has not already paid: the
258    /// stages above the painter are the same ones.
259    #[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}