Skip to content
API

Sessions

Sessions in fleuron are useful when you want to render and re-render a book multiple times, while only changing parts of the input: some of the prose or CSS rules. layout_book rebuilds every stage on every call. This is useful for a program like the CLI that renders and outputs the book once, but a live preview benefits from a session that keeps state in between renders.

A Session keeps the output of each stage and works out which stages an edit invalidates.

use fleuron::session::Session;
use fleuron::style::{Source, Stylesheets};
let mut session = Session::new(&registry);
session.set_content(book);
session.set_style(Stylesheets::parse(&[Source::author("book.css", &css)]));
let output = session.preview(); // the display structure
let bytes = session.export()?; // the same run, as PDF

preview and export are two painters over one set of stages, so an export cannot contradict the preview it came from.

A writer that reads the book rather than the pages, such as the EPUB writer, reads the inputs of the session instead. book, sheets, images, and font_files return what the host set and sent. Reading them runs no stage.

changedeepest surviving cachewhat runs
a property the engine models nothing ofthe display structurenothing
margin box content, page countersthe page boxesthe furniture
@page geometry, counters, named pagesthe linesfragmentation, then the furniture
face, size, line width, leadingthe style treeline breaking, and everything under it
one file’s contentevery other section’s linesthat file’s sections, then fragmentation
the whole booknothingall of it

Session::stages() reports how many times each stage has run, so a host or a test can see what an edit cost without timing it.

If the references in a book print page numbers, the engine lays out the book twice. Stages::settle counts the second layouts. In a second layout, the engine breaks lines again only in a section whose printed page numbers changed. See the CSS subset for how a reference prints a page number.

set_content replaces the book. replace_source(name, sections) replaces every section that came from one file. One markdown file may split into several sections, and they all go together. A name the book does not already have appends instead.

use fleuron_markdown::{Options, to_sections};
let reading = Options::default();
let text = std::fs::read_to_string("ch03.md")?;
let (sections, warnings) = to_sections(&text, "ch03.md", &reading);
session.replace_source("ch03.md", sections);
let output = session.preview();

Nothing re-reads the files that did not change. fleuron_markdown::Cache stores each source’s sections against its name and a hash of its bytes.

set_source_attributes(name, attributes) gives every section from one file the classes and the id that a sheet reaches it by, such as section.front. The text does not change, so no node moves in the source. The following example names a preface:

use fleuron::content::Attributes;
session.set_source_attributes(
"preface.md",
&Attributes { id: Some("preface".into()), classes: vec!["front".into()] },
);

Node ids belong to the engine. The tree is renumbered on the way in, so sections built by hand need no ids of their own, and nothing downstream is keyed on an id that renumbering will move.

A content edit re-breaks only the sections it changed. The rest keep their lines, and the whole book is fragmented from the top. Page assembly then resolves counters, recto opens, running heads and blank pages.

A style editor completes a selector from the names that the book already uses. Session::names(Some(source)) answers with the classes and the ids that the blocks and inlines of one source carry. Session::names(None) answers for the whole book.

The answer is a Names: two sorted lists, classes and ids. Each name is in its list once, without the . or the #.

The answer comes from the content tree that the engine read. A brace run that stayed prose names nothing. Under Dialect::common_mark(), attributes are off, so the source names nothing. The classes and the id of a section are not in the answer, because the frontmatter or set_source_attributes sets them.

The following example reads a chapter and gets the names that it writes:

let text = "# Chapter One {.opening #ch1}\n\n{.epigraph}\n> It is a truth universally acknowledged.\n";
let (sections, _) = to_sections(text, "ch01.md", &Options::default());
session.replace_source("ch01.md", sections);
let names = session.names(Some("ch01.md"));
assert_eq!(names.classes, ["epigraph", "opening"]);
assert_eq!(names.ids, ["ch1"]);

In the npm package, Client.names(source) asks the same question of the worker. If you leave out source, the answer is about the whole book.

Session::folios(nodes) answers where each node’s content is set, one answer per node, in the order asked about.

A new face repaginates the book. The chapter that opened on page 41 opens on 38, and a reader who was looking at page 41 is now looking at words that were somewhere else a moment ago. This is the question a host asks to put the reader back, to turn to a chapter, or to name the chapter on screen.

An answer names four numbers, because what is printed on a page and where that page falls in the book are two different things. counter-reset: page restarts the count, so page 1 of a chapter can be the fortieth page of the book.

