The output contract
A viewer's whole output is one stream of UTF-8 HTML on stdout. This page
explains what the host does with it and why the rules are as strict as they are.
The rules themselves are in section 4 of SPEC.md; where this page and the spec
differ, the spec wins.
What the host does with the bytes
- The host runs the parser, and pipes the parser's stdout (one
*Ast) into the viewer's stdin. - It reads the viewer's stdout as UTF-8. Output that is not valid UTF-8 is an error. Empty output is an error in the web engine.
- It wraps the HTML in an
HtmlPreview(aPreviewResponseof typeTYPE_HTML) and hands it to the front end, which shows it in a frame.
The same HTML is shown on desktop, web and mobile. That is the point of the contract: a viewer is written once and every surface renders it the same way. A viewer does not return a protobuf message and does not choose a surface.
Why the HTML is sandboxed
The HTML comes from a file the user did not write. A preview tool opens files people were sent, so the HTML has to be treated as untrusted even though a viewer produced it: a viewer prints text out of the file, and text out of a file can be anything.
The frames are built to contain that:
- The frame is a sandboxed iframe with
allow-scriptsand withoutallow-same-origin, so it gets an opaque origin and cannot reach the host page. Scripts run, because some viewers (the ebook viewer, for one) need them. - The frame's own content security policy forbids the network. In the web app it
is
default-src 'none'withconnect-src 'none'and images allowed only fromdata:andblob:. The desktop frame loads the same kind of shell. The policy lives inpoka-interfaces(web/src/preview-frame.html,desktop/src-tauri/src/preview_frame.rs), not here, so check it there if the details matter to you.
Three rules follow, and they are the same three that AGENTS.md lists as golden
rules.
Escape everything that comes from the file. A cell, a filename, a document
title: put it through poka_viewer_common::html_escape before it goes into the
HTML. Escaping is the viewers' main security boundary. The sandbox limits the
damage of a mistake; it is not a licence to make one. html_escape covers
&, <, > and ", so never place its result inside a single-quoted
attribute.
Assume no network. A stylesheet link, a web font, an <img src="https://...">
simply does not load. Embed what you draw: SVG inline, and raster images as
base64 data: URLs, or, for raw RGBA pixels a parser decoded
(ImageWithMetaAst.image_format "rgba"), as base64 text that a small inline
script paints on a <canvas>, so no browser image codec is used
(poka-image-with-meta-viewer, poka-parsers#451). This also means a preview
never contacts a server because a file asked it to.
Never panic. A module that exceeds memory_mb or max_time_ms, or that
traps, is killed and reported as a failure. A viewer that cannot decode its input
prints a short HTML error (poka_viewer_common::error_html) and exits normally,
so the user still gets a preview frame with a reason in it.
Three behaviours around the contract
{{POKA_ASSET_URL}}. If the output contains this placeholder, the host replaces it with a URL for the original file: anasset://URL on desktop, ablob:URL in the web app.poka-metadata-card-vieweruses it. One caution from the host source: a comment inpoka-interfaces(desktop/src-tauri/src/asset_scope.rs) says that on desktop the sandboxed frame's policy allows noasset:source, so that URL does not load inside the frame.SPEC.mddoes not mention this. Do not build a viewer that depends on the placeholder loading on desktop without checking.- The information card. Every viewer draws one shared card
(
poka_viewer_common::card, poka-viewers#98): a header with the file's name, format, size and dates, the format's own sections, the parser'sfactsand integrity notes, and a footer with the handlers that ran, the result, the parse time and the Poka version. A viewer's stdin is only the*Ast, so the header and footer are{{POKA_…}}placeholders that the host fills in, HTML-escaped, the way it fills{{POKA_ASSET_URL}}; a row whose value comes out empty is hidden by CSS. The host draws the same card itself for the results no viewer renders. Because the header shows the name and size, a viewer must not print the Ast'sfilenameorfile_sizeagain. poka-3d-viewerdraws with a canvas. Its HTML carries the vertex buffers the wasm built and a small WebGL presenter; the hosts still special-case it until they run it like the others. See Howpoka-3d-viewerdraws a model.
When a viewer is not reached
A parser may answer with a complete PreviewResponse instead of an *Ast: an
error, plain text, or several tabs (TYPE_MULTIVIEW, whose viewer_input items
are one *Ast each, run through the viewer in tab order). That answer never
reaches a viewer; the host recognises it before composition. A viewer therefore
only ever sees an *Ast, and never needs to handle a parser's error response.