How to create a plugin library with one click (tiddlyhost)

Note: If you know how to improve the script, please edit this post!

To get started, simply clone the wiki, add your plugins, and enable the “Allow site to be loaded inside an iframe” option in the advanced settings:

image

To install the library, drag and drop the link in the “About” tiddler in the tiddlywiki of your choice.

You can test it here: plugin-library.tiddlyhost.com/

And here’s a wiki making use of that library: https://test-plugin-library.tiddlyhost.com/

Here’s the script that make it possible:

`<script>
/*\ 
title: $:/plugins/tiddlywiki/pluginlibrary/libraryserver.js 
type: application/javascript 
module-type: library 

A simple HTTP-over-window.postMessage implementation of a standard TiddlyWeb-compatible server. 
It uses real HTTP to load the individual tiddler JSON files. 
\*/
(function () {
  /*jslint node: true, browser: true */
  /*global $tw: false */
  "use strict";

  var assetList = [`{{assetList}}`];

  // Listen for window messages
  window.addEventListener(
    "message",
    function listener(event) {
      console.log("plugin library: Received message from", event.origin);
      console.log("plugin library: Message content", event.data);
      switch (event.data.verb) {
        case "GET":
          if (event.data.url === "recipes/library/tiddlers.json") {
            // Route for recipes/library/tiddlers.json
            event.source.postMessage(
              {
                verb: "GET-RESPONSE",
                status: "200",
                cookies: event.data.cookies,
                url: event.data.url,
                type: "application/json",
                body: JSON.stringify(assetList, null, 4),
              },
              "*"
            );
          } else if (event.data.url.indexOf("recipes/library/tiddlers/") === 0) {
            var pluginTitle = removePrefix(
              event.data.url,
              "recipes/library/tiddlers/"
            );

            // Strip the `.json` suffix if present
            if (pluginTitle.endsWith(".json")) {
              pluginTitle = pluginTitle.slice(0, -5); // Remove the last 5 characters (".json")
            }

            var url =
              "`{{$:/SiteUrl}}`/tiddlers.json?title=" +
              pluginTitle +
              "&pretty=1&include_system=1";

            // Route for recipes/library/tiddlers/<uri-encoded-tiddler-title>.json
            httpGet(url, function (err, responseText) {
              if (err) {
                event.source.postMessage(
                  {
                    verb: "GET-RESPONSE",
                    status: "404",
                    cookies: event.data.cookies,
                    url: event.data.url,
                    type: "text/plain",
                    body: "Not found",
                  },
                  "*"
                );
              } else {
                var cleanedResponse = stripOuterBrackets(responseText);
                event.source.postMessage(
                  {
                    verb: "GET-RESPONSE",
                    status: "200",
                    cookies: event.data.cookies,
                    url: event.data.url,
                    type: "application/json",
                    body: cleanedResponse,
                  },
                  "*"
                );
              }
            });
          } else {
            event.source.postMessage(
              {
                verb: "GET-RESPONSE",
                status: "404",
                cookies: event.data.cookies,
                url: event.data.url,
                type: "text/plain",
                body: "Not found",
              },
              "*"
            );
          }
          break;
      }
    },
    false
  );

  // Helper to remove string prefixes
  function removePrefix(string, prefix) {
    if (string.indexOf(prefix) === 0) {
      return string.substr(prefix.length);
    } else {
      return string;
    }
  }

  // Helper to strip outer brackets `[]` from JSON response
  function stripOuterBrackets(jsonString) {
    try {
      var parsed = JSON.parse(jsonString);
      if (Array.isArray(parsed) && parsed.length === 1) {
        return JSON.stringify(parsed[0], null, 4); // Return the first (and only) object in the array
      }
      return jsonString; // Return as-is if not an array or has multiple items
    } catch (error) {
      console.error("Error parsing JSON:", error);
      return jsonString; // Return original string if parsing fails
    }
  }

  // Helper for HTTP GET
  function httpGet(url, callback) {
    var http = new XMLHttpRequest();
    http.open("GET", url, true);
    http.onreadystatechange = function () {
      if (http.readyState == 4) {
        if (http.status == 200) {
          callback(null, http.responseText);
        } else {
          callback(new Error("HTTP error: " + http.status), null);
        }
      }
    };
    http.send();
  }
})();
</script>`

https://plugin-library.tiddlyhost.com/#%24%3A%2Fplugins%2Fdesignthinkerer%2Ftiddlyhostpluginlibrary%2Fscript

2 Likes

@telumire I am amazed and grateful you created this, I have played with the example library and it all seems quite usable.

  • For my personal use I can go strait ahead and use it
  • I had researched something similar in the past, and asked the community with no responce.

Why so good?

We can now democratise the use of libraries for pubilc or personal use with the advantage that such a distribution method would allow changes to be detected and reinstalled.

It seems to me your solution may benifit from the following and I would be happy to submit some content if you are interested;

  • A high level technical description of the solution
    • I may be ble to do this but would need a technical review
  • Some simple point and click named library creation and select plugins
  • Add some content to support users arriving at the plugin library wiki
    • Including the library owner
  • A guide to responcible library publishing

Uses and futures

  • I wonder if we can use this to distribute our own plugin/content from file:// wikis ?
  • It would be nice to introduce the ability to drive the assetList via a confgurable filter. Allowing sophisticated sites, such as an edition to use multiple plugins but only add specific plugins to the library.

Questions

  • How can we include Themes and Languages?
  • Why is the PluginLibraryScript introduced via TopHead rather than installed as a plugin?
    • So it is not in the plugin list? A method worth documenting.
  • Could we support multiple libraries from the same site? I expect not as the url to the library is the site, I expect thisisrelated to the last question, the HTML needs to be read without loading in a browser.

I have more ideas I will keep to myself for now, so as to not be too verbose. Hint other tiddlers via plugin mechanisium such as data tiddlers, additional plugin types

Prior art

Glad you find this helpful, @TW_Tones! :slight_smile:

I’m using the tag $:/tags/RawMarkupWikified/TopHead to insert the necessary JavaScript directly into the <head> section of the TiddlyWiki file. You can view the code by visiting this URL: view-source:https://plugin-library.tiddlyhost.com (or pressing Ctrl + U).

Here’s an overview of how a plugin library works:

  1. When you click “Open plugin library,” TiddlyWiki sends a GET request to the library’s URL (e.g., https://tiddlywiki.com/library/v5.2.7/index.html for the official plugin library).
  2. The library responds with a list of available plugins (in this case, defined in the assetList tiddler)
  3. TiddlyWiki then displays this list to allow you to choose a plugin to install.
  4. When you select a plugin to install, TiddlyWiki makes another request, specifying the plugin. The library fetches the content of the plugin and sends it back to TiddlyWiki.

Instead of fetching the plugin file from the same directory as the library, the script redirects to the JSON endpoint provided by Tiddlyhost. Since TiddlyWiki appends a .json extension to the requested file, I adapted the library script to handle this properly. However, if Tiddlyhost ever stops offering these endpoints, the script will unfortunately stop working.

To demonstrate, I’ve added two plugins to the library along with a short tutorial on creating your own plugin.

While I considered including the Gatha plugin to simplify plugin creation, I decided against it to keep the library’s TiddlyWiki lightweight and avoid long loading times.

I’m not sure how this would need to be adapted to also serve themes and languages, if you have a solution please share it and I will update the wiki (or you can put your own wiki in the first post if you’d like)

Edit: I think it should be possible to adapt the script to work without the json endpoint. Not sure how well this would perform but for small wikis/plugins it should be fine. I will do some test and report my findings

1 Like