Interactive Cells
Interactive cells are ordinary MDX code blocks with metadata comments at the top. Oxiquill extracts those blocks during generation and replaces them with a runtime UI.
Rust cells are compiled to WebAssembly at build time. Python cells run in a browser Pyodide worker. Haskell cells are compiled to a WASI WebAssembly module at build time. All three languages use the same input metadata and run-mode model.
Reruns retain the last successful output, with an error and retry action if execution fails. Cancellation and invalid input also keep the output. Compatible charts update the same canvas with a default 180ms transition, respecting OS reduced-motion preferences. See rich output for the exact style schema, limits, and Rust overloads. Explicit artifact IDs are recommended for dynamically changing output sequences.
Fence and Metadata Grammar
Section titled “Fence and Metadata Grammar”Interactive fences use rust, python, or haskell (including Markdown’s {.rust} form) and may use legal backtick or tilde fences and indentation. Metadata is one contiguous block of language-appropriate option comments at the beginning of the fence: //| or ///| for Rust, #| for Python, and --| for Haskell. An option-looking comment after the first source line remains source code.
Only these top-level fields are accepted; unknown fields are errors:
| Field | Type and rule | Default |
|---|---|---|
id | Required lowercase kebab-case matching ^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$; unique within the page and after route scoping. | — |
title | Non-empty string shown in the cell header; explicit values are trimmed. | id |
run | button, reactive, or autorun. | button |
inputs | Mapping from lowercase cross-language identifiers matching ^[a-z][a-z0-9_]*$ to input specs. Not allowed with autorun. | {} |
packages | Unique non-empty Pyodide package names; Python only. | [] |
crates | Unique non-empty helper package names from direct children of cratesDir; Rust only. | [] |
timeoutMs | Integer milliseconds from 1 through 2147483647, inclusive. | 30000 |
showSource | Boolean controlling initial source visibility. | true |
The source must contain non-metadata code. Malformed YAML, a wrong scalar/collection type, duplicate normalized ID/binding/function, unknown package/crate, or language-mismatched dependency field fails before output is written. Diagnostics include page path, fence start line, cell ID when available, and the exact field path.
Haskell cells use standard-library code only and accept neither packages nor crates. Rust cells without helpers should explicitly use crates: [] in examples. Package and crate lists are deduplicated and sorted after strict validation.
Run Modes
Section titled “Run Modes”button: show inputs and a Run button; do not execute on mount.reactive: show inputs, hide the Run button, execute once on mount, then use a 150 ms trailing debounce. At most one execution and one newest replacement are retained.autorun: show neither inputs nor Run button and execute exactly once for eachcell.id + runtimeVersion + source. Source updates during development invalidate the previous execution even if the runtime-version update arrives first.
This button cell waits for an explicit run even when its input changes.
Rust + Wasm
Explicit button execution
This autorun cell has no execution controls and runs once for the current generated runtime.
Rust + Wasm
One-time autorun execution
println!("autorun ready");Waiting for the runtime to start.
Superseding a reactive run actively cancels its worker request, rejects every request affected by the worker recycle, clears their timers, and starts the latest complete values after the debounce. Cancellation is not shown as an execution error, and stale results cannot replace newer state. Timeout and worker-fault recovery use the same deterministic recycle boundary. Unmounting cancels pending and active work.
Input Schema
Section titled “Input Schema”Every input accepts only fields relevant to its type. Common fields are type, label, description, and value; numeric inputs also use min, max, step, and integer, while select and radio use options.
| Type | Value and constraints |
|---|---|
range, number | Finite number; step > 0, min <= max, and the default must be within bounds and on the step grid. |
integer | Signed 32-bit integer from -2147483648 through 2147483647; bounds and step use the same domain. |
text, textarea | String value. |
checkbox | Boolean value. |
select, radio | Non-empty unique string options or { label, value } objects; the default must equal an option value. |
type defaults to text, and label defaults to the input name. description is an optional non-empty string associated with the control for assistive technology. Default values are false for checkbox, 0 for numeric inputs, and an empty string for text inputs. Numeric-only fields are rejected on non-numeric controls, and options is rejected outside select and radio.
The signed 32-bit integer domain is exactly representable by JavaScript and maps consistently to Rust i32, Haskell Int, and Python int. Authoring rejects an integer value, min, max, or step outside that domain. Browser controls apply the same domain before constructing a worker request, including for integer: true numeric inputs.
Numeric defaults must align with the same step grid enforced by browser controls. The effective step is step when provided and 1 otherwise. The grid starts at min when provided; without min, the declared or default numeric value is the base. Oxiquill rejects a misaligned default during authoring instead of rounding or clamping it.
Number and integer controls keep their raw edit text separate from committed runtime values. Empty, incomplete, non-finite, out-of-range, and step-mismatched text remains visible but cannot update cell inputs. The Run button is disabled while any control is invalid, and reactive execution resumes exactly once with the latest complete value set after validity returns. Range labels derive their displayed precision from step.
The visible label is the control’s accessible name; the input key is only the generated language binding. Normalization must not make two Rust or Haskell bindings collide. Labels, descriptions, current values, validation messages, and grouped controls use stable IDs, keyboard focus remains visible, and run/error status is announced without stealing focus.
Rust and Plots
Section titled “Rust and Plots”This Rust cell uses a helper crate and redraws a plot whenever the sliders change.
Rust + Wasm
Calculate the logistic map with Rust
let steps = u32::try_from(steps).map_err(|_| "steps must be non-negative".to_owned())?;
let points = doc_rust::logistic_series(r, x0, steps).map_err(|error| error.to_string())?;
for point in points.iter().take(5) {
println!("n={} x={:.6}", point.n, point.x);
}
emit_line_plot!(&points, "n", "x");Waiting for the runtime to start.
Rust Input UI
Section titled “Rust Input UI”A cell with crates: [] does not depend on helper crates. This example shows checkbox, select, and radio inputs.
Rust + Wasm
Rust input UI
let base_score = match operation.as_str() {
"triple" => 21_i32,
"double" => 14_i32,
_ => 7_i32,
};
let score = if include_bonus {
base_score + 5
} else {
base_score
};
println!("style = {style}");
println!("score = {score}");Waiting for the runtime to start.
Multiple Rust Helper Crates
Section titled “Multiple Rust Helper Crates”Each Rust cell declares the helper crates it needs. The names must match Cargo package names under crates/*.
Rust + Wasm
Rust cell with multiple crates
let points = doc_rust::logistic_series(3.2, 0.2, 8).map_err(|error| error.to_string())?;
let final_point = points.last().ok_or_else(|| "series is empty".to_owned())?;
println!("{}", doc_rust_text::labeled_value("final step", final_point.n));
println!("{}", doc_rust_text::labeled_value("final x", format!("{:.6}", final_point.x)));
println!("{}", doc_rust_text::style_note(style.as_str()));Waiting for the runtime to start.
Python Inputs
Section titled “Python Inputs”Python cells use the same input metadata. Input values are available as Python variables.
Python + Pyodide
Python input UI
values = [1, 2, 3, 4]
if method == "sum":
result = sum(values) * scale
elif method == "max":
result = max(values) * scale
else:
result = sum(values) / len(values) * scale
name = label.upper() if enabled else label
print(f"{name}: {method} = {result}")
print(f"style = {style}")Waiting for the runtime to start.
Haskell Inputs
Section titled “Haskell Inputs”Haskell cells use --| metadata comments. Input values become Haskell variables, and text output written with putStrLn or print appears in the result panel.
Haskell + WASI
Haskell input UI
{- Imports may use normal multiline Haskell layout. -}
import Data.List
( intercalate
, sort
)
let base = [1 .. 4 :: Int]
let scaled = map (* factor) base
let displayed = sort (if include_squares then map (\value -> value * value) scaled else scaled)
putStrLn (label ++ ": " ++ intercalate ", " (map show displayed))
putStrLn ("total = " ++ show (sum displayed))Waiting for the runtime to start.
Python cells show Preparing Python… during initialization and package loading, then Running while authored code executes. See Python Runtime Assets for optional page preparation with python.preload.