Follow the host theme and escape file content
Two things every viewer's HTML must do: look right in light and dark mode, and
never let text from the file become markup. Both come from poka-viewer-common.
The reasons are on the output contract.
Follow light and dark mode
A viewer's HTML runs in a sandboxed iframe of its own, so the app's CSS does not
reach it. The palette is delivered as a string, THEME_CSS, which each viewer
puts into its own <style>.
Import it:
use poka_viewer_common::{html_escape, THEME_CSS};Put it first inside the
<style>element:let mut out = String::new(); out.push_str("<style>"); out.push_str(THEME_CSS); out.push_str(".my-box { background: var(--surface); border: 1px solid var(--border); }"); out.push_str("</style>");Write your own rules with the custom properties, not with colours:
var(--bg),var(--fg),var(--muted),var(--faint),var(--border),var(--line),var(--surface),var(--highlight), and for a diffvar(--add-bg),var(--add-fg),var(--rem-bg),var(--rem-fg).
THEME_CSS declares color-scheme: light dark, defines the light values in
:root, overrides them under @media (prefers-color-scheme: dark), and sets the
page background and foreground. The palette is plain black and white with the
host's grey steps. The authoritative list of tokens is theme.rs, which also has
a test that the text colours keep their contrast against the backgrounds.
Use colour only where it carries meaning (an added or removed line, a chart
series) or where the file supplies it (a spreadsheet cell's fill). A hard-coded
#fff page or color: black text is the usual cause of a viewer that is
unreadable in dark mode.
For a failure, do not write your own message block. Call
poka_viewer_common::error_html(viewer_name, message): it returns the theme and
a one-line message with both strings escaped.
Escape everything that comes from the file
Anything the file supplied is text, not markup: cell values, names, titles, authors, paths, the contents of a text document. Escape it at the point where you put it in the HTML.
out.push_str(&format!("<td>{}</td>", html_escape(cell)));
html_escapereplaces&,<,>and". It leaves single quotes alone, so never put its result inside a single-quoted attribute. Use double quotes.- Escape once. Escaping a string you already escaped shows
&on screen. - Numbers you format yourself, and text you wrote, need no escaping. When in doubt about where a string came from, escape it.
- A value that goes into a
styleattribute or a URL is a different context. Do not pass file text there at all. Parse it into a number or pick from a fixed set, then format that.
Check it
Add a test that feeds a hostile string through every field you render:
use poka_viewer_common::test_support::{assert_inert, HOSTILE};
#[test]
fn every_string_from_the_file_is_escaped() {
let ast = TextAst { filename: HOSTILE.into(), content: HOSTILE.into(), ..Default::default() };
assert_inert(&render(&ast));
}
assert_inert fails if the string did not reach the HTML at all, which catches a
field the test forgot to render, and if any of its markup came through live. The
full recipe is in Test a viewer.