Skip to content

Instantly share code, notes, and snippets.

@henrik42
Last active August 7, 2024 17:57
Show Gist options
  • Select an option

  • Save henrik42/885547a26ddf0bfacef4344bf128683f to your computer and use it in GitHub Desktop.

Select an option

Save henrik42/885547a26ddf0bfacef4344bf128683f to your computer and use it in GitHub Desktop.
The "Scittle Bookmarklet creator" bookmarklet

The "Scittle Bookmarklet creator"-Bookmarklet-Creator

Thank you so much!!

First of all I want to thank borkdude and all the other people around Babashka for building SCI, Scittle and many other things that so many people can use and learn from! You guys rock!

Background

At https://babashka.org/scittle/bookmarklet.html you can use the "Scittle Bookmarklet creator" (SBC from now on) to interactively create bookmarklets. This app and the bookmarklet that it creates are powered by a few JavaScript libs and the "Small Clojure Interpreter (SCI)".

SBC is written in a Clojure dialect that can be interpreted by SCI which is also written in a Clojure dialect that can be transpiled to JavaScript which runs in your browser.

SBC lets you write bookmarklets in the Clojure dialect that SCI can interpret. When the bookmarklet gets executed it uses SCI to interpret your bookmarklet code in your browser.

Invoking as a bookmarklet

One thing that bugged me was that I couldn't change the source of the SBC. The source is public so I could have set up a web server hosting/serving that code, but I thought 'why not use a bookmarklet to run SBC'?

The first step was to come up with a bookmarklet that runs the original code from https://babashka.org/scittle/cljs/bookmarklet.cljs. That's what The "Scittle Bookmarklet creator" bookmarklet is.

This bookmarklet is different from the ones that SBC generates: it produces a new DOM and loads the sources via <script> elements. So when using this bookmarklet it will replace the current page/DOM. So it's more like an app which creates a new page than a function you call within your current app/page.

When you use this kind of bookmarklet you can ctrl-click on it to open a new page in which the bookmarklet will run.

Hosting via github/gist

The second step was easy (The henrik42 "Scittle Bookmarklet creator" bookmarklet): instead of including the original source from https://babashka.org/scittle/cljs/bookmarklet.cljs I include my own gist from https://gist.githubusercontent.com/henrik42/885547a26ddf0bfacef4344bf128683f/raw/bookmarklet.cljs

Now I can run my own SBC.

Extending bookmarklet.cljs

I wanted to extend the original SBC so that it offers the option to create an "app-like" bookmarklet, that would run your bookmarklet code in a new page/DOM -- just like the 'The henrik42 "Scittle Bookmarklet creator" bookmarklet"'.

So this is what app-bookmarklet? is for in the code.

Pulling yourself out of the swamp by your own hair!

Now that we have a bookmarklet creator that can create bookmarklets which run Clojure code, you may ask: can we run the (extended) bookmarklet creator as a bookmarklet which is created by the (extended) bookmarklet creator to create such a bookmarklet creator creator ... ähh...WAT? Yes you can!

Of course we all know this from building compilers ;-)

Local Development

When developing your bookmarklet it's a lot more fun to serve the code from your local file system rather than editing a gist all the time.

I'm using an nginx docker container for this:

docker run --rm -p 8080:80 --name nginx -v %CD%:/usr/share/nginx/html -v %CD%\default.conf:/etc/nginx/conf.d/default.conf nginx

And then use:

