example

<tosi-example> makes it easy to insert interactive code examples in a web page. It started life as a super lightweight, easier-to-embed implementation of b8rjs's fiddle component—which I dearly missed—but now the student is, by far, the master. And it's still super lightweight.

You're probably looking at it right now.

// this code executes in an async function body
// it has tosijs, tosijsui, and preview (the preview div) available as local variables
import { div } from 'tosijs'.elements
preview.append(div({class: 'example'}, 'fiddle de dee!'))
preview.append('Try editing some code and hitting refresh…')
<h2>Example</h2>
.preview {
  padding: 0 var(--spacing);
}

.example {
  animation: throb ease-in-out 1s infinite alternate;
}

@keyframes throb {
  from { color: blue }
  to { color: red }
}

How examples run: one shared page (read this)

By default every example on a page runs inline — in the page's own document and JavaScript realm, not in a sandbox. Each example gets its own preview element (and its css is scoped to that preview), but they all share the one tosijs module, so tosi() state singletons are shared across every example on the page.

This is a deliberate choice, and it buys two things:

  1. It's a live demonstration of how clean tosi's isolation is — many independent components mount into one page and coexist without stepping on each other's DOM.
  2. You can drive any demo from the console (or from another example) through the same global singleton — the state is right there, not walled off in a frame.

The trade-off is the gotcha to know about: examples can see and clobber each other's state. Two examples that both do tosi({ app: … }) bind to the same app singleton (the tosi registry is keyed by the top-level name), so one overwrites the other — and you get "impossible" bugs where an example misbehaves only when some other example happens to be on the same page. Worth it, but plan for it:

For real isolation of the DOM and CSS, add the iframe attribute (below). Note it does not isolate tosijs state: the tosijs/tosijs-ui handed to an iframe example are still the host page's module instances, so tosi() singletons stay shared. Namespacing is the fix for state; iframe is the fix for DOM/CSS bleed.

Source dialects: js, tjs, ts

The executable block's fence language picks how the source is compiled before it runs, via tjs-lang:

