Skip to content

Code Play#

View Markdown

This page uses @ox-content/code-play with JavaScript, TypeScript, Rust, and Go enabled. Other languages stay ordinary fences until a site opts them in. The standalone examples/code-play app uses the same Rust and Go playground adapters, and renders Python with an explicit remote executor when OX_CODE_PLAY_PYTHON_ENDPOINT is set. Routes without a play fence stay ordinary docs pages and do not load ox-code-play.js.

A copy-paste Vite app lives at examples/code-play in the repository.

Enable the plugin#

import { oxContent } from "@ox-content/vite-plugin";
import { codePlay } from "@ox-content/code-play";

export default {
  plugins: [
    oxContent({ highlight: true }),
    codePlay({
      languages: {
        javascript: true,
        typescript: { execute: true, typecheck: true },
        rust: true,
        go: true,
        python: { endpoint: "https://piston.example/api/v2/piston" },
      },
      ui: "default",
      viewers: { config: true, stdio: true, stderr: true, provenance: true, timing: true },
    }),
  ],
};

Live TypeScript sample#

The fence below is marked play. Use Run to execute it. Typecheck appears during vite dev (the /__ox-code-play/typecheck proxy) or when the site sets a reachable endpoints.typecheck. Published pages still run TypeScript by stripping types into the sandbox iframe. The stdio, stderr, config, provenance, and timing tabs are the same objects the headless API returns. console.warn lands in run.stderr.

const message: string = "hello from Code Play";
console.log(message);
console.warn("this warning is a stderr chunk");

Per-sample config#

play-<config-key>=... overrides the language config for one sample. This sample intentionally disables strict TypeScript checking while keeping the page-level defaults strict.

const label = "works without an explicit type annotation";
console.log(label.toUpperCase());

Live JavaScript sample#

function add(left, right) {
  return left + right;
}

console.log(add(2, 40));

Live Rust sample#

This sample uses the official Rust playground adapter. It slugifies headings from a small Markdown document and asserts the result before printing the navigation targets.

#[derive(Debug, PartialEq, Eq)]
struct Heading {
    level: usize,
    text: String,
    slug: String,
}

fn collect_headings(markdown: &str) -> Vec<Heading> {
    markdown
        .lines()
        .filter_map(|line| {
            let trimmed = line.trim_start();
            let level = trimmed.chars().take_while(|&ch| ch == '#').count();
            if level == 0
                || level > 6
                || !trimmed
                    .as_bytes()
                    .get(level)
                    .is_some_and(|byte| byte.is_ascii_whitespace())
            {
                return None;
            }
            let text = trimmed[level..].trim();
            Some(Heading {
                level,
                text: text.to_string(),
                slug: slugify(text),
            })
        })
        .collect()
}

fn slugify(text: &str) -> String {
    let mut slug = String::new();
    let mut pending_dash = false;

    for ch in text.chars().flat_map(char::to_lowercase) {
        if ch.is_ascii_alphanumeric() {
            if pending_dash && !slug.is_empty() {
                slug.push('-');
            }
            slug.push(ch);
            pending_dash = false;
        } else if ch.is_whitespace() || matches!(ch, '-' | '_' | ':' | '/') {
            pending_dash = true;
        }
    }

    if slug.is_empty() {
        "section".to_string()
    } else {
        slug
    }
}

fn main() {
    let markdown = "# Code Play\n\n## Rust Runner\n\n### Stdio & Timing";
    let headings = collect_headings(markdown);
    assert_eq!(
        headings.iter().map(|heading| heading.slug.as_str()).collect::<Vec<_>>(),
        ["code-play", "rust-runner", "stdio-timing"]
    );

    for heading in &headings {
        println!("h{} {} -> #{}", heading.level, heading.text, heading.slug);
    }
}

Live Go sample#

The Go sample uses the Go playground adapter with vet enabled. It counts fenced code blocks by language, sorts the result, and prints a small summary.

package main

import (
	"fmt"
	"sort"
	"strings"
)

type Fence struct {
	Language string
	Lines    int
}

func collectFences(markdown string) []Fence {
	var fences []Fence
	inFence := false
	current := Fence{Language: "text"}

	for _, line := range strings.Split(markdown, "\n") {
		if strings.HasPrefix(line, "```") {
			if inFence {
				fences = append(fences, current)
				inFence = false
				current = Fence{Language: "text"}
				continue
			}
			language := strings.TrimSpace(strings.TrimPrefix(line, "```"))
			if language == "" {
				language = "text"
			}
			current = Fence{Language: language}
			inFence = true
			continue
		}
		if inFence {
			current.Lines++
		}
	}

	return fences
}

func main() {
	markdown := strings.Join([]string{
		"# Samples",
		"",
		"```go",
		`fmt.Println("ok")`,
		"```",
		"",
		"```rust",
		`println!("ok");`,
		"```",
	}, "\n")

	fences := collectFences(markdown)
	sort.Slice(fences, func(i, j int) bool {
		return fences[i].Language < fences[j].Language
	})

	if len(fences) != 2 {
		panic("expected two fenced code blocks")
	}

	for _, fence := range fences {
		fmt.Printf("%s: %d line(s)\n", fence.Language, fence.Lines)
	}
}

Typecheck failure#

During vite dev, Typecheck should fail on this sample. On a published page the button is omitted unless endpoints.typecheck is set. Run still executes after types are stripped, so execute and type-check stay separate.

const n: number = "not a number";
console.log(n);

Runtime error#

throw becomes a diagnostic and a stderr chunk. The stderr tab opens when the run produces stderr or an error diagnostic.

console.log("before");
throw new Error("boom from the example");

Headless usage#

import { createCodePlay } from "@ox-content/code-play";

const play = createCodePlay({ languages: { typescript: true } });
const session = play.createSession({
  language: "ts",
  code: "const n: number = 1;",
});

const result = await session.run();
result.stdio;
result.stdout;
result.stderr;
result.provenance.compile;
result.provenance.execute;
result.timing.phases;

RunActionState helpers model idle, running, result, error, and offline states for custom UIs. Transport/CORS failures return status: "offline". ui: "compact" hides the tab list and keeps stdio plus stderr. ui: "headless" renders no chrome — use createCodePlay() from your own UI.

Remote languages#

Rust and Go use typed playground adapters. During vite dev, their browser payloads use the Vite dev proxy by default; production builds embed endpoints.rust and endpoints.go. Python uses the generic remote adapter and needs a Piston-compatible languages.python.endpoint.

```python play play-title="Python via Piston"
print("ok")
```

If Python is enabled without an endpoint, Run reports status: "unsupported" and explains that a configured HTTP executor is required. Transport and CORS failures report status: "offline".

See @ox-content/code-play and the roadmap.

Last updated: