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
| Layer | What | Pick |
|---|---|---|
| Framework wrapper | Mounts the renderer into your app | one of react / vue / svelte / wc (or vanilla core) |
| Core renderer | ipynb → cells → DOM. Markdown pipeline + Lezer syntax + DOMPurify. | core (always) |
| Plugins | remark / rehype / setup / onMarkdownRendered / renderOutput hooks | zero or more of editor-codemirror, katex / katex-cdn, mathjax / mathjax-cdn, widgets |
| Executor | execute(src, lang) + optional commProvider for Run / Shift+Enter | optional, 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:
| Export | Purpose |
|---|---|
createRenderer(options) | Returns { mount(target, ipynb) → handle }. The handle exposes update, cell(i), cells(), destroy. |
Plugin interface | Described under Plugins. |
Executor interface | execute(src, lang, signal?) → OutputType[] + optional commProvider. |
defaultHtmlFilter | DOMPurify-based sanitiser used for raw HTML cells / outputs. |
buildMarkdownProcessor | The unified pipeline factory plugins inject themselves into. |
highlight, remarkLatexEnvironment, remarkPromoteDisplayMath | Reusable 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:
- Takes props matching
RendererOptionsplusipynb. - On mount, creates an internal container
<div>, callscreateRenderer().mount(div, ipynb). - On
ipynbchange, calls the cheaperhandle.update(ipynb)so DOM (and active CodeMirror editors) isn’t torn down. - On option-shape change (plugins / executor / className / …) destroys and remounts.
- On unmount, calls
handle.destroy().
The four wrappers share semantically identical props:
| Wrapper | Component / element | Notes |
|---|---|---|
@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 element | object 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:
| Package | What it hooks |
|---|---|
editor-codemirror | Inline editor + Shift+Enter run + per-cell toolbar; replaces the read-only highlighted view with a CodeMirror instance |
katex / katex-cdn | remark-math + KaTeX — bundled vs CDN-loaded |
mathjax / mathjax-cdn | remark-math + MathJax SVG — bundled vs CDN-loaded |
widgets | Claims 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:
| Package | Where the code runs | Comm support |
|---|---|---|
executor-pyodide | Python in a browser Web Worker | yes (in-process Comm shim) |
executor-webr | R in a browser Web Worker | no (WebR doesn’t expose Comm) |
executor-jupyter | Remote Jupyter Server kernel via WebSocket | yes (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:
Notebookmounts →createRenderer({plugins, executor, languages, …}).mount(target, ipynb).- Core appends a
.jknb-rootcontainer, runs every plugin’ssetup(ctx)in declaration order. - Per cell:
- Markdown → unified pipeline (with plugin remark/rehype additions) →
DOMPurify sanitise →
onMarkdownRendered(el)callbacks. - Code → Lezer highlight (or
editor-codemirrorif installed) → outputs rendered viaOutputArea; first plugin returningtruefromrenderOutputclaims the MIME, otherwise the default renderer takes over.
- Markdown → unified pipeline (with plugin remark/rehype additions) →
DOMPurify sanitise →
- User clicks Run →
cellHandle.rerun()→ctx.executor.execute(src, lang)→ outputs replace the cell’soutput_wrapperchildren. ctx.notebook()exposes live state for save/download (Cmd+S triggersRendererOptions.onSaveor 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
| Tool | Where | Why |
|---|---|---|
| tsup (esbuild) | core, all framework wrappers, all plugins, all executors | Single-file ESM with .d.ts. Fast, zero-config. |
@sveltejs/package | svelte | .svelte → published component via the official Svelte tooling. |
build.mjs (LESS + esbuild) | theme | Compiles 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 + Starlight | docs | This site. SSG with React islands for live demos. |
| Storybook 8 + Vite | storybook | Interactive stories for every executor / plugin / theme combo. |
| Playwright | e2e | Headless-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.