<script type="application/x-scittle" src="http://localhost:8080/bookmarklet.cljs"></script>
;; This has been copied from https://babashka.org/scittle/cljs/bookmarklet.cljs
;; I changed just a few things:
;; * removed "Copy this link to share", since we cannot share links to our local DOM (yet ;-)
;; * added app-bookmarklet feature
;; * added add-tailwind feature
(ns bookmarklet
(:require [reagent.core :as r]
[reagent.dom :as rdom]
[clojure.string :as str]))
(defn append-tag [tag {:keys [body onload onerror] :as attributes}]
(str "var s=document.createElement('" (name tag) "');"
(clojure.string/join ";" (map (fn [[k v]] (str "s.setAttribute('" (name k) "','" (name v) "')")) (dissoc attributes :body :onload :onerror)))
(when body
(str ";s.innerText=" body))
(when onload
(str ";s.onload=" onload))
(when onerror
(str ";s.onerror=" onerror))
";document.body.appendChild(s);"))
(defn pr-code [code-str]
(pr-str (str "#_CODE_" code-str "#_CODE_")))
(defn read-code [code-str]
(when-let [raw-code (second (re-find #"#_CODE_(.+)#_CODE_" code-str))]
;; Use read-string to undo escaping of characters by pr-str (e.g. newlines)
(read-string (str "\"" raw-code "\""))))
(defn load-gist [gist callback]
(let [set-content (fn [progress-event]
(callback (.. progress-event -srcElement -responseText)))
oreq (js/XMLHttpRequest.)]
(.addEventListener oreq "load" set-content)
(.open oreq "GET" (str "https://gist.githubusercontent.com/" gist "/raw"))
(.send oreq)))
(def hello-world!
"'Hello'\\&%s%24, \n\"</script>!\"")
;; needed this for developing `externalize-code-string` :-)
(defn spy [& xs]
#_ (js/console.log (apply str (mapcat str xs)))
(last xs))
;; Special treatment:
;; line-breaks: -> \n
;; double-quotes: -> \"
;; quotes: -> \'
;; backslashes: -> \\
;; percent: -> \u0025
;; <script: -> \u003C/script
(defn externalize-code-string [s]
(let [_ (spy "s = " s)
;; new-lines -> \n, double-quotes around
s (spy "pr-str = " (pr-str s))
;; backslashes are special in javascript URL quoted output we're producing. So they have to be escaped
s (spy (str/replace s #"\\" "\\\\"))
;; % -> \u0025
s (spy (str/replace s #"%" "\\\\u0025"))
;; quotes are special in javascript URL quoted output we're producing. So they have to be escaped
s (spy (str/replace s #"'" "\\'"))
;; https://stackoverflow.com/questions/14780858/escape-in-script-tag-contents
;; https://stackoverflow.com/questions/39193510/how-to-insert-arbitrary-json-in-htmls-script-tag
s (spy (str/replace s #"</script" "\\\\u003C/script"))
]
s))
(defn bookmarklet-href [code-str app-bookmarklet? add-tailwind?]
(if app-bookmarklet?
(str/replace
(str
"javascript:'<!DOCTYPE html>"
(when add-tailwind?
"<script src=\"https://cdn.tailwindcss.com\"></script>")
"<script src=\"https://unpkg.com/[email protected]/umd/react.production.min.js\" defer=\"true\"></script>
<script src=\"https://unpkg.com/[email protected]/umd/react-dom.production.min.js\" defer=\"true\"></script>
<script src=\"https://cdn.jsdelivr.net/npm/[email protected]/dist/scittle.js\" defer=\"true\"></script>
<script src=\"https://cdn.jsdelivr.net/npm/[email protected]/dist/scittle.reagent.js\" defer=\"true\"></script>"
"<script type=\"application/x-scittle\">"
"(js/scittle.core.eval_string " (externalize-code-string code-str) ")"
"</script>'")
;; Take care of line-breaks which become %0A in the URL link.
#"[\n\r]" "")
(str "javascript:(function(){"
"var runCode = function() {
try {
scittle.core.eval_string(" (str/replace (pr-code code-str) #"%" "\\\\u0025") ")
} catch (error) {
console.log('Error in code', error);
alert('Error running code, see console')
}
};"
"if(typeof scittle === 'undefined'){"
(append-tag :script {:src "https://babashka.github.io/scittle/js/scittle.js"
:onerror "function(){alert('Error loading ' + this.src)}"
:onload "runCode"})
"} else {
runCode() }"
"})();")))
(defn query-params []
(let [query-str (.substring js/window.location.search 1)]
(into {}
(map (fn [pair]
(let [[k v] (.split pair "=" 2)]
[(keyword (js/decodeURIComponent k))
(js/decodeURIComponent v)])))
(.split query-str "&"))))
(def *initial-name (r/atom nil))
(def *initial-code (r/atom nil))
;; Initialize code
(let [{:keys [gist code name]} (query-params)]
(cond gist
(do
(reset! *initial-name "---")
(reset! *initial-code ";; loading from gist")
(load-gist gist (fn [content]
(let [[code meta-str] (reverse (clojure.string/split content #";;---+\n"))
{bookmark-name :name} (when meta-str
(read-string meta-str))]
(when bookmark-name
(reset! *initial-name bookmark-name))
(reset! *initial-code code)))))
code
(do
(reset! *initial-name (or name "My first bookmarklet"))
(reset! *initial-code code))
:else
(do
(reset! *initial-name "My first bookmarklet")
(reset! *initial-code (str "; This is the code of your bookmarklet\n"
(pr-str (list 'js/alert hello-world!)))))))
(defn bookmark-name-field [initial-name *bookmark-name]
(let [*name (r/atom initial-name)]
[(fn []
[:input {:type "text"
:placeholder "The name of the Bookmarklet"
:value @*name
:on-change (fn [e]
(let [v (.. e -target -value)]
(reset! *name v)
(reset! *bookmark-name
(if (clojure.string/blank? v)
(str "Bookmarklet " (rand-int 1000))
v))))}])]))
(defn app-use-case-select-field [*app-bookmarklet?]
[:span "Create app-use-case bookmarklet?"
[:input {:type "checkbox"
:checked @*app-bookmarklet?
:on-change #(swap! *app-bookmarklet? not)}]])
(defn add-tailwind-select-field [*add-tailwind?]
[:span "Include tailwind-lib-script?"
[:input {:type "checkbox"
:checked @*add-tailwind?
:on-change #(swap! *add-tailwind? not)}]])
(defn editor [*code]
[:textarea
{:rows 10 :cols 80
:value @*code
:on-drop (fn [e]
(let [bookmarklet (js/decodeURIComponent (.. e -dataTransfer (getData "text")))
cljs-snippet (read-code bookmarklet)
new-code (if cljs-snippet
(str "; Extracted snippet\n" cljs-snippet)
(str "; Failed to extract snippet\n" bookmarklet))]
(js/console.log "Dropped" bookmarklet)
(set! (.. e -target -value) new-code)
(reset! *code new-code)
(.preventDefault e)))
:on-change (fn [e] (reset! *code (.. e -target -value)))}])
(defn workspace []
(let [value @*initial-code
*code (r/atom value)
bookmark-name @*initial-name
*bookmark-name (r/atom bookmark-name)
*app-bookmarklet? (r/atom false)
*add-tailwind? (r/atom false)]
[:div
[bookmark-name-field bookmark-name *bookmark-name]
[app-use-case-select-field *app-bookmarklet?]
[add-tailwind-select-field *add-tailwind?]
[:br]
[editor *code]
[:br]
[:br]
"Click the following link or drag it to the bookmarks bar: "
[(fn []
[(fn [] [:a {:href (bookmarklet-href @*code @*app-bookmarklet? @*add-tailwind?)} @*bookmark-name])])]]))
(rdom/render [workspace] js/document.body)
## /etc/nginx/conf.d/default.conf
## docker run --rm -p 8080:80 --name nginx -v %CD%:/usr/share/nginx/html -v %CD%\default.conf:/etc/nginx/conf.d/default.conf nginx:1.21.0
server {
listen 80;
listen [::]:80;
server_name localhost;
location / {
add_header 'Access-Control-Allow-Origin' '*';
root /usr/share/nginx/html;
}
}
javascript:'
<!DOCTYPE html>
<script src="https://unpkg.com/[email protected]/umd/react.production.min.js" defer="true"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js" defer="true"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/scittle.js" defer="true"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/scittle.reagent.js" defer="true"></script>
<script type="application/x-scittle" src="https://babashka.org/scittle/cljs/bookmarklet.cljs"></script>
<body id="app"></body>
'
javascript:'
<!DOCTYPE html>
<script src="https://unpkg.com/[email protected]/umd/react.production.min.js" defer="true"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js" defer="true"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/scittle.js" defer="true"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/scittle.reagent.js" defer="true"></script>
<script type="application/x-scittle" src="https://gist.githubusercontent.com/henrik42/885547a26ddf0bfacef4344bf128683f/raw/bookmarklet.cljs"></script>
'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment