A small browser-based Jupyter Notebook viewer that renders
.ipynbfiles in the browser. This repository also includes a plugin-mode HTML that accepts notebook content from a host (QuickLook, WebView2, etc.) so you can embed the renderer in a native host.
The UI shell mirrors adoc: toolbar, Contents sidebar, Shadow DOM content pane, and host messaging.
- Requirements: Node.js 20+ and npm.
- Clone the repository and install dependencies:
cd src/ipynb
npm install- Run development server (hot-reload):
npm run devOpen http://localhost:5173 (Vite default) and use the UI to open a .ipynb file. Sample notebooks live in samples/.
- Standard build (multi-file output):
npm run buildThis produces dist/index.html, dist/plugin.html and a dist/assets/ folder with JS/CSS bundles.
- Produce self-contained single-file HTML outputs (inlines assets into each HTML):
npm run build:allAfter npm run build:all, dist/index.html and dist/plugin.html will be inlined (single-file) for easy distribution. The intermediate dist/assets/ folder is removed automatically after the assets are inlined.
Files of interest:
- Main app entry: src/app.ts
- Converter: src/lib/convert.ts
- Plugin HTML: plugin.html
- Build helper that inlines assets: scripts/make-singlefiles.js
- Build config: vite.config.ts
- Markdown cells — GFM markdown, tables, images, KaTeX math (
$…$/$$…$$) - Code cells — syntax highlighting (highlight.js),
In [n]:prompts - Outputs —
stream(stdout/stderr),execute_result/display_data(text/plain,text/html, images, SVG, JSON, LaTeX),error(ANSI stripped) - TOC — built from markdown headings; sidebar navigation with back/forward history
The plugin page is dist/plugin.html. In plugin mode the page hides the regular "Open" UI and waits for the host to provide a notebook to render. There are multiple ways a host can provide content:
- Query parameters (quick tests)
- Provide a remote URL to fetch (subject to CORS):
dist/plugin.html?ipynb=https://example.com/foo.ipynb&name=Foo.ipynb
- Provide base64-encoded notebook JSON inline:
dist/plugin.html?ipynbBase64=<BASE64_DATA>&name=Foo.ipynb
- Host → Page messaging (preferred for embedded hosts)
Send a postMessage (or WebView2 message) with type: 'open-ipynb' and a payload object. The renderer accepts the following payload fields:
base64— file bytes as a base64 string (recommended for local files)text— raw notebook JSON stringjson— parsed notebook object (or JSON string)url— HTTP(S) URL to fetch (requires CORS)path— treated likeurl(use with caution)name— optional display name for the document
Example (page postMessage):
window.postMessage({
type: 'open-ipynb',
payload: { url: 'https://example.com/foo.ipynb', name: 'Foo.ipynb' }
}, '*');
window.postMessage({
type: 'open-ipynb',
payload: { text: '{"nbformat":4,"cells":[]}', name: 'empty.ipynb' }
}, '*');
window.postMessage({
type: 'open-ipynb',
payload: { base64: '<BASE64_DATA>', name: 'Foo.ipynb' }
}, '*');- WebView2 (C#) example (recommended on Windows hosts)
using System.IO;
using System.Text.Json;
var bytes = File.ReadAllBytes(@"C:\path\to\file.ipynb");
var base64 = Convert.ToBase64String(bytes);
var msgObj = new {
type = "open-ipynb",
payload = new { base64 = base64, name = Path.GetFileName(@"C:\path\to\file.ipynb") }
};
var json = JsonSerializer.Serialize(msgObj);
webView.CoreWebView2.PostWebMessageAsJson(json);Notes:
- Using base64 / text / json avoids CORS and
file://restrictions. - Relative image paths inside markdown cells only resolve when fetchable (typically not for a lone dropped file). Prefer embedded base64 image outputs or data URIs.
{
"type": "open-ipynb",
"payload": { "base64": "...", "name": "Foo.ipynb" }
}Internal navigation from rendered content:
{ "type": "ipynb-navigate", "href": "#introduction" }Default is Auto (follow OS light/dark). Use the toolbar sun/moon button to toggle Light ↔ Dark; the choice is saved in localStorage (ipynb-theme).
Also supported:
- Query:
?theme=auto|light|dark - Host message:
window.postMessage({ type: 'set-theme', payload: { theme: 'dark' } }, '*');- If fetching via
urlfails: check CORS on the server or prefer sendingbase64/text/jsonfrom the host. - If conversion fails: ensure the file is valid nbformat JSON; check the toolbar status message and browser console.
- If the host webview doesn't forward messages: verify
PostWebMessageAsJson(WebView2) and that CoreWebView2 is initialized.
- Dev server:
npm run dev - Entry: src/app.ts — host-message handling and plugin-mode detection (
window.__ipynb_PLUGINor?plugin=1) - Regenerate single-file HTMLs:
npm run build:all