A tjs block — note the type annotation (which a plain js block couldn't run) and the inline test '…' { … } unit test. Open the code panel: the source tab is labeled tjs, with read-only JS (the compiled output) and tjs tests (the inline-test results) tabs, alongside the DOM tests tab.

import { div } from 'tosijs'.elements

function badge(label: string, n: number) {
  return `${label}: ${n}`
}

test 'badge formats label and number' {
  expect(badge('count', 42)).toBe('count: 42')
}

preview.append(div({ class: 'badge' }, badge('count', 42)))
test('tjs example transpiled and ran', () => {
  const badge = preview.querySelector('.badge')
  expect(badge).not.toBe(null)
  expect(badge.textContent).toBe('count: 42')
})

The inline test above is tjs's native test '…' { … } syntax. In a TypeScript example the equivalent is written inside a comment (so it survives tsc) — but that comment form can't appear inside a doc comment like this one, since the test's own closing delimiter would end the doc comment.

The console

Each example has a Console tab beside its code (open the code panel with the <> button). It shows what the example logged, and it is a REPL: type an expression and press Enter to evaluate it in the example's scope, as in the browser's console. On a phone, where browsers have no console at all, this is the console.

Each example gets its own console, injected the way preview is, so its output is never mixed with another example's. Everything still reaches the browser's console too.

const words = ['tosijs', 'tjs', 'xinjs']
console.log('words:', words)
for (const word of words) console.log(word, word.length)
console.warn('xinjs is the old name')
preview.textContent = `${words.length} words logged`
test('the console holds what the example logged', () => {
  const example = preview.closest('tosi-example')
  const lines = example.consoleOutput
  expect(lines.length).toBe(5)
  expect(lines[1].text).toBe('tosijs 6')
  expect(lines[4].level).toBe('warn')
})
test('and evaluates in its scope', async () => {
  const example = preview.closest('tosi-example')
  expect(await example.consoleEval('preview.textContent')).toBe('3 words logged')
  expect(await example.consoleEval('words.length')).toBe(3) // its own variables
})

An example that only logs still shows something. When a run finishes with nothing rendered into its preview, the lines it logged appear where the preview would be:

const total = [1, 2, 3].reduce((sum, n) => sum + n, 0)
console.log('1 + 2 + 3 =', total)

That is the same output as the Console tab, placed where a reader will see it without opening the code panel. It steps aside as soon as the example renders anything, and it is drawn in an element of its own, so preview stays empty for your code and tests. Turning the console off (below) turns this off too. It applies to inline examples; an :iframe example's output stays in its Console tab.

Opening with the code showing

An example normally opens as its preview, and the <> button opens the code full-screen. A page that is teaching wants the code and its effect in view together, in the page. Ask for that with a fence option on the example's source block, written after the language: {"view": "code"} puts the preview beside the code, and {"view": "console"} puts the console beside the code, where the preview would be.

"code" is for "edit this and watch it change":

import { elements } from 'tosijs'

const { button } = elements
let clicks = 0
preview.append(
  button('clicked 0 times', {
    onClick(event) {
      event.target.textContent = `clicked ${++clicks} times`
    },
  })
)
test('the button starts at zero', () => {
  expect(preview.querySelector('button').textContent).toBe('clicked 0 times')
})

That example has a test block, and its result is the line under the preview. In an inline view the tests are part of the lesson, so:

"console" is for "type a line, see the answer", where the point is a value and not a rendering. Try total * 2 at the prompt:

const total = [1, 2, 3].reduce((sum, n) => sum + n, 0)
console.log('total is', total)

Adding a dialect

js, tjs and ts are the built-in entries of a dialect registry, and a site can add its own with registerDialect from tosijs-ui/live-example. A fence in a registered language then becomes a live example like any other: it groups with html, css and test blocks, honours :static and the site's example policy, and saves back to source.

A dialect gives exactly one of:

A tjs or ts example may be written as a module: export is removed before it runs (an example runs as a function body, where export is a syntax error). This needs tjs-lang 0.14.0-rc.4 or later, whose parser does the removing; with an older one an export fails as before. A dialect of your own strips its own.

Optional label names the source tab and editorMode picks the code editor's language. Optional docs(source, options) returns markdown for a Docs tab beside the code: the place for documentation generated from the source (in TJS the signature is the docs). It is called only when someone opens the code panel, and again as they edit. Replacing a built-in is allowed: registerDialect('tjs', { transform }) runs tjs examples through your own tjs-lang build instead of the pinned copy.

Register in your site's bundle entry, before the doc system starts: examples take their dialect when they are created, and until then a fence in the new language is plain code.

import { registerDialect } from 'tosijs-ui/live-example'

registerDialect('ajs', {
  label: 'AJS',
  run: async (source, { options, signal }) => myVm.run(source, { ...options, signal }),
})

Options. Any fence can carry a JSON object after its language — ```ajs {"fuel": 1000} — which arrives as the dialect's options. Malformed JSON is reported in the console with the example it belongs to, rather than silently ignored. (CommonMark unescapes backslashes in a fence's info string, so avoid \" inside values.)

On a tosijs-ui/site site, also list your dialects in the site config: dialects: ['ajs'], and any built-in you replace (dialects: ['ajs', 'tjs']). The build runs where your page's registrations don't, so this is how it learns them:

Here a tiny run dialect is registered and an example is created in it. The example in the reverse dialect is the one INSIDE this example's preview, so it has its own toolbar: open its code with its <> button (not this example's) to see its Docs tab beside its source.

import { registerDialect, liveExample } from 'tosijs-ui'

registerDialect('reverse', {
  label: 'Reverse',
  run: (source, { options }) =>
    (options.shout ? source.toUpperCase() : source).split('').reverse().join(''),
  docs: (source) => `## reverse\n\nReverses its source, so \`${source}\` becomes its mirror image.`,
})

const example = liveExample()
example.dialect = 'reverse'
example.options = { shout: true }
example.js = 'stressed'
preview.append(example)
await example.whenHydrated
await example.refresh()
test('a run dialect runs its source and reports the result', () => {
  const results = [...preview.querySelectorAll('.dialect-result')]
  expect(results.length).toBe(1)
  expect(results[0].textContent).toBe('DESSERTS')
})
test('its docs appear in a Docs tab once the code panel is open', async () => {
  const example = preview.querySelector('tosi-example')
  example.showCode()
  // showCode() goes full-screen: close it again whatever happens, or (on localhost, where
  // tests run on the page itself) the reader is left with a full-screen nested example
  const close = () => example.closeCode()
  const docs = await new Promise((resolve) => {
    const started = Date.now()
    const check = () => {
      const found = example.querySelector('.example-docs')
      if (found?.textContent.includes('mirror image')) resolve(found)
      else if (Date.now() - started > 5000) resolve(null)
      else setTimeout(check, 50)
    }
    check()
  })
  close()
  expect(docs).not.toBe(null)
  expect(docs.getAttribute('name')).toBe('Docs')
  expect(docs.textContent).toContain('stressed')
})

Inline WebAssembly (SIMD)

A tjs example can drop a hot loop into WebAssembly with a wasm { … } fallback { … } block — compiled to bytecode at transpile time, embedded as base64, and run in your browser. No Emscripten, no .wasm file, no build step, no server.

WASM earns its keep with SIMD: scalar WASM roughly ties the JS JIT, but a branchless f32x4 kernel does four values per instruction. Below, a field of a few thousand particles is held on a grid by springs and pushed aside by your pointer — every particle's position updated four at a time in WASM SIMD. Click the button to toggle WASM ↔ JavaScript and watch the per-step time — the same code, one path compiled to SIMD bytecode:

// One physics step for the whole field, 4 particles per iteration with f32x4 SIMD:
// a spring pulls each particle to its home cell, and an inverse-square push shoves
// it away from the pointer. Pure arithmetic — no per-lane branches (tjs's f32x4 has
// no compare/select yet), which is exactly what SIMD wants. `fallback { }` is the
// scalar-JS twin.
function step(! px: Float32Array, py: Float32Array, vx: Float32Array, vy: Float32Array, hx: Float32Array, hy: Float32Array, n: 0, tx: 0.0, ty: 0.0) {
  wasm {
    let txv = f32x4_splat(tx)
    let tyv = f32x4_splat(ty)
    let spring = f32x4_splat(0.03)
    let damp = f32x4_splat(0.86)
    let repel = f32x4_splat(1600.0)
    let soft = f32x4_splat(140.0)
    for (let i = 0; i < n; i = i + 4) {
      let off = i * 4
      let x = f32x4_load(px, off)
      let y = f32x4_load(py, off)
      let sx = f32x4_mul(f32x4_sub(f32x4_load(hx, off), x), spring)
      let sy = f32x4_mul(f32x4_sub(f32x4_load(hy, off), y), spring)
      let rx = f32x4_sub(x, txv)
      let ry = f32x4_sub(y, tyv)
      let push = f32x4_div(repel, f32x4_add(f32x4_add(f32x4_mul(rx, rx), f32x4_mul(ry, ry)), soft))
      let nvx = f32x4_mul(f32x4_add(f32x4_load(vx, off), f32x4_add(sx, f32x4_mul(rx, push))), damp)
      let nvy = f32x4_mul(f32x4_add(f32x4_load(vy, off), f32x4_add(sy, f32x4_mul(ry, push))), damp)
      f32x4_store(vx, off, nvx)
      f32x4_store(vy, off, nvy)
      f32x4_store(px, off, f32x4_add(x, nvx))
      f32x4_store(py, off, f32x4_add(y, nvy))
    }
  } fallback {
    for (let i = 0; i < n; i++) {
      const sx = (hx[i] - px[i]) * 0.03, sy = (hy[i] - py[i]) * 0.03
      const rx = px[i] - tx, ry = py[i] - ty
      const push = 1600.0 / (rx * rx + ry * ry + 140.0)
      const nvx = (vx[i] + sx + rx * push) * 0.86, nvy = (vy[i] + sy + ry * push) * 0.86
      vx[i] = nvx; vy[i] = nvy; px[i] += nvx; py[i] += nvy
    }
  }
}

// 100k particles, not 6k. At 6k each step took ~0.02ms — right at the resolution floor
// of performance.now(), so the readout was mostly measuring the timer. At this size both
// paths land in the hundreds of microseconds and the comparison actually means something.
const W = 480, H = 300, cols = 400, rows = 250, N = cols * rows

// Allocate the particle arrays INSIDE wasm memory. This is the whole ballgame: if a
// typed array's buffer isn't the wasm memory, every call copies it in and out again —
// six 6,000-float arrays per frame — and you end up timing memcpy, not SIMD. (With
// plain `new Float32Array(N)` this demo measured ~4x SLOWER than its own JS fallback.)
// `wasmBuffer` is a bump allocator handing out views into the shared wasm memory, so
// the wrapper passes a byte offset and copies nothing. Fall back gracefully if the
// wasm block didn't compile, so the JS twin still runs.
const f32 = (n) => globalThis.wasmBuffer ? wasmBuffer(Float32Array, n) : new Float32Array(n)
const px = f32(N), py = f32(N)
const vx = f32(N), vy = f32(N)
const hx = f32(N), hy = f32(N)
for (let i = 0; i < N; i++) {
  const c = i % cols, r = (i / cols) | 0
  hx[i] = (c + 0.5) * W / cols
  hy[i] = (r + 0.5) * H / rows
  px[i] = hx[i]
  py[i] = hy[i]
}

