From 903a9356a9eb24a9eb489a87b8751265c550f458 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 22 Mar 2022 08:12:31 +0100 Subject: [PATCH] :bug: Fix many issues after PR review --- docker/devenv/files/nginx.conf | 8 +- exporter/package.json | 2 +- exporter/src/app/browser.cljs | 8 +- exporter/src/app/handlers/resources.cljs | 7 +- exporter/src/app/http.cljs | 2 +- exporter/src/app/renderer/bitmap.cljs | 13 +++- exporter/src/app/renderer/pdf.cljs | 14 +++- exporter/src/app/renderer/svg.cljs | 47 +++++++----- exporter/yarn.lock | 37 ++++++--- frontend/gulpfile.js | 12 ++- frontend/resources/templates/render.mustache | 26 +++++++ frontend/shadow-cljs.edn | 6 ++ frontend/src/app/main.cljs | 17 +---- frontend/src/app/main/repo.cljs | 4 +- frontend/src/app/main/ui/render.cljs | 15 ++-- .../sidebar/options/menus/exports.cljs | 3 +- frontend/src/app/render.cljs | 76 +++++++++++++++++++ frontend/src/app/util/dom.cljs | 45 +++++------ 18 files changed, 243 insertions(+), 99 deletions(-) create mode 100644 frontend/resources/templates/render.mustache create mode 100644 frontend/src/app/render.cljs diff --git a/docker/devenv/files/nginx.conf b/docker/devenv/files/nginx.conf index 0560dc0b04..2d62807818 100644 --- a/docker/devenv/files/nginx.conf +++ b/docker/devenv/files/nginx.conf @@ -103,6 +103,10 @@ http { add_header x-internal-redirect "$upstream_http_x_accel_redirect"; } + location /api/export { + proxy_pass http://127.0.0.1:6061; + } + location /api { proxy_pass http://127.0.0.1:6060/api; } @@ -115,10 +119,6 @@ http { proxy_pass http://127.0.0.1:6060/dbg; } - location /export { - proxy_pass http://127.0.0.1:6061; - } - location /telemetry { proxy_pass http://127.0.0.1:6070/inbox; } diff --git a/exporter/package.json b/exporter/package.json index 6c3dfd5a7e..83f70bfca1 100644 --- a/exporter/package.json +++ b/exporter/package.json @@ -15,7 +15,7 @@ "inflation": "^2.0.0", "ioredis": "^4.28.5", "luxon": "^2.3.1", - "playwright": "^1.19.2", + "playwright": "^1.20.0", "raw-body": "^2.5.1", "xml-js": "^1.6.11", "xregexp": "^5.0.2" diff --git a/exporter/src/app/browser.cljs b/exporter/src/app/browser.cljs index e12e6e79cc..1a37e9f0c2 100644 --- a/exporter/src/app/browser.cljs +++ b/exporter/src/app/browser.cljs @@ -69,13 +69,9 @@ (defn pdf ([page] (pdf page {})) - ([page {:keys [width height scale save-path] - :or {width default-viewport-width - height default-viewport-height - scale 1}}] + ([page {:keys [scale save-path] + :or {scale 1}}] (.pdf ^js page #js {:path save-path - :width width - :height height :scale scale :printBackground true :preferCSSPageSize true}))) diff --git a/exporter/src/app/handlers/resources.cljs b/exporter/src/app/handlers/resources.cljs index 396c2cf654..f9e3716fa4 100644 --- a/exporter/src/app/handlers/resources.cljs +++ b/exporter/src/app/handlers/resources.cljs @@ -27,10 +27,11 @@ (defn- get-mtype [type] (case (d/name type) - "zip" "application/zip" + "zip" "application/zip" + "pdf" "application/pdf" + "svg" "image/svg+xml" "jpeg" "image/jpeg" - "png" "image/png" - "pdf" "application/pdf")) + "png" "image/png")) (defn create "Generates ephimeral resource object." diff --git a/exporter/src/app/http.cljs b/exporter/src/app/http.cljs index cb8156bfa4..846a8e28da 100644 --- a/exporter/src/app/http.cljs +++ b/exporter/src/app/http.cljs @@ -65,7 +65,7 @@ (defn- wrap-body-params [handler] - (let [opts #js {:limit "2mb" :encoding "utf8"}] + (let [opts #js {:limit "60mb" :encoding "utf8"}] (fn [{:keys [:request/method :request/headers request] :as exchange}] (let [ctype (get headers "content-type")] (if (= method "post") diff --git a/exporter/src/app/renderer/bitmap.cljs b/exporter/src/app/renderer/bitmap.cljs index 2e06cbd624..40ae118477 100644 --- a/exporter/src/app/renderer/bitmap.cljs +++ b/exporter/src/app/renderer/bitmap.cljs @@ -13,6 +13,7 @@ [app.common.logging :as l] [app.common.pages :as cp] [app.common.spec :as us] + [app.common.uri :as u] [app.config :as cf] [cljs.spec.alpha :as s] [cuerdas.core :as str] @@ -20,10 +21,14 @@ (defn screenshot-object [{:keys [file-id page-id object-id token scale type uri]}] - (p/let [path (str "/render-object/" file-id "/" page-id "/" object-id) - uri (-> (or uri (cf/get :public-uri)) - (assoc :path "/") - (assoc :fragment path))] + (p/let [params {:file-id file-id + :page-id page-id + :object-id object-id + :route "render-object"} + + uri (-> (or uri (cf/get :public-uri)) + (assoc :path "/render.html") + (assoc :query (u/map->query-string params)))] (bw/exec! #js {:screen #js {:width bw/default-viewport-width :height bw/default-viewport-height} diff --git a/exporter/src/app/renderer/pdf.cljs b/exporter/src/app/renderer/pdf.cljs index 6055a900c4..3131ce22ea 100644 --- a/exporter/src/app/renderer/pdf.cljs +++ b/exporter/src/app/renderer/pdf.cljs @@ -11,16 +11,21 @@ [app.common.exceptions :as ex :include-macros true] [app.common.logging :as l] [app.common.spec :as us] + [app.common.uri :as u] [app.config :as cf] [cljs.spec.alpha :as s] [promesa.core :as p])) (defn pdf-from-object [{:keys [file-id page-id object-id token scale type save-path uri] :as params}] - (p/let [path (str "/render-object/" file-id "/" page-id "/" object-id) - uri (-> (or uri (cf/get :public-uri)) - (assoc :path "/") - (assoc :fragment path))] + (p/let [params {:file-id file-id + :page-id page-id + :object-id object-id + :route "render-object"} + uri (-> (or uri (cf/get :public-uri)) + (assoc :path "/render.html") + (assoc :query (u/map->query-string params)))] + (bw/exec! #js {:screen #js {:width bw/default-viewport-width :height bw/default-viewport-height} @@ -37,6 +42,7 @@ (p/let [dom (bw/select page "#screenshot")] (bw/wait-for dom) (bw/screenshot dom {:full-page? true}) + (bw/sleep page 2000) ; the good old fix with sleep (if save-path (bw/pdf page {:save-path save-path}) (bw/pdf page)))))))) diff --git a/exporter/src/app/renderer/svg.cljs b/exporter/src/app/renderer/svg.cljs index da5f6e768e..20d883d24c 100644 --- a/exporter/src/app/renderer/svg.cljs +++ b/exporter/src/app/renderer/svg.cljs @@ -14,6 +14,7 @@ [app.common.logging :as l] [app.common.pages :as cp] [app.common.spec :as us] + [app.common.uri :as u] [app.config :as cf] [app.util.shell :as sh] [cljs.spec.alpha :as s] @@ -322,30 +323,34 @@ result)) ] - (p/let [path (str "/render-object/" file-id "/" page-id "/" object-id "?render-texts=true") - uri (-> (or uri (cf/get :public-uri)) - (assoc :path "/") - (assoc :fragment path))] + (p/let [params {:file-id file-id + :page-id page-id + :object-id object-id + :render-texts true + :route "render-object"} - (bw/exec! - #js {:screen #js {:width bw/default-viewport-width + uri (-> (or uri (cf/get :public-uri)) + (assoc :path "/render.html") + (assoc :query (u/map->query-string params)))] + + (bw/exec! + #js {:screen #js {:width bw/default-viewport-width + :height bw/default-viewport-height} + :viewport #js {:width bw/default-viewport-width :height bw/default-viewport-height} - :viewport #js {:width bw/default-viewport-width - :height bw/default-viewport-height} - :locale "en-US" - :storageState #js {:cookies (bw/create-cookies uri {:token token})} - :deviceScaleFactor scale - :userAgent bw/default-user-agent} - (fn [page] - (l/info :uri uri) - (p/do! - (bw/nav! page uri) - (p/let [dom (bw/select page "#screenshot")] - (js/console.log "FFFF" dom) - (bw/wait-for dom) - (bw/sleep page 2000)) + :locale "en-US" + :storageState #js {:cookies (bw/create-cookies uri {:token token})} + :deviceScaleFactor scale + :userAgent bw/default-user-agent} + (fn [page] + (l/info :uri uri) + (p/do! + (bw/nav! page uri) + (p/let [dom (bw/select page "#screenshot")] + (bw/wait-for dom) + (bw/sleep page 2000)) - (extract page))))))) + (extract page))))))) (s/def ::name ::us/string) (s/def ::suffix ::us/string) diff --git a/exporter/yarn.lock b/exporter/yarn.lock index 32bc6a44bd..a33887816b 100644 --- a/exporter/yarn.lock +++ b/exporter/yarn.lock @@ -239,6 +239,11 @@ cluster-key-slot@^1.1.0: resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d" integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw== +colors@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== + commander@8.3.0: version "8.3.0" resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" @@ -827,17 +832,26 @@ pend@~1.2.0: resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= -playwright-core@1.19.2: - version "1.19.2" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.19.2.tgz#90b9209554f174c649abf495952fcb4335437218" - integrity sha512-OsL3sJZIo1UxKNWSP7zW7sk3FyUGG06YRHxHeBw51eIOxTCQRx5t+hXd0cvXashN2CHnd3hIZTs2aKa/im4hZQ== +pixelmatch@5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-5.2.1.tgz#9e4e4f4aa59648208a31310306a5bed5522b0d65" + integrity sha512-WjcAdYSnKrrdDdqTcVEY7aB7UhhwjYQKYhHiBXdJef0MOaQeYpUdQ+iVyBLa5YBKS8MPVPPMX7rpOByISLpeEQ== dependencies: + pngjs "^4.0.1" + +playwright-core@1.20.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.20.0.tgz#57e84d7663cada92fe0d5574e9cd42e5fa2e74e1" + integrity sha512-d25IRcdooS278Cijlp8J8A5fLQZ+/aY3dKRJvgX5yjXA69N0huIUdnh3xXSgn+LsQ9DCNmB7Ngof3eY630jgdA== + dependencies: + colors "1.4.0" commander "8.3.0" debug "4.3.3" extract-zip "2.0.1" https-proxy-agent "5.0.0" jpeg-js "0.4.3" mime "3.0.0" + pixelmatch "5.2.1" pngjs "6.0.0" progress "2.0.3" proper-lockfile "4.1.2" @@ -849,18 +863,23 @@ playwright-core@1.19.2: yauzl "2.10.0" yazl "2.5.1" -playwright@^1.19.2: - version "1.19.2" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.19.2.tgz#d9927ae8512482642356e50a286c5767dfb7a621" - integrity sha512-2JmGWr/Iw/Uu27bZULeHgjn8doNrRVxIYdhspMuMlfKNpzwAe/sfm7wH8uey6jiZxnPL4bC5V4ACQcF4dAGWnw== +playwright@^1.20.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.20.0.tgz#f80b131c0146afc4645fcd5cfcc2ca11895ba58a" + integrity sha512-YcFXhXttk9yvpc8PMbfvts6KEopXjxdBh47BdOiA7xhjF/gkXeSM0Hs9CSdbL9mp2xtlB5xqE7+D+F2soQOjbA== dependencies: - playwright-core "1.19.2" + playwright-core "1.20.0" pngjs@6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-6.0.0.tgz#ca9e5d2aa48db0228a52c419c3308e87720da821" integrity sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg== +pngjs@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-4.0.1.tgz#f803869bb2fc1bfe1bf99aa4ec21c108117cfdbe" + integrity sha512-rf5+2/ioHeQxR6IxuYNYGFytUyG3lma/WW1nsmjeHlWwtb2aByla6dkVc8pmJ9nplzkTA0q2xx7mMWrOTqT4Gg== + printj@~1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/printj/-/printj-1.3.1.tgz#9af6b1d55647a1587ac44f4c1654a4b95b8e12cb" diff --git a/frontend/gulpfile.js b/frontend/gulpfile.js index ddf283dc65..6126e33859 100644 --- a/frontend/gulpfile.js +++ b/frontend/gulpfile.js @@ -136,6 +136,7 @@ function templatePipeline(options) { return function() { const input = options.input; const output = options.output; + const name = options.name; const ts = Math.floor(new Date()); const th = process.env.APP_THEME || "default"; @@ -154,7 +155,7 @@ function templatePipeline(options) { return gulp.src(input) .pipe(tmpl) - .pipe(gulpRename("index.html")) + .pipe(gulpRename(name)) .pipe(gulp.dest(output)) .pipe(touch()); }; @@ -191,11 +192,18 @@ gulp.task("svg:sprite:cursors", function() { }); gulp.task("template:main", templatePipeline({ + name: "index.html", input: paths.resources + "templates/index.mustache", output: paths.output })); -gulp.task("templates", gulp.series("svg:sprite:icons", "svg:sprite:cursors", "template:main")); +gulp.task("template:render", templatePipeline({ + name: "render.html", + input: paths.resources + "templates/render.mustache", + output: paths.output +})); + +gulp.task("templates", gulp.series("svg:sprite:icons", "svg:sprite:cursors", "template:main", "template:render")); gulp.task("polyfills", function() { return gulp.src(paths.resources + "polyfills/*.js") diff --git a/frontend/resources/templates/render.mustache b/frontend/resources/templates/render.mustache new file mode 100644 index 0000000000..fa0c9b24c1 --- /dev/null +++ b/frontend/resources/templates/render.mustache @@ -0,0 +1,26 @@ + + + + + + Penpot - Render + + + + + {{# manifest}} + + + {{/manifest}} + + +
+ {{# manifest}} + + + {{/manifest}} + + diff --git a/frontend/shadow-cljs.edn b/frontend/shadow-cljs.edn index f5fd67d0f2..60985de8c4 100644 --- a/frontend/shadow-cljs.edn +++ b/frontend/shadow-cljs.edn @@ -15,9 +15,15 @@ :modules {:shared {:entries []} + :main {:entries [app.main] :depends-on #{:shared} :init-fn app.main/init} + + :render {:entries [app.render] + :depends-on #{:shared} + :init-fn app.render/init} + :worker {:entries [app.worker] :web-worker true :depends-on #{:shared}}} diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index 4c8a403555..bddddc9bf3 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -21,11 +21,9 @@ [app.main.ui.routes :as rt] [app.main.worker :as worker] [app.util.dom :as dom] - [app.util.globals :as glob] [app.util.i18n :as i18n] [app.util.theme :as theme] [beicon.core :as rx] - [cuerdas.core :as str] [debug] [potok.core :as ptk] [rumext.alpha :as mf])) @@ -68,19 +66,12 @@ (rx/take 1) (rx/map #(ws/initialize))))))) - -(def essential-only? - (let [href (.-href ^js glob/location)] - (str/includes? href "essential=t"))) - (defn ^:export init [] - (when-not essential-only? - (worker/init!) - (sentry/init!) - (i18n/init! cf/translations) - (theme/init! cf/themes)) - + (worker/init!) + (sentry/init!) + (i18n/init! cf/translations) + (theme/init! cf/themes) (init-ui) (st/emit! (initialize))) diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index 8574e19051..fab5ef9b7f 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -108,7 +108,7 @@ (defn- send-export-command [& {:keys [cmd params blob?]}] (->> (http/send! {:method :post - :uri (u/join base-uri "export") + :uri (u/join base-uri "api/export") :body (http/transit-data (assoc params :cmd cmd)) :credentials "include" :response-type (if blob? :blob :text)}) @@ -137,7 +137,7 @@ :wait false :exports exports}] (->> (http/send! {:method :post - :uri (u/join base-uri "export") + :uri (u/join base-uri "api/export") :body (http/transit-data params) :credentials "include" :response-type :blob}) diff --git a/frontend/src/app/main/ui/render.cljs b/frontend/src/app/main/ui/render.cljs index af1a5ae66e..3b34dee801 100644 --- a/frontend/src/app/main/ui/render.cljs +++ b/frontend/src/app/main/ui/render.cljs @@ -52,7 +52,9 @@ (mf/defc object-svg {::mf/wrap [mf/memo]} - [{:keys [objects object-id zoom render-texts?] :or {zoom 1} :as props}] + [{:keys [objects object-id zoom render-texts?] + :or {zoom 1} + :as props}] (let [object (get objects object-id) frame-id (if (= :frame (:type object)) (:id object) @@ -70,9 +72,9 @@ objects (reduce updt-fn objects mod-ids) object (get objects object-id) - object (cond-> object - (:hide-fill-on-export object) - (assoc :fills [])) + object (cond-> object + (:hide-fill-on-export object) + (assoc :fills [])) all-children (cph/get-children objects object-id) @@ -100,8 +102,9 @@ render-texts? (and render-texts? (d/seek (comp nil? :position-data) text-shapes))] (mf/with-effect [width height] - (dom/set-page-style {:size (str (mth/ceil width) "px " - (mth/ceil height) "px")})) + (dom/set-page-style! + {:size (str (mth/ceil width) "px " + (mth/ceil height) "px")})) [:& (mf/provider embed/context) {:value false} [:svg {:id "screenshot" diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs index 0fe548cf95..c28bcccb14 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs @@ -60,7 +60,8 @@ :object-id (first ids)} exports (mapv #(merge % defaults) exports)] (if (= 1 (count exports)) - (st/emit! (de/request-simple-export {:export (first exports)})) + (let [export (first exports)] + (st/emit! (de/request-simple-export {:export export :filename (:name export)}))) (st/emit! (de/request-multiple-export {:exports exports :filename filename}))))))) ;; TODO: maybe move to specific events for avoid to have this logic here? diff --git a/frontend/src/app/render.cljs b/frontend/src/app/render.cljs new file mode 100644 index 0000000000..b8681028fc --- /dev/null +++ b/frontend/src/app/render.cljs @@ -0,0 +1,76 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.render + "The main entry point for UI part needed by the exporter." + (:require + [app.common.logging :as log] + [app.common.spec :as us] + [app.common.uri :as u] + [app.config :as cf] + [app.main.ui.render :as render] + [app.util.dom :as dom] + [app.util.globals :as glob] + [clojure.spec.alpha :as s] + [rumext.alpha :as mf])) + +(log/initialize!) +(log/set-level! :root :warn) +(log/set-level! :app :info) + +(declare reinit) + +(declare ^:private render-object) + +(log/info :hint "Welcome to penpot (Export)" + :version (:full @cf/version) + :public-uri (str cf/public-uri)) + + +(defn- parse-params + [loc] + (let [href (unchecked-get loc "href")] + (some-> href u/uri :query u/query-string->map))) + +(defn init-ui + [] + (when-let [params (parse-params glob/location)] + (when-let [component (case (:route params) + "render-object" (render-object params) + nil)] + (mf/mount component (dom/get-element "app"))))) + +(defn ^:export init + [] + (init-ui)) + +(defn reinit + [] + (mf/unmount (dom/get-element "app")) + (init-ui)) + +(defn ^:dev/after-load after-load + [] + (reinit)) + +(s/def ::page-id ::us/uuid) +(s/def ::file-id ::us/uuid) +(s/def ::object-id ::us/uuid) +(s/def ::render-text ::us/boolean) + +(s/def ::render-object-params + (s/keys :req-un [::file-id ::page-id ::object-id] + :opt-un [::render-text])) + +(defn- render-object + [params] + (let [{:keys [page-id file-id object-id render-texts]} (us/conform ::render-object-params params)] + (mf/html + [:& render/render-object + {:file-id file-id + :page-id page-id + :object-id object-id + :render-texts? (and (some? render-texts) (= render-texts "true"))}]))) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index dac8dffc0b..a08ac9a681 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -6,13 +6,14 @@ (ns app.util.dom (:require - [app.common.exceptions :as ex] - [app.common.geom.point :as gpt] - [app.util.globals :as globals] - [app.util.object :as obj] - [cuerdas.core :as str] - [goog.dom :as dom] - [promesa.core :as p])) + [app.common.data.macros :as dm] + [app.common.exceptions :as ex] + [app.common.geom.point :as gpt] + [app.util.globals :as globals] + [app.util.object :as obj] + [cuerdas.core :as str] + [goog.dom :as dom] + [promesa.core :as p])) ;; --- Deprecated methods @@ -33,25 +34,25 @@ ;; --- New methods +(declare get-elements-by-tag) + (defn set-html-title [^string title] (set! (.-title globals/document) title)) -(defn set-page-style - [style] - (let [head (first (.getElementsByTagName ^js globals/document "head")) - style-str (str/join "\n" - (map (fn [[k v]] - (str (name k) ": " v ";")) - style))] - (.insertAdjacentHTML head "beforeend" - (str "")))) +(defn set-page-style! + [styles] + (let [node (first (get-elements-by-tag globals/document "head")) + style (reduce-kv (fn [res k v] + (conj res (dm/str (str/css-selector k) ":" v ";"))) + [] + styles) + style (dm/str "")] + (.insertAdjacentHTML ^js node "beforeend" style))) + (defn get-element-by-class ([classname]