Tom MacWright

tom@macwright.com

Obsidian Freeform

Prefer video? You can also watch an intro video that I recorded for this on YouTube.

Obsidian Freeform is an extremely small Obsidian plugin that enables totally custom JavaScript-powered frames alongside your notes. I created it because I use Obsidian as my note-taking application, and I found myself wanting to create charts for certain concepts.

For example, I recently learned about the availability of mortgage discounts from some banks in exchange for having a high balance. I am not buying a house, but nevertheless, these were numbers from multiple sources that could be charted and compared and nobody was doing it, so I had to. So I made a chart:

Chart of mortgage discounts

It seems like mortgage discounts are mostly a bad idea, but now I at least have a quick visual reference for how they work across multiple banks.

The font I’m using in that screenshot is iA Writer Quattro, and the theme is minimal, both of which I learned about from Everyday Obsidian.

How it works

The code is small enough to read in a few minutes.

Obsidian Freeform adds a new code block type to Obsidian, tagged with freeform. Here’s the simplest possible block:

```freeform
display(1);
```

When this block is displayed, it creates an iframe in your Obsidian document and injects the code display(1) into it via a dynamic import.

Why inject the code as a dynamic import? Because I want to make it possible to use import within your custom code to import modules, and import needs to be top-level. In practice, this detail shouldn’t matter to you.

The display method comes from observablehq/inspector, which is added to the global namespace as display, just like how it works in Observable Framework.

There is no implicit display in Obsidian Freeform, and while it provides a width variable, that variable isn’t reactive. I’m trying to avoid adding too much magic. It’s just your JavaScript, as verbatim as I can make it.

Example: Making charts with Observable Plot

Observable Plot example of a house

You can recreate a lot of Observable examples in Freeform. Generally, the gap between the two is:

Plot isn’t available globally. You have to import it from jsdelivr like:

import * as Plot from "https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6/+esm";

But then you’re basically good to go, as long as you remember:

  • It’s just JavaScript, not Observable-flavored JavaScript, so you need to use let or const in front of variables.
  • There’s no implicit display, so you need to call display() if you want to display something.

Example: Loading data from Dataview

I use the fantastic Dataview Obsidian plugin to do things like show charts of all the electronics I have in my house and which batteries they require.

Because the Obsidian context is available via window.top in Obsidian Freeform, I can access the Dataview JavaScript API:

Things I own cost vs when I bought them

import * as Plot from "https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6/+esm";

const items = await window.top.app.plugins.plugins.dataview.api
  .query(`table price, purchased, color
from "03 Stuff"
where price and sold = undefined
sort purchased desc`);

const mapped = items.value.values
  .map((item) => {
    if (!item[2]) return;
    return {
      price: item[1],
      date: new Date(item[2].toMillis()),
    };
  })
  .filter((r) => r);

display(Plot.dot(mapped, { y: "price", x: "date" }).plot());

No, I don’t think window.top.app.plugins.plugins.dataview.api is a graceful, beautiful line of code, but it works and it’s explicit.

Example: Loading data from a table

One of my few complaints with Obsidian is that it doesn’t expose its own Markdown parser and the internal syntax tree for its CodeMirror instance tree is oddly structured and undocumented. So it’s tough for a plugin to interact with notes in a structured way.

Parsing a table

But Freeform makes this easy possible! Here’s some magic that parses the current document, finds its first table, and turns that table’s contents into data that you can use in charts:

import { unified } from "https://esm.sh/unified?bundle";
import remarkGfm from "https://esm.sh/remark-gfm?bundle";
import { toString } from "https://esm.sh/mdast-util-to-string?bundle";
import remarkParse from "https://esm.sh/remark-parse@11?bundle";
import remarkFrontmatter from "https://esm.sh/remark-frontmatter?bundle";
import { selectAll } from "https://esm.sh/unist-util-select?bundle";
import { autoType } from "https://esm.sh/d3-dsv";

const md = window.top.app.workspace.activeEditor?.editor.getDoc().getValue();

const file = await unified()
  .use(remarkParse)
  .use(remarkGfm)
  .use(remarkFrontmatter)
  .parse(md);