const canvas = document.createElement('canvas')
canvas.width = W
canvas.height = H
canvas.style.cssText = 'width:100%;max-width:480px;border-radius:8px;display:block;background:#0b0e14;touch-action:none;cursor:crosshair'
const ctx = canvas.getContext('2d')

// Plot straight into an ImageData buffer (one 32-bit store per particle) instead of
// 100k fillRect() calls — otherwise the DRAW would dominate and we'd be benchmarking
// canvas, not the kernel.
const img = ctx.createImageData(W, H)
const pix = new Uint32Array(img.data.buffer)
const BG = 0xff140e0b        // #0b0e14, little-endian ABGR
const TEAL = 0xffc5d14f      // #4fd1c5
const AMBER = 0xff55adf6     // #f6ad55

let tx = W / 2, ty = H / 2, idle = 0, frame = 0
canvas.addEventListener('pointermove', (e) => {
  const r = canvas.getBoundingClientRect()
  tx = (e.clientX - r.left) / r.width * W
  ty = (e.clientY - r.top) / r.height * H
  idle = 0
})

// tjs-lang 0.9.1+ gates every wasm call on `globalThis.__tjs_wasm_enabled`, so this
// flips the kernel between WASM and its JS twin without touching tjs internals.
// (Don't reach for `__tjs_wasm_0` — it's private AND index-keyed per transpile, so
// a second wasm example on the page registers the same name and you'd clobber it.)
let useWasm = true
const btn = document.createElement('button')
btn.style.cssText = 'margin:8px 0;padding:6px 12px;border-radius:6px;cursor:pointer'
const readout = document.createElement('p')
let acc = 0, frames = 0
function applyMode() {
  globalThis.__tjs_wasm_enabled = useWasm
  acc = 0; frames = 0 // don't average across a mode switch
  btn.textContent = useWasm ? '⚡ WebAssembly SIMD — click for JavaScript' : '🐢 JavaScript — click for WASM SIMD'
}
btn.onclick = () => { useWasm = !useWasm; applyMode() }

// Warm BOTH paths before timing anything. The JS twin never executes until you click
// over to it, so an un-warmed comparison times cold, un-JITed JavaScript and flatters
// WASM (it read ~2x too good). A warmed V8 does this kernel in ~0.024 ms/step; the
// zero-copy SIMD kernel does ~0.015 — a real but modest ~1.6x, which is the honest
// number this demo should show.
function warmUp() {
  for (const on of [false, true]) {
    globalThis.__tjs_wasm_enabled = on
    for (let i = 0; i < 150; i++) step(px, py, vx, vy, hx, hy, N, W / 2, H / 2)
  }
  // the warm-up perturbed the particles — put them back on the grid
  for (let i = 0; i < N; i++) { px[i] = hx[i]; py[i] = hy[i]; vx[i] = 0; vy[i] = 0 }
}
function loop() {
  if (!document.body.contains(canvas)) return // example removed → stop
  frame++
  idle++
  if (idle > 45) {
    tx = W / 2 + Math.cos(frame * 0.02) * W * 0.32
    ty = H / 2 + Math.sin(frame * 0.031) * H * 0.32
  }
  const t0 = performance.now()
  step(px, py, vx, vy, hx, hy, N, tx, ty)
  acc += performance.now() - t0
  frames++

  pix.fill(BG)
  const dot = useWasm ? TEAL : AMBER
  for (let i = 0; i < N; i++) {
    const x = px[i] | 0, y = py[i] | 0
    if (x >= 0 && x < W && y >= 0 && y < H) pix[y * W + x] = dot
  }
  ctx.putImageData(img, 0, 0)

  if (frames >= 20) {
    readout.textContent = N.toLocaleString() + ' particles · ' + (acc / frames).toFixed(3) +
      ' ms/step (' + (useWasm ? 'WASM SIMD' : 'JS') + ') · move the pointer'
    acc = 0
    frames = 0
  }
  requestAnimationFrame(loop)
}

;(async () => {
  // The wasm bootstrap is async. tjs-lang 0.9.1+ exposes an awaitable ready signal,
  // so we start only once the kernel is instantiated — otherwise the first frames
  // silently run the JS fallback while the button claims "WebAssembly SIMD".
  await globalThis.__tjs_wasm_ready?.()
  warmUp()
  applyMode()
  requestAnimationFrame(loop)
})()

