From b6e85b0d813b493145eaa3a41be7ee0e3b9a259f Mon Sep 17 00:00:00 2001 From: Tom MacWright Date: Sat, 18 May 2024 16:13:12 -0400 Subject: [PATCH] Update reamde --- README.md | 66 ++++++++++++++++++++++++++ main.js | 127 +++++++++++++++++++++++--------------------------- manifest.json | 6 +-- styles.css | 2 +- 4 files changed, 129 insertions(+), 72 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..14b3a89 --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# Freeform + +Obsidian freeform plugin. This lets you write arbitrary JavaScript, +including importing ESM modules, injecting styles, and much more. + +### Inspired by Observable + +This brings a taste of [Observable](https://observablehq.com/) to +[Obsidian](https://obsidian.md/). Some common elements include: + +- You can write blocks of code which run in a light sandboxed environment +- There's a `display()` function to show values and elements, just like + Observable Framework's [explicit display system](https://observablehq.com/framework/javascript#explicit-display). + +### Based on iframes + +Everything you write is run within a sandboxed iframe, making it safer to do more +creative coding within Obsidian without affecting the surrounding page. + +## Get started + +Install the freeform plugin, and create a fenced code block with the language +set to `freeform`. For example: + + ```freeform + display(42); + ``` + +That should, once you're done editing it and have clicked away from the code block, +show the number 42. Now you've learned all of the concepts in freeform! It's +JavaScript, and you can use the `display()` method to show a value. See the rest +of this readme for some examples. + +### Examples + + ```freeform + import * as Plot from "https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6/+esm"; + const widths = [ + ["Val Town", 1024], + ["Val Town (after)", 900], + ["GitHub", 830], + ["Radicle", 723], + ["Replicate", 653], + ["Glitch", 612], + ["GitLab", 842], + ["Observable", 640] + ].map(([name, width]) => ({ name, width })) + + display(Plot.barX(widths, { + x: "width", + y: "name", + marginLeft: 100, + fill: "name" + }).plot({ height: 400, width })) + ``` + +### Notes + +- There is a `width` variable, much like [Observable's](https://observablehq.com/framework/javascript#width), but + it is not live-updating or reactive. This project does not include + Observable-style reactivity: your JavaScript runs just the same + way as it does on any webpage. +- Only HTTP ESM imports are supported. This isn't Node.js or Deno - there + isn't a node_modules directory, and you don't have short names for dependencies. + Thankfully, this usually isn't a problem because you can use https://esm.sh/ + https://www.jsdelivr.com/ and more to import modules. diff --git a/main.js b/main.js index 126a01a..98df4ad 100644 --- a/main.js +++ b/main.js @@ -2,40 +2,20 @@ const { Plugin, MarkdownRenderer } = require("obsidian"); module.exports = class IframeBlockPlugin extends Plugin { async onload() { - this.registerMarkdownCodeBlockProcessor("iframe", async (src, el, ctx) => { - // Get Parameters - try { - const rootEl = el.createEl("div", { - cls: "run-block", - }); - const iframe = rootEl.createEl("iframe", { cls: "run-block-frame" }); - iframe.srcdoc = src; - iframe.height = "200px"; - iframe.addEventListener("load", () => { - const height = iframe.contentWindow?.document.body.offsetHeight + 40; - iframe.height = height + "px"; - }); - const srcEl = rootEl.createEl("div", { cls: "run-block-src" }); - MarkdownRenderer.render( - this.app, - `\`\`\`html\n${src}\n\`\`\``, - srcEl, - ctx.sourcePath, - this, - ); - } catch (error) { - el.createEl("h3", { text: error }); - } - }); + this.registerMarkdownCodeBlockProcessor( + "freeform", + async (src, el, ctx) => { + try { + // Give this iframe an identifier so that when it sends messages + // from the iframe context back up, we know which iframe's messages + // belong to which code blocks + const iframeId = crypto.randomUUID(); + const rootEl = el.createEl("div", { + cls: "run-block", + }); + const iframe = rootEl.createEl("iframe", { cls: "run-block-frame" }); - this.registerMarkdownCodeBlockProcessor("iframeturbo", async (src, el, ctx) => { - // Get Parameters - try { - const rootEl = el.createEl("div", { - cls: "run-block", - }); - const iframe = rootEl.createEl("iframe", { cls: "run-block-frame" }); - iframe.srcdoc = ` + iframe.srcdoc = ` @@ -43,53 +23,64 @@ module.exports = class IframeBlockPlugin extends Plugin { import * as Plot from "https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6/+esm"; import { Inspector } from "https://unpkg.com/@observablehq/inspector@5.0.0?module"; -try { - ${src} - if (typeof output !== 'undefined') { - inspect(output); - } -} catch (e) { - inspect(e); -} - -function inspect(val) { +globalThis.display = function display(val) { const d = document.body.appendChild(document.createElement('div')) const inspector = new Inspector(d); inspector.fulfilled(val); } +globalThis.width = window.innerWidth; + const resizeObserver = new ResizeObserver((entries) => { - parent.postMessage({ type: 'height', height: document.body.offsetHeight }, '*'); + parent.postMessage({ + type: 'height', + height: document.body.offsetHeight, + id: ${JSON.stringify(iframeId)} + }, '*'); }); resizeObserver.observe(document.body); +import(${JSON.stringify(`data:text/javascript;base64,${btoa(src)}`)}).catch(e => { +console.log(e); + display(e); +}); `; - iframe.height = "200px"; - iframe.addEventListener("load", () => { - const height = iframe.contentWindow?.document.body.offsetHeight + 40; - iframe.height = height + "px"; - }); - window.addEventListener("message", (evt) => { - if (evt.data.type === 'height') { - iframe.height = evt.data.height + 40 + "px"; - } - }); - const srcEl = rootEl.createEl("div", { cls: "run-block-src" }); - MarkdownRenderer.render( - this.app, - `\`\`\`js\n${src}\n\`\`\``, - srcEl, - ctx.sourcePath, - this, - ); - } catch (error) { - el.createEl("h3", { text: error }); - } - }); + iframe.height = "200px"; + + // Resize the iframe as soon as it loads + iframe.addEventListener("load", () => { + const height = + iframe.contentWindow?.document.body.offsetHeight + 40; + iframe.height = height + "px"; + }); + + // Resize the iframe when we get a message that its contents + // have been measured. + window.addEventListener("message", (evt) => { + if (evt.data.type === "height" && evt.data.id === iframeId) { + iframe.height = evt.data.height + 40 + "px"; + } + }); + + const srcEl = rootEl.createEl("div", { cls: "run-block-src" }); + + // Show the source code below the iframe + MarkdownRenderer.render( + this.app, + `\`\`\`js\n${src}\n\`\`\``, + srcEl, + ctx.sourcePath, + this, + ); + } catch (error) { + el.createEl("h3", { text: error }); + } + }, + ); } onunload() { - console.log("Unloading iframe plugin..."); + // console.log("Unloading iframe plugin..."); } -} +}; diff --git a/manifest.json b/manifest.json index abb7485..bfcb5f4 100644 --- a/manifest.json +++ b/manifest.json @@ -1,9 +1,9 @@ { - "id": "iframe-block-plugin", - "name": "Iframe code block", + "id": "iframe-freeform", + "name": "Freeform", "version": "0.0.0", "minAppVersion": "0.0.0", - "description": "Creates an iframe block", + "description": "Make visualizations and run arbitrary code with JavaScript + iframe blocks.", "author": "tmcw", "authorUrl": "https://github.com/tmcw", "isDesktopOnly": false diff --git a/styles.css b/styles.css index 97b9777..44f6d27 100644 --- a/styles.css +++ b/styles.css @@ -6,7 +6,7 @@ .run-block-frame { border-top-left-radius: 4px; - border-top-right: 4px; + border-top-right-radius: 4px; background: #fff; width: 100%; padding: 10px;