fleuron/session/mod.rs
1//! A retained pipeline: every stage kept, and only what an edit
2//! changed run again.
3//!
4//! `layout_book` is a pure function of its inputs, so every call
5//! rebuilds every stage. A process that renders one book and exits
6//! wants that. A live preview does not, because the common event
7//! there is a small change to one input while the others stand. A
8//! session retains the output of each stage and works out the deepest
9//! stage an edit reaches: a colour serves the display structure back, page
10//! furniture repaints, `@page` geometry re-fragments over cached
11//! lines, and only the measure or the text itself breaks lines
12//! again.
13//!
14//! # Section-local lines
15//!
16//! Line *breaking* is section-local: where the breaks fall depends on
17//! the measure, the face and the text, not on where the section
18//! starts vertically. Line *placement* depends on position and
19//! belongs to fragmentation. So the cache stores breaks, shaped runs
20//! and advances, and no page coordinates at all. An edit to one
21//! chapter re-breaks that chapter and re-fragments the book, which
22//! for a whole novel costs about what tracking the pages that moved
23//! would.
24//!
25//! Two preconditions make that sound, and both are checked rather
26//! than remembered. The first is a uniform measure: masters that
27//! resolve different content widths make breaking depend on which
28//! page a line lands on. The second is that no inline text depends
29//! on pagination, which the parser guarantees: `counter(page)` is
30//! legal only inside a margin box. When either one fails, the session re-breaks
31//! everything instead of serving stale lines.
32//!
33//! A reference that prints a page is the one piece of inline text
34//! that pagination decides, and it is set on a pass of its own. The
35//! first pass sets a placeholder where the number goes, so its lines
36//! are section-local like any others. The second builds again only
37//! the sections whose references print a page, and keys each one by
38//! the folios it prints.
39//!
40//! The parts have a file each: `edit` is what a host changes,
41//! `output` is what it asks for, `inspect` answers what styled one
42//! node and where it landed, `stage` runs the stages, `invalidate`
43//! decides which of them an edit reaches, and `key` fingerprints one
44//! section.
45
46use std::borrow::Cow;
47
48use crate::content::{Book, Names, NodeId, SourceRange};
49use crate::fonts::{FontError, FontRegistry};
50use crate::images::{Assets, Contours};
51use crate::layout::{Fragment, Numbering, PageInfo, Piece, References, no_assets};
52use crate::pages::PageBox;
53use crate::style::{FontLoader, StyleTree, Stylesheets};
54use crate::{LayoutOutput, Warning};
55
56mod edit;
57mod faces;
58mod inspect;
59mod invalidate;
60mod key;
61mod output;
62mod stage;
63
64#[cfg(test)]
65mod testing;
66
67use invalidate::{Prints, section_local};
68
69/// How many times each stage has run since the session was made.
70///
71/// A host reads these to see what an edit cost. The tests read them
72/// to prove what an edit did *not* cost, which a clock cannot show.
73#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
74pub struct Stages {
75 /// Style compilations: parse, match, cascade.
76 pub style: u32,
77 /// Images decoded to trace a contour. A book whose sheet names no
78 /// contour never decodes one, and a contour already traced is not
79 /// traced again.
80 pub trace: u32,
81 /// Sections broken into lines. One per section, per rebuild.
82 pub lines: u32,
83 /// Fragmentation and page assembly runs.
84 pub flow: u32,
85 /// Furniture paints: numbering and margin boxes.
86 pub paint: u32,
87 /// Second layout passes, which print the pages the references in
88 /// the book name. A book whose sheet prints no page number never
89 /// runs one.
90 pub settle: u32,
91}
92
93/// The deepest stage a change invalidates, which is the shallowest
94/// cache that survives it. Ordered: a deeper stage implies every
95/// stage under it.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
97enum Stale {
98 /// Everything stands; the display structure is served as it is.
99 Nothing,
100 /// Numbering and margin boxes.
101 Paint,
102 /// Fragmentation, over lines that survived.
103 Flow,
104 /// Line breaking, and everything below it.
105 Break,
106 /// Tracing the contours the sheet asks for, which is above line
107 /// breaking because a contour that moved is a measure that moved.
108 Trace,
109}
110
111/// A table a session lays out against: a caller's, or one of its
112/// own.
113///
114/// A host with faces and images to lend keeps lending them. A worker
115/// has nowhere to keep either, since the module is all there is, so
116/// it hands them over once and adds to them through the session.
117enum Table<'a, T> {
118 Borrowed(&'a T),
119 Owned(Box<T>),
120}
121
122impl<T> Table<'_, T> {
123 fn get(&self) -> &T {
124 match self {
125 Table::Borrowed(table) => table,
126 Table::Owned(table) => table,
127 }
128 }
129
130 fn get_mut(&mut self) -> Option<&mut T> {
131 match self {
132 Table::Borrowed(_) => None,
133 Table::Owned(table) => Some(table),
134 }
135 }
136}
137
138/// Why a face did not reach a session's registry.
139#[derive(Debug, thiserror::Error)]
140pub enum AddFontError {
141 /// The session lays out against a registry it borrowed, and the
142 /// caller who owns it is the one who can add to it.
143 #[error("the session borrows its font registry")]
144 Borrowed,
145 /// The bytes are not a face this build can read.
146 #[error(transparent)]
147 Font(#[from] FontError),
148}
149
150/// Why an image did not reach a session's asset table.
151#[derive(Debug, thiserror::Error)]
152pub enum AddImageError {
153 /// The session lays out against an asset table it borrowed, and
154 /// the caller who owns it is the one who can add to it.
155 #[error("the session borrows its asset table")]
156 Borrowed,
157}
158
159/// One section's lines, and what building them had to complain about.
160struct Cached {
161 key: u64,
162 /// The id the section had when its lines were broken.
163 section: NodeId,
164 fragments: Vec<Fragment>,
165 warnings: Vec<Warning>,
166}
167
168impl Cached {
169 /// Whether the lines can be moved onto the ids the book hands out
170 /// now. The notes a line carries were built with ids of their
171 /// own, and moving those is not what `renumber` does, so a
172 /// section that holds one is broken again instead.
173 fn renumbers(&self, section: NodeId) -> bool {
174 section == self.section
175 || !self
176 .fragments
177 .iter()
178 .any(|fragment| fragment.notes.is_some())
179 }
180
181 /// Moves the source ranges onto the ids the book hands out now.
182 /// Ids renumber globally on every edit, so a chapter nothing
183 /// touched comes back out of the cache under a new number; its
184 /// nodes are dense and in document order from the section's own,
185 /// so all of them move by the same step.
186 fn renumber(&mut self, section: NodeId) {
187 let step = section.get() as i64 - self.section.get() as i64;
188 self.section = section;
189 if step == 0 {
190 return;
191 }
192 for fragment in &mut self.fragments {
193 if let Some(marks) = &mut fragment.marks {
194 for node in &mut marks.targets {
195 *node = node.shifted(step);
196 }
197 }
198 if let Some(decorations) = &mut fragment.decorations {
199 for decoration in &mut decorations.opens {
200 decoration.node = decoration.node.shifted(step);
201 }
202 }
203 if let Piece::Anchor(node) = &mut fragment.piece {
204 *node = node.shifted(step);
205 }
206 if let Piece::Row(row) = &mut fragment.piece {
207 for (node, _) in &mut row.boxes {
208 *node = node.shifted(step);
209 }
210 for item in &mut row.items {
211 if let crate::pages::DrawItem::Text {
212 origin,
213 pseudo_element,
214 ..
215 } = item
216 {
217 shift_run(origin, pseudo_element, step);
218 }
219 }
220 continue;
221 }
222 let Piece::Line { line, cap } = &mut fragment.piece else {
223 continue;
224 };
225 let caps = cap.iter_mut().map(|cap| &mut cap.line);
226 for line in std::iter::once(line).chain(caps) {
227 for run in &mut line.runs {
228 shift_run(&mut run.origin, &mut run.pseudo_element, step);
229 }
230 }
231 }
232 }
233}
234
235/// Moves the ids one run names by `step`.
236fn shift_run(origin: &mut Option<SourceRange>, pseudo_element: &mut Option<NodeId>, step: i64) {
237 if let Some(origin) = origin {
238 origin.node = origin.node.shifted(step);
239 }
240 if let Some(node) = pseudo_element {
241 *node = node.shifted(step);
242 }
243}
244
245/// A retained pipeline: content, styling, and every stage between
246/// them and the page.
247///
248/// ```
249/// # use fleuron::content::Book;
250/// # use fleuron::session::Session;
251/// # use fleuron::style::Stylesheets;
252/// # let registry = fleuron::fonts::bundled_registry().unwrap();
253/// let mut session = Session::new(®istry);
254/// session.set_content(Book::default());
255/// session.set_style(Stylesheets::parse(&[]));
256/// let pages = &session.preview().pages;
257/// ```
258///
259/// A computed style can only resolve to a face already in the
260/// registry. A session over a borrowed registry leaves it as the
261/// host made it, so a sheet that brings its own `@font-face` needs
262/// the host to load that face before the sheet is set. A session
263/// that owns its registry registers the faces the sheet declares from
264/// the files [`add_font_file`](Session::add_font_file) hands over.
265pub struct Session<'a> {
266 registry: Table<'a, FontRegistry>,
267 assets: Table<'a, Assets>,
268 fonts: faces::FontFiles,
269 /// What the trace stage made of the assets the sheet wraps prose
270 /// around. Empty for a sheet that names no contour.
271 contours: Contours,
272 book: Cow<'a, Book>,
273 /// The sheets the tree was compiled from. `None` on the one-shot
274 /// path, where the caller compiled the tree itself and nothing
275 /// will ask for another.
276 sheets: Option<Stylesheets>,
277 styles: Cow<'a, StyleTree>,
278 prints: Prints,
279 /// Whether the preconditions for reusing a section's lines are met.
280 section_local: bool,
281 /// Whether the book places an image, which is what makes the
282 /// page's height an input to breaking.
283 images: bool,
284 /// Whether the stages are kept between calls. This is the only
285 /// difference on the one-shot path, which keeps one section's
286 /// lines at a time and drops each as it is flowed.
287 retain: bool,
288 lines: Vec<Cached>,
289 /// What the book's references resolve against on the pass that
290 /// finds the pages.
291 references: References,
292 /// What the notes of the book are numbered.
293 notes: Numbering,
294 /// Each section's lines on the pass that prints the pages its
295 /// references name. `None` for a section with no such reference,
296 /// whose lines from the pass before stand.
297 settled: Vec<Option<Cached>>,
298 /// What that pass had to complain about: its own sections, and
299 /// every element it set on another page than the pass before.
300 settle_warnings: Vec<Warning>,
301 /// What the links had to complain about: a url that names nothing
302 /// in the book.
303 link_warnings: Vec<Warning>,
304 infos: Vec<PageInfo>,
305 /// The border box of each block, on each page it reaches.
306 boxes: Vec<(NodeId, PageBox)>,
307 output: Option<LayoutOutput>,
308 /// What building lines complained about, deduped in the order the
309 /// sections raised it.
310 flow_warnings: Vec<Warning>,
311 /// What a frontend had to say about the sources it read, which
312 /// happened upstream of every stage here.
313 source_warnings: Vec<Warning>,
314 stale: Stale,
315 stages: Stages,
316}
317
318impl<'a> Session<'a> {
319 /// A session over the faces in `registry`, with no content and
320 /// the built-in sheet alone.
321 pub fn new(registry: &'a FontRegistry) -> Session<'a> {
322 Session::with_assets(registry, no_assets())
323 }
324
325 /// The same, over images the host has already probed.
326 pub fn with_assets(registry: &'a FontRegistry, assets: &'a Assets) -> Session<'a> {
327 Session::over(Table::Borrowed(registry), Table::Borrowed(assets))
328 }
329
330 fn over(registry: Table<'a, FontRegistry>, assets: Table<'a, Assets>) -> Session<'a> {
331 let book = Book::default();
332 let sheets = Stylesheets::parse(&[]);
333 let styles = sheets.compile(&book, registry.get());
334 let fonts = faces::FontFiles::over(registry.get().len());
335 let mut session = Session {
336 registry,
337 assets,
338 fonts,
339 contours: Contours::none(),
340 book: Cow::Owned(book),
341 sheets: Some(sheets),
342 prints: Prints::of(&styles, false),
343 section_local: section_local(&styles),
344 images: false,
345 styles: Cow::Owned(styles),
346 retain: true,
347 lines: Vec::new(),
348 references: References::default(),
349 notes: Numbering::default(),
350 settled: Vec::new(),
351 settle_warnings: Vec::new(),
352 link_warnings: Vec::new(),
353 infos: Vec::new(),
354 boxes: Vec::new(),
355 output: None,
356 flow_warnings: Vec::new(),
357 source_warnings: Vec::new(),
358 stale: Stale::Trace,
359 stages: Stages {
360 style: 1,
361 ..Stages::default()
362 },
363 };
364 session.load_faces();
365 session
366 }
367
368 /// A session that owns the faces it lays out against, and takes
369 /// more through [`add_font`](Session::add_font).
370 ///
371 /// This is the shape a worker needs: font bytes cross the
372 /// boundary once, the module keeps them, and no caller on the
373 /// other side of the wall has a registry to lend.
374 pub fn owning(registry: FontRegistry) -> Session<'static> {
375 Session::over(
376 Table::Owned(Box::new(registry)),
377 Table::Owned(Box::new(Assets::none())),
378 )
379 }
380
381 /// The single run `layout_book` makes, over inputs the caller
382 /// owns and will not edit. Nothing is fingerprinted, because
383 /// nothing will be compared against it.
384 pub(crate) fn once(
385 book: &'a Book,
386 styles: &'a StyleTree,
387 registry: &'a FontRegistry,
388 assets: &'a Assets,
389 ) -> Session<'a> {
390 Session {
391 registry: Table::Borrowed(registry),
392 assets: Table::Borrowed(assets),
393 fonts: faces::FontFiles::over(registry.len()),
394 contours: Contours::none(),
395 book: Cow::Borrowed(book),
396 sheets: None,
397 styles: Cow::Borrowed(styles),
398 prints: Prints::default(),
399 section_local: false,
400 images: false,
401 retain: false,
402 lines: Vec::new(),
403 references: References::default(),
404 notes: Numbering::default(),
405 settled: Vec::new(),
406 settle_warnings: Vec::new(),
407 link_warnings: Vec::new(),
408 infos: Vec::new(),
409 boxes: Vec::new(),
410 output: None,
411 flow_warnings: Vec::new(),
412 source_warnings: Vec::new(),
413 stale: Stale::Trace,
414 stages: Stages::default(),
415 }
416 }
417}
418
419impl Session<'_> {
420 /// The session's own copy of the book, node ids assigned.
421 pub fn book(&self) -> &Book {
422 &self.book
423 }
424
425 /// The classes and the ids the blocks and inlines of one source
426 /// carry, or of the whole book when `source` is `None`, as the
427 /// book holds them now. A section's own names are not in the
428 /// answer.
429 pub fn names(&self, source: Option<&str>) -> Names {
430 self.book.names(source)
431 }
432
433 /// The compiled styling behind the last update.
434 pub fn styles(&self) -> &StyleTree {
435 &self.styles
436 }
437
438 /// The sheets the styling was compiled from, as the host last
439 /// set them.
440 pub fn sheets(&self) -> Option<&Stylesheets> {
441 self.sheets.as_ref()
442 }
443
444 /// The images this session holds, by the url each was registered
445 /// under.
446 pub fn images(&self) -> &Assets {
447 self.assets.get()
448 }
449
450 /// The files [`add_font_file`](Session::add_font_file) handed
451 /// over, by the url a `@font-face` names them by.
452 pub fn font_files(&self) -> &dyn FontLoader {
453 &self.fonts
454 }
455
456 /// The faces this session lays out against.
457 ///
458 /// A painter that has to draw with the same file the shaper used
459 /// reaches the bytes through here; the display structure names
460 /// ids, and the registry is what they index.
461 pub fn fonts(&self) -> &FontRegistry {
462 self.registry.get()
463 }
464
465 /// How many times each stage has run.
466 pub fn stages(&self) -> Stages {
467 self.stages
468 }
469
470 /// Whether a section's lines survive an edit elsewhere in the
471 /// book. This goes false when the styling breaks a precondition,
472 /// either masters of different measures or inline content that
473 /// depends on pagination, and everything is re-broken instead.
474 pub fn reuses_sections(&self) -> bool {
475 self.section_local
476 }
477}