const table = selectAll("table", file)[0];

const [header, ...rows] = table.children.map((row) => {
  return row.children.map((child) => toString(child));
});

const data = body.map((row) => {
  return autoType(Object.fromEntries(row.map((text, i) => [header[i], text])));
});

display(data);

I think this is pretty cool. The magic ingredients here are totally free access to NPM modules like remark, a Markdown parser, plus access to the Obsidian API via window.top.app, plus the ability to display any kind of data pretty easily using @observablehq/inspector.

The plugin is basically those things and not much more, but it adds up to a lot.

Annoyance: the edit cycle

The cycle of editing code and seeing the results is pretty rough: you need to stop editing and click outside of the fenced code block to see what will happen. Plus, Obsidian doesn’t highlight freeform code blocks as JavaScript. This seems like a common problem for plugins, given that Dataview, one of the most popular plugins in the entire ecosystem, doesn’t have code highlighting for its query blocks.

I wish that this plugin used a Decoration instead of a markdown code block processor: that’d make the editing cycle a lot smoother. I think that, possibly, if I were to re-parse the document using remark like I do in the table example above, it’d be somewhat possible, but it’d be a lot more complicated, and there is the unknown of how Obsidian flavored Markdown will play with these parsers.

Missing: Reactive blocks and shared variables

You can technically set variables in one block using window.top.foo = 1 and get that variable in another block. After I announced the plugin, a few people requested some kind of shared variable namespace, like in Observable.

Honestly, this doesn’t seem like a great thing to invest time in; making blocks reactive would make the scope of this project much broader. It might require parsing code to find references (like Observable does) and running blocks in a more magical way. Plus, it’d open up the question of how cross-document block references would work. So also unlike Observable, there isn’t an idea of response caching at the moment: it’s just JavaScript, and without changing the language or implementing a heavyweight middle layer, it isn’t possible to do that.

I don’t use “small cells” in my own editing and am inclined to think that they just aren’t worth it - they make it harder to refactor, harder to repurpose code, and add a bunch of cognitive overhead for “where things are.”

But I’m open to ideas for how to implement something like reactive cells without blowing up the scope. It’d be neat to use a reactive signals module like signia for them and put them in userspace.

Caveats: Security

As some of these examples demonstrate, you can reach up to window.top and access APIs from Obsidian. This is super powerful: it enables access to the Dataview API, lets you access Obsidian’s own APIs.

This does mean that if you import a module, you need to trust that they aren’t going to try to do something nefarious. And you shouldn’t blindly copy-and-paste huge amounts of code into a code block from someone you don’t trust. It’s your vault, you have the power, hence the responsibility.

I think that a separate-origin version of this plugin is possible in which window.top is inaccessible, which would give a different power / security tradeoff.

Caveats: Just JavaScript

There are more accessible ways to create charts in Obsidian, like the Charts plugin. There is basically no abstraction in using Freeform, so it requires knowledge of JavaScript.

How does this compare to DataviewJS blocks?

I love the Dataview plugin, and it has a codeblock way to run JavaScript code, including access to its API. How is Freeform different?

  • dataviewjs codeblocks run in the Obsidian top frame, so if you add weird CSS, it affects the whole application. You can get into more trouble doing things in the top frame. Freeform runs in an iframe, so by default, at most you’re mucking up the iframe.
  • dataviewjs runs via eval, whereas Freeform runs code via a dynamic import(), so you can use ES module import statements to pull in new modules in Freeform, whereas you can’t in dataviewjs

Lest I be interpreted as throwing shade on Dataview: its tradeoffs make a lot of sense for what it’s targeted to do: create inline tables and listings based on Dataview information with clickable links. Dataview is amazing. To access the dataview API from Freeform, you need to write that tedious window.top.app.plugins.plugins.dataview.api variable reference and deal with the fact that Date objects have separate prototype identities when transferred between frame contexts.

Also, that being said: someone has probably already implemented something similar to this plugin, but I haven’t found it yet.


You can install Freeform in Obsidian, and here’s a link to the GitHub repo for the plugin!