poka.sh

Docs

The output contract

explanation

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

  1. The host runs the parser, and pipes the parser's stdout (one *Ast) into the viewer's stdin.
  2. 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.
  3. It wraps the HTML in an HtmlPreview (a PreviewResponse of type TYPE_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-scripts and without allow-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' with connect-src 'none' and images allowed only from data: and blob:. The desktop frame loads the same kind of shell. The policy lives in poka-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: an asset:// URL on desktop, a blob: URL in the web app. poka-metadata-card-viewer uses it. One caution from the host source: a comment in poka-interfaces (desktop/src-tauri/src/asset_scope.rs) says that on desktop the sandboxed frame's policy allows no asset: source, so that URL does not load inside the frame. SPEC.md does 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's facts and 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's filename or file_size again.
  • poka-3d-viewer draws 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 How poka-3d-viewer draws 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.