preview.append(canvas, btn, readout)
// Guard the claim the button makes. A `wasm {}` block that fails to compile falls
// back to JS *silently*, so without this the demo can advertise "⚡ WebAssembly SIMD"
// while running the JS twin, and every test stays green.
test('the wasm kernel actually compiled (no silent fallback to JS)', async () => {
  await globalThis.__tjs_wasm_ready?.()
  expect(typeof globalThis.__tjs_wasm_ready).toBe('function')
  // 0.10.x names each compiled wasm export `__tjs_wasm_<hash>_<n>` on globalThis — the
  // hash makes it collision-free across examples (tjs-lang#11). The kernel calls that
  // global when present and falls back to its JS twin when it's not, so a truthy one
  // means the wasm really compiled. Matched by pattern (the hash is per-transpile), and
  // asserting >0 keeps the guard from ever passing vacuously.
  const compiled = Object.keys(globalThis).filter(
    (k) => /^__tjs_wasm_[a-z0-9]+_\d+$/.test(k) && globalThis[k]
  )
  expect(compiled.length).toBeGreaterThan(0)
})

Execution modes

An example runs in one of three modes. Signal the mode on a code fence with ```<lang>:<mode> — ```js:iframe, ```css:ide — on any block in the group; the first mode in the group wins (contradictory modes across a group are an authoring error: they log to the console and the first is used). Or set the mode attribute directly.

Fully-isolated examples (a separate module realm, so even tosi() state and imported dependencies are sandboxed — the way tjs-lang's playgrounds do it with a service worker intercepting imports) are a possible future option. For actual examples the shared-page default is usually the nicer behavior: it shows off tosi's isolation and lets you poke demos through the live global state.

Test Blocks

Add ```test code blocks to write inline tests that run against the preview:

<button class="demo-btn">Click me</button>
preview.querySelector('.demo-btn').onclick = () => {
  preview.querySelector('.demo-btn').textContent = 'Clicked!'
}
test('button exists', () => {
  const btn = preview.querySelector('.demo-btn')
  expect(btn).toBeDefined()
  expect(btn.textContent).toBe('Click me')
})

test('slow test shows running state', async () => {
  await waitMs(500)
  expect(true).toBe(true)
})

Tests have access to:

Tests run CONCURRENTLY — one test() per flow

Each test() body is invoked as it is registered, and async bodies are gathered with Promise.all. So test 2's synchronous body runs while test 1 is sitting at its first await, and tests in other examples on the same page overlap too.

This matters because splitting one flow across two test() blocks that share state is a latent race that can pass for months. It passes whenever the first block's await resolves on the next microtask with nothing pending, and fails the day some unrelated activity makes that await wait out a real settling round — one that now includes the second block's mutation. Nothing about the code changed; the timing did.

So: steps that depend on each other belong in a single test().

// WRONG — two blocks sharing state; they interleave
test('it hydrates', async () => { await updates(); expect(input.value).toBe('a') })
test('it responds to input', async () => { input.value = 'b'; await updates() })

// RIGHT — one flow, one test
test('it hydrates, then responds to input', async () => {
  await updates()
  expect(input.value).toBe('a')
  input.value = 'b'
  await updates()
  expect(state.value).toBe('b')
})

Two consequences worth knowing. Assertions about counts should be deltas rather than absolutes, since another example may be adding elements while you look. And a test that leaves global state behind can perturb a concurrent one — clean up inside the test that made the mess.

Async Tests

Tests can be async functions. Use waitMs for simple delays and waitFor to wait for dynamically created elements:

<button class="async-btn">Load Data</button>
<div class="result"></div>
preview.querySelector('.async-btn').onclick = () => {
  setTimeout(() => {
    preview.querySelector('.result').innerHTML = '<span class="data">Loaded!</span>'
  }, 100)
}
// Auto-click to trigger the async behavior
preview.querySelector('.async-btn').click()
test('waitFor finds dynamically created element', async () => {
  const data = await waitFor('.data')
  expect(data.textContent).toBe('Loaded!')
})

test('waitMs delays execution', async () => {
  const start = Date.now()
  await waitMs(50)
  expect(Date.now() - start).toBeGreaterThan(40)
})

A test-only example shows its results

A ```test fence with nothing beside it has no preview to render. Rather than an empty box — which reads as a broken example and hides the one thing the block is there to show — the results become the example's body:

test('a test-only example renders its own results', () => {
  expect(1 + 1).toBe(2)
})

test('…including several of them', () => {
  expect('tosijs-ui'.length).toBeGreaterThan(3)
})

Useful for documenting a pure function, an invariant, or a behaviour that has no visual output. The example above is a real one — it runs on every build of this page.

context

A <tosi-example> is given a context object which is the set of values available in the javascript's execution context. The context always includes preview.

import * as tosijs from 'tosijs'
import * as tosijsui from 'tosijs-ui'

context = {
  tosijs,
  'tosijs-ui': tosijsui
}