|
;; 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) |