poka.sh

Docs

Follow the host theme and escape file content

how-to

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>.

  1. Import it:

    use poka_viewer_common::{html_escape, THEME_CSS};
    
  2. 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>");
    
  3. 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 diff var(--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_escape replaces &, <, > 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 &amp; 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 style attribute 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.