Tutorial: JSON Compare
BetaLoad the reference plugin, then read its code to see how a view-only plugin
fits together. examples/plugins/json-compare ships with Wrengle's source: 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.
Confirm it works
Work through this checklist once, to see every piece of the plugin behave correctly in a running copy of Wrengle.
- 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 does not persist across a 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 as you type. 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 land in 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 itemThere is no [runtime] block and no plugin.wasm here. 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's a walkthrough of the key pieces.
Pure logic
Four 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.
normalizeJson, buildRows, and diffCounts are exposed on
globalThis.__wrengleJsonCompare, so Wrengle's own tests can exercise them
without a browser. diffOps stays internal to the script.
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();
}
});Wrengle installs and readies the host-owned per-document channel automatically,
then sends wrengle:init with the current theme and active themeTokens.
Later theme changes send wrengle:theme with the same token map shape. On
wrengle:selection, the plugin fills the left side first, then the right side
on the next invocation. Direct parent-window messages are ignored.
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 your 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 listener (
window.addEventListener) so the view receives theme and selection messages from Wrengle's host-owned channel. - Open Settings → Plugins, add the new folder path, and click Reload. Your view appears in the command palette right away.
No build step is required unless you choose to use a bundler yourself. After an edit, click Reload, then reopen the tab so Wrengle validates and captures a new immutable HTML snapshot.
Trust reminder
Every view has a mandatory host-owned offline policy; direct network requests
are blocked along with WebSockets, WebRTC, navigation, forms, nested frames,
and external resources. The httpHosts capability applies only to Wrengle
host-bridge HTTP made by WASM actions and never relaxes this view policy.
Wrengle binds positive grants to the exact loaded code snapshot, including the manifest, WASM, and view bytes. Changed code needs approval again after reload, while plugin state and keychain values stay in the same plugin-and-path namespace. Load only local plugin code you trust.