Add CLAUDE.md and the test harness

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
This commit is contained in:
2026-08-20 23:02:08 -03:00
co-authored by Claude Opus 5
parent 8a597e19a8
commit aebc72b85e
8 changed files with 544 additions and 0 deletions
+142
View File
@@ -0,0 +1,142 @@
# 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
1. `mermaidpreview.preview` resolves the frontmost editor to a source
(`describeSource`), refusing untitled and remote files — they have no path to
resolve images and links against.
2. `installAssets` copies `Assets/` into `<output>/assets/` if missing or stale,
stamped with `ASSET_VERSION`. **Bump `ASSET_VERSION` in `Scripts/preview.js`
whenever anything in `Assets/` changes**, or installed copies go stale.
3. `buildDocument` writes a ~2 KB shell: the Markdown embedded as JSON, plus
script tags. No rendering happens in the extension.
4. `Assets/preview.js` renders in the WebView — markdown-it, then DOM fix-ups,
then Mermaid one diagram at a time so a bad diagram fails alone.
5. Saving rewrites step 3 only. Nova reloads the preview because the file
changed. Three megabytes are not rewritten per keystroke — keep it that way.
6. 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 through `util.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/*.js` against a stubbed `nova` global backed
by the real filesystem (`Tests/nova-stub.js`). Covers generation, asset reuse,
and discard/pruning.
- **`close.test.js`** — drives the whole `Controller` through 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 real `render.js` and dumps the
DOM from headless Chromium, asserting on diagrams, rebased paths, task lists,
headings and both themes. Skips itself if no browser is found; set
`MERMAID_PREVIEW_CHROME` to 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:
```sh
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_runtime` is `10`; 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_MS` in `Scripts/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.png` at the root is what Nova
picks up (see the sibling `claude-nova` extension).
- 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 a `highlight`
option in the markdown-it constructor if it becomes worth it.
- **Scroll sync** — impossible, see the constraints table.
- **Replacing Nova's built-in `.md` preview** — 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.