Skip to content
API

The wire

The display structure crosses as postcard: a non-self-describing, varint-packed format. Field names do not travel, small integers cost one byte, and there is no parse step that allocates a tree of maps before the first page can be read. Pride and Prejudice, 333 pages and 9,667 lines, encodes to about 6 MB.

Inputs are a different contract. The content tree is a Rust type a frontend constructs, and it serializes to JSON, which is also how a host hands one over with the content op.

A version leads the encoding, and a host reads it before anything else. The encoding is positional: a reader walks fields in the order they were written and cannot detect a change to that order. So a module and a host that disagree about the shape of the display structure have to fail at the first byte. decodeDisplayList rejects an unknown version, and wireVersion() is what the module writes.

WIRE_VERSION is the shape of what crosses, and it moves whenever either the ops or the display structure changes shape. What it refuses is a display structure it cannot read; an op in a shape the module no longer takes is answered on the error channel. VERSION is the release the package was published at, and it is the one to quote in a bug report or to pin in a host’s manifest; the two move independently.

In: whatever changed. Markdown source, stylesheets, font bytes, a content tree. Inputs are ops on a session the module keeps, not a book re-sent per frame. The engine opens nothing, so a face that has not crossed cannot be used.

Out: one transferable ArrayBuffer: the postcard-encoded display structure, PDF bytes on the export path, or an EPUB with its warnings. Transferred rather than copied.

A preview reply need not be the whole book. first and count on the request ask for count pages starting at first, counting from 0, and the reply’s pages is that slice: first says where it begins and bookPages says how many pages the book has, so a reply with one page still answers that. fonts, assets and warnings ride every reply whole, since none of them is per page. Leaving first and count out asks for the whole book.

The display structure reference has the full shape. Four things about it matter to a host in particular.

Coordinates are points, origin top-left. Every painter, SVG, canvas or PDF, consumes the same numbers. A preview that disagrees with the export about where a glyph goes has a bug in the painter, not in the engine.

Faces include their instance. The font table records where on its file’s axes each face sits. A variable file names several styles and they are one file, so a painter that does not pin the axes draws the default weight for every one of them.

Glyphs are tied to their text. Each text run has the string it was shaped from, and each glyph a byte range into it. Only the shaper knew the correspondence, so the display structure records it. A painter that supports selection or accessible text reads it through those ranges; a painter that only draws ignores it.

Runs are tied to the manuscript. Each text run names the content node it was shaped from and the bytes of that node it stands for, so a host maps a cursor in the manuscript onto a page, and a click on a page back onto the manuscript. Text the engine wrote itself, a page number or a running head, names no node. A node id is the engine’s own name for a place in the book, and the two questions below turn it into a file and a byte of one.

Layout is deterministic and the wire is positional, so the display structure a worker produces is the one a native run produces, byte for byte.

PDF bytes are not identical across builds, though the PDF is the same book, of the same length, with the same pages and the same text: two builds can number the same two font objects the other way round. One build renders one book to one file every time, so a digest taken to pin the output down is taken of the display structure rather than of the PDF.

A request is an edit, a render, a question, or an edit and one of those:

{ id, generation, ops: [{ op: 'style', sheets: [{ name, css }] }], want: 'preview' }

The style op takes the author’s sheets in cascade order, each under a name. A warning names the sheet its declaration was written in, as preset.css:12:3, so a host that builds its styling out of layers sends the layers.

Request and response are paired by id, and each request has a generation the worker echoes back untouched.

Some requests are questions rather than renders, and none overtakes a render or is overtaken by one. want: 'font' is one: the file a font_id was registered from, for a painter that has to draw with the bytes the engine shaped with. A face that the host registers without a url keeps its id for the session’s life. A face that a @font-face rule declares can take a new id when the sheet or its file changes, so a painter asks with an id from the latest font table.

A want: 'preview' naming first and count with no ops of its own is another: it asks what a page of the book already is rather than what an edit produced. An edit that also names a range, fetching the one page it changed rather than the whole book, is still a render for supersession’s sake, since it does say what that edit produced.

