CLAUDE.md records the Nova constraints this extension is built around — established by reading Nova 14.1's bundle and probing its preview server, and expensive to re-derive: no previewer extension point, generated pages must live under the preview root, dot-directories are served, and no API reaches into a preview WebView. It also flags the one unverified assumption, that a tab showing Preview still appears in nova.workspace.textEditors, with its symptom and fix. Tests/ is the harness that built the extension, kept rather than left in a scratchpad: a stubbed Nova runtime backed by the real filesystem, node suites for generation and the four close paths, and a headless-browser render test that skips itself without a browser. It caught the ./ doubling in rebased links and checkbox state not surviving serialization. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TueXpMi7eAtgWPuMr4ii52
7.8 KiB
CLAUDE.md
Context for working on this repository.
What this is
A Nova extension that previews Markdown as rendered HTML, with Mermaid diagrams drawn inline. Nova previews Markdown already; the thing it cannot do is run JavaScript in that preview, which is what diagrams need.
The extension is not a previewer and cannot be one. It writes an ordinary HTML file inside the workspace and opens it, and Nova's own HTML preview — a WebView that does run JavaScript — renders it. Every awkward part of the design follows from that one fact.
Hard constraints (do not re-derive these)
These were established by reading Nova 14.1's bundle and probing its running preview server. They are facts about Nova, not preferences.
| Constraint | Consequence |
|---|---|
No preview/previewer extension point. Manifest keys are activationEvents, clips, commands, completions, entitlements, main, sidebars, syntaxes; categories are clips, commands, completions, issues, languages, sidebars, tasks, themes |
Cannot supply the contents of a preview tab. Generate a file and open it |
| No webview / HTML UI for extensions; sidebars are TreeView only | Rendering can only happen inside Nova's own preview WebView |
AssistantsRegistry covers completions, issues, symbols, colors, tasks only |
No hook to intercept how a file is displayed |
| Nova's preview server only serves files under the preview root | Generated pages must live inside the workspace. A page outside it fails as a blank tab, not an error |
The preview server does serve dot-directories (.git/HEAD returns 200) |
.nova/MermaidPreview/ is a valid home for generated files |
The preview server renders .md to HTML on the fly, with Nova's own stylesheet |
A relative link from a generated page to another .md file shows Nova's rendering, not raw text |
Nova's built-in Markdown preview is NovaPreviewMarkdownRenderer over discount, styled by MarkdownAuto/Bright/Dark.css with a custom-stylesheet preference |
A stylesheet cannot run Mermaid. That preference is not a route to this feature |
| No API to reach into a preview WebView | No scroll sync, ever. Do not promise it |
| No API to close a tab, or to ask whether a tab is showing preview | Cleanup infers a close from onDidDestroy plus a re-check |
nova.workspace.openFile options are line and column only |
Cannot open a file directly into its preview state |
The runtime is JavaScriptCore, not Node: no npm require, only relative
require("./x.js") between Scripts/ files. setTimeout/clearTimeout and
fetch exist. Vendored libraries are not required by the extension — they are
copied into the workspace and loaded by the page.
Type definitions worth having open: npm pack @types/nova-editor-node (a single
index.d.ts, more precise than the website).
How a preview happens
mermaidpreview.previewresolves the frontmost editor to a source (describeSource), refusing untitled and remote files — they have no path to resolve images and links against.installAssetscopiesAssets/into<output>/assets/if missing or stale, stamped withASSET_VERSION. BumpASSET_VERSIONinScripts/preview.jswhenever anything inAssets/changes, or installed copies go stale.buildDocumentwrites a ~2 KB shell: the Markdown embedded as JSON, plus script tags. No rendering happens in the extension.Assets/preview.jsrenders in the WebView — markdown-it, then DOM fix-ups, then Mermaid one diagram at a time so a bad diagram fails alone.- Saving rewrites step 3 only. Nova reloads the preview because the file changed. Three megabytes are not rewritten per keystroke — keep it that way.
- Closing either tab discards the page; the last one out removes
assets/and the directory.
Layout
extension.json manifest: commands, config, entitlements
Scripts/ runs in Nova (JavaScriptCore)
main.js Controller: lifecycle, commands, watchers, cleanup
preview.js output paths, asset install, generate/discard
render.js builds the HTML shell — no Nova APIs, so node can test it
util.js conf, mkdirp, writeFile, copyFile, removeTree, shortHash
Assets/ copied into the workspace, runs in the WebView
preview.js markdown-it wiring, DOM fix-ups, Mermaid
preview.css three themes, resolved in CSS so the page never flashes
vendor/ markdown-it 14.3.0, mermaid 11.17.0 (both UMD, MIT)
Tests/ node harness + headless-browser render test
install.sh copy into Nova's extension directory, register, restart
Two files are called preview.js. Scripts/preview.js decides where files go;
Assets/preview.js renders the page. They never see each other.
Conventions
- 4-space indent, CommonJS, no build step, no runtime dependencies.
- Comments explain why, especially where something encodes an API limitation. Several exist to stop a future reader "simplifying" a constraint back into a bug — the cleanup grace period is the sharpest example.
- User-visible strings are sentence case and plain English.
- Config keys are namespaced
mermaidpreview.*and read throughutil.conf(), which prefers the workspace value. - Vendored libraries are committed, never fetched at runtime. The extension makes no network requests and holds no network entitlement.
Testing
Tests/run.sh runs everything: syntax, manifest, and three suites.
generate.test.js—Scripts/*.jsagainst a stubbednovaglobal backed by the real filesystem (Tests/nova-stub.js). Covers generation, asset reuse, and discard/pruning.close.test.js— drives the wholeControllerthrough the stub, including command invocation and fake editors that can be destroyed. Covers all four close paths.render.test.js— builds a page through the realrender.jsand dumps the DOM from headless Chromium, asserting on diagrams, rebased paths, task lists, headings and both themes. Skips itself if no browser is found; setMERMAID_PREVIEW_CHROMEto point at one.
The stub is the highest-value tool here and it already caught real bugs
(./ doubling in rebased links, checkbox state not surviving serialization).
Extend it rather than testing by hand.
In Nova itself:
nova extension validate . # manifest + bundle (makes network calls)
nova extension activate . # load into Nova for development
Logs appear in Nova's Extension Console.
State
- Never published.
min_runtimeis10; developed against Nova 14.1. - Unverified assumption, worth settling first. Cleanup on tab-close assumes a
tab showing Preview still appears in
nova.workspace.textEditors. If it does not, flipping a page into Preview will delete it ~2.5 s later (CLEANUP_GRACE_MSinScripts/main.js). Symptom: the preview stops updating and goes blank after a save. Fix is small — gate the page-tab cleanup on the source file being closed too, leaving only the source-close and quit paths. Recovery for the user is pressing ⌃⇧M again. - No icon.
Images/extension/is empty;extension.pngat the root is what Nova picks up (see the siblingclaude-novaextension). - No commits yet.
Things deliberately not done
- Syntax highlighting in code blocks — another bundle to vendor, and the
page is already 3.5 MB. Add highlight.js to
Assets/vendor/and ahighlightoption in the markdown-it constructor if it becomes worth it. - Scroll sync — impossible, see the constraints table.
- Replacing Nova's built-in
.mdpreview — also impossible. This is a second tab, and the README says so plainly. - Inlining assets into a single self-contained page — would make every save a 3.5 MB write.