Tutorial: the JSON Compare plugin
Betaexamples/plugins/json-compare is the reference view plugin: a side-by-side
JSON comparison tool built from one manifest and one HTML file. No build step.
Load it in three steps
- Open Settings → Plugins.
- Under Developer → Dev plugin paths, add the absolute path to
examples/plugins/json-comparein your local Wrengle source checkout. - Click Reload.
The plugin appears in Loaded plugins as "Compare JSON". Open the command palette (⌘K on macOS) and choose Open Compare JSON to open the view tab.
Smoke it in the app
Use this checklist before calling the JSON Compare plugin path done:
- Start Wrengle with
just dev. - Open Settings -> Plugins, add
<repo>/examples/plugins/json-compare, and click Reload. - Open the command palette and run Open Compare JSON.
- Paste two JSON objects and confirm the diff renders.
- Flip the app theme and confirm the view follows via
wrengle:theme. - Select text in a note and choose Actions -> Compare selection twice. The first selection fills the left side; the second fills the right side.
- Restart the app and confirm the tab restores as a fresh plugin view. Iframe state is not persisted across restart.
- Remove the dev path, click Reload, and confirm the restored tab shows the "plugin isn't loaded" placeholder.
Use it
Basic comparison:
- Paste a JSON object into the left textarea.
- Paste a JSON object into the right textarea.
- The diff updates automatically. Key order and whitespace are ignored — only structural differences appear.
- The
+N −Msummary above the diff shows added and removed lines.
Raw text fallback:
If either input is invalid JSON, a Diff as raw text button appears. Click it to compare the raw strings line by line without normalization.
Compare selection:
- Select text in a note.
- Open the Actions menu in the floating toolbar.
- Choose Compare selection.
- The first selection fills the left side; the next fills the right side. Both sides are filled within the same view tab.
Read the manifest
id = "plugin.json-compare"
name = "JSON Compare"
version = "0.1.0"
schema_version = 1
description = "Compare two JSON objects side by side, ignoring key order and formatting."
[[views]]
id = "plugin.json-compare.view" # must start with the plugin id + "."
label = "Compare JSON" # shown in the palette and tab title
entry = "ui/index.html" # relative path to the single HTML file
accepts_selection = true # enables the Actions menu item
selection_label = "Compare selection" # label for that Actions menu itemNotice there is no [runtime] block and no plugin.wasm. This is a view-only
plugin — no WASM, no build step, no compiled runtime dependency.
Read the UI file
ui/index.html is a single self-contained file with inline CSS and JavaScript.
Here is a walkthrough of the key pieces.
Pure logic
Three pure functions handle all data work:
normalizeJson(text)— parses the input, recursively sorts object keys, and re-serializes with two-space indentation. Returns{ ok, text }or{ ok: false, error }.diffOps(aLines, bLines)— LCS-based line diff. Trims a common prefix/suffix first, then runs the DP only over the changed middle section. Falls back to a single delete-run + insert-run when the DP grid would exceed 4 million cells.buildRows(leftText, rightText)— groups the diff ops into aligned pairs: consecutive delete-runs and insert-runs are zipped intochanged,leftOnly, orrightOnlyrows;samerows pass through unchanged.diffCounts(rows)— counts added and removed lines for the+N −Msummary.
These functions are exposed on globalThis.__wrengleJsonCompare so the host
repo can unit-test them without a browser.
The bridge block
Near the bottom of the script, the bridge:
function applyWrengleTheme(message) {
if (message.theme) {
document.documentElement.dataset.theme = message.theme;
}
if (!message.themeTokens || typeof message.themeTokens !== "object") return;
for (const [name, value] of Object.entries(message.themeTokens)) {
if (/^--[A-Za-z0-9_-]+$/.test(name) && typeof value === "string" && value.length > 0) {
document.documentElement.style.setProperty(name, value);
}
}
}
window.addEventListener("message", function (event) {
if (event.source !== window.parent) return;
var msg = event.data;
if (!msg || typeof msg !== "object") return;
if (msg.type === "wrengle:init" || msg.type === "wrengle:theme") {
applyWrengleTheme(msg);
} else if (msg.type === "wrengle:selection" && typeof msg.text === "string") {
// Two-step flow: first selection fills the left side, the next fills the right.
if (leftEl.value === "") { leftEl.value = msg.text; leftEl.focus(); }
else { rightEl.value = msg.text; rightEl.focus(); }
render();
}
});
parent.postMessage({ type: "wrengle:ready" }, "*");The wrengle:ready call signals the host that the view is ready. The host then
sends wrengle:init with the current theme and active themeTokens. Later app
theme changes send wrengle:theme with the same token map shape. On
wrengle:selection, the plugin implements the two-step fill: left side first,
right side on the next invocation.
Theme CSS variables
Wrengle injects active theme variables into the iframe before first paint, and
the bridge applies fresh token values from wrengle:init and wrengle:theme.
Use Wrengle CSS variables directly for surfaces, text, borders, muted states,
controls, and diff colors:
:root { color-scheme: light dark; }
html, body { background: var(--background); color: var(--foreground); }
textarea { border: 1px solid var(--input); color: var(--foreground); }
button { background: var(--primary); color: var(--primary-foreground); }
.diff { border: 1px solid var(--border); }
.cell.same { color: var(--muted-foreground); }
.cell.add { background: var(--diff-added-bg); color: var(--diff-added); }
.cell.del { background: var(--diff-removed-bg); color: var(--diff-removed); }This keeps the view aligned with the user's active Wrengle theme, including custom token overrides, without maintaining a separate plugin color palette.
Make it yours
- Copy the
json-comparefolder to a new location. - Edit
plugin.toml: changeid,name, and optionally[[views]][0].labelandentry. - Replace the
ui/index.htmlbody with your own interface. - Keep the bridge block (the
window.addEventListener+parent.postMessagecall). Your view will not receive messages from Wrengle without it. - Open Settings → Plugins, add the new folder path, and click Reload. Your view appears in the command palette immediately.
No build step is needed unless you choose to use a bundler. The HTML file is read fresh each time the tab opens, so you can iterate quickly.
Trust reminder
View JavaScript can make outbound network requests. The sandbox does not
restrict fetch or XMLHttpRequest. Load only local plugin code you trust;
the same principle applies to any plugin in a local folder you add.