Webux Lab - Blog
Webux Lab Logo

Webux Lab

By Studio Webux

Search

By Tommy Gingras

Last update 2023-02-07

Obsidian

Custom Obsidian Plugin

Goal: Learning !

  • Experiment with the Custom Protocol, to be more precise the registerObsidianProtocolHandler.
    • I've played with it in the past (for another obsidian plugin and it works great !)
  • This article will cover a simple prototype that
    1. Receive a JSON Array (from anywhere... To be defined in fact ..)
    2. Convert the JSON objects to flatten object
    3. Convert all Objects to a CSV format (with Header)
    4. Convert the CSV File to Markdown Table
    5. Save the content in a predefined page in obsidian

Note

I will only copy/paste the modified file in this article to avoid overloading it; Here is the official template I used : https://marcus.se.net/obsidian-plugin-docs/getting-started


The Code !

npm i --save @json2csv/plainjs@^6.1.2
npm i --save csv-to-markdown-table@^1.3.0

npm i --save-dev open

main.js

import {
  App,
  normalizePath,
  Notice,
  Plugin,
  PluginSettingTab,
  Setting,
} from "obsidian";
import { Parser } from "@json2csv/plainjs";
import csvToMarkdown from "csv-to-markdown-table";

interface Settings {
  filename: string;
  endpoint: string;
}

const DEFAULT_SETTINGS: Settings = {
  filename: "default.md",
  endpoint: "endpoint-local",
};

function flattenJSON(obj: any = {}, res: any = {}, extraKey: string = "") {
  for (const key in obj) {
    if (typeof obj[key] !== "object") {
      res[extraKey + key] = obj[key];
    } else {
      flattenJSON(obj[key], res, `${extraKey}${key}_`);
    }
  }
  return res;
}

export default class DataCollectorPlugin extends Plugin {
  settings: Settings;

  processJson(data: string, withHeader: boolean): string {
    try {
      const _data = JSON.parse(Buffer.from(data, "base64").toString("utf-8"));

      const opts = {};
      const parser = new Parser(opts);
      const csv = parser.parse(_data.map((_d: Object) => flattenJSON(_d)));
      return csvToMarkdown(csv, ",", withHeader);
    } catch (err) {
      console.error(err);
      return "\n";
    }
  }

  async onload() {
    await this.loadSettings();

    // Listen for local callback
    this.registerObsidianProtocolHandler(
      this.settings.endpoint,
      async (params) => {
        try {
          console.debug("Received data on local endpoint");
          console.debug(params);
          const filename = normalizePath(`/${this.settings.filename}`);
          const exists = await this.app.vault.adapter.exists(filename, true);

          if (exists) {
            const data = this.processJson(params.data, true);
            const s = await this.app.vault.adapter.append(filename, data);
          } else {
            const data = this.processJson(params.data, true);
            const w = await this.app.vault.create(filename, data);
          }
        } catch (e) {
          new Notice(e.message);
        }
      }
    );

    // This adds a settings tab so the user can configure various aspects of the plugin
    this.addSettingTab(new SettingTab(this.app, this));
  }

  onunload() {}

  async loadSettings() {
    this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
  }

  async saveSettings() {
    await this.saveData(this.settings);
  }
}

class SettingTab extends PluginSettingTab {
  plugin: DataCollectorPlugin;

  constructor(app: App, plugin: DataCollectorPlugin) {
    super(app, plugin);
    this.plugin = plugin;
  }

  display(): void {
    const { containerEl } = this;

    containerEl.empty();

    containerEl.createEl("h2", { text: "Settings" });

    new Setting(containerEl)
      .setName("filename")
      .setDesc("Filename to store the json objects")
      .addText((text) =>
        text
          .setPlaceholder("Enter your filename")
          .setValue(this.plugin.settings.filename)
          .onChange(async (value) => {
            this.plugin.settings.filename = value;
            await this.plugin.saveSettings();
          })
      );

    new Setting(containerEl)
      .setName("endpoint")
      .setDesc("Endpoint to call")
      .addText((text) =>
        text
          .setPlaceholder("Enter your endpoint")
          .setValue(this.plugin.settings.endpoint)
          .onChange(async (value) => {
            this.plugin.settings.endpoint = value;
            await this.plugin.saveSettings();
          })
      );
  }
}

To test the plugin:

node test.js

test.js

const open = require("open");

(async () => {
  const data = Buffer.from(
    JSON.stringify([
      {
        foo: "bar",
        one: {
          plus: "two",
          is: { three: true },
        },
      },
    ])
  ).toString("base64");

  await open(`obsidian://endpoint-local/?data=${data}`);
})();

Will append this table in your file:

| "foo" | "one_plus" | "one_is_three" |
| ----- | ---------- | -------------- |
| "bar" | "two"      | true           |

I did go further, I was trying to figure out how to use NodeJS to send the data directly, but so far I didn't found any way...

My goal was to centralize the data in obsidian for further processing and links, but at the end it doesn't seem to be a good idea..

I've tried:

  • axios
  • node-fetch
  • Net
  • Https

All of them were not happy because of the unsupported protocol (obsidian://)


Next Step(s)

Mozilla Firefox Extension

I've created one for another application, it is easy and quick to create something custom.

I did not fully tested the approach, but using window.open(url) works...

I'll think about what I want to achieve, because I need puppeteer to parse and extract my data... So maybe this is simply the wrong way to proceeed...


Source