Two more are about the manuscript rather than the page. want: 'node', with source and byte, answers with the node that byte of that file was read into, which is the node the display structure’s runs name and a cursor’s first step onto a page. want: 'source', with node, answers with { source, start, end }: the file the node was read from and the bytes of it, which is the way back from a run under the pointer. Both answer in JSON, the same contract the content tree crosses in, and both answer null where nothing was read: a blank line between chapters, a node the engine synthesized, a tree the host built rather than parsed. Client.nodeAt and Client.sourceOf are the two on the host’s side.

want: 'names' is also about the manuscript. With source, it answers with { classes, ids }: the classes and the ids that the blocks and inlines of that file carry. Without source, the answer is about the whole book. A style editor completes a selector from this answer. The answer comes from the content tree that the engine read, so a brace run that stayed prose names nothing. Client.names is the host’s side, and sessions describes the answer in full.

The last is about where a node ended up. want: 'folios', with nodes, answers where those nodes’ content is set: one answer per node, in the order asked about. A new face repaginates the book, and the chapter that opened on page 41 opens on 38. A host that puts the reader back where they were, that turns to a chapter, or that names the chapter on screen asks this question. A node covers itself and everything under it, so a heading answers with the page its own text is on, and a chapter with the pages it runs across. The answer is null for a node the book does not hold, for one the engine synthesized, and for one whose content reaches no page. No page crosses the wall to answer it. Client.foliosOf is the host’s side.

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. { first: at, count } is a want: 'preview' range over every page the content is on.

Two more are about what styled the page. want: 'inspect', with node, answers with what styled that node. The answer names the element, the rules that matched in cascade order, and the declarations that won. It also gives the computed value of every property and the border box on each page the element reaches. With page and box in place of node, the answer is about one margin box of that page, such as bottom-center.

want: 'hit', with page, x, and y, answers with the innermost element at that point on the page, or null outside every box. A page counts from 0, and a point is in points from the top-left corner of the page. Both questions answer in JSON, from the same cascade that styled the book, so a host matches no selectors of its own. A node id names a node only until the next edit. Client.inspect, Client.inspectMarginBox, and Client.hit are the host’s side, and sessions describes the answer in full.

want: 'epub' is a render, as want: 'pdf' is. It answers with the book that the session holds as a reflowable EPUB, and runs no layout stage. The reply is postcard behind the same version as a display structure: the warnings, then the file. decodeEpub reads it, and Client.exportEpub is the host’s side. EPUB describes what the EPUB holds. With unzipped: true, the reply is the files of the EPUB instead of the zip. It holds the warnings, the spine, then each file with its path, its media type, and its bytes. Each entry of the spine has the path of a document and the node id of its section, which is the id that want: 'source' takes. decodeEpubFiles reads it, and Client.exportEpubFiles is the host’s side. See The files one by one and A place in the book.

The host raises the generation whenever the input goes stale, at a keystroke in a stylesheet or a new manuscript. A response whose generation is behind the current one is dropped without painting.

The worker lets everything already sent arrive before it renders anything. Ops are applied in the order they arrived. Only the newest render in the batch runs, and the ones it overtook come back as superseded. A render the reader typed past costs nothing, and the render that follows it is the same as if nobody had typed.

That is also what makes it cache-safe. A superseded render is one that never started, not one abandoned half-way through a stage, so no stage is left partly rebuilt for the next call to serve.

A request the engine cannot apply, font bytes that are not a font, a content tree that will not parse: each replies with an error, and the session continues rendering.

A warning is different. A book that laid out anyway reports through the display structure’s own warnings, which is the whole run’s, the frontend’s included. An EPUB reply carries warnings of the same shape: the frontend’s, then the writer’s.

The host fetches the fonts, caches them, and decides when a face has changed. The engine registers what it is handed and warns about what it is not. A @font-face url is a name, and the host sends the bytes under that name.

The host fetches each image file and hands the bytes over. The engine reads the header for the intrinsic size and decodes nothing, so the painter decodes the pixels.

The host starts the worker. Layout runs there.

The host checks the version tag and refuses a mismatch at the first byte.