first, lastThe folios the content runs between, as printed. This is what a host puts on screen.
at, countWhere those pages fall in the book, counting from 0. These are the numbers a host fetches the pages by.

The following example prints the page range of every chapter of a book:

let chapters: Vec<NodeId> = session.book().sections.iter().map(|section| section.id).collect();
for (chapter, folios) in chapters.iter().zip(session.folios(&chapters)) {
if let Some(folios) = folios {
println!("node {} runs from page {} to {}", chapter.get(), folios.first, folios.last);
}
}

A node covers itself and everything under it. A heading’s text is a node inside the heading, so the heading answers with the page that text is on, and a chapter answers with the pages it runs across.

The answer is nothing for a node the book does not hold, for one the engine synthesized, and for one whose content reaches no page.

The answer is a walk over the pages the session already holds. Asking runs a stage only when an edit has left one to run.

What styled a node, and what is at a point

Section titled “What styled a node, and what is at a point”

Session::inspect(node) answers what styled one node and where it is on the pages. A text node answers for the element that holds it. A pseudo-element, such as a drop cap, answers for itself. The answer is an Inspection:

element, id, classesThe element, and the names a selector reaches it by.
element_nodeThe element, by its id. For a pseudo-element, the element it belongs to. Nothing for a margin box.
pseudo_elementFor a pseudo-element, its name, such as ::first-letter. element is then the element that it belongs to.
ancestorsThe elements it sits inside, the book first.
rulesThe rules that matched, in cascade order. Each rule gives its sheet, line, column, selector, and specificity.
computedThe computed value of every property in the CSS subset, written as CSS, with lengths in points.
boxesThe border box on each page the element reaches, in points. A block that continues onto a second page has two boxes.

applied is true on each declaration that won the cascade. A shorthand such as margin counts as won where any of its longhands won. The answer comes from the same cascade that styled the book, so a host does not match selectors or compute styles itself.

Session::inspect_margin_box(page, which) answers the same for one margin box of one page, such as @top-left. page counts from 0. The answer names the page selector that the page matches, such as @page chapter:left. Its rules are the @page rules that name the box.

Session::hit(page, x, y) answers which element is at a point on a page, in points from the top-left corner. Where text is at the point, the answer is the element that holds the text. Elsewhere, the answer is the innermost block whose border box holds the point, so padding and empty space count. Where the point is on a pseudo-element, the answer is the id of that pseudo-element. The pseudo-elements are a drop cap, the first line of a paragraph, and the box or text of ::before or ::after. See Ids of pseudo-elements in the display structure.

The following example finds the element at a point and prints the rules that declare its color:

if let Some(node) = session.hit(0, 200.0, 300.0) {
let inspection = session.inspect(node).expect("the book holds the node at a point");
for rule in &inspection.rules {
for declaration in rule.declarations.iter().filter(|d| d.property == "color") {
println!(
"{}:{}:{} {} {{ color: {} }} won: {}",
rule.sheet, rule.line, rule.column, rule.selector, declaration.value, declaration.applied
);
}
}
}

The answer is nothing for a node the book does not hold, and for one the engine synthesized. A point outside every box answers nothing, and so does a page the book does not have. A node id names a node only until the next edit, so ask about an id from the pages the reader is looking at.

Breaks, shaped glyph runs and advance widths, and no coordinates at all. Where a line breaks depends on the line width, the font and the text. Which page it lands on and at what baseline is determined by fragmentation, and fragmentation runs every time. A chapter that an edit above it pushed onto a different page paints at new coordinates with the same breaks.

The session checks two preconditions to determine this.

The first is a single line width. Masters with different line widths make where a line breaks depend on which page it lands on, and that depends on everything before it. Asymmetric @page :left and @page :right margins are this case, so is a named master set narrower, and so is one that divides its content box into a different number of columns. Mirrored margins are not: the built-in sheet mirrors the spine margin across the spread and both sides come to the same line width. The lines of a block with column-span: all break to the width of the whole content box. If a book has such a block, every master also needs the same content box width.

The second is that no prose depends on pagination. counter(page) and string() are legal only inside a margin box, so nothing in the text can depend on where the text fell. An index, or a table of contents with real page numbers, makes inline text depend on pagination and pagination on breaking, and that has no fixed point a cache can serve.

When either precondition fails, reuses_sections() becomes false and every edit re-breaks the whole book.