Skip to content

Architecture

@jupyter-kit is built as four orthogonal layers that compose at runtime through small, framework-agnostic interfaces. You can swap any one (framework wrapper, executor, theme, plugins) without touching the others.

Layers

LayerWhatPick
Framework wrapperMounts the renderer into your appone of react / vue / svelte / wc (or vanilla core)
Core rendereripynb → cells → DOM. Markdown pipeline + Lezer syntax + DOMPurify.core (always)
Pluginsremark / rehype / setup / onMarkdownRendered / renderOutput hookszero or more of editor-codemirror, katex / katex-cdn, mathjax / mathjax-cdn, widgets
Executorexecute(src, lang) + optional commProvider for Run / Shift+Enteroptional, one of executor-pyodide, executor-webr, executor-jupyter

1. Core (@jupyter-kit/core)

The renderer. Takes an Ipynb document and a target HTMLElement, walks the cells, and produces DOM with proper .cell / .text_cell_render / .output_area structure (the Jupyter classnames). Everything below is configuration.

Key public surface:

ExportPurpose
createRenderer(options)Returns { mount(target, ipynb) → handle }. The handle exposes update, cell(i), cells(), destroy.
Plugin interfaceDescribed under Plugins.
Executor interfaceexecute(src, lang, signal?) → OutputType[] + optional commProvider.
defaultHtmlFilterDOMPurify-based sanitiser used for raw HTML cells / outputs.
buildMarkdownProcessorThe unified pipeline factory plugins inject themselves into.
highlight, remarkLatexEnvironment, remarkPromoteDisplayMathReusable building blocks, also consumed internally.

Core has zero framework dependencies. It only touches the DOM via document.createElement, mutating in place. SSR is out of scope by design — the renderer is for the client.

2. Framework wrappers (react, vue, svelte, wc)

Each wrapper is a ~100-line file that:

  1. Takes props matching RendererOptions plus ipynb.
  2. On mount, creates an internal container <div>, calls createRenderer().mount(div, ipynb).
  3. On ipynb change, calls the cheaper handle.update(ipynb) so DOM (and active CodeMirror editors) isn’t torn down.
  4. On option-shape change (plugins / executor / className / …) destroys and remounts.
  5. On unmount, calls handle.destroy().

The four wrappers share semantically identical props:

WrapperComponent / elementNotes
@jupyter-kit/react<Notebook>hooks-based; safe under StrictMode double-mount.
@jupyter-kit/vue<Notebook>composition API; uses shallowRef for ipynb.
@jupyter-kit/svelte<Notebook>Svelte 5 runes ($state, $effect).
@jupyter-kit/wc<jk-notebook> custom elementobject props are JS properties, primitives are HTML attributes (getAttribute).

Adding a fifth wrapper (Solid? Lit? Preact?) is straightforward — copy one of the existing files and translate the lifecycle hooks.

3. Plugins

A plugin is a plain object implementing some subset of:

type Plugin = {
name: string;
remarkPlugins?: PluggableList; // injected into the markdown pipeline
rehypePlugins?: PluggableList; // injected after remark→rehype
setup?(ctx: RuntimeContext): void; // called once per mount; receives executor / cell APIs
onMarkdownRendered?(el: HTMLElement): void; // after each markdown cell renders
renderOutput?(out: OutputType, slot: HTMLElement, ctx: RuntimeContext): boolean;
// claim a specific output type; return true to skip default
teardown?(): void;
};

Existing plugins in this repo:

PackageWhat it hooks
editor-codemirrorInline editor + Shift+Enter run + per-cell toolbar; replaces the read-only highlighted view with a CodeMirror instance
katex / katex-cdnremark-math + KaTeX — bundled vs CDN-loaded
mathjax / mathjax-cdnremark-math + MathJax SVG — bundled vs CDN-loaded
widgetsClaims application/vnd.jupyter.widget-view+json outputs and renders them via @jupyter-widgets/html-manager

Plugins are pure data — they don’t import from each other. The renderOutput hook is checked in registration order; first plugin that returns true wins, falling through to the default renderer otherwise.

4. Executors (optional)

Anything implementing the Executor type. Cells with code call ctx.executor?.execute(source, language, signal) when the user clicks Run (or Shift+Enter). The returned OutputType[] replaces the cell’s outputs.

Three first-party executors:

PackageWhere the code runsComm support
executor-pyodidePython in a browser Web Workeryes (in-process Comm shim)
executor-webrR in a browser Web Workerno (WebR doesn’t expose Comm)
executor-jupyterRemote Jupyter Server kernel via WebSocketyes (full kernel-side Comm bridge)

Building a custom executor (Deno, WASI, your-proprietary-API) is just implementing the two-method Executor interface — see reference/core/#executor.

Data flow: a cell render

In words:

  1. Notebook mounts → createRenderer({plugins, executor, languages, …}).mount(target, ipynb).
  2. Core appends a .jknb-root container, runs every plugin’s setup(ctx) in declaration order.
  3. Per cell:
    • Markdown → unified pipeline (with plugin remark/rehype additions) → DOMPurify sanitise → onMarkdownRendered(el) callbacks.
    • Code → Lezer highlight (or editor-codemirror if installed) → outputs rendered via OutputArea; first plugin returning true from renderOutput claims the MIME, otherwise the default renderer takes over.
  4. User clicks Run → cellHandle.rerun()ctx.executor.execute(src, lang) → outputs replace the cell’s output_wrapper children.
  5. ctx.notebook() exposes live state for save/download (Cmd+S triggers RendererOptions.onSave or the default download).

Module relationships at a glance

  • Core depends on nothing in this workspace.
  • Wrappers depend only on core (peer).
  • Plugins / executors depend on core (peer) and comm (where applicable).
  • The user installs one wrapper + zero or more plugins / executors.

Build pipeline

ToolWhereWhy
tsup (esbuild)core, all framework wrappers, all plugins, all executorsSingle-file ESM with .d.ts. Fast, zero-config.
@sveltejs/packagesvelte.svelte → published component via the official Svelte tooling.
build.mjs (LESS + esbuild)themeCompiles every *.less chrome under themes/chrome/*.css, then assembles publishable theme-<chrome> and theme-all packages under dist-publish/. The version is read from the source package.json so pnpm bump keeps everything aligned.
Astro + StarlightdocsThis site. SSG with React islands for live demos.
Storybook 8 + VitestorybookInteractive stories for every executor / plugin / theme combo.
Playwrighte2eHeadless-browser regression suite (XSS, MathJax, KaTeX, theme rendering, …).

Versioning + release wiring

All packages publish on the same git tag, at the same version. See Versioning for the policy and the scripts that automate it.