0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-15 17:21:17 -05:00

Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Andrey Antukh 2022-04-07 10:16:29 +02:00
commit 58905f0b99
95 changed files with 2088 additions and 1836 deletions

View file

@ -47,6 +47,11 @@
### :bug: Bugs fixed
- "Show in exports" is showing in multiselections [Taiga 3194](https://tree.taiga.io/project/penpot/issue/3194)
- Fix line gap between shapes [Taiga #3181](https://tree.taiga.io/project/penpot/issue/3181)
- Edit file name navigates to the file workspace [Taiga #3183](https://tree.taiga.io/project/penpot/issue/3183)
- Fix scroll into view behind fixed element [Taiga #3170](https://tree.taiga.io/project/penpot/issue/3170)
- Fix sidebar icon in viewer mode [Taiga #3184](https://tree.taiga.io/project/penpot/issue/3184)
- Fix send to back several shapes at a time [Taiga #3077](https://tree.taiga.io/project/penpot/issue/3077)
- Fix duplicate multi selected elements [Taiga #3155](https://tree.taiga.io/project/penpot/issue/3155)
- Fix add fills to artboard modify children [Taiga #3151](https://tree.taiga.io/project/penpot/issue/3151)
@ -81,6 +86,8 @@
- Fix drag guides to delete target area [#1679](https://github.com/penpot/penpot/issues/1679)
- Fix undo when rotating groups [Taiga #3136](https://tree.taiga.io/project/penpot/issue/3136)
- Fix component name in sidebar widget [Taiga #3144](https://tree.taiga.io/project/penpot/issue/3144)
- Fix resize rotated shape with top&down constraints [Taiga #3167](https://tree.taiga.io/project/penpot/issue/3167)
- Fix multi user not working [Taiga #3195](https://tree.taiga.io/project/penpot/issue/3195)
### :arrow_up: Deps updates
### :heart: Community contributions by (Thank you!)

View file

@ -20,7 +20,7 @@
io.lettuce/lettuce-core {:mvn/version "6.1.6.RELEASE"}
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
funcool/yetti {:git/tag "v9.0" :git/sha "e09e46c"
funcool/yetti {:git/tag "v9.1" :git/sha "63f35d9"
:git/url "https://github.com/funcool/yetti.git"
:exclusions [org.slf4j/slf4j-api]}
@ -42,6 +42,9 @@
io.sentry/sentry {:mvn/version "5.6.1"}
dawran6/emoji {:mvn/version "0.1.5"}
markdown-clj/markdown-clj {:mvn/version "1.11.0"}
;; Pretty Print specs
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
software.amazon.awssdk/s3 {:mvn/version "2.17.136"}}

View file

@ -8,6 +8,7 @@ rm -rf target;
mkdir -p target/classes;
mkdir -p target/dist;
echo "$CURRENT_VERSION" > target/classes/version.txt;
cp ../CHANGES.md target/classes/changelog.md;
clojure -T:build jar;
mv target/penpot.jar target/dist/penpot.jar

View file

@ -2,7 +2,7 @@
export PENPOT_HOST=devenv
export PENPOT_TENANT=dev
export PENPOT_FLAGS="$PENPOT_FLAGS enable-backend-asserts enable-secure-session-cookies enable-audit-log enable-cors enable-transit-readable-response enable-demo-users"
export PENPOT_FLAGS="$PENPOT_FLAGS enable-backend-asserts enable-audit-log enable-cors enable-transit-readable-response enable-demo-users"
# export PENPOT_DATABASE_URI="postgresql://172.17.0.1:5432/penpot"
# export PENPOT_DATABASE_USERNAME="penpot"
@ -13,11 +13,15 @@ export PENPOT_FLAGS="$PENPOT_FLAGS enable-backend-asserts enable-secure-session-
# export PENPOT_DATABASE_USERNAME="penpot_pre"
# export PENPOT_DATABASE_PASSWORD="penpot_pre"
# export PENPOT_LOGGERS_LOKI_URI="http://172.17.0.1:3100/loki/api/v1/push"
# export PENPOT_AUDIT_LOG_ARCHIVE_URI="http://localhost:6070/api/audit"
# Initialize MINIO config
# mc alias set penpot-s3/ http://minio:9000 minioadmin minioadmin
# mc admin user add penpot-s3 penpot-devenv penpot-devenv
# mc admin policy set penpot-s3 readwrite user=penpot-devenv
# mc mb penpot-s3/penpot -p
mc alias set penpot-s3/ http://minio:9000 minioadmin minioadmin
mc admin user add penpot-s3 penpot-devenv penpot-devenv
mc admin policy set penpot-s3 readwrite user=penpot-devenv
mc mb penpot-s3/penpot -p
export AWS_ACCESS_KEY_ID=penpot-devenv
export AWS_SECRET_ACCESS_KEY=penpot-devenv
export PENPOT_ASSETS_STORAGE_BACKEND=assets-fs

View file

@ -41,8 +41,7 @@
data))
(def defaults
{:host "devenv"
:tenant "dev"
{
:database-uri "postgresql://postgres/penpot"
:database-username "penpot"
:database-password "penpot"
@ -54,8 +53,10 @@
:file-change-snapshot-timeout "3h"
:public-uri "http://localhost:3449"
:redis-uri "redis://redis/0"
:host "localhost"
:tenant "main"
:redis-uri "redis://redis/0"
:srepl-host "127.0.0.1"
:srepl-port 6062

View file

@ -8,6 +8,7 @@
"Main api for send emails."
(:require
[app.common.logging :as l]
[app.common.pprint :as pp]
[app.common.spec :as us]
[app.config :as cf]
[app.db :as db]
@ -165,19 +166,25 @@
(let [enabled? (or (contains? cf/flags :smtp)
(cf/get :smtp-enabled)
(:enabled task))]
(if enabled?
(emails/send! cfg props)
(when enabled?
(emails/send! cfg props))
(when (contains? cf/flags :log-emails)
(send-console! cfg props)))))
(defn- send-console!
[cfg email]
(let [baos (java.io.ByteArrayOutputStream.)
mesg (emails/smtp-message cfg email)]
(.writeTo mesg baos)
(let [out (with-out-str
(println "email console dump:")
(println "******** start email" (:id email) "**********")
(println (.toString baos))
(println "******** end email "(:id email) "**********"))]
(l/info :email out))))
[_ email]
(let [body (:body email)
out (with-out-str
(println "email console dump:")
(println "******** start email" (:id email) "**********")
(pp/pprint (dissoc email :body))
(if (string? body)
(println body)
(println (->> body
(filter #(= "text/plain" (:type %)))
(map :content)
first)))
(println "******** end email" (:id email) "**********"))]
(l/info ::l/raw out)))

View file

@ -44,7 +44,7 @@
(merge {:name "http"
:port 6060
:host "0.0.0.0"
:max-body-size (* 1024 1024 24) ; 24 MiB
:max-body-size (* 1024 1024 30) ; 30 MiB
:max-multipart-body-size (* 1024 1024 120)} ; 120 MiB
(d/without-nils cfg)))
@ -145,6 +145,7 @@
["/dbg" {:middleware [(:middleware session)]}
["" {:handler (:index debug)}]
["/changelog" {:handler (:changelog debug)}]
["/error-by-id/:id" {:handler (:retrieve-error debug)}]
["/error/:id" {:handler (:retrieve-error debug)}]
["/error" {:handler (:retrieve-error-list debug)}]

View file

@ -22,8 +22,11 @@
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[datoteka.core :as fs]
[emoji.core :as emj]
[fipp.edn :as fpp]
[integrant.core :as ig]
[markdown.core :as md]
[markdown.transformers :as mdt]
[promesa.core :as p]
[promesa.exec :as px]
[yetti.request :as yrq]
@ -213,6 +216,18 @@
(db/exec-one! conn ["select count(*) as count from server_prop;"])
(yrs/response 200 "OK")))
(defn changelog
[_ _]
(letfn [(transform-emoji [text state]
[(emj/emojify text) state])
(md->html [text]
(md/md-to-html-string text :replacement-transformers (into [transform-emoji] mdt/transformer-vector)))]
(if-let [clog (io/resource "changelog.md")]
(yrs/response :status 200
:headers {"content-type" "text/html; charset=utf-8"}
:body (-> clog slurp md->html))
(yrs/response :status 404 :body "NOT FOUND"))))
(defn- wrap-async
[{:keys [executor] :as cfg} f]
(fn [request respond raise]
@ -230,4 +245,5 @@
:retrieve-file-changes (wrap-async cfg retrieve-file-changes)
:retrieve-error (wrap-async cfg retrieve-error)
:retrieve-error-list (wrap-async cfg retrieve-error-list)
:file-data (wrap-async cfg file-data)})
:file-data (wrap-async cfg file-data)
:changelog (wrap-async cfg changelog)})

View file

@ -15,6 +15,8 @@
[yetti.request :as yrq]
[yetti.response :as yrs]))
(def ^:dynamic *context* {})
(defn- parse-client-ip
[request]
(or (some-> (yrq/get-header request "x-forwarded-for") (str/split ",") first)
@ -24,6 +26,7 @@
(defn get-context
[request]
(merge
*context*
{:path (:path request)
:method (:method request)
:params (:params request)
@ -49,12 +52,20 @@
(defmethod handle-exception :validation
[err _]
(let [data (ex-data err)
explain (us/pretty-explain data)]
(yrs/response :status 400
:body (-> data
(dissoc ::s/problems ::s/value)
(cond-> explain (assoc :explain explain))))))
(let [{:keys [code] :as data} (ex-data err)]
(cond
(= code :spec-validation)
(let [explain (us/pretty-explain data)]
(yrs/response :status 400
:body (-> data
(dissoc ::s/problems ::s/value)
(cond-> explain (assoc :explain explain)))))
(= code :request-body-too-large)
(yrs/response :status 413 :body data)
:else
(yrs/response :status 400 :body data))))
(defmethod handle-exception :assertion
[error request]
@ -129,9 +140,21 @@
:code :unhandled
:hint (ex-message error)
:data edata})))))
(defn handle
[error request]
(if (or (instance? java.util.concurrent.CompletionException error)
(instance? java.util.concurrent.ExecutionException error))
(handle-exception (.getCause ^Throwable error) request)
(handle-exception error request)))
[cause request]
(cond
(or (instance? java.util.concurrent.CompletionException cause)
(instance? java.util.concurrent.ExecutionException cause))
(handle-exception (.getCause ^Throwable cause) request)
(ex/wrapped? cause)
(let [context (meta cause)
cause (deref cause)]
(binding [*context* context]
(handle-exception cause request)))
:else
(handle-exception cause request)))

View file

@ -16,7 +16,10 @@
[yetti.middleware :as ymw]
[yetti.request :as yrq]
[yetti.response :as yrs])
(:import java.io.OutputStream))
(:import
com.fasterxml.jackson.core.io.JsonEOFException
io.undertow.server.RequestTooBigException
java.io.OutputStream))
(def server-timing
{:name ::server-timing
@ -46,16 +49,29 @@
(update :params merge params))))
:else
request)))]
request)))
(handle-error [raise cause]
(cond
(instance? RequestTooBigException cause)
(raise (ex/error :type :validation
:code :request-body-too-large
:hint (ex-message cause)))
(instance? JsonEOFException cause)
(raise (ex/error :type :validation
:code :malformed-json
:hint (ex-message cause)))
:else
(raise cause)))]
(fn [request respond raise]
(when-let [request (try
(process-request request)
(catch Exception cause
(raise (ex/error :type :validation
:code :malformed-params
:hint (ex-message cause)
:cause cause))))]
(catch RuntimeException cause
(handle-error raise (or (.getCause cause) cause)))
(catch Throwable cause
(handle-error raise cause)))]
(handler request respond raise)))))
(def parse-request
@ -99,7 +115,10 @@
(let [body (yrs/body response)]
(if (coll? body)
(let [qs (yrq/query request)
opts {:type (if (str/includes? qs "verbose") :json-verbose :json)}]
opts (if (or (contains? cf/flags :transit-readable-response)
(str/includes? qs "transit_verbose"))
{:type :json-verbose}
{:type :json})]
(-> response
(update :headers assoc "content-type" "application/transit+json")
(assoc :body (transit-streamable-body body opts))))

View file

@ -166,11 +166,9 @@
(a/go
;; Only allow receive pointer updates when active subscription
(when-let [{:keys [topic]} (get-in @wsp [::subscriptions subs-id])]
(l/trace :fn "handle-message" :event :pointer-update :message message)
(let [msgbus-fn (:msgbus @wsp)
profile-id (::profile-id @wsp)
session-id (::session-id @wsp)
message (-> message
(dissoc :subs-id)
(assoc :profile-id profile-id)

View file

@ -20,9 +20,9 @@
:read-only (cf/get :database-readonly false)
:metrics (ig/ref :app.metrics/metrics)
:migrations (ig/ref :app.migrations/all)
:name :main
:min-size (cf/get :database-min-pool-size 0)
:max-size (cf/get :database-max-pool-size 30)}
:name :main
:min-size (cf/get :database-min-pool-size 0)
:max-size (cf/get :database-max-pool-size 30)}
;; Default thread pool for IO operations
[::default :app.worker/executor]
@ -115,7 +115,9 @@
:router (ig/ref :app.http/router)
:metrics (ig/ref :app.metrics/metrics)
:executor (ig/ref [::default :app.worker/executor])
:io-threads (cf/get :http-server-io-threads)}
:io-threads (cf/get :http-server-io-threads)
:max-body-size (cf/get :http-server-max-body-size)
:max-multipart-body-size (cf/get :http-server-max-multipart-body-size)}
:app.http/router
{:assets (ig/ref :app.http.assets/handlers)

View file

@ -217,6 +217,12 @@
{:name "0069-add-file-thumbnail-table"
:fn (mg/resource "app/migrations/sql/0069-add-file-thumbnail-table.sql")}
{:name "0070-del-frame-thumbnail-table"
:fn (mg/resource "app/migrations/sql/0070-del-frame-thumbnail-table.sql")}
{:name "0071-add-file-object-thumbnail-table"
:fn (mg/resource "app/migrations/sql/0071-add-file-object-thumbnail-table.sql")}
])

View file

@ -0,0 +1 @@
DROP TABLE file_frame_thumbnail;

View file

@ -0,0 +1,11 @@
CREATE TABLE file_object_thumbnail (
file_id uuid NOT NULL REFERENCES file(id) ON DELETE CASCADE,
object_id uuid NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
data text NULL,
PRIMARY KEY(file_id, object_id)
);
ALTER TABLE file_object_thumbnail
ALTER COLUMN data SET STORAGE external;

View file

@ -59,7 +59,9 @@
(-> (method data)
(p/then handle-response)
(p/then respond)
(p/catch raise)))))
(p/catch (fn [cause]
(let [context {:profile-id profile-id}]
(raise (ex/wrap-with-context cause context)))))))))
(defn- rpc-mutation-handler
"Ring handler that dispatches mutation requests and convert between
@ -81,7 +83,9 @@
(-> (method data)
(p/then handle-response)
(p/then respond)
(p/catch raise)))))
(p/catch (fn [cause]
(let [context {:profile-id profile-id}]
(raise (ex/wrap-with-context cause context)))))))))
(defn- wrap-metrics
"Wrap service method with metrics measurement."

View file

@ -476,30 +476,31 @@
:revn revn
:data (blob/encode data)}
{:id id})))
nil)))
;; --- Mutation: upsert object thumbnail
;; --- Mutation: Upsert frame thumbnail
(def sql:upsert-frame-thumbnail
"insert into file_frame_thumbnail(file_id, frame_id, data)
(def sql:upsert-object-thumbnail
"insert into file_object_thumbnail(file_id, object_id, data)
values (?, ?, ?)
on conflict(file_id, frame_id) do
on conflict(file_id, object_id) do
update set data = ?;")
(s/def ::data ::us/string)
(s/def ::upsert-file-frame-thumbnail
(s/keys :req-un [::profile-id ::file-id ::frame-id ::data]))
(s/def ::data (s/nilable ::us/string))
(s/def ::object-id ::us/uuid)
(s/def ::upsert-file-object-thumbnail
(s/keys :req-un [::profile-id ::file-id ::object-id ::data]))
(sv/defmethod ::upsert-file-frame-thumbnail
[{:keys [pool] :as cfg} {:keys [profile-id file-id frame-id data]}]
(sv/defmethod ::upsert-file-object-thumbnail
[{:keys [pool] :as cfg} {:keys [profile-id file-id object-id data]}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
(db/exec-one! conn [sql:upsert-frame-thumbnail file-id frame-id data data])
(if data
(db/exec-one! conn [sql:upsert-object-thumbnail file-id object-id data data])
(db/delete! conn :file-object-thumbnail {:file-id file-id :object-id object-id}))
nil))
;; --- Mutation: Upsert file thumbnail
;; --- Mutation: upsert file thumbnail
(def sql:upsert-file-thumbnail
"insert into file_thumbnail (file_id, revn, data, props)

View file

@ -8,6 +8,7 @@
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
@ -412,6 +413,9 @@
{:iss :profile-identity
:profile-id (:id profile)})]
(when (contains? cf/flags :log-invitation-tokens)
(l/trace :hint "invitation token" :token itoken))
(when (and member (not (eml/allow-send-emails? conn member)))
(ex/raise :type :validation
:code :member-is-muted

View file

@ -7,11 +7,11 @@
(ns app.rpc.queries.files
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.pages.helpers :as cph]
[app.common.pages.migrations :as pmg]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.db :as db]
[app.db.sql :as sql]
[app.rpc.helpers :as rpch]
@ -21,7 +21,8 @@
[app.rpc.queries.teams :as teams]
[app.util.blob :as blob]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
(declare decode-row)
(declare decode-row-xf)
@ -187,12 +188,30 @@
;; --- Query: File (By ID)
(defn retrieve-object-thumbnails
([{:keys [pool]} file-id]
(let [sql (str/concat
"select object_id, data "
" from file_object_thumbnail"
" where file_id=?")]
(->> (db/exec! pool [sql file-id])
(d/index-by :object-id :data))))
([{:keys [pool]} file-id frame-ids]
(with-open [conn (db/open pool)]
(let [sql (str/concat
"select object_id, data "
" from file_object_thumbnail"
" where file_id=? and object_id = ANY(?)")
ids (db/create-array conn "uuid" (seq frame-ids))]
(->> (db/exec! conn [sql file-id ids])
(d/index-by :object-id :data))))))
(defn retrieve-file
[{:keys [pool] :as cfg} id]
(let [item (db/get-by-id pool :file id)]
(->> item
(decode-row)
(pmg/migrate-file))))
(->> (db/get-by-id pool :file id)
(decode-row)
(pmg/migrate-file)))
(s/def ::file
(s/keys :req-un [::profile-id ::id]))
@ -202,133 +221,135 @@
[{:keys [pool] :as cfg} {:keys [profile-id id] :as params}]
(let [perms (get-permissions pool profile-id id)]
(check-read-permissions! perms)
(-> (retrieve-file cfg id)
(assoc :permissions perms))))
(let [file (retrieve-file cfg id)
thumbs (retrieve-object-thumbnails cfg id)]
(-> file
(assoc :thumbnails thumbs)
(assoc :permissions perms)))))
(declare trim-file-data)
;; --- QUERY: page
(defn- prune-objects
"Given the page data and the object-id returns the page data with all
other not needed objects removed from the `:objects` data
structure."
[{:keys [objects] :as page} object-id]
(let [objects (cph/get-children-with-self objects object-id)]
(assoc page :objects (d/index-by :id objects))))
(defn- prune-thumbnails
"Given the page data, removes the `:thumbnail` prop from all
shapes."
[page]
(update page :objects d/update-vals #(dissoc % :thumbnail)))
(s/def ::page-id ::us/uuid)
(s/def ::object-id ::us/uuid)
(s/def ::trimmed-file
(s/keys :req-un [::profile-id ::id ::object-id ::page-id]))
(sv/defmethod ::trimmed-file
"Retrieve a file by its ID and trims all unnecesary content from
it. It is mainly used for rendering a concrete object, so we don't
need force download all shapes when only a small subset is
necesseary."
[{:keys [pool] :as cfg} {:keys [profile-id id] :as params}]
(let [perms (get-permissions pool profile-id id)]
(check-read-permissions! perms)
(-> (retrieve-file cfg id)
(trim-file-data params)
(assoc :permissions perms))))
(defn- trim-file-data
[file {:keys [page-id object-id]}]
(let [page (get-in file [:data :pages-index page-id])
objects (->> (cph/get-children-with-self (:objects page) object-id)
(map #(dissoc % :thumbnail))
(d/index-by :id))
page (assoc page :objects objects)]
(-> file
(update :data assoc :pages-index {page-id page})
(update :data assoc :pages [page-id]))))
;; --- FILE THUMBNAIL
(declare strip-frames-with-thumbnails)
(declare extract-file-thumbnail)
(declare get-first-page-data)
(declare get-thumbnail-data)
(s/def ::strip-frames-with-thumbnails ::us/boolean)
(s/def ::page
(s/keys :req-un [::profile-id ::file-id]
:opt-un [::strip-frames-with-thumbnails]))
(s/and
(s/keys :req-un [::profile-id ::file-id]
:opt-un [::page-id ::object-id])
(fn [obj]
(if (contains? obj :object-id)
(contains? obj :page-id)
true))))
(sv/defmethod ::page
"Retrieves the first page of the file. Used mainly for render
thumbnails on dashboard.
"Retrieves the page data from file and returns it. If no page-id is
specified, the first page will be returned. If object-id is
specified, only that object and its children will be returned in the
page objects data structure.
DEPRECATED: still here for backward compatibility."
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as props}]
If you specify the object-id, the page-id parameter becomes
mandatory.
Mainly used for rendering purposes."
[{:keys [pool] :as cfg} {:keys [profile-id file-id page-id object-id] :as props}]
(check-read-permissions! pool profile-id file-id)
(let [file (retrieve-file cfg file-id)
data (get-first-page-data file props)]
data))
(let [file (retrieve-file cfg file-id)
page-id (or page-id (-> file :data :pages first))
page (get-in file [:data :pages-index page-id])]
(cond-> (prune-thumbnails page)
(uuid? object-id)
(prune-objects object-id))))
;; --- QUERY: file-data-for-thumbnail
(defn- get-file-thumbnail-data
[cfg {:keys [data id] :as file}]
(letfn [;; function responsible on finding the frame marked to be
;; used as thumbnail; the returned frame always have
;; the :page-id set to the page that it belongs.
(get-thumbnail-frame [data]
(d/seek :use-for-thumbnail?
(for [page (-> data :pages-index vals)
frame (-> page :objects cph/get-frames)]
(assoc frame :page-id (:id page)))))
;; function responsible to filter objects data strucuture of
;; all unneded shapes if a concrete frame is provided. If no
;; frame, the objects is returned untouched.
(filter-objects [objects frame-id]
(d/index-by :id (cph/get-children-with-self objects frame-id)))
;; function responsible of assoc available thumbnails
;; to frames and remove all children shapes from objects if
;; thumbnails is available
(assoc-thumbnails [objects thumbnails]
(loop [objects objects
frames (filter cph/frame-shape? (vals objects))]
(if-let [{:keys [id] :as frame} (first frames)]
(let [frame (if-let [thumb (get thumbnails id)]
(assoc frame :thumbnail thumb :shapes [])
(dissoc frame :thumbnail))]
(if (:thumbnail frame)
(recur (-> (assoc objects id frame)
(d/without-keys (cph/get-children-ids objects id)))
(rest frames))
(recur (assoc objects id frame)
(rest frames))))
objects)))]
(let [frame (get-thumbnail-frame data)
frame-id (:id frame)
page-id (or (:page-id frame)
(-> data :pages first))
page (dm/get-in data [:pages-index page-id])
obj-ids (or (some-> frame-id list)
(map :id (cph/get-frames page)))
thumbs (retrieve-object-thumbnails cfg id obj-ids)]
(cond-> page
;; If we have frame, we need to specify it on the page level
;; and remove the all other unrelated objects.
(some? frame-id)
(-> (assoc :thumbnail-frame-id frame-id)
(update :objects filter-objects frame-id))
;; Assoc the available thumbnails and prune not visible shapes
;; for avoid transfer unnecesary data.
:always
(update :objects assoc-thumbnails thumbs)))))
(s/def ::file-data-for-thumbnail
(s/keys :req-un [::profile-id ::file-id]
:opt-un [::strip-frames-with-thumbnails]))
(s/keys :req-un [::profile-id ::file-id]))
(sv/defmethod ::file-data-for-thumbnail
"Retrieves the data for generate the thumbnail of the file. Used mainly for render
thumbnails on dashboard."
"Retrieves the data for generate the thumbnail of the file. Used
mainly for render thumbnails on dashboard."
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as props}]
(check-read-permissions! pool profile-id file-id)
(let [file (retrieve-file cfg file-id)]
{:data (get-thumbnail-data file props)
:file-id file-id
:revn (:revn file)}))
{:file-id file-id
:revn (:revn file)
:page (get-file-thumbnail-data cfg file)}))
(defn get-thumbnail-data
[{:keys [data] :as file} props]
(if-let [[page frame] (first
(for [page (-> data :pages-index vals)
frame (-> page :objects cph/get-frames)
:when (:file-thumbnail frame)]
[page frame]))]
(let [objects (->> (cph/get-children-with-self (:objects page) (:id frame))
(d/index-by :id))]
(cond-> (assoc page :objects objects)
(:strip-frames-with-thumbnails props)
(strip-frames-with-thumbnails)
:always
(assoc :thumbnail-frame frame)))
(let [page-id (-> data :pages first)]
(cond-> (get-in data [:pages-index page-id])
(:strip-frames-with-thumbnails props)
(strip-frames-with-thumbnails)))))
(defn get-first-page-data
[file props]
(let [page-id (get-in file [:data :pages 0])
data (cond-> (get-in file [:data :pages-index page-id])
(true? (:strip-frames-with-thumbnails props))
(strip-frames-with-thumbnails))]
data))
(defn strip-frames-with-thumbnails
"Remove unnecesary shapes from frames that have thumbnail."
[data]
(let [filter-shape?
(fn [objects [id shape]]
(let [frame-id (:frame-id shape)]
(or (= id uuid/zero)
(= frame-id uuid/zero)
(not (some? (get-in objects [frame-id :thumbnail]))))))
;; We need to remove from the attribute :shapes its children because
;; they will not be sent in the data
remove-frame-children
(fn [[id shape]]
[id (cond-> shape
(some? (:thumbnail shape))
(assoc :shapes []))])
update-objects
(fn [objects]
(into {}
(comp (map remove-frame-children)
(filter (partial filter-shape? objects)))
objects))]
(update data :objects update-objects)))
;; --- Query: Shared Library Files
@ -428,20 +449,6 @@
(teams/check-read-permissions! pool profile-id team-id)
(db/exec! pool [sql:team-recent-files team-id]))
;; --- QUERY: get all file frame thumbnails
(s/def ::file-frame-thumbnails
(s/keys :req-un [::profile-id ::file-id]
:opt-un [::frame-id]))
(sv/defmethod ::file-frame-thumbnails
[{:keys [pool]} {:keys [profile-id file-id frame-id]}]
(check-read-permissions! pool profile-id file-id)
(let [params (cond-> {:file-id file-id}
frame-id (assoc :frame-id frame-id))
rows (db/query pool :file-frame-thumbnail params)]
(d/index-by :frame-id :data rows)))
;; --- QUERY: get file thumbnail
(s/def ::revn ::us/integer)

View file

@ -6,18 +6,19 @@
(ns app.tasks.file-gc
"A maintenance task that is responsible of: purge unused file media,
clean unused frame thumbnails and remove old file thumbnails. The
clean unused object thumbnails and remove old file thumbnails. The
file is eligible to be garbage collected after some period of
inactivity (the default threshold is 72h)."
(:require
[app.common.data :as d]
[app.common.logging :as l]
[app.common.pages.helpers :as cph]
[app.common.pages.migrations :as pmg]
[app.db :as db]
[app.util.blob :as blob]
[app.util.time :as dt]
[clojure.set :as set]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig]))
(declare ^:private retrieve-candidates)
@ -117,26 +118,26 @@
;; them.
(db/delete! conn :file-media-object {:id (:id mobj)}))))
(defn- collect-frames
[data]
(let [xform (comp
(map :objects)
(mapcat vals)
(filter cph/frame-shape?)
(keep :id))
pages (concat
(vals (:pages-index data))
(vals (:components data)))]
(into #{} xform pages)))
(defn- clean-file-frame-thumbnails!
[conn file-id data]
(let [sql (str "delete from file_frame_thumbnail "
" where file_id=? and not (frame_id=ANY(?))")
ids (->> (collect-frames data)
(db/create-array conn "uuid"))
res (db/exec-one! conn [sql file-id ids])]
(l/debug :hint "delete frame thumbnails" :total (:next.jdbc/update-count res))))
(let [stored (->> (db/query conn :file-object-thumbnail
{:file-id file-id}
{:columns [:object-id]})
(into #{} (map :object-id)))
using (->> (concat (vals (:pages-index data))
(vals (:components data)))
(into #{} (comp (map :objects)
(mapcat keys))))
unused (set/difference stored using)]
(when (seq unused)
(let [sql (str/concat
"delete from file_object_thumbnail "
" where file_id=? and object_id=ANY(?)")
res (db/exec-one! conn [sql file-id (db/create-array conn "uuid" unused)])]
(l/debug :hint "delete object thumbnails" :total (:next.jdbc/update-count res))))))
(defn- clean-file-thumbnails!
[conn file-id revn]

View file

@ -413,75 +413,217 @@
(t/is (= (:type error-data) :not-found))))
))
(t/deftest query-frame-thumbnails
(t/deftest object-thumbnails-ops
(let [prof (th/create-profile* 1 {:is-active true})
file (th/create-file* 1 {:profile-id (:id prof)
:project-id (:default-project-id prof)
:is-shared false})
data {::th/type :file-frame-thumbnails
:profile-id (:id prof)
:file-id (:id file)
:frame-id (uuid/next)}]
page-id (get-in file [:data :pages 0])
frame1-id (uuid/next)
shape1-id (uuid/next)
frame2-id (uuid/next)
shape2-id (uuid/next)
;; insert an entry on the database with a test value for the thumbnail of this frame
(th/db-insert! :file-frame-thumbnail
{:file-id (:file-id data)
:frame-id (:frame-id data)
:data "testvalue"})
changes [{:type :add-obj
:page-id page-id
:id frame1-id
:parent-id uuid/zero
:frame-id uuid/zero
:obj {:id frame1-id
:use-for-thumbnail? true
:name "test-frame1"
:type :frame}}
{:type :add-obj
:page-id page-id
:id shape1-id
:parent-id frame1-id
:frame-id frame1-id
:obj {:id shape1-id
:name "test-shape1"
:type :rect}}
{:type :add-obj
:page-id page-id
:id frame2-id
:parent-id uuid/zero
:frame-id uuid/zero
:obj {:id frame2-id
:name "test-frame2"
:type :frame}}
{:type :add-obj
:page-id page-id
:id shape2-id
:parent-id frame2-id
:frame-id frame2-id
:obj {:id shape2-id
:name "test-shape2"
:type :rect}}]]
;; Update the file
(th/update-file* {:file-id (:id file)
:profile-id (:id prof)
:revn 0
:changes changes})
(let [{:keys [result error] :as out} (th/query! data)]
;; (th/print-result! out)
(t/is (nil? error))
(t/is (= 1 (count result)))
(t/is (= "testvalue" (get result (:frame-id data)))))))
(t/testing "RPC page query (rendering purposes)"
(t/deftest insert-frame-thumbnails
(let [prof (th/create-profile* 1 {:is-active true})
file (th/create-file* 1 {:profile-id (:id prof)
:project-id (:default-project-id prof)
:is-shared false})
data {::th/type :upsert-file-frame-thumbnail
:profile-id (:id prof)
:file-id (:id file)
:frame-id (uuid/next)
:data "test insert new value"}]
;; Query :page RPC method without passing page-id
(let [data {::th/type :page
:profile-id (:id prof)
:file-id (:id file)}
{:keys [error result] :as out} (th/query! data)]
(let [out (th/mutation! data)]
(t/is (nil? (:error out)))
(t/is (nil? (:result out)))
(let [[result] (th/db-query :file-frame-thumbnail
{:file-id (:file-id data)
:frame-id (:frame-id data)})]
(t/is (= "test insert new value" (:data result)))))))
;; (th/print-result! out)
(t/is (map? result))
(t/is (contains? result :objects))
(t/is (contains? (:objects result) frame1-id))
(t/is (contains? (:objects result) shape1-id))
(t/is (contains? (:objects result) frame2-id))
(t/is (contains? (:objects result) shape2-id))
(t/is (contains? (:objects result) uuid/zero)))
(t/deftest upsert-frame-thumbnails
(let [prof (th/create-profile* 1 {:is-active true})
file (th/create-file* 1 {:profile-id (:id prof)
:project-id (:default-project-id prof)
:is-shared false})
data {::th/type :upsert-file-frame-thumbnail
:profile-id (:id prof)
:file-id (:id file)
:frame-id (uuid/next)
:data "updated value"}]
;; Query :page RPC method with page-id
(let [data {::th/type :page
:profile-id (:id prof)
:file-id (:id file)
:page-id page-id}
{:keys [error result] :as out} (th/query! data)]
;; (th/print-result! out)
(t/is (map? result))
(t/is (contains? result :objects))
(t/is (contains? (:objects result) frame1-id))
(t/is (contains? (:objects result) shape1-id))
(t/is (contains? (:objects result) frame2-id))
(t/is (contains? (:objects result) shape2-id))
(t/is (contains? (:objects result) uuid/zero)))
;; insert an entry on the database with and old value for the thumbnail of this frame
(th/db-insert! :file-frame-thumbnail
{:file-id (:file-id data)
:frame-id (:frame-id data)
:data "old value"})
;; Query :page RPC method with page-id and object-id
(let [data {::th/type :page
:profile-id (:id prof)
:file-id (:id file)
:page-id page-id
:object-id frame1-id}
{:keys [error result] :as out} (th/query! data)]
;; (th/print-result! out)
(t/is (map? result))
(t/is (contains? result :objects))
(t/is (contains? (:objects result) frame1-id))
(t/is (contains? (:objects result) shape1-id))
(t/is (not (contains? (:objects result) uuid/zero)))
(t/is (not (contains? (:objects result) frame2-id)))
(t/is (not (contains? (:objects result) shape2-id))))
(let [out (th/mutation! data)]
;; (th/print-result! out)
;; Query :page RPC method with wrong params
(let [data {::th/type :page
:profile-id (:id prof)
:file-id (:id file)
:object-id frame1-id}
{:keys [error result] :as out} (th/query! data)]
;; (th/print-result! out)
(t/is (= :validation (th/ex-type error)))
(t/is (= :spec-validation (th/ex-code error)))))
(t/is (nil? (:error out)))
(t/is (nil? (:result out)))
(t/testing "RPC :file-data-for-thumbnail"
;; Insert a thumbnail data for the frame-id
(let [data {::th/type :upsert-file-object-thumbnail
:profile-id (:id prof)
:file-id (:id file)
:object-id frame1-id
:data "random-data-1"}
;; retrieve the value from the database and check its content
(let [[result] (th/db-query :file-frame-thumbnail
{:file-id (:file-id data)
:frame-id (:frame-id data)})]
(t/is (= "updated value" (:data result)))))))
{:keys [error result] :as out} (th/mutation! data)]
(t/is (nil? error))
(t/is (nil? result)))
;; Check the result
(let [data {::th/type :file-data-for-thumbnail
:profile-id (:id prof)
:file-id (:id file)}
{:keys [error result] :as out} (th/query! data)]
;; (th/print-result! out)
(t/is (map? result))
(t/is (contains? result :page))
(t/is (contains? result :revn))
(t/is (contains? result :file-id))
(t/is (= (:id file) (:file-id result)))
(t/is (= "random-data-1" (get-in result [:page :objects frame1-id :thumbnail])))
(t/is (= [] (get-in result [:page :objects frame1-id :shapes]))))
;; Delete thumbnail data
(let [data {::th/type :upsert-file-object-thumbnail
:profile-id (:id prof)
:file-id (:id file)
:object-id frame1-id
:data nil}
{:keys [error result] :as out} (th/mutation! data)]
(t/is (nil? error))
(t/is (nil? result)))
;; Check the result
(let [data {::th/type :file-data-for-thumbnail
:profile-id (:id prof)
:file-id (:id file)}
{:keys [error result] :as out} (th/query! data)]
;; (th/print-result! out)
(t/is (map? result))
(t/is (contains? result :page))
(t/is (contains? result :revn))
(t/is (contains? result :file-id))
(t/is (= (:id file) (:file-id result)))
(t/is (nil? (get-in result [:page :objects frame1-id :thumbnail])))
(t/is (not= [] (get-in result [:page :objects frame1-id :shapes])))))
(t/testing "TASK :file-gc"
;; insert object snapshot for known frame
(let [data {::th/type :upsert-file-object-thumbnail
:profile-id (:id prof)
:file-id (:id file)
:object-id frame1-id
:data "new-data"}
{:keys [error result] :as out} (th/mutation! data)]
(t/is (nil? error))
(t/is (nil? result)))
;; Wait to file be ellegible for GC
(th/sleep 300)
;; run the task again
(let [task (:app.tasks.file-gc/handler th/*system*)
res (task {})]
(t/is (= 1 (:processed res))))
;; check that object thumbnails are still here
(let [res (th/db-exec! ["select * from file_object_thumbnail"])]
(t/is (= 1 (count res)))
(t/is (= "new-data" (get-in res [0 :data]))))
;; insert object snapshot for for unknown frame
(let [data {::th/type :upsert-file-object-thumbnail
:profile-id (:id prof)
:file-id (:id file)
:object-id (uuid/next)
:data "new-data-2"}
{:keys [error result] :as out} (th/mutation! data)]
(t/is (nil? error))
(t/is (nil? result)))
;; Mark file as modified
(th/db-exec! ["update file set has_media_trimmed=false where id=?" (:id file)])
;; check that we have all object thumbnails
(let [res (th/db-exec! ["select * from file_object_thumbnail"])]
(t/is (= 2 (count res))))
;; run the task again
(let [task (:app.tasks.file-gc/handler th/*system*)
res (task {})]
(t/is (= 1 (:processed res))))
;; check that the unknown frame thumbnail is deleted
(let [res (th/db-exec! ["select * from file_object_thumbnail"])]
(t/is (= 1 (count res)))
(t/is (= "new-data" (get-in res [0 :data])))))))
(t/deftest file-thumbnail-ops

View file

@ -11,6 +11,7 @@
[app.common.pages :as cp]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.common.pprint :as pp]
[app.config :as cf]
[app.db :as db]
[app.main :as main]
@ -303,7 +304,7 @@
(println "====> END ERROR"))
(do
(println "====> START RESPONSE")
(fipp.edn/pprint result)
(pp/pprint result)
(println "====> END RESPONSE"))))
(defn exception?

View file

@ -22,7 +22,7 @@
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
funcool/promesa {:mvn/version "8.0.450"}
funcool/cuerdas {:mvn/version "2022.01.14-391"}
funcool/cuerdas {:mvn/version "2022.03.27-397"}
lambdaisland/uri {:mvn/version "1.13.95"
:exclusions [org.clojure/data.json]}
@ -42,7 +42,7 @@
{:extra-deps
{org.clojure/tools.namespace {:mvn/version "RELEASE"}
org.clojure/test.check {:mvn/version "RELEASE"}
thheller/shadow-cljs {:mvn/version "2.17.5"}
thheller/shadow-cljs {:mvn/version "2.17.8"}
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
criterium/criterium {:mvn/version "RELEASE"}
mockery/mockery {:mvn/version "RELEASE"}}

View file

@ -13,7 +13,7 @@
"test": "yarn run compile-test && yarn run run-test"
},
"devDependencies": {
"shadow-cljs": "2.17.5",
"shadow-cljs": "2.17.8",
"source-map-support": "^0.5.19",
"ws": "^7.4.6"
}

View file

@ -23,9 +23,9 @@
#?(:clj
(:import linked.set.LinkedSet)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Structures
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn ordered-set
([] lks/empty-linked-set)
@ -49,9 +49,14 @@
([a] (into (queue) [a]))
([a & more] (into (queue) (cons a more))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Structures Manipulation
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn editable-collection?
[m]
#?(:clj (instance? clojure.lang.IEditableCollection m)
:cljs (implements? core/IEditableCollection m)))
(defn deep-merge
([a b]
@ -101,7 +106,6 @@
(defn preconj
[coll elem]
(assert (or (vector? coll) (nil? coll)))
(into [elem] coll))
(defn enumerate
@ -174,9 +178,12 @@
"Return a map without the keys provided
in the `keys` parameter."
[data keys]
(when (map? data)
(persistent!
(reduce #(dissoc! %1 %2) (transient data) keys))))
(persistent!
(reduce dissoc!
(if (editable-collection? data)
(transient data)
(transient {}))
keys)))
(defn remove-at-index
"Takes a vector and returns a vector with an element in the
@ -209,8 +216,7 @@
(with-meta
(persistent!
(reduce-kv (fn [acc k v] (assoc! acc k (f v)))
(if #?(:clj (instance? clojure.lang.IEditableCollection m)
:cljs (implements? core/IEditableCollection m))
(if (editable-collection? m)
(transient m)
(transient {}))
m))
@ -344,13 +350,14 @@
(do (vswap! seen conj input*)
(rf result input)))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Parsing / Conversion
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn nan?
[v]
(not= v v))
#?(:cljs (js/isNaN v)
:clj (not= v v)))
(defn- impl-parse-integer
[v]
@ -408,9 +415,9 @@
[val default]
(or val default))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Parsing / Conversion
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn nilf
"Returns a new function that if you pass nil as any argument will
return nil"

View file

@ -13,6 +13,7 @@
#?(:clj [clojure.core :as c]
:cljs [cljs.core :as c])
[app.common.data :as d]
[cuerdas.core :as str]
[cljs.analyzer.api :as aapi]))
(defmacro select-keys
@ -36,61 +37,9 @@
`(let [v# (-> ~target ~@(map (fn [key] (list `c/get key)) keys))]
(if (some? v#) v# ~default))))
;; => benchmarking: clojure.core/str
;; --> WARM: 100000
;; --> BENCH: 500000
;; --> TOTAL: 197.82ms
;; --> MEAN: 395.64ns
;; => benchmarking: app.commons.data.macros/str
;; --> WARM: 100000
;; --> BENCH: 500000
;; --> TOTAL: 20.31ms
;; --> MEAN: 40.63ns
(defmacro str
"CLJS only macro variant of `str` function that performs string concat much faster."
([a]
(if (:ns &env)
(list 'js* "\"\"+~{}" a)
(list `c/str a)))
([a b]
(if (:ns &env)
(list 'js* "\"\"+~{}+~{}" a b)
(list `c/str a b)))
([a b c]
(if (:ns &env)
(list 'js* "\"\"+~{}+~{}+~{}" a b c)
(list `c/str a b c)))
([a b c d]
(if (:ns &env)
(list 'js* "\"\"+~{}+~{}+~{}+~{}" a b c d)
(list `c/str a b c d)))
([a b c d e]
(if (:ns &env)
(list 'js* "\"\"+~{}+~{}+~{}+~{}+~{}" a b c d e)
(list `c/str a b c d e)))
([a b c d e f]
(if (:ns &env)
(list 'js* "\"\"+~{}+~{}+~{}+~{}+~{}+~{}" a b c d e f)
(list `c/str a b c d e f)))
([a b c d e f g]
(if (:ns &env)
(list 'js* "\"\"+~{}+~{}+~{}+~{}+~{}+~{}+~{}" a b c d e f g)
(list `c/str a b c d e f g)))
([a b c d e f g h]
(if (:ns &env)
(list 'js* "\"\"+~{}+~{}+~{}+~{}+~{}+~{}+~{}+~{}" a b c d e f g h)
(list `c/str a b c d e f g h)))
([a b c d e f g h & rest]
(let [all (into [a b c d e f g h] rest)]
(if (:ns &env)
(let [xf (map (fn [items] `(str ~@items)))
pall (partition-all 8 all)]
(if (<= (count all) 64)
`(str ~@(sequence xf pall))
`(c/str ~@(sequence xf pall))))
`(c/str ~@all)))))
[& params]
`(str/concat ~@params))
(defmacro export
"A helper macro that allows reexport a var in a current namespace."
@ -129,36 +78,6 @@
;; (.setMacro (var ~n)))
~vr))))
(defn- interpolate
[s params]
(loop [items (->> (re-seq #"([^\%]+)*(\%(\d+)?)?" s)
(remove (fn [[full seg]] (and (nil? seg) (not full)))))
result []
index 0]
(if-let [[_ segment var? sidx] (first items)]
(cond
(and var? sidx)
(let [cidx (dec (d/read-string sidx))]
(recur (rest items)
(-> result
(conj segment)
(conj (nth params cidx)))
(inc index)))
var?
(recur (rest items)
(-> result
(conj segment)
(conj (nth params index)))
(inc index))
:else
(recur (rest items)
(conj result segment)
(inc index)))
(remove nil? result))))
(defmacro fmt
"String interpolation helper. Can only be used with strings known at
compile time. Can be used with indexed params access or sequential.
@ -169,7 +88,7 @@
(dm/fmt \"url(%1)\" my-url) ; indexed
"
[s & params]
(cons 'app.common.data.macros/str (interpolate s (vec params))))
`(str/ffmt ~s ~@params))

View file

@ -57,3 +57,31 @@
(defn exception?
[v]
(instance? #?(:clj java.lang.Throwable :cljs js/Error) v))
#?(:cljs
(deftype WrappedException [cause meta]
cljs.core/IMeta
(-meta [_] meta)
cljs.core/IDeref
(-deref [_] cause))
:clj
(deftype WrappedException [cause meta]
clojure.lang.IMeta
(meta [_] meta)
clojure.lang.IDeref
(deref [_] cause)))
(ns-unmap 'app.common.exceptions '->WrappedException)
(ns-unmap 'app.common.exceptions 'map->WrappedException)
(defn wrapped?
[o]
(instance? WrappedException o))
(defn wrap-with-context
[cause context]
(WrappedException. cause context))

View file

@ -9,7 +9,7 @@
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.common :as gco]
[app.common.geom.shapes.transforms :as gtr]
[app.common.geom.shapes.rect :as gre]
[app.common.math :as mth]
[app.common.uuid :as uuid]))
@ -77,18 +77,16 @@
(defmethod constraint-modifier :fixed
[_ axis parent child _ transformed-parent-rect]
(let [parent-rect (:selrect parent)
child-rect (:selrect child)
child-rect (gre/points->rect (:points child))
delta-start (get-delta-start axis parent-rect transformed-parent-rect)
delta-size (get-delta-size axis parent-rect transformed-parent-rect)
child-size (get-size axis child-rect)
child-center (gco/center-rect child-rect)]
child-size (get-size axis child-rect)]
(if (or (not (mth/almost-zero? delta-start))
(not (mth/almost-zero? delta-size)))
{:displacement (get-displacement axis delta-start)
:resize-origin (-> (get-displacement axis delta-start (:x1 child-rect) (:y1 child-rect))
(gtr/transform-point-center child-center (:transform child (gmt/matrix))))
:resize-origin (get-displacement axis delta-start (:x child-rect) (:y child-rect))
:resize-vector (get-scale axis (/ (+ child-size delta-size) child-size))}
{})))

View file

@ -6,14 +6,13 @@
(ns app.common.logging
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.uuid :as uuid]
[app.common.spec :as us]
[clojure.pprint :refer [pprint]]
[cuerdas.core :as str]
[clojure.spec.alpha :as s]
[fipp.edn :as fpp]
#?(:clj [io.aviso.exception :as ie])
#?(:cljs [goog.log :as glog]))
#?(:cljs (:require-macros [app.common.logging])
:clj (:import
@ -22,7 +21,6 @@
org.apache.logging.log4j.Logger
org.apache.logging.log4j.ThreadContext
org.apache.logging.log4j.CloseableThreadContext
org.apache.logging.log4j.message.MapMessage
org.apache.logging.log4j.spi.LoggerContext)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -31,11 +29,22 @@
#?(:clj (set! *warn-on-reflection* true))
#?(:clj
(defn build-map-message
[m]
(let [message (MapMessage. (count m))]
(reduce-kv #(.with ^MapMessage %1 (name %2) %3) message m))))
(def ^:private reserved-props
#{:level :cause ::logger ::async ::raw ::context})
(def ^:private props-xform
(comp (partition-all 2)
(remove (fn [[k]] (contains? reserved-props k)))
(map vec)))
(defn build-message
[props]
(loop [pairs (sequence props-xform props)
result []]
(if-let [[k v] (first pairs)]
(recur (rest pairs)
(conj result (str/concat (d/name k) "=" (pr-str v))))
result)))
#?(:clj
(def logger-context
@ -45,13 +54,6 @@
(def logging-agent
(agent nil :error-mode :continue)))
(defn- simple-prune
([s] (simple-prune s (* 1024 1024)))
([s max-length]
(if (> (count s) max-length)
(str (subs s 0 max-length) " [...]")
s)))
#?(:clj
(defn stringify-data
[val]
@ -132,22 +134,25 @@
(defn write-log!
[logger level exception message]
#?(:clj
(if exception
(.log ^Logger logger
^Level level
^Object message
^Throwable exception)
(.log ^Logger logger
^Level level
^Object message))
:cljs
(when glog/ENABLED
(when-let [l (get-logger logger)]
(let [level (get-level level)
record (glog/LogRecord. level message (.getName ^js l))]
(when exception (.setException record exception))
(glog/publishLogRecord l record))))))
(let [message (if (string? message)
message
(str/join ", " message))]
#?(:clj
(if exception
(.log ^Logger logger
^Level level
^Object message
^Throwable exception)
(.log ^Logger logger
^Level level
^Object message))
:cljs
(when glog/ENABLED
(when-let [l (get-logger logger)]
(let [level (get-level level)
record (glog/LogRecord. level message (.getName ^js l))]
(when exception (.setException record exception))
(glog/publishLogRecord l record)))))))
#?(:clj
(defn enabled?
@ -167,13 +172,13 @@
{:spec-explain (us/pretty-explain data)})))))
(defmacro log
[& {:keys [level cause ::logger ::async ::raw ::context] :or {async true} :as props}]
[& props]
(if (:ns &env) ; CLJS
`(write-log! ~(or logger (str *ns*))
~level
~cause
(or ~raw ~(dissoc props :level :cause ::logger ::raw ::context)))
(let [props (dissoc props :level :cause ::logger ::async ::raw ::context)
(let [{:keys [level cause ::logger ::raw]} props
message (or raw (build-message props))]
`(write-log! ~(or logger (str *ns*)) ~level ~cause (or ~raw (build-message ~(vec props)))))
(let [{:keys [level cause ::logger ::async ::raw ::context] :or {async true}} props
logger (or logger (str *ns*))
logger-sym (gensym "log")
level-sym (gensym "log")]
@ -184,17 +189,17 @@
`(do
(send-off logging-agent
(fn [_#]
(try
(let [message# (or ~raw (build-message ~(vec props)))]
(with-context (-> {:id (uuid/next)}
(into ~context)
(into (get-error-context ~cause)))
(->> (or ~raw (build-map-message ~props))
(write-log! ~logger-sym ~level-sym ~cause)))
(catch Throwable cause#
(write-log! ~logger-sym (get-level :error) cause#
"unexpected error on writting log")))))
(try
(write-log! ~logger-sym ~level-sym ~cause message#)
(catch Throwable cause#
(write-log! ~logger-sym (get-level :error) cause#
"unexpected error on writting log")))))))
nil)
`(let [message# (or ~raw (build-map-message ~props))]
`(let [message# (or ~raw (build-message ~(vec props)))]
(write-log! ~logger-sym ~level-sym ~cause message#)
nil)))))))
@ -284,8 +289,8 @@
#?(:cljs
(defn- prepare-message
[message]
(loop [kvpairs (seq message)
message (array-map)
(loop [kvpairs (seq message)
message []
specials []]
(if (nil? kvpairs)
[message specials]
@ -304,7 +309,7 @@
:else
(recur (next kvpairs)
(assoc message k v)
(conj message (str/concat (d/name k) "=" (pr-str v)))
specials)))))))
#?(:cljs
@ -320,7 +325,7 @@
(js/console.log message header-styles normal-styles))
(let [[message specials] (prepare-message message)]
(if (seq specials)
(let [message (str header "%c" (pr-str message))]
(let [message (str header "%c" message)]
(js/console.group message header-styles normal-styles)
(doseq [[type n v] specials]
(case type
@ -329,7 +334,7 @@
(js/console.error (pr-str v))
(js/console.error v))))
(js/console.groupEnd message))
(let [message (str header "%c" (pr-str message))]
(let [message (str header "%c" message)]
(js/console.log message header-styles normal-styles)))))
(when exception

View file

@ -96,6 +96,7 @@
(-> (update :touched cph/set-touched-group :shapes-group)
(dissoc :remote-synced?)))))
;; TODO: this looks wrong, why we allow nil values?
(update-objects [objects parent-id]
(if (and (or (nil? parent-id) (contains? objects parent-id))
(or (nil? frame-id) (contains? objects frame-id)))

View file

@ -0,0 +1,27 @@
;; 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.common.pprint
(:refer-clojure :exclude [prn])
(:require
[cuerdas.core :as str]
[fipp.edn :as fpp]))
(defn pprint-str
[expr]
(binding [*print-level* 8
*print-length* 25]
(with-out-str
(fpp/pprint expr {:width 110}))))
(defn pprint
([expr]
(println (pprint-str expr)))
([label expr]
(println (str/concat "============ " label "============"))
(pprint expr)))

View file

@ -3,10 +3,10 @@ LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
ARG DEBIAN_FRONTEND=noninteractive
ENV NODE_VERSION=v16.14.0 \
CLOJURE_VERSION=1.10.3.1075 \
CLJKONDO_VERSION=2022.02.09 \
BABASHKA_VERSION=0.7.6 \
ENV NODE_VERSION=v16.14.2 \
CLOJURE_VERSION=1.11.0.1100 \
CLJKONDO_VERSION=2022.03.09 \
BABASHKA_VERSION=0.8.0 \
LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8
@ -30,6 +30,7 @@ RUN set -ex; \
rsync \
fakeroot \
netcat \
file \
; \
echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen; \
locale-gen; \
@ -102,22 +103,14 @@ RUN set -ex; \
; \
rm -rf /var/lib/apt/lists/*;
RUN set -x; \
apt-get -qq update; \
curl -LfsSo /tmp/chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb; \
dpkg -i /tmp/chrome.deb; \
apt-get -fy install; \
rm -rf /var/lib/apt/lists/*; \
rm -rf /tmp/chrome.deb;
RUN set -ex; \
curl -LfsSo /tmp/openjdk.tar.gz https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.2%2B8/OpenJDK17U-jdk_x64_linux_hotspot_17.0.2_8.tar.gz; \
mkdir -p /usr/lib/jvm/openjdk17; \
cd /usr/lib/jvm/openjdk17; \
curl -LfsSo /tmp/openjdk.tar.gz https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18%2B36/OpenJDK18U-jdk_x64_linux_hotspot_18_36.tar.gz; \
mkdir -p /usr/lib/jvm/openjdk; \
cd /usr/lib/jvm/openjdk; \
tar -xf /tmp/openjdk.tar.gz --strip-components=1; \
rm -rf /tmp/openjdk.tar.gz;
ENV PATH="/usr/lib/jvm/openjdk17/bin:/usr/local/nodejs/bin:$PATH" JAVA_HOME=/usr/lib/jvm/openjdk17
ENV PATH="/usr/lib/jvm/openjdk/bin:/usr/local/nodejs/bin:$PATH" JAVA_HOME=/usr/lib/jvm/openjdk
RUN set -ex; \
curl -LfsSo /tmp/clojure.sh https://download.clojure.org/install/linux-install-$CLOJURE_VERSION.sh; \

View file

@ -1,6 +1,6 @@
#!/usr/bin/env bash
export PATH=/usr/lib/jvm/openjdk17/bin:/usr/local/nodejs/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
export PATH=/usr/lib/jvm/openjdk/bin:/usr/local/nodejs/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
alias l='ls --color -GFlh'
alias rm='rm -r'

View file

@ -1,6 +1,6 @@
#!/usr/bin/env bash
export PATH=/usr/lib/jvm/openjdk17/bin:/usr/local/nodejs/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
export PATH=/usr/lib/jvm/openjdk/bin:/usr/local/nodejs/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
set -e
usermod -u ${EXTERNAL_UID:-1000} penpot

View file

@ -46,7 +46,7 @@ http {
listen 3449 default_server;
server_name _;
client_max_body_size 50M;
client_max_body_size 30M;
charset utf-8;
proxy_http_version 1.1;

View file

@ -14,6 +14,7 @@ yarn install
popd
pushd ~/penpot/exporter/
yarn install
npx playwright install chromium
popd
tmux -2 new-session -d -s penpot

View file

@ -1,5 +1,6 @@
# Should be set to the public domain where penpot is going to be served.
PENPOT_PUBLIC_URI=http://localhost:9001
PENPOT_TENANT=pro
# Temporal workaround because of bad builtin default
PENPOT_HTTP_SERVER_HOST=0.0.0.0
@ -16,7 +17,7 @@ PENPOT_REDIS_URI=redis://penpot-redis/0
# can be configured to store in AWS S3 or completely in de the database.
# Storing in the database makes the backups more easy but will make access to
# media less performant.
ASSETS_STORAGE_BACKEND=assets-fs
PENPOT_ASSETS_STORAGE_BACKEND=assets-fs
PENPOT_STORAGE_ASSETS_FS_DIRECTORY=/opt/data/assets
# Telemetry. When enabled, a periodical process will send anonymous data about

View file

@ -56,11 +56,12 @@
(defn screenshot
([frame] (screenshot frame {}))
([frame {:keys [full-page? omit-background? type quality]
([frame {:keys [full-page? omit-background? type quality path]
:or {type "png" full-page? false omit-background? false quality 95}}]
(let [options (-> (obj/new)
(obj/set! "type" (name type))
(obj/set! "omitBackground" omit-background?)
(cond-> path (obj/set! "path" path))
(cond-> (= "jpeg" type) (obj/set! "quality" quality))
(cond-> full-page? (-> (obj/set! "fullPage" true)
(obj/set! "clip" nil))))]
@ -73,10 +74,10 @@
(defn pdf
([page] (pdf page {}))
([page {:keys [scale save-path page-ranges]
([page {:keys [scale path page-ranges]
:or {page-ranges "1"
scale 1}}]
(.pdf ^js page #js {:path save-path
(.pdf ^js page #js {:path path
:scale scale
:pageRanges page-ranges
:printBackground true

View file

@ -6,7 +6,7 @@
(ns app.handlers
(:require
[app.common.data.macros :as dm]
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
@ -32,6 +32,7 @@
(let [explain (us/pretty-explain data)
data (-> data
(assoc :explain explain)
(assoc :type :validation)
(dissoc ::s/problems ::s/value ::s/spec))]
(-> exchange
(assoc :response/status 400)
@ -46,19 +47,24 @@
(and (= :internal type)
(= :browser-not-ready code))
(-> exchange
(assoc :response/status 503)
(assoc :response/body (t/encode data))
(assoc :response/headers {"content-type" "application/transit+json"}))
(let [data {:type :server-error
:code :internal
:hint (ex-message error)
:data data}]
(-> exchange
(assoc :response/status 503)
(assoc :response/body (t/encode data))
(assoc :response/headers {"content-type" "application/transit+json"})))
:else
(let [data {:type :server-error
:code type
:hint (ex-message error)
:data data}]
(l/error :hint "unexpected internal error" :cause error)
(-> exchange
(assoc :response/status 500)
(assoc :response/body (t/encode data))
(assoc :response/body (t/encode (d/without-nils data)))
(assoc :response/headers {"content-type" "application/transit+json"}))))))
(defmulti command-spec :cmd)
@ -98,4 +104,4 @@
:export-frames (export-frames/handler exchange params)
(ex/raise :type :internal
:code :method-not-implemented
:hint (dm/fmt "method % not implemented" cmd)))))
:hint (str/istr "method ~{cmd} not implemented")))))

View file

@ -7,12 +7,14 @@
(ns app.handlers.export-frames
(:require
["path" :as path]
[app.common.data.macros :as dm]
[app.common.exceptions :as exc :include-macros true]
[app.common.logging :as l]
[app.common.exceptions :as exc]
[app.common.spec :as us]
[app.common.pprint :as pp]
[app.handlers.resources :as rsc]
[app.handlers.export-shapes :refer [prepare-exports]]
[app.redis :as redis]
[app.renderer.pdf :as rp]
[app.renderer :as rd]
[app.util.shell :as sh]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
@ -20,19 +22,17 @@
(declare ^:private handle-export)
(declare ^:private create-pdf)
(declare ^:private export-frame)
(declare ^:private join-pdf)
(declare ^:private move-file)
(declare ^:private clean-tmp)
(s/def ::name ::us/string)
(s/def ::file-id ::us/uuid)
(s/def ::page-id ::us/uuid)
(s/def ::frame-id ::us/uuid)
(s/def ::object-id ::us/uuid)
(s/def ::uri ::us/uri)
(s/def ::export
(s/keys :req-un [::file-id ::page-id ::frame-id ::name]))
(s/keys :req-un [::file-id ::page-id ::object-id ::name]))
(s/def ::exports
(s/every ::export :kind vector? :min-count 1))
@ -42,42 +42,53 @@
:opt-un [::uri ::name]))
(defn handler
[{:keys [:request/auth-token] :as exchange} {:keys [exports uri] :as params}]
(let [xform (map #(assoc % :token auth-token :uri uri))
exports (sequence xform exports)]
[{:keys [:request/auth-token] :as exchange} {:keys [exports uri profile-id] :as params}]
;; NOTE: we need to have the `:type` prop because the exports
;; datastructure preparation uses it for creating the groups.
(let [exports (-> (map #(assoc % :type :pdf :scale 1 :suffix "") exports)
(prepare-exports auth-token uri))]
(handle-export exchange (assoc params :exports exports))))
(defn handle-export
[exchange {:keys [exports wait uri name] :as params}]
(let [topic (-> exports first :file-id str)
[exchange {:keys [exports wait uri name profile-id] :as params}]
(let [total (count exports)
topic (str profile-id)
resource (rsc/create :pdf (or name (-> exports first :name)))
on-progress (fn [progress]
(let [data {:type :export-update
:resource-id (:id resource)
:name (:name resource)
:status "running"
:progress progress}]
(redis/pub! topic data)))
on-progress (fn [{:keys [done]}]
(when-not wait
(let [data {:type :export-update
:resource-id (:id resource)
:name (:name resource)
:filename (:filename resource)
:status "running"
:total total
:done done}]
(redis/pub! topic data))))
on-complete (fn [resource]
(let [data {:type :export-update
:resource-id (:id resource)
:name (:name resource)
:size (:size resource)
:status "ended"}]
(redis/pub! topic data)))
on-complete (fn []
(when-not wait
(let [data {:type :export-update
:resource-id (:id resource)
:name (:name resource)
:filename (:filename resource)
:status "ended"}]
(redis/pub! topic data))))
on-error (fn [cause]
(let [data {:type :export-update
:resource-id (:id resource)
:name (:name resource)
:status "error"
:cause (ex-message cause)}]
(redis/pub! topic data)))
(l/error :hint "unexpected error on frames exportation" :cause cause)
(if wait
(p/rejected cause)
(let [data {:type :export-update
:resource-id (:id resource)
:name (:name resource)
:filename (:filename resource)
:status "error"
:cause (ex-message cause)}]
(redis/pub! topic data))))
proc (create-pdf :resource resource
:items exports
:exports exports
:on-progress on-progress
:on-complete on-complete
:on-error on-error)]
@ -86,70 +97,46 @@
(assoc exchange :response/body (dissoc resource :path)))))
(defn create-pdf
[& {:keys [resource items on-progress on-complete on-error]
:or {on-progress identity
on-complete identity
on-error identity}}]
(p/let [progress (atom 0)
tmpdir (sh/create-tmpdir! "pdfexport")
file-id (-> items first :file-id)
items (into [] (map #(partial export-frame tmpdir %)) items)
xform (map (fn [export-fn]
#(p/finally
(export-fn)
(fn [result _]
(on-progress {:total (count items)
:done (swap! progress inc)
:name (:name result)})))))]
(-> (reduce (fn [res export-fn]
(p/let [res res
out (export-fn)]
(cons (:path out) res)))
(p/resolved nil)
(into '() xform items))
(p/then (partial join-pdf tmpdir file-id))
[& {:keys [resource exports on-progress on-complete on-error]
:or {on-progress (constantly nil)
on-complete (constantly nil)
on-error p/rejected}}]
(let [file-id (-> exports first :file-id)
result (atom [])
on-object
(fn [{:keys [path] :as object}]
(let [res (swap! result conj path)]
(on-progress {:done (count res)})))]
(-> (p/loop [exports (seq exports)]
(when-let [export (first exports)]
(p/let [proc (rd/render export on-object)]
(p/recur (rest exports)))))
(p/then (fn [_] (deref result)))
(p/then (partial join-pdf file-id))
(p/then (partial move-file resource))
(p/then (partial clean-tmp tmpdir))
(p/then (constantly resource))
(p/then (fn [resource]
(-> (sh/stat (:path resource))
(p/then #(merge resource %)))))
(p/catch on-error)
(p/finally (fn [result cause]
(if cause
(on-error cause)
(on-complete result)))))))
(defn- export-frame
[tmpdir {:keys [file-id page-id frame-id token uri] :as params}]
(let [file-name (dm/fmt "%.pdf" frame-id)
save-path (path/join tmpdir file-name)]
(-> (rp/render {:name (dm/str frame-id)
:uri uri
:suffix ""
:token token
:file-id file-id
:page-id page-id
:object-id frame-id
:scale 1
:save-path save-path})
(p/then (fn [_]
{:name file-name
:path save-path})))))
(when-not cause
(on-complete)))))))
(defn- join-pdf
[tmpdir file-id paths]
(let [output-path (path/join tmpdir (str file-id ".pdf"))
paths-str (str/join " " paths)]
(-> (sh/run-cmd! (str "pdfunite " paths-str " " output-path))
(p/then (constantly output-path)))))
[file-id paths]
(p/let [tmpdir (sh/mktmpdir! "join-pdf")
path (path/join tmpdir (str/concat file-id ".pdf"))]
(sh/run-cmd! (str "pdfunite " (str/join " " paths) " " path))
path))
(defn- move-file
[{:keys [path] :as resource} output-path]
(p/do
(sh/move! output-path path)
(sh/rmdir! (path/dirname output-path))
resource))
(defn- clean-tmp
[tdpath data]
(p/do!
(sh/rmdir! tdpath)
data))

View file

@ -6,34 +6,35 @@
(ns app.handlers.export-shapes
(:require
[app.common.exceptions :as exc :include-macros true]
["path" :as path]
[app.common.data :as d]
[app.common.exceptions :as exc]
[app.common.logging :as l]
[app.common.spec :as us]
[app.redis :as redis]
[app.handlers.resources :as rsc]
[app.renderer.bitmap :as rb]
[app.renderer.pdf :as rp]
[app.renderer.svg :as rs]
[app.redis :as redis]
[app.renderer :as rd]
[app.util.mime :as mime]
[app.util.shell :as sh]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[promesa.core :as p]))
(declare ^:private handle-exports)
(declare ^:private handle-single-export)
(declare ^:private handle-multiple-export)
(declare ^:private run-export)
(declare ^:private assign-file-name)
(declare ^:private assoc-file-name)
(declare prepare-exports)
(s/def ::name ::us/string)
(s/def ::page-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::filename ::us/string)
(s/def ::name ::us/string)
(s/def ::object-id ::us/uuid)
(s/def ::page-id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::scale ::us/number)
(s/def ::suffix ::us/string)
(s/def ::type ::us/keyword)
(s/def ::suffix string?)
(s/def ::scale number?)
(s/def ::uri ::us/uri)
(s/def ::profile-id ::us/uuid)
(s/def ::wait ::us/boolean)
(s/def ::export
@ -47,13 +48,13 @@
:opt-un [::uri ::wait ::name]))
(defn handler
[{:keys [:request/auth-token] :as exchange} {:keys [exports] :as params}]
(let [xform (comp
(map #(assoc % :token auth-token))
(assign-file-name))
exports (into [] xform exports)]
(if (= 1 (count exports))
(handle-single-export exchange (assoc params :export (first exports)))
[{:keys [:request/auth-token] :as exchange} {:keys [exports uri] :as params}]
(let [exports (prepare-exports exports auth-token uri)]
(if (and (= 1 (count exports))
(= 1 (count (-> exports first :objects))))
(handle-single-export exchange (-> params
(assoc :export (first exports))
(dissoc :exports)))
(handle-multiple-export exchange (assoc params :exports exports)))))
(defn- handle-single-export
@ -61,87 +62,102 @@
(let [topic (str profile-id)
resource (rsc/create (:type export) (or name (:name export)))
on-progress (fn [progress]
(let [data {:type :export-update
:resource-id (:id resource)
:status "running"
:progress progress}]
(redis/pub! topic data)))
on-complete (fn [resource]
(let [data {:type :export-update
:resource-id (:id resource)
:size (:size resource)
:name (:name resource)
:status "ended"}]
(redis/pub! topic data)))
on-progress (fn [{:keys [path] :as object}]
(p/do
;; Move the generated path to the resource
;; path destination.
(sh/move! path (:path resource))
(when-not wait
(redis/pub! topic {:type :export-update
:resource-id (:id resource)
:status "running"
:total 1
:done 1})
(redis/pub! topic {:type :export-update
:resource-id (:id resource)
:filename (:filename resource)
:name (:name resource)
:status "ended"}))))
on-error (fn [cause]
(let [data {:type :export-update
:resource-id (:id resource)
:name (:name resource)
:status "error"
:cause (ex-message cause)}]
(redis/pub! topic data)))
(l/error :hint "unexpected error happened on export multiple process"
:cause cause)
(if wait
(p/rejected cause)
(redis/pub! topic {:type :export-update
:resource-id (:id resource)
:status "error"
:cause (ex-message cause)})))
proc (-> (rd/render export on-progress)
(p/then (constantly resource))
(p/catch on-error))]
proc (rsc/create-simple :task #(run-export export)
:resource resource
:on-progress on-progress
:on-error on-error
:on-complete on-complete)]
(if wait
(p/then proc #(assoc exchange :response/body (dissoc % :path)))
(assoc exchange :response/body (dissoc resource :path)))))
(defn- handle-multiple-export
[exchange {:keys [exports wait uri profile-id name] :as params}]
(let [tasks (map #(fn [] (run-export %)) exports)
(let [resource (rsc/create :zip (or name (-> exports first :name)))
total (count exports)
topic (str profile-id)
resource (rsc/create :zip (or name (-> exports first :name)))
on-progress (fn [progress]
(let [data {:type :export-update
:resource-id (:id resource)
:name (:name resource)
:status "running"
:progress progress}]
(redis/pub! topic data)))
to-delete (atom #{})
on-complete (fn [resource]
(let [data {:type :export-update
:resource-id (:id resource)
:name (:name resource)
:size (:size resource)
:status "ended"}]
(redis/pub! topic data)))
on-progress (fn [{:keys [done]}]
(when-not wait
(let [data {:type :export-update
:resource-id (:id resource)
:status "running"
:total total
:done done}]
(redis/pub! topic data))))
on-complete (fn []
(when-not wait
(let [data {:type :export-update
:name (:name resource)
:filename (:filename resource)
:resource-id (:id resource)
:status "ended"}]
(redis/pub! topic data))))
on-error (fn [cause]
(let [data {:type :export-update
:resource-id (:id resource)
:name (:name resource)
:status "error"
:cause (ex-message cause)}]
(redis/pub! topic data)))
(l/error :hint "unexpected error on multiple exportation" :cause cause)
(if wait
(p/rejected cause)
(redis/pub! topic {:type :export-update
:resource-id (:id resource)
:status "error"
:cause (ex-message cause)})))
proc (rsc/create-zip :resource resource
:tasks tasks
:on-progress on-progress
zip (rsc/create-zip :resource resource
:on-complete on-complete
:on-error on-error)]
:on-error on-error
:on-progress on-progress)
append (fn [{:keys [filename path] :as object}]
(swap! to-delete conj path)
(rsc/add-to-zip! zip path filename))
proc (-> (p/do
(p/loop [exports (seq exports)]
(when-let [export (first exports)]
(p/let [proc (rd/render export append)]
(p/recur (rest exports)))))
(.finalize zip))
(p/then (fn [_] (p/run! #(sh/rmdir! (path/dirname %)) @to-delete)))
(p/then (constantly resource))
(p/catch on-error))
]
(if wait
(p/then proc #(assoc exchange :response/body (dissoc % :path)))
(assoc exchange :response/body (dissoc resource :path)))))
(defn- run-export
[{:keys [type] :as params}]
(p/let [res (case type
:png (rb/render params)
:jpeg (rb/render params)
:svg (rs/render params)
:pdf (rp/render params))]
(assoc res :type type)))
(defn- assign-file-name
(defn- assoc-file-name
"A transducer that assocs a candidate filename and avoid duplicates."
[]
(letfn [(find-candidate [params used]
@ -149,12 +165,8 @@
(let [candidate (str (:name params)
(:suffix params "")
(when (pos? index)
(str "-" (inc index)))
(case (:type params)
:png ".png"
:jpeg ".jpg"
:svg ".svg"
:pdf ".pdf"))]
(str/concat "-" (inc index)))
(mime/get-extension (:type params)))]
(if (contains? used candidate)
(recur (inc index))
candidate))))]
@ -168,3 +180,37 @@
params (assoc params :filename candidate)]
(vswap! used conj candidate)
(rf result params))))))))
(def ^:const ^:private
default-partition-size 50)
(defn prepare-exports
[exports token uri]
(letfn [(process-group [group]
(sequence (comp (partition-all default-partition-size)
(map process-partition))
group))
(process-partition [[part1 :as part]]
{:file-id (:file-id part1)
:page-id (:page-id part1)
:name (:name part1)
:token token
:uri uri
:type (:type part1)
:scale (:scale part1)
:objects (mapv part-entry->object part)})
(part-entry->object [entry]
{:id (:object-id entry)
:filename (:filename entry)
:name (:name entry)
:suffix (:suffix entry)})]
(let [xform (comp
(map #(assoc % :token token))
(assoc-file-name))]
(->> (sequence xform exports)
(d/group-by (juxt :scale :type))
(map second)
(into [] (mapcat process-group))))))

View file

@ -12,104 +12,33 @@
["os" :as os]
["path" :as path]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.uuid :as uuid]
[app.util.shell :as sh]
[app.util.mime :as mime]
[cljs.core :as c]
[cuerdas.core :as str]
[promesa.core :as p]))
(defn- get-path
[type id]
(path/join (os/tmpdir) (dm/str "exporter." (d/name type) "." id)))
(defn- get-mtype
[type]
(case (d/name type)
"zip" "application/zip"
"pdf" "application/pdf"
"svg" "image/svg+xml"
"jpeg" "image/jpeg"
"png" "image/png"))
(path/join (os/tmpdir) (str/concat "exporter-resource." (c/name type) "." id)))
(defn create
"Generates ephimeral resource object."
[type name]
(let [task-id (uuid/next)]
{:path (get-path type task-id)
:mtype (get-mtype type)
:name name
:id (dm/str (c/name type) "." task-id)}))
(defn- write-as-zip!
[{:keys [id path]} items on-progress]
(let [^js zip (arc/create "zip")
^js out (fs/createWriteStream path)
append! (fn [{:keys [data name] :as result}]
(.append zip data #js {:name name}))
progress (atom 0)]
(p/create
(fn [resolve reject]
(.on zip "error" #(reject %))
(.on zip "end" resolve)
(.on zip "entry" (fn [data]
(let [name (unchecked-get data "name")
num (swap! progress inc)]
;; Sample code used for testing failing exports
#_(when (= 2 num)
(.abort ^js zip)
(reject (js/Error. "unable to create zip file")))
(on-progress
{:total (count items)
:done num}))))
(.pipe zip out)
(-> (reduce (fn [res export-fn]
(p/then res (fn [_] (-> (export-fn) (p/then append!)))))
(p/resolved 1)
items)
(p/then #(.finalize zip))
(p/catch reject))))))
(defn create-simple
[& {:keys [task resource on-progress on-complete on-error]
:or {on-progress identity
on-complete identity
on-error identity}
:as params}]
(let [path (:path resource)]
(-> (task)
(p/then (fn [{:keys [data name]}]
(on-progress {:total 1 :done 1 :name name})
(.writeFile fs/promises path data)))
(p/then #(sh/stat path))
(p/then #(merge resource %))
(p/finally (fn [result cause]
(if cause
(on-error cause)
(on-complete result)))))))
(defn create-zip
"Creates a resource with multiple files merget into a single zip file."
[& {:keys [resource tasks on-error on-progress on-complete]
:or {on-error identity
on-progress identity
on-complete identity}}]
(let [{:keys [path id] :as resource} resource]
(-> (write-as-zip! resource tasks on-progress)
(p/then #(sh/stat path))
(p/then #(merge resource %))
(p/finally (fn [result cause]
(if cause
(on-error cause)
(on-complete result)))))))
{:path (get-path type task-id)
:mtype (mime/get type)
:name name
:filename (str/concat name (mime/get-extension type))
:id (str/concat (c/name type) "." task-id)}))
(defn- lookup
[id]
(p/let [[type task-id] (str/split id "." 2)
path (get-path type task-id)
mtype (get-mtype type)
mtype (mime/get (keyword type))
stat (sh/stat path)]
(when-not stat
@ -131,3 +60,25 @@
(assoc :response/status 200)
(assoc :response/body stream)
(assoc :response/headers headers))))))
(defn create-zip
[& {:keys [resource on-complete on-progress on-error]}]
(let [^js zip (arc/create "zip")
^js out (fs/createWriteStream (:path resource))
progress (atom 0)]
(.on zip "error" on-error)
(.on zip "end" on-complete)
(.on zip "entry" (fn [data]
(let [name (unchecked-get data "name")
num (swap! progress inc)]
(on-progress {:done num :filename name}))))
(.pipe zip out)
zip))
(defn add-to-zip!
[zip path name]
(.file ^js zip path #js {:name name}))
(defn close-zip!
[zip]
(.finalize ^js zip))

View file

@ -0,0 +1,45 @@
;; 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.renderer
"Common renderer interface."
(:require
[app.common.spec :as us]
[app.renderer.bitmap :as rb]
[app.renderer.pdf :as rp]
[app.renderer.svg :as rs]
[cljs.spec.alpha :as s]))
(s/def ::name ::us/string)
(s/def ::suffix ::us/string)
(s/def ::type #{:jpeg :png :pdf :svg})
(s/def ::page-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::scale ::us/number)
(s/def ::token ::us/string)
(s/def ::uri ::us/uri)
(s/def ::filename ::us/string)
(s/def ::object
(s/keys :req-un [::id ::name ::suffix ::filename]))
(s/def ::objects
(s/coll-of ::object :min-count 1))
(s/def ::render-params
(s/keys :req-un [::file-id ::page-id ::scale ::token ::type ::objects]
:opt-un [::uri]))
(defn- render
[{:keys [type] :as params} on-object]
(us/verify ::render-params params)
(us/verify fn? on-object)
(case type
:png (rb/render params on-object)
:jpeg (rb/render params on-object)
:pdf (rp/render params on-object)
:svg (rs/render params on-object)))

View file

@ -7,75 +7,61 @@
(ns app.renderer.bitmap
"A bitmap renderer."
(:require
["path" :as path]
[app.browser :as bw]
[app.common.data :as d]
[app.common.exceptions :as ex :include-macros true]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[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.mime :as mime]
[app.util.shell :as sh]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[promesa.core :as p]))
(defn screenshot-object
[{:keys [file-id page-id object-id token scale type uri]}]
(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}
: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 (str uri))
(p/let [node (bw/select page "#screenshot")]
(bw/wait-for node)
(bw/eval! page (js* "() => document.body.style.background = 'transparent'"))
(bw/sleep page 2000) ; the good old fix with sleep
(case type
:png (bw/screenshot node {:omit-background? true :type type})
:jpeg (bw/screenshot node {:omit-background? false :type type}))))))))
(s/def ::name ::us/string)
(s/def ::suffix ::us/string)
(s/def ::type #{:jpeg :png})
(s/def ::page-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::object-id ::us/uuid)
(s/def ::scale ::us/number)
(s/def ::token ::us/string)
(s/def ::uri ::us/uri)
(s/def ::params
(s/keys :req-un [::name ::suffix ::type ::object-id ::page-id ::scale ::token ::file-id]
:opt-un [::uri]))
(defn render
[params]
(us/verify ::params params)
(p/let [content (screenshot-object params)]
{:data content
:name (str (:name params)
(:suffix params "")
(case (:type params)
:png ".png"
:jpeg ".jpg"))
:size (alength content)
:mtype (case (:type params)
:png "image/png"
:jpeg "image/jpeg")}))
[{:keys [file-id page-id token scale type uri objects] :as params} on-object]
(letfn [(prepare-options [uri]
#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}
:locale "en-US"
:storageState #js {:cookies (bw/create-cookies uri {:token token})}
:deviceScaleFactor scale
:userAgent bw/default-user-agent})
(render-object [page {:keys [id] :as object}]
(p/let [tmpdir (sh/mktmpdir! "bitmap-render")
path (path/join tmpdir (str/concat id (mime/get-extension type)))
node (bw/select page (str/concat "#screenshot-" id))]
(bw/wait-for node)
(case type
:png (bw/screenshot node {:omit-background? true :type type :path path})
:jpeg (bw/screenshot node {:omit-background? false :type type :path path}))
(on-object (assoc object :path path))))
(render [uri page]
(l/info :uri uri)
(p/do
;; navigate to the page and perform basic setup
(bw/nav! page (str uri))
(bw/sleep page 1000) ; the good old fix with sleep
(bw/eval! page (js* "() => document.body.style.background = 'transparent'"))
;; take the screnshot of requested objects, one by one
(p/run! (partial render-object page) objects)
nil))]
(p/let [params {:file-id file-id
:page-id page-id
:object-id (mapv :id objects)
:route "objects"}
uri (-> (or uri (cf/get :public-uri))
(assoc :path "/render.html")
(assoc :query (u/map->query-string params)))]
(bw/exec! (prepare-options uri) (partial render uri)))))

View file

@ -7,68 +7,62 @@
(ns app.renderer.pdf
"A pdf renderer."
(:require
["path" :as path]
[app.browser :as bw]
[app.common.data.macros :as dm]
[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]
[app.util.mime :as mime]
[app.util.shell :as sh]
[cuerdas.core :as str]
[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 [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}
: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")]
(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))))))))
(s/def ::name ::us/string)
(s/def ::suffix ::us/string)
(s/def ::page-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::object-id ::us/uuid)
(s/def ::scale ::us/number)
(s/def ::token ::us/string)
(s/def ::save-path ::us/string)
(s/def ::uri ::us/uri)
(s/def ::render-params
(s/keys :req-un [::name ::suffix ::object-id ::page-id ::scale ::token ::file-id]
:opt-un [::save-path ::uri]))
(defn render
[params]
(us/assert ::render-params params)
(p/let [content (pdf-from-object params)]
{:data content
:name (str (:name params)
(:suffix params "")
".pdf")
:size (alength content)
:mtype "application/pdf"}))
[{:keys [file-id page-id token scale type uri objects] :as params} on-object]
(letfn [(prepare-options [uri]
#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}
:locale "en-US"
:storageState #js {:cookies (bw/create-cookies uri {:token token})}
:deviceScaleFactor scale
:userAgent bw/default-user-agent})
(prepare-uri [base-uri object-id]
(let [params {:file-id file-id
:page-id page-id
:object-id object-id
:route "objects"}]
(-> base-uri
(assoc :path "/render.html")
(assoc :query (u/map->query-string params)))))
(render-object [page base-uri {:keys [id] :as object}]
(p/let [uri (prepare-uri base-uri id)
tmp (sh/mktmpdir! "pdf-render")
path (path/join tmp (str/concat id (mime/get-extension type)))]
(l/info :uri uri)
(bw/nav! page uri)
(p/let [dom (bw/select page (dm/str "#screenshot-" id))]
(bw/wait-for dom)
(bw/screenshot dom {:full-page? true})
(bw/sleep page 2000) ; the good old fix with sleep
(bw/pdf page {:path path})
path)))
(render [base-uri page]
(p/loop [objects (seq objects)]
(when-let [object (first objects)]
(p/let [uri (prepare-uri base-uri (:id object))
path (render-object page base-uri object)]
(on-object (assoc object :path path))
(p/recur (rest objects))))))]
(let [base-uri (or uri (cf/get :public-uri))]
(bw/exec! (prepare-options base-uri)
(partial render base-uri)))))

View file

@ -10,12 +10,14 @@
["xml-js" :as xml]
[app.browser :as bw]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex :include-macros true]
[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.mime :as mime]
[app.util.shell :as sh]
[cljs.spec.alpha :as s]
[clojure.walk :as walk]
@ -111,9 +113,8 @@
{:width width
:height height}))
(defn- render-object
[{:keys [page-id file-id object-id token scale suffix type uri]}]
(defn render
[{:keys [page-id file-id objects token scale suffix type uri]} on-object]
(letfn [(convert-to-ppm [pngpath]
(l/trace :fn :convert-to-ppm)
(let [basepath (path/dirname pngpath)
@ -246,7 +247,7 @@
(trace-node [{:keys [data] :as node}]
(l/trace :fn :trace-node)
(p/let [tdpath (sh/create-tmpdir! "svgexport-")
(p/let [tdpath (sh/mktmpdir! "svgexport")
pngpath (path/join tdpath "origin.png")
_ (sh/write-file! pngpath data)
ppmpath (convert-to-ppm pngpath)
@ -293,88 +294,78 @@
(sh/rmdir! tempdir)
(dissoc node :tempdir)))
(process-text-node [page item]
(extract-txt-node [page item]
(-> (p/resolved item)
(p/then (partial resolve-text-node page))
(p/then extract-single-node)
(p/then trace-node)
(p/then clean-temp-data)))
(process-text-nodes [page]
(extract-txt-nodes [page {:keys [id] :as objects}]
(l/trace :fn :process-text-nodes)
(-> (bw/select-all page "#screenshot foreignObject")
(p/then (fn [nodes] (p/all (map (partial process-text-node page) nodes))))))
(-> (bw/select-all page (str/concat "#screenshot-" id " foreignObject"))
(p/then (fn [nodes] (p/all (map (partial extract-txt-node page) nodes))))
(p/then (fn [nodes] (d/index-by :id nodes)))))
(extract [page]
(p/let [dom (bw/select page "#screenshot")
xmldata (bw/eval! dom (fn [elem] (.-outerHTML ^js elem)))
nodes (process-text-nodes page)
nodes (d/index-by :id nodes)
result (replace-text-nodes xmldata nodes)
(extract-svg [page {:keys [id] :as object}]
(let [node (bw/select page (str/concat "#screenshot-" id))]
(bw/wait-for node)
(bw/eval! node (fn [elem] (.-outerHTML ^js elem)))))
;; SVG standard don't allow the entity nbsp. &#160; is equivalent but
;; compatible with SVG
result (str/replace result "&nbsp;" "&#160;")]
;; (println "------- ORIGIN:")
;; (cljs.pprint/pprint (xml->clj xmldata))
;; (println "------- RESULT:")
;; (cljs.pprint/pprint (xml->clj result))
;; (println "-------")
result))
]
(prepare-options [uri]
#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}
:locale "en-US"
:storageState #js {:cookies (bw/create-cookies uri {:token token})}
:deviceScaleFactor scale
:userAgent bw/default-user-agent})
(p/let [params {:file-id file-id
:page-id page-id
:object-id object-id
:render-texts true
:embed true
:route "render-object"}
(render-object [page {:keys [id] :as object}]
(p/let [tmpdir (sh/mktmpdir! "svg-render")
path (path/join tmpdir (str/concat id (mime/get-extension type)))
node (bw/select page (str/concat "#screenshot-" id))]
(bw/wait-for node)
(p/let [xmldata (extract-svg page object)
txtdata (extract-txt-nodes page object)
result (replace-text-nodes xmldata txtdata)
uri (-> (or uri (cf/get :public-uri))
(assoc :path "/render.html")
(assoc :query (u/map->query-string params)))]
;; SVG standard don't allow the entity
;; nbsp. &#160; is equivalent but compatible
;; with SVG.
result (str/replace result "&nbsp;" "&#160;")]
(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}
: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))
;; (println "------- ORIGIN:")
;; (cljs.pprint/pprint (xml->clj xmldata))
;; (println "------- RESULT:")
;; (cljs.pprint/pprint (xml->clj result))
;; (println "-------")
(extract page)))))))
(sh/write-file! path result)
(on-object (assoc object :path path))
path)))
(s/def ::name ::us/string)
(s/def ::suffix ::us/string)
(s/def ::type #{:svg})
(s/def ::page-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::object-id ::us/uuid)
(s/def ::scale ::us/number)
(s/def ::token ::us/string)
(s/def ::uri ::us/uri)
(render [uri page]
(l/info :uri uri)
(p/do
;; navigate to the page and perform basic setup
(bw/nav! page (str uri))
(bw/sleep page 1000) ; the good old fix with sleep
(s/def ::params
(s/keys :req-un [::name ::suffix ::type ::object-id ::page-id ::file-id ::scale ::token]
:opt-un [::uri]))
;; take the screnshot of requested objects, one by one
(p/run! (partial render-object page) objects)
nil))]
(defn render
[params]
(us/assert ::params params)
(p/let [content (render-object params)]
{:data content
:name (str (:name params)
(:suffix params "")
".svg")
:size (alength content)
:mtype "image/svg+xml"}))
(p/let [params {:file-id file-id
:page-id page-id
:render-texts true
:render-embed true
:object-id (mapv :id objects)
:route "objects"}
uri (-> (or uri (cf/get :public-uri))
(assoc :path "/render.html")
(assoc :query (u/map->query-string params)))]
(bw/exec! (prepare-options uri)
(partial render uri)))))

View file

@ -0,0 +1,32 @@
;; 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.util.mime
"Mimetype and file extension helpers."
(:refer-clojure :exclude [get])
(:require
[app.common.data :as d]
[cljs.core :as c]))
(defn get-extension
[type]
(case type
:png ".png"
:jpeg ".jpg"
:svg ".svg"
:pdf ".pdf"
:zip ".zip"))
(defn- get
[type]
(case type
:zip "application/zip"
:pdf "application/pdf"
:svg "image/svg+xml"
:jpeg "image/jpeg"
:png "image/png"))

View file

@ -16,12 +16,9 @@
(l/set-level! :trace)
(defn create-tmpdir!
(defn mktmpdir!
[prefix]
(-> (.mkdtemp fs/promises prefix)
(p/then (fn [result]
(path/join (os/tmpdir) result)))))
(.mkdtemp fs/promises (path/join (os/tmpdir) prefix)))
(defn move!
[origin-path dest-path]

View file

@ -5,15 +5,17 @@
org.clojure/clojure {:mvn/version "1.10.3"}
binaryage/devtools {:mvn/version "RELEASE"}
metosin/reitit-core {:mvn/version "0.5.15"}
metosin/reitit-core {:mvn/version "0.5.17"}
funcool/beicon {:mvn/version "2021.07.05-1"}
funcool/okulary {:mvn/version "2020.04.14-0"}
funcool/okulary {:mvn/version "2022.04.01-10"}
funcool/potok {:mvn/version "2021.09.20-0"}
funcool/rumext {:mvn/version "2022.01.20.128"}
funcool/rumext {:mvn/version "2022.03.31-133"}
funcool/tubax {:mvn/version "2021.05.20-0"}
instaparse/instaparse {:mvn/version "1.4.10"}
garden/garden {:mvn/version "1.3.10"}
}
:aliases
@ -30,9 +32,9 @@
:dev
{:extra-paths ["dev"]
:extra-deps
{thheller/shadow-cljs {:mvn/version "2.17.5"}
{thheller/shadow-cljs {:mvn/version "2.17.8"}
org.clojure/tools.namespace {:mvn/version "RELEASE"}
cider/cider-nrepl {:mvn/version "0.28.2"}}}
cider/cider-nrepl {:mvn/version "0.28.3"}}}
:shadow-cljs
{:main-opts ["-m" "shadow.cljs.devtools.cli"]}

View file

@ -25,8 +25,8 @@
"test-e2e-gui": "cypress open"
},
"devDependencies": {
"autoprefixer": "^10.4.2",
"cypress": "^9.5.0",
"autoprefixer": "^10.4.4",
"cypress": "^9.5.3",
"cypress-file-upload": "^5.0.8",
"gettext-parser": "^4.2.0",
"gulp": "4.0.2",
@ -43,12 +43,12 @@
"mkdirp": "^1.0.4",
"nodemon": "^2.0.15",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.6",
"postcss": "^8.4.12",
"postcss-clean": "^1.2.2",
"prettier": "^2.5.1",
"prettier": "^2.6.1",
"rimraf": "^3.0.0",
"sass": "^1.49.7",
"shadow-cljs": "2.17.5"
"sass": "^1.49.9",
"shadow-cljs": "2.17.8"
},
"dependencies": {
"@sentry/browser": "^6.17.4",
@ -56,16 +56,16 @@
"date-fns": "^2.28.0",
"draft-js": "^0.11.7",
"highlight.js": "^11.4.0",
"js-beautify": "^1.14.0",
"js-beautify": "^1.14.2",
"jszip": "^3.6.0",
"luxon": "^2.3.0",
"luxon": "^2.3.1",
"mousetrap": "^1.6.5",
"opentype.js": "^1.3.4",
"randomcolor": "^0.6.2",
"react": "~17.0.2",
"react-dom": "~17.0.2",
"react-virtualized": "^9.22.3",
"rxjs": "~7.5.2",
"rxjs": "~7.5.5",
"sax": "^1.2.4",
"source-map-support": "^0.5.21",
"tdigest": "^0.1.1",

View file

@ -82,7 +82,10 @@
}
.tool-window-bar-icon {
height: 15px;
height: 21px;
display: flex;
align-items: center;
justify-content: center;
svg {
width: 15px;

View file

@ -23,6 +23,7 @@
window.penpotTranslations = JSON.parse({{& translations}});
window.penpotThemes = {{& themes}};
window.penpotVersion = "%version%";
window.penpotBuildDate = "%buildDate%";
</script>
{{# manifest}}

View file

@ -3,6 +3,7 @@
set -ex
CURRENT_VERSION=$1;
BUILD_DATE=$(date -R);
CURRENT_HASH=${CURRENT_HASH:-$(git rev-parse --short HEAD)};
EXTRA_PARAMS=$SHADOWCLJS_EXTRA_PARAMS;
@ -14,4 +15,4 @@ npx gulp dist:clean || exit 1;
npx gulp dist:copy || exit 1;
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./target/dist/index.html;
sed -i -re "s/\%buildDate\%/$BUILD_DATE/g" ./target/dist/index.html;

View file

@ -68,6 +68,13 @@
(-> (obj/get global "penpotVersion")
(v/parse)))
(defn parse-build-date
[global]
(let [date (obj/get global "penpotBuildDate")]
(if (= date "%buildDate%")
"unknown"
date)))
;; --- Globar Config Vars
(def default-theme "default")
@ -83,6 +90,7 @@
(def sentry-dsn (obj/get global "penpotSentryDsn"))
(def onboarding-form-id (obj/get global "penpotOnboardingQuestionsFormId"))
(def build-date (parse-build-date global))
(def flags (atom (parse-flags global)))
(def version (atom (parse-version global)))
(def target (atom (parse-target global)))

View file

@ -33,7 +33,10 @@
(log/set-level! :app :info)
(when (= :browser @cf/target)
(log/info :message "Welcome to penpot" :version (:full @cf/version) :public-uri (str cf/public-uri)))
(log/info :message "Welcome to penpot"
:version (:full @cf/version)
:build-date cf/build-date
:public-uri (str cf/public-uri)))
(declare reinit)

View file

@ -6,7 +6,6 @@
(ns app.main.data.exports
(:require
[app.common.data.macros :as dm]
[app.common.uuid :as uuid]
[app.main.data.modal :as modal]
[app.main.data.workspace.persistence :as dwp]
@ -47,6 +46,7 @@
state
(dissoc state :export))))))
(defn show-workspace-export-dialog
([] (show-workspace-export-dialog nil))
([{:keys [selected]}]
@ -55,8 +55,6 @@
(watch [_ state _]
(let [file-id (:current-file-id state)
page-id (:current-page-id state)
filename (-> (wsh/lookup-page state page-id) :name)
selected (or selected (wsh/lookup-selected state page-id {}))
shapes (if (seq selected)
@ -74,11 +72,10 @@
(assoc :name (:name shape))))]
(rx/of (modal/show :export-shapes
{:exports (vec exports)
:filename filename})))))))
{:exports (vec exports)})))))))
(defn show-viewer-export-dialog
[{:keys [shapes filename page-id file-id exports]}]
[{:keys [shapes page-id file-id exports]}]
(ptk/reify ::show-viewer-export-dialog
ptk/WatchEvent
(watch [_ _ _]
@ -91,51 +88,44 @@
(assoc :object-id (:id shape))
(assoc :shape (dissoc shape :exports))
(assoc :name (:name shape))))]
(rx/of (modal/show :export-shapes {:exports (vec exports)
:filename filename}))))))
(rx/of (modal/show :export-shapes {:exports (vec exports)}))))))
(defn show-workspace-export-frames-dialog
([frames]
(ptk/reify ::show-workspace-export-frames-dialog
ptk/WatchEvent
(watch [_ state _]
(let [file-id (:current-file-id state)
page-id (:current-page-id state)
filename (-> (wsh/lookup-page state page-id)
:name
(dm/str ".pdf"))
[frames]
(ptk/reify ::show-workspace-export-frames-dialog
ptk/WatchEvent
(watch [_ state _]
(let [file-id (:current-file-id state)
page-id (:current-page-id state)
exports (for [frame frames]
{:enabled true
:page-id page-id
:file-id file-id
:object-id (:id frame)
:shape frame
:name (:name frame)})]
exports (for [frame frames]
{:enabled true
:page-id page-id
:file-id file-id
:frame-id (:id frame)
:shape frame
:name (:name frame)})]
(rx/of (modal/show :export-frames
{:exports (vec exports)
:filename filename})))))))
(rx/of (modal/show :export-frames
{:exports (vec exports)}))))))
(defn- initialize-export-status
[exports filename resource-id query-name]
[exports cmd resource]
(ptk/reify ::initialize-export-status
ptk/UpdateEvent
(update [_ state]
(assoc state :export {:in-progress true
:resource-id resource-id
:resource-id (:id resource)
:healthy? true
:error false
:progress 0
:widget-visible true
:detail-visible true
:exports exports
:filename filename
:last-update (dt/now)
:query-name query-name}))))
:cmd cmd}))))
(defn- update-export-status
[{:keys [progress status resource-id name] :as data}]
[{:keys [done status resource-id filename] :as data}]
(ptk/reify ::update-export-status
ptk/UpdateEvent
(update [_ state]
@ -144,10 +134,10 @@
healthy? (< time-diff (dt/duration {:seconds 6}))]
(cond-> state
(= status "running")
(update :export assoc :progress (:done progress) :last-update (dt/now) :healthy? healthy?)
(update :export assoc :progress done :last-update (dt/now) :healthy? healthy?)
(= status "error")
(update :export assoc :error (:cause data) :last-update (dt/now) :healthy? healthy?)
(update :export assoc :in-progress false :error (:cause data) :last-update (dt/now) :healthy? healthy?)
(= status "ended")
(update :export assoc :in-progress false :last-update (dt/now) :healthy? healthy?))))
@ -155,12 +145,12 @@
ptk/WatchEvent
(watch [_ _ _]
(when (= status "ended")
(->> (rp/query! :download-export-resource resource-id)
(->> (rp/query! :exporter {:cmd :get-resource :blob? true :id resource-id})
(rx/delay 500)
(rx/map #(dom/trigger-download name %)))))))
(rx/map #(dom/trigger-download filename %)))))))
(defn request-simple-export
[{:keys [export filename]}]
[{:keys [export]}]
(ptk/reify ::request-simple-export
ptk/UpdateEvent
(update [_ state]
@ -170,22 +160,26 @@
(watch [_ state _]
(let [profile-id (:profile-id state)
params {:exports [export]
:profile-id profile-id}]
:profile-id profile-id
:cmd :export-shapes
:wait true}]
(rx/concat
(rx/of ::dwp/force-persist)
(->> (rp/query! :export-shapes-simple params)
(rx/map (fn [data]
(dom/trigger-download filename data)
(clear-export-state uuid/zero)))
(->> (rp/query! :exporter params)
(rx/mapcat (fn [{:keys [id filename]}]
(->> (rp/query! :exporter {:cmd :get-resource :blob? true :id id})
(rx/map (fn [data]
(dom/trigger-download filename data)
(clear-export-state uuid/zero))))))
(rx/catch (fn [cause]
(prn "KKKK" cause)
(rx/concat
(rx/of (clear-export-state uuid/zero))
(rx/throw cause))))))))))
(defn request-multiple-export
[{:keys [filename exports query-name]
:or {query-name :export-shapes-multiple}
[{:keys [exports cmd]
:or {cmd :export-shapes}
:as params}]
(ptk/reify ::request-multiple-export
ptk/WatchEvent
@ -194,7 +188,7 @@
profile-id (:profile-id state)
ws-conn (:ws-conn state)
params {:exports exports
:name filename
:cmd cmd
:profile-id profile-id
:wait false}
@ -219,11 +213,10 @@
;; Launch the exportation process and stores the resource id
;; locally.
(->> (rp/query! query-name params)
(rx/tap (fn [{:keys [id]}]
(vreset! resource-id id)))
(rx/map (fn [{:keys [id]}]
(initialize-export-status exports filename id query-name))))
(->> (rp/query! :exporter params)
(rx/map (fn [{:keys [id] :as resource}]
(vreset! resource-id id)
(initialize-export-status exports cmd resource))))
;; We proceed to update the export state with incoming
;; progress updates. We delay the stoper for give some time
@ -246,13 +239,12 @@
(rx/map #(clear-export-state @resource-id))
(rx/take-until (rx/delay 6000 stoper))))))))
(defn retry-last-export
[]
(ptk/reify ::retry-last-export
ptk/WatchEvent
(watch [_ state _]
(let [params (select-keys (:export state) [:filename :exports :query-name])]
(let [params (select-keys (:export state) [:exports :cmd])]
(when (seq params)
(rx/of (request-multiple-export params)))))))

View file

@ -964,18 +964,23 @@
(ptk/reify ::toggle-file-thumbnail-selected
ptk/WatchEvent
(watch [_ state _]
(let [selected (wsh/lookup-selected state)
pages (-> state :workspace-data :pages-index vals)
extract (fn [{:keys [objects id] :as page}]
(->> (cph/get-frames objects)
(filter :file-thumbnail)
(map :id)
(remove selected)
(map (fn [frame-id] [id frame-id]))))]
(let [selected (wsh/lookup-selected state)
pages (-> state :workspace-data :pages-index vals)
get-frames (fn [{:keys [objects id] :as page}]
(->> (cph/get-frames objects)
(sequence
(comp (filter :use-for-thumbnail?)
(map :id)
(remove selected)
(map (partial vector id))))))]
(rx/concat
(rx/from (for [[page-id frame-id] (mapcat extract pages)]
(dch/update-shapes [frame-id] #(dissoc % :file-thumbnail) page-id nil)))
(rx/of (dch/update-shapes selected #(assoc % :file-thumbnail true))))))))
(rx/from
(->> (mapcat get-frames pages)
(d/group-by first second)
(map (fn [[page-id frame-ids]]
(dch/update-shapes frame-ids #(dissoc % :use-for-thumbnail?) {:page-id page-id})))))
(rx/of (dch/update-shapes selected #(update % :use-for-thumbnail? not))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Navigation

View file

@ -32,10 +32,9 @@
(def commit-changes? (ptk/type? ::commit-changes))
(defn update-shapes
([ids update-fn] (update-shapes ids update-fn nil nil))
([ids update-fn keys] (update-shapes ids update-fn nil keys))
([ids update-fn page-id {:keys [reg-objects? save-undo? attrs ignore-tree]
:or {reg-objects? false save-undo? true attrs nil}}]
([ids update-fn] (update-shapes ids update-fn nil))
([ids update-fn {:keys [reg-objects? save-undo? attrs ignore-tree page-id]
:or {reg-objects? false save-undo? true}}]
(us/assert ::coll-of-uuid ids)
(us/assert fn? update-fn)
@ -49,20 +48,15 @@
changes (reduce
(fn [changes id]
(pcb/update-shapes changes
[id]
update-fn
{:attrs attrs
:ignore-geometry? (get ignore-tree id)}))
(let [opts {:attrs attrs :ignore-geometry? (get ignore-tree id)}]
(pcb/update-shapes changes [id] update-fn opts)))
(-> (pcb/empty-changes it page-id)
(pcb/set-save-undo? save-undo?)
(pcb/with-objects objects))
ids)]
(when (seq (:redo-changes changes))
(let [changes (cond-> changes
reg-objects?
(pcb/resize-parents ids))]
(let [changes (cond-> changes reg-objects? (pcb/resize-parents ids))]
(rx/of (commit-changes changes)))))))))
(defn update-indices

View file

@ -55,8 +55,8 @@
(->> stream
(rx/filter (ptk/type? ::dws/message))
(rx/map deref)
(rx/map process-message)
(rx/filter #(= subs-id (:subs-id %))))
(rx/filter #(= subs-id (:subs-id %)))
(rx/map process-message))
;; On reconnect, send again the subscription messages
(->> stream
@ -158,9 +158,7 @@
(update-presence [presence]
(-> presence
(update session-id update-session presence)
(d/without-nils)))
]
(d/without-nils)))]
(ptk/reify ::handle-presence
ptk/UpdateEvent

View file

@ -20,39 +20,45 @@
([state]
(lookup-page-objects state (:current-page-id state)))
([state page-id]
(get-in state [:workspace-data :pages-index page-id :objects])))
(dm/get-in state [:workspace-data :pages-index page-id :objects])))
(defn lookup-page-options
([state]
(lookup-page-options state (:current-page-id state)))
([state page-id]
(get-in state [:workspace-data :pages-index page-id :options])))
(dm/get-in state [:workspace-data :pages-index page-id :options])))
(defn lookup-component-objects
([state component-id]
(get-in state [:workspace-data :components component-id :objects])))
(dm/get-in state [:workspace-data :components component-id :objects])))
(defn lookup-local-components
([state]
(get-in state [:workspace-data :components])))
(dm/get-in state [:workspace-data :components])))
(defn process-selected-shapes
([objects selected]
(process-selected-shapes objects selected nil))
([objects selected {:keys [omit-blocked?] :or {omit-blocked? false}}]
(letfn [(selectable? [id]
(and (contains? objects id)
(or (not omit-blocked?)
(not (get-in objects [id :blocked] false)))))]
(let [selected (->> selected (cph/clean-loops objects))]
(into (d/ordered-set)
(filter selectable?)
selected)))))
;; TODO: improve performance of this
(defn lookup-selected
([state]
(lookup-selected state nil))
([state options]
(lookup-selected state (:current-page-id state) options))
([state page-id {:keys [omit-blocked?] :or {omit-blocked? false}}]
([state page-id options]
(let [objects (lookup-page-objects state page-id)
selected (->> (dm/get-in state [:workspace-local :selected])
(cph/clean-loops objects))
selectable? (fn [id]
(and (contains? objects id)
(or (not omit-blocked?)
(not (get-in objects [id :blocked] false)))))]
(into (d/ordered-set)
(filter selectable?)
selected))))
selected (dm/get-in state [:workspace-local :selected])]
(process-selected-shapes objects selected options))))
(defn lookup-shapes
([state ids]
@ -79,7 +85,7 @@
[state file-id]
(if (= file-id (:current-file-id state))
(get state :workspace-data)
(get-in state [:workspace-libraries file-id :data])))
(dm/get-in state [:workspace-libraries file-id :data])))
(defn get-libraries
"Retrieve all libraries, including the local file."

View file

@ -7,8 +7,10 @@
(ns app.main.errors
"Generic error handling"
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.pprint :as pp]
[app.config :as cf]
[app.main.data.messages :as msg]
[app.main.data.users :as du]
@ -17,8 +19,6 @@
[app.util.i18n :refer [tr]]
[app.util.router :as rt]
[app.util.timers :as ts]
[expound.alpha :as expound]
[fipp.edn :as fpp]
[potok.core :as ptk]))
(defn on-error
@ -60,16 +60,15 @@
(defmethod ptk/handle-error :validation
[error]
(ts/schedule
(st/emitf
(msg/show {:content "Unexpected validation error."
:type :error
:timeout 3000})))
#(st/emit! (msg/show {:content "Validation error"
:type :error
:timeout 3000})))
;; Print to the console some debug info.
(js/console.group "Validation Error:")
(ex/ignoring
(js/console.info
(with-out-str (fpp/pprint (dissoc error :explain)))))
(pp/pprint-str (dissoc error :explain))))
(when-let [explain (:explain error)]
(js/console.group "Spec explain:")
@ -79,24 +78,46 @@
(js/console.groupEnd "Validation Error:"))
;; All the errors that happens on worker are handled here.
(defmethod ptk/handle-error :worker-error
[{:keys [code data hint] :as error}]
(let [hint (or hint (:hint data) (:message data) (d/name code))
info (pp/pprint-str (dissoc data :explain))
msg (dm/str "Internal Worker Error: " hint)]
(ts/schedule
#(st/emit!
(msg/show {:content "Something wrong has happened (on worker)."
:type :error
:timeout 3000})))
(js/console.group msg)
(js/console.info info)
(when-let [explain (:explain data)]
(js/console.group "Spec explain:")
(js/console.log explain)
(js/console.groupEnd "Spec explain:"))
(js/console.groupEnd msg)))
;; Error on parsing an SVG
;; TODO: looks unused and deprecated
(defmethod ptk/handle-error :svg-parser
[_]
(ts/schedule
(st/emitf
(msg/show {:content "SVG is invalid or malformed"
:type :error
:timeout 3000}))))
#(st/emit! (msg/show {:content "SVG is invalid or malformed"
:type :error
:timeout 3000}))))
;; TODO: should be handled in the event and not as general error handler
(defmethod ptk/handle-error :comment-error
[_]
(ts/schedule
(st/emitf
(msg/show {:content "There was an error with the comment"
:type :error
:timeout 3000}))))
#(st/emit! (msg/show {:content "There was an error with the comment"
:type :error
:timeout 3000}))))
;; This is a pure frontend error that can be caused by an active
;; assertion (assertion that is preserved on production builds). From
@ -111,15 +132,13 @@
(dm/str cf/public-uri "js/cljs-runtime/" (:file error))
(:line error))]
(ts/schedule
(st/emitf
(msg/show {:content "Internal error: assertion."
:type :error
:timeout 3000})))
#(st/emit! (msg/show {:content "Internal error: assertion."
:type :error
:timeout 3000})))
;; Print to the console some debugging info
(js/console.group message)
(js/console.info context)
(js/console.error (with-out-str (expound/printer error)))
(js/console.groupEnd message)))
;; That are special case server-errors that should be treated
@ -141,7 +160,7 @@
(defmethod ptk/handle-error :server-error
[{:keys [data hint] :as error}]
(let [hint (or hint (:hint data) (:message data))
info (with-out-str (fpp/pprint (dissoc data :explain)))
info (pp/pprint-str (dissoc data :explain))
msg (dm/str "Internal Server Error: " hint)]
(ts/schedule

View file

@ -102,8 +102,22 @@
(l/derived :workspace-drawing st/state))
;; TODO: rename to workspace-selected (?)
;; Don't use directly from components, this is a proxy to improve performance of selected-shapes
(def ^:private selected-shapes-data
(l/derived
(fn [state]
(let [objects (wsh/lookup-page-objects state)
selected (dm/get-in state [:workspace-local :selected])]
{:objects objects :selected selected}))
st/state (fn [v1 v2]
(and (identical? (:objects v1) (:objects v2))
(= (:selected v1) (:selected v2))))))
(def selected-shapes
(l/derived wsh/lookup-selected st/state =))
(l/derived
(fn [{:keys [objects selected]}]
(wsh/process-selected-shapes objects selected))
selected-shapes-data))
(defn make-selected-ref
[id]
@ -258,7 +272,7 @@
(defn objects-by-id
[ids]
(l/derived #(wsh/lookup-shapes % ids) st/state =))
(l/derived #(into [] (keep (d/getf %)) ids) workspace-page-objects))
(defn- set-content-modifiers [state]
(fn [id shape]

View file

@ -14,7 +14,8 @@
(:require
["react-dom/server" :as rds]
[app.common.colors :as clr]
[app.common.geom.align :as gal]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
@ -22,10 +23,12 @@
[app.common.pages.helpers :as cph]
[app.config :as cfg]
[app.main.fonts :as fonts]
[app.main.ui.context :as muc]
[app.main.ui.shapes.bool :as bool]
[app.main.ui.shapes.circle :as circle]
[app.main.ui.shapes.embed :as embed]
[app.main.ui.shapes.export :as export]
[app.main.ui.shapes.filters :as filters]
[app.main.ui.shapes.frame :as frame]
[app.main.ui.shapes.group :as group]
[app.main.ui.shapes.image :as image]
@ -57,11 +60,9 @@
:fill color}])
(defn- calculate-dimensions
[{:keys [objects] :as data} vport]
(let [shapes (cph/get-immediate-children objects)
rect (cond->> (gsh/selection-rect shapes)
(some? vport)
(gal/adjust-to-viewport vport))]
[objects]
(let [shapes (cph/get-immediate-children objects)
rect (gsh/selection-rect shapes)]
(-> rect
(update :x mth/finite 0)
(update :y mth/finite 0)
@ -156,24 +157,63 @@
(->> [x y width height]
(map #(ust/format-precision % viewbox-decimal-precision)))))
(defn adapt-root-frame
[objects object]
(let [shapes (cph/get-immediate-children objects)
srect (gsh/selection-rect shapes)
object (merge object (select-keys srect [:x :y :width :height]))
object (gsh/transform-shape object)]
(assoc object :fill-color "#f0f0f0")))
(defn adapt-objects-for-shape
[objects object-id]
(let [object (get objects object-id)
object (cond->> object
(cph/root-frame? object)
(adapt-root-frame objects))
;; Replace the previous object with the new one
objects (assoc objects object-id object)
modifier (-> (gpt/point (:x object) (:y object))
(gpt/negate)
(gmt/translate-matrix))
mod-ids (cons object-id (cph/get-children-ids objects object-id))
updt-fn #(-> %1
(assoc-in [%2 :modifiers :displacement] modifier)
(update %2 gsh/transform-shape))]
(reduce updt-fn objects mod-ids)))
(defn get-object-bounds
[objects object-id]
(let [object (get objects object-id)
padding (filters/calculate-padding object)
bounds (-> (filters/get-filters-bounds object)
(update :x - (:horizontal padding))
(update :y - (:vertical padding))
(update :width + (* 2 (:horizontal padding)))
(update :height + (* 2 (:vertical padding))))]
(if (cph/group-shape? object)
(if (:masked-group? object)
(get-object-bounds objects (-> object :shapes first))
(->> (:shapes object)
(into [bounds] (map (partial get-object-bounds objects)))
(gsh/join-rects)))
bounds)))
(mf/defc page-svg
{::mf/wrap [mf/memo]}
[{:keys [data width height thumbnails? embed? include-metadata?] :as props
:or {embed? false include-metadata? false}}]
[{:keys [data thumbnails? render-embed? include-metadata?] :as props
:or {render-embed? false include-metadata? false}}]
(let [objects (:objects data)
shapes (cph/get-immediate-children objects)
root-children
(->> shapes
(remove cph/frame-shape?)
(mapcat #(cph/get-children-with-self objects (:id %))))
vport (when (and (some? width) (some? height))
{:width width :height height})
dim (calculate-dimensions data vport)
dim (calculate-dimensions objects)
vbox (format-viewbox dim)
background-color (get-in data [:options :background] default-color)
bgcolor (dm/get-in data [:options :background] default-color)
frame-wrapper
(mf/use-memo
@ -185,7 +225,7 @@
(mf/deps objects)
#(shape-wrapper-factory objects))]
[:& (mf/provider embed/context) {:value embed?}
[:& (mf/provider embed/context) {:value render-embed?}
[:& (mf/provider export/include-metadata-ctx) {:value include-metadata?}
[:svg {:view-box vbox
:version "1.1"
@ -194,12 +234,17 @@
:xmlns:penpot (when include-metadata? "https://penpot.app/xmlns")
:style {:width "100%"
:height "100%"
:background background-color}}
:background bgcolor}}
(when include-metadata?
[:& export/export-page {:options (:options data)}])
[:& ff/fontfaces-style {:shapes root-children}]
(let [shapes (->> shapes
(remove cph/frame-shape?)
(mapcat #(cph/get-children-with-self objects (:id %))))]
[:& ff/fontfaces-style {:shapes shapes}])
(for [item shapes]
(let [frame? (= (:type item) :frame)]
(cond
@ -214,6 +259,10 @@
[:& shape-wrapper {:shape item
:key (:id item)}])))]]]))
;; Component that serves for render frame thumbnails, mainly used in
;; the viewer and handoff
(mf/defc frame-svg
{::mf/wrap [mf/memo]}
[{:keys [objects frame zoom show-thumbnails?] :or {zoom 1} :as props}]
@ -260,6 +309,10 @@
[:> shape-container {:shape frame}
[:& frame/frame-thumbnail {:shape frame}]]))]))
;; Component for rendering a thumbnail of a single componenent. Mainly
;; used to render thumbnails on assets panel.
(mf/defc component-svg
{::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]}
[{:keys [objects group zoom] :or {zoom 1} :as props}]
@ -304,81 +357,122 @@
[:> shape-container {:shape group}
[:& group-wrapper {:shape group :view-box vbox}]]]))
(mf/defc object-svg
{::mf/wrap [mf/memo]}
[{:keys [objects object zoom render-texts? render-embed?]
:or {zoom 1 render-embed? false}
:as props}]
(let [object (cond-> object
(:hide-fill-on-export object)
(assoc :fills []))
obj-id (:id object)
x (* (:x object) zoom)
y (* (:y object) zoom)
width (* (:width object) zoom)
height (* (:height object) zoom)
vbox (dm/str x " " y " " width " " height)
frame-wrapper
(mf/with-memo [objects]
(frame-wrapper-factory objects))
group-wrapper
(mf/with-memo [objects]
(group-wrapper-factory objects))
shape-wrapper
(mf/with-memo [objects]
(shape-wrapper-factory objects))
text-shapes (sequence (filter cph/text-shape?) (vals objects))
render-texts? (and render-texts? (d/seek (comp nil? :position-data) text-shapes))]
[:& (mf/provider embed/context) {:value render-embed?}
[:svg {:id (dm/str "screenshot-" obj-id)
:view-box vbox
:width width
:height height
:version "1.1"
:xmlns "http://www.w3.org/2000/svg"
:xmlnsXlink "http://www.w3.org/1999/xlink"
;; Fix Chromium bug about color of html texts
;; https://bugs.chromium.org/p/chromium/issues/detail?id=1244560#c5
:style {:-webkit-print-color-adjust :exact}}
(let [shapes (cph/get-children objects obj-id)]
[:& ff/fontfaces-style {:shapes shapes}])
(case (:type object)
:frame [:& frame-wrapper {:shape object :view-box vbox}]
:group [:> shape-container {:shape object}
[:& group-wrapper {:shape object}]]
[:& shape-wrapper {:shape object}])]
;; Auxiliary SVG for rendering text-shapes
(when render-texts?
(for [object text-shapes]
[:& (mf/provider muc/text-plain-colors-ctx) {:value true}
[:svg
{:id (dm/str "screenshot-text-" (:id object))
:view-box (dm/str "0 0 " (:width object) " " (:height object))
:width (:width object)
:height (:height object)
:version "1.1"
:xmlns "http://www.w3.org/2000/svg"
:xmlnsXlink "http://www.w3.org/1999/xlink"}
[:& shape-wrapper {:shape (assoc object :x 0 :y 0)}]]]))]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SPRITES (DEBUG)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(mf/defc component-symbol
{::mf/wrap-props false}
[props]
(let [id (obj/get props "id")
data (obj/get props "data")
name (:name data)
path (:path data)
objects (:objects data)
root (get objects id)
selrect (:selrect root)
[{:keys [id data] :as props}]
(let [name (:name data)
objects (-> (:objects data)
(adapt-objects-for-shape id))
object (get objects id)
selrect (:selrect object)
vbox
(format-viewbox
{:width (:width selrect)
:height (:height selrect)})
modifier
(mf/use-memo
(mf/deps (:x root) (:y root))
(fn []
(-> (gpt/point (:x root) (:y root))
(gpt/negate)
(gmt/translate-matrix))))
objects
(mf/use-memo
(mf/deps modifier id objects)
(fn []
(let [modifier-ids (cons id (cph/get-children-ids objects id))
update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier)]
(reduce update-fn objects modifier-ids))))
root
(mf/use-memo
(mf/deps modifier root)
(fn [] (assoc-in root [:modifiers :displacement] modifier)))
group-wrapper
(mf/use-memo
(mf/deps objects)
(fn [] (group-wrapper-factory objects)))]
[:> "symbol" #js {:id (str id)
:viewBox vbox
"penpot:path" path}
[:> "symbol" #js {:id (str id) :viewBox vbox}
[:title name]
[:> shape-container {:shape root}
[:& group-wrapper {:shape root :view-box vbox}]]]))
[:> shape-container {:shape object}
[:& group-wrapper {:shape object :view-box vbox}]]]))
(mf/defc components-sprite-svg
{::mf/wrap-props false}
[props]
(let [data (obj/get props "data")
children (obj/get props "children")
embed? (obj/get props "embed?")
render-embed? (obj/get props "render-embed?")
include-metadata? (obj/get props "include-metadata?")]
[:& (mf/provider embed/context) {:value embed?}
[:& (mf/provider embed/context) {:value render-embed?}
[:& (mf/provider export/include-metadata-ctx) {:value include-metadata?}
[:svg {:version "1.1"
:xmlns "http://www.w3.org/2000/svg"
:xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns:penpot (when include-metadata? "https://penpot.app/xmlns")
:style {:width "100vw"
:height "100vh"
:display (when-not (some? children) "none")}}
:style {:display (when-not (some? children) "none")}}
[:defs
(for [[component-id component-data] (:components data)]
[:& component-symbol {:id component-id
:key (str component-id)
:data component-data}])]
(for [[id data] (:components data)]
[:& component-symbol {:id id :key (dm/str id) :data data}])]
children]]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; RENDERING
;; RENDER FOR DOWNLOAD (wrongly called exportation)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- get-image-data [shape]
@ -426,7 +520,7 @@
(->> (rx/of data)
(rx/map
(fn [data]
(let [elem (mf/element page-svg #js {:data data :embed? true :include-metadata? true})]
(let [elem (mf/element page-svg #js {:data data :render-embed? true :include-metadata? true})]
(rds/renderToStaticMarkup elem)))))))
(defn render-components
@ -445,5 +539,6 @@
(->> (rx/of data)
(rx/map
(fn [data]
(let [elem (mf/element components-sprite-svg #js {:data data :embed? true :include-metadata? true})]
(let [elem (mf/element components-sprite-svg
#js {:data data :render-embed? true :include-metadata? true})]
(rds/renderToStaticMarkup elem))))))))

View file

@ -31,6 +31,10 @@
(= 200 status)
(rx/of body)
(= 413 status)
(rx/throw {:type :validation
:code :request-body-too-large})
(and (>= status 400)
(map? body))
(rx/throw body)
@ -105,34 +109,22 @@
(rx/map http/conditional-decode-transit)
(rx/mapcat handle-response)))
(defn- send-export-command
[& {:keys [cmd params blob?]}]
(defn- send-export
[{:keys [blob?] :as params}]
(->> (http/send! {:method :post
:uri (u/join base-uri "api/export")
:body (http/transit-data (assoc params :cmd cmd))
:body (http/transit-data (dissoc params :blob?))
:credentials "include"
:response-type (if blob? :blob :text)})
(rx/map http/conditional-decode-transit)
(rx/mapcat handle-response)))
(defmethod query :export-shapes-simple
(defmethod query :exporter
[_ params]
(let [params (merge {:wait true} params)]
(->> (rx/of params)
(rx/mapcat #(send-export-command :cmd :export-shapes :params % :blob? false))
(rx/mapcat #(send-export-command :cmd :get-resource :params % :blob? true)))))
(defmethod query :export-shapes-multiple
[_ params]
(send-export-command :cmd :export-shapes :params params :blob? false))
(defmethod query :export-frames-multiple
[_ params]
(send-export-command :cmd :export-frames :params (assoc params :uri (str base-uri)) :blob? false))
(defmethod query :download-export-resource
[_ id]
(send-export-command :cmd :get-resource :params {:id id} :blob? true))
(let [default {:wait false
:blob? false
:uri (str base-uri)}]
(send-export (merge default params))))
(derive :upload-file-media-object ::multipart-upload)
(derive :update-profile-photo ::multipart-upload)

View file

@ -19,7 +19,6 @@
[app.main.ui.onboarding]
[app.main.ui.onboarding.questions]
[app.main.ui.releases]
[app.main.ui.render :as render]
[app.main.ui.settings :as settings]
[app.main.ui.static :as static]
[app.main.ui.viewer :as viewer]
@ -110,15 +109,6 @@
:index index
:share-id share-id}]))
;; TODO: maybe move to `app.render` entrypoint (handled by render.html)
:render-sprite
(do
(let [file-id (uuid (get-in route [:path-params :file-id]))
component-id (get-in route [:query-params :component-id])
component-id (when (some? component-id) (uuid component-id))]
[:& render/render-sprite {:file-id file-id
:component-id component-id}]))
:workspace
(let [project-id (some-> params :path :project-id uuid)
file-id (some-> params :path :file-id uuid)

View file

@ -44,6 +44,7 @@
on-keyup
(mf/use-callback
(fn [event]
(dom/stop-propagation event)
(cond
(kbd/esc? event)
(on-cancel)

View file

@ -23,7 +23,7 @@
[rumext.alpha :as mf]))
(mf/defc export-multiple-dialog
[{:keys [exports filename title query-name no-selection]}]
[{:keys [exports title cmd no-selection]}]
(let [lstate (mf/deref refs/export)
in-progress? (:in-progress lstate)
@ -33,7 +33,10 @@
all-checked? (every? :enabled all-exports)
all-unchecked? (every? (complement :enabled) all-exports)
enabled-exports (into [] (filter :enabled) all-exports)
enabled-exports (into []
(comp (filter :enabled)
(map #(dissoc % :shape :enabled)))
all-exports)
cancel-fn
(fn [event]
@ -45,9 +48,8 @@
(dom/prevent-default event)
(st/emit! (modal/hide)
(de/request-multiple-export
{:filename filename
:exports enabled-exports
:query-name query-name})))
{:exports enabled-exports
:cmd cmd})))
on-toggle-enabled
(fn [index]
@ -145,25 +147,23 @@
(mf/defc export-shapes-dialog
{::mf/register modal/components
::mf/register-as :export-shapes}
[{:keys [exports filename]}]
[{:keys [exports]}]
(let [title (tr "dashboard.export-shapes.title")]
[:& export-multiple-dialog
{:exports exports
:filename filename
:title title
:query-name :export-shapes-multiple
:cmd :export-shapes
:no-selection shapes-no-selection}]))
(mf/defc export-frames
{::mf/register modal/components
::mf/register-as :export-frames}
[{:keys [exports filename]}]
[{:keys [exports]}]
(let [title (tr "dashboard.export-frames.title")]
[:& export-multiple-dialog
{:exports exports
:filename filename
:title title
:query-name :export-frames-multiple}]))
:cmd :export-frames}]))
(mf/defc export-progress-widget
{::mf/wrap [mf/memo]}

View file

@ -1,203 +0,0 @@
;; 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.main.ui.render
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[app.common.pages.helpers :as cph]
[app.common.uuid :as uuid]
[app.main.data.fonts :as df]
[app.main.render :as render]
[app.main.repo :as repo]
[app.main.store :as st]
[app.main.ui.context :as muc]
[app.main.ui.shapes.embed :as embed]
[app.main.ui.shapes.filters :as filters]
[app.main.ui.shapes.shape :refer [shape-container]]
[app.main.ui.shapes.text.fontfaces :as ff]
[app.util.dom :as dom]
[beicon.core :as rx]
[cuerdas.core :as str]
[rumext.alpha :as mf]))
(defn calc-bounds
[object objects]
(let [xf-get-bounds (comp (map #(get objects %)) (map #(calc-bounds % objects)))
padding (filters/calculate-padding object)
obj-bounds (-> (filters/get-filters-bounds object)
(update :x - (:horizontal padding))
(update :y - (:vertical padding))
(update :width + (* 2 (:horizontal padding)))
(update :height + (* 2 (:vertical padding))))]
(cond
(and (= :group (:type object))
(:masked-group? object))
(calc-bounds (get objects (first (:shapes object))) objects)
(= :group (:type object))
(->> (:shapes object)
(into [obj-bounds] xf-get-bounds)
(gsh/join-rects))
:else
obj-bounds)))
(mf/defc object-svg
{::mf/wrap [mf/memo]}
[{:keys [objects object-id zoom render-texts? embed?]
:or {zoom 1 embed? false}
:as props}]
(let [object (get objects object-id)
frame-id (if (= :frame (:type object))
(:id object)
(:frame-id object))
modifier (-> (gpt/point (:x object) (:y object))
(gpt/negate)
(gmt/translate-matrix))
mod-ids (cons frame-id (cph/get-children-ids objects frame-id))
updt-fn #(-> %1
(assoc-in [%2 :modifiers :displacement] modifier)
(update %2 gsh/transform-shape))
objects (reduce updt-fn objects mod-ids)
object (get objects object-id)
object (cond-> object
(:hide-fill-on-export object)
(assoc :fills []))
all-children (cph/get-children objects object-id)
{:keys [x y width height] :as bs} (calc-bounds object objects)
[_ _ width height :as coords] (->> [x y width height] (map #(* % zoom)))
vbox (str/join " " coords)
frame-wrapper
(mf/with-memo [objects]
(render/frame-wrapper-factory objects))
group-wrapper
(mf/with-memo [objects]
(render/group-wrapper-factory objects))
shape-wrapper
(mf/with-memo [objects]
(render/shape-wrapper-factory objects))
is-text? (fn [shape] (= :text (:type shape)))
text-shapes (sequence (comp (map second) (filter is-text?)) objects)
render-texts? (and render-texts? (d/seek (comp nil? :position-data) text-shapes))]
(mf/with-effect [width height]
(dom/set-page-style!
{:size (dm/str (mth/ceil width) "px "
(mth/ceil height) "px")}))
[:& (mf/provider embed/context) {:value embed?}
[:svg {:id "screenshot"
:view-box vbox
:width width
:height height
:version "1.1"
:xmlns "http://www.w3.org/2000/svg"
:xmlnsXlink "http://www.w3.org/1999/xlink"
;; Fix Chromium bug about color of html texts
;; https://bugs.chromium.org/p/chromium/issues/detail?id=1244560#c5
:style {:-webkit-print-color-adjust :exact}}
[:& ff/fontfaces-style {:shapes all-children}]
(case (:type object)
:frame [:& frame-wrapper {:shape object :view-box vbox}]
:group [:> shape-container {:shape object}
[:& group-wrapper {:shape object}]]
[:& shape-wrapper {:shape object}])]
;; Auxiliary SVG for rendering text-shapes
(when render-texts?
(for [object text-shapes]
[:& (mf/provider muc/text-plain-colors-ctx) {:value true}
[:svg {:id (str "screenshot-text-" (:id object))
:view-box (str "0 0 " (:width object) " " (:height object))
:width (:width object)
:height (:height object)
:version "1.1"
:xmlns "http://www.w3.org/2000/svg"
:xmlnsXlink "http://www.w3.org/1999/xlink"}
[:& shape-wrapper {:shape (assoc object :x 0 :y 0)}]]]))]))
(defn- adapt-root-frame
[objects object-id]
(if (uuid/zero? object-id)
(let [object (get objects object-id)
shapes (cph/get-immediate-children objects)
srect (gsh/selection-rect shapes)
object (merge object (select-keys srect [:x :y :width :height]))
object (gsh/transform-shape object)
object (assoc object :fill-color "#f0f0f0")]
(assoc objects (:id object) object))
objects))
(mf/defc render-object
[{:keys [file-id page-id object-id render-texts? embed?] :as props}]
(let [objects (mf/use-state nil)]
(mf/with-effect [file-id page-id object-id]
(->> (rx/zip
(repo/query! :font-variants {:file-id file-id})
(repo/query! :trimmed-file {:id file-id :page-id page-id :object-id object-id}))
(rx/subs
(fn [[fonts {:keys [data]}]]
(when (seq fonts)
(st/emit! (df/fonts-fetched fonts)))
(let [objs (get-in data [:pages-index page-id :objects])
objs (adapt-root-frame objs object-id)]
(reset! objects objs)))))
(constantly nil))
(when @objects
[:& object-svg {:objects @objects
:object-id object-id
:embed? embed?
:render-texts? render-texts?
:zoom 1}])))
(mf/defc render-sprite
[{:keys [file-id component-id] :as props}]
(let [file (mf/use-state nil)]
(mf/with-effect [file-id]
(->> (repo/query! :file {:id file-id})
(rx/subs
(fn [result]
(reset! file result))))
(constantly nil))
(when @file
[:*
[:& render/components-sprite-svg {:data (:data @file) :embed true}
(when (some? component-id)
[:use {:x 0 :y 0
:xlinkHref (str "#" component-id)}])]
(when-not (some? component-id)
[:ul
(for [[id data] (get-in @file [:data :components])]
(let [url (str "#/render-sprite/" (:id @file) "?component-id=" id)]
[:li [:a {:href url} (:name data)]]))])])))

View file

@ -61,7 +61,6 @@
["/debug/icons-preview" :debug-icons-preview])
;; Used for export
["/render-object/:file-id/:page-id/:object-id" :render-object]
["/render-sprite/:file-id" :render-sprite]
["/dashboard/team/:team-id"

View file

@ -9,6 +9,7 @@
[app.common.data :as d]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[app.config :as cfg]
[app.main.ui.context :as muc]
[app.main.ui.shapes.attrs :as attrs]
[app.main.ui.shapes.custom-stroke :refer [shape-custom-strokes]]
@ -50,9 +51,16 @@
[:> :g group-props
(for [[index data] (d/enumerate position-data)]
(let [props (-> #js {:x (mth/round (:x data))
:y (mth/round (- (:y data) (:height data)))
:alignmentBaseline "text-before-edge"
(let [y (if (cfg/check-browser? :safari)
(mth/round (- (:y data) (:height data)))
(mth/round (:y data)))
alignment-bl (when (cfg/check-browser? :safari) "text-before-edge")
dominant-bl (when-not (cfg/check-browser? :safari) "ideographic")
props (-> #js {:x (mth/round (:x data))
:y y
:alignmentBaseline alignment-bl
:dominantBaseline dominant-bl
:style (-> #js {:fontFamily (:font-family data)
:fontSize (:font-size data)
:fontWeight (:font-weight data)

View file

@ -11,6 +11,7 @@
[app.util.code-gen :as cg]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[cuerdas.core :as str]
[rumext.alpha :as mf]))
(defn has-image? [shape]
@ -34,12 +35,10 @@
[:div.attributes-value (-> shape :metadata :height) "px"]
[:& copy-button {:data (cg/generate-css-props shape :height)}]]
(let [mtype (-> shape :metadata :mtype)
name (:name shape)
(let [mtype (-> shape :metadata :mtype)
name (:name shape)
extension (dom/mtype->extension mtype)]
[:a.download-button {:target "_blank"
:download (if extension
(str name "." extension)
name)
:download (cond-> name extension (str/concat extension))
:href (cfg/resolve-file-media (-> shape :metadata))}
(tr "handoff.attributes.image.download")])])))

View file

@ -7,8 +7,6 @@
(ns app.main.ui.viewer.shapes
"The main container for a frame in viewer mode"
(:require
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as geom]
[app.common.pages.helpers :as cph]
[app.common.spec.interactions :as cti]
@ -390,34 +388,3 @@
:bool [:> bool-container {:shape shape :frame frame :objects objects}]
:svg-raw [:> svg-raw-container {:shape shape :frame frame :objects objects}])))))))
(mf/defc frame-svg
{::mf/wrap [mf/memo]}
[{:keys [objects frame zoom] :or {zoom 1} :as props}]
(let [modifier (-> (gpt/point (:x frame) (:y frame))
(gpt/negate)
(gmt/translate-matrix))
update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier)
frame-id (:id frame)
modifier-ids (into [frame-id] (cph/get-children-ids objects frame-id))
objects (reduce update-fn objects modifier-ids)
frame (assoc-in frame [:modifiers :displacement] modifier)
width (* (:width frame) zoom)
height (* (:height frame) zoom)
vbox (str "0 0 " (:width frame 0)
" " (:height frame 0))
wrapper (mf/use-memo
(mf/deps objects)
#(frame-container-factory objects))]
[:svg {:view-box vbox
:width width
:height height
:version "1.1"
:xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns "http://www.w3.org/2000/svg"}
[:& wrapper {:shape frame
:view-box vbox}]]))

View file

@ -8,6 +8,7 @@
"A workspace specific context menu (mouse right click)."
(:require
[app.common.data :as d]
[app.common.pages.helpers :as cph]
[app.common.spec.page :as csp]
[app.main.data.events :as ev]
[app.main.data.modal :as modal]
@ -167,13 +168,12 @@
(mf/defc context-menu-thumbnail
[{:keys [shapes]}]
(let [single? (= (count shapes) 1)
has-frame? (->> shapes (d/seek #(= :frame (:type %))))
is-frame? (and single? has-frame?)
(let [single? (= (count shapes) 1)
has-frame? (some cph/frame-shape? shapes)
do-toggle-thumbnail (st/emitf (dw/toggle-file-thumbnail-selected))]
(when is-frame?
(when (and single? has-frame?)
[:*
(if (every? :file-thumbnail shapes)
(if (every? :use-for-thumbnail? shapes)
[:& menu-entry {:title (tr "workspace.shape.menu.thumbnail-remove")
:on-click do-toggle-thumbnail}]
[:& menu-entry {:title (tr "workspace.shape.menu.thumbnail-set")

View file

@ -13,9 +13,9 @@
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.icons :as i]
[app.util.data :refer [matches-search]]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.strings :refer [matches-search]]
[cuerdas.core :as str]
[okulary.core :as l]
[rumext.alpha :as mf]))

View file

@ -18,7 +18,6 @@
[app.main.ui.shapes.rect :as rect]
[app.main.ui.shapes.text.fontfaces :as ff]
[app.main.ui.workspace.shapes.bool :as bool]
[app.main.ui.workspace.shapes.bounding-box :refer [bounding-box]]
[app.main.ui.workspace.shapes.common :as common]
[app.main.ui.workspace.shapes.frame :as frame]
[app.main.ui.workspace.shapes.group :as group]
@ -26,7 +25,6 @@
[app.main.ui.workspace.shapes.svg-raw :as svg-raw]
[app.main.ui.workspace.shapes.text :as text]
[app.util.object :as obj]
[debug :refer [debug?]]
[rumext.alpha :as mf]))
(declare shape-wrapper)
@ -87,10 +85,7 @@
;; Only used when drawing a new frame.
:frame [:> frame-wrapper opts]
nil)
(when (debug? :bounding-boxes)
[:> bounding-box opts])])))
nil)])))
(def group-wrapper (group/group-wrapper-factory shape-wrapper))
(def svg-raw-wrapper (svg-raw/svg-raw-wrapper-factory shape-wrapper))

View file

@ -1,90 +0,0 @@
;; 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.main.ui.workspace.shapes.bounding-box
(:require
["randomcolor" :as rdcolor]
[app.common.geom.shapes :as gsh]
[app.main.refs :as refs]
[cuerdas.core :as str]
[rumext.alpha :as mf]))
(defn fixed
[num]
(when num (.toFixed num 2)))
(mf/defc cross-point [{:keys [point zoom color]}]
(let [width (/ 5 zoom)]
[:g.point
[:line {:x1 (- (:x point) width) :y1 (- (:y point) width)
:x2 (+ (:x point) width) :y2 (+ (:y point) width)
:stroke color
:stroke-width "1px"
:stroke-opacity 0.5}]
[:line {:x1 (+ (:x point) width) :y1 (- (:y point) width)
:x2 (- (:x point) width) :y2 (+ (:y point) width)
:stroke color
:stroke-width "1px"
:stroke-opacity 0.5}]]))
(mf/defc render-rect [{{:keys [x y width height]} :rect :keys [color transform]}]
[:rect {:x x
:y y
:width width
:height height
:transform (or transform "none")
:style {:stroke color
:fill "none"
:stroke-width "1px"
:pointer-events "none"}}])
(mf/defc render-rect-points [{:keys [points color]}]
(for [[p1 p2] (map vector points (concat (rest points) [(first points)]))]
[:line {:x1 (:x p1)
:y1 (:y p1)
:x2 (:x p2)
:y2 (:y p2)
:style {:stroke color
:stroke-width "1px"}}]))
(mf/defc bounding-box
{::mf/wrap-props false}
[props]
(let [shape (unchecked-get props "shape")
bounding-box (gsh/points->selrect (-> shape :points))
shape-center (gsh/center-shape shape)
line-color (rdcolor #js {:seed (str (:id shape))})
zoom (mf/deref refs/selected-zoom)]
[:g.bounding-box
[:text {:x (:x bounding-box)
:y (- (:y bounding-box) 5)
:font-size 10
:fill line-color
:stroke "var(--color-white)"
:stroke-width 0.1}
(str/format "%s - (%s, %s)" (str/slice (str (:id shape)) 0 8) (fixed (:x bounding-box)) (fixed (:y bounding-box)))]
[:g.center
[:& cross-point {:point shape-center
:zoom zoom
:color line-color}]]
[:g.points
(for [point (:points shape)]
[:& cross-point {:point point
:zoom zoom
:color line-color}])
#_[:& render-rect-points {:points (:points shape)
:color line-color}]]
[:g.selrect
[:& render-rect {:rect (:selrect shape)
;; :transform (gsh/transform-matrix shape)
:color line-color}]
#_[:& render-rect {:rect bounding-box
:color line-color}]]]))

View file

@ -32,12 +32,12 @@
[app.main.ui.context :as ctx]
[app.main.ui.icons :as i]
[app.main.ui.workspace.sidebar.options.menus.typography :refer [typography-entry]]
[app.util.data :refer [matches-search]]
[app.util.dom :as dom]
[app.util.dom.dnd :as dnd]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.router :as rt]
[app.util.strings :refer [matches-search]]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[okulary.core :as l]

View file

@ -189,7 +189,7 @@
(when (and single? selected?)
(ts/schedule
100
#(dom/scroll-into-view! node #js {:block "nearest", :behavior "smooth"})))]
#(dom/scroll-into-view! node #js {:block "center", :behavior "smooth"})))]
#(when (some? subid)
(rx/dispose! subid)))))

View file

@ -30,7 +30,7 @@
state (mf/deref refs/export)
in-progress? (:in-progress state)
filename (when (seqable? exports)
sname (when (seqable? exports)
(let [shapes (wsh/lookup-shapes @st/state ids)
sname (-> shapes first :name)
suffix (-> exports first :suffix)]
@ -56,13 +56,13 @@
;; separatelly by the export-modal.
(let [defaults {:page-id page-id
:file-id file-id
:name filename
:name sname
:object-id (first ids)}
exports (mapv #(merge % defaults) exports)]
(if (= 1 (count 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})))))))
(st/emit! (de/request-simple-export {:export export})))
(st/emit! (de/request-multiple-export {:exports exports})))))))
;; TODO: maybe move to specific events for avoid to have this logic here?
add-export

View file

@ -155,7 +155,7 @@
:on-blur on-blur}])])
(when (or (= type :frame)
(and (= type :multiple) (some? hide-fill-on-export?)))
(and (= type :multiple) (some? (:hide-fill-on-export values))))
[:div.input-checkbox
[:input {:type "checkbox"
:id "show-fill-on-export"

View file

@ -7,6 +7,7 @@
(ns app.main.ui.workspace.sidebar.options.shapes.svg-raw
(:require
[app.common.colors :as clr]
[app.common.data :as d]
[app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]]
[app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]]
[app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]]
@ -15,7 +16,6 @@
[app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu]]
[app.main.ui.workspace.sidebar.options.menus.svg-attrs :refer [svg-attrs-menu]]
[app.util.color :as uc]
[app.util.data :as d]
[cuerdas.core :as str]
[rumext.alpha :as mf]))
@ -75,7 +75,7 @@
stroke-width (-> (or (get-in shape [:content :attrs :stroke-width])
(get-in shape [:content :attrs :style :stroke-width])
"1")
(d/parse-int))
(d/parse-integer))
stroke-values (if (empty? stroke-values)
{:stroke-color stroke-color

View file

@ -212,6 +212,7 @@
:xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns:penpot "https://penpot.app/xmlns"
:preserveAspectRatio "xMidYMid meet"
:shape-rendering "crispEdges"
:key (str "render" page-id)
:width (:width vport 0)
:height (:height vport 0)

View file

@ -32,8 +32,8 @@
:pattern-units "userSpaceOnUse"}
[:path {:d "M 1 0 L 0 0 0 1"
:style {:fill "none"
:stroke "var(--color-gray-20)"
:stroke-opacity "1"
:stroke "var(--color-info)"
:stroke-opacity "0.2"
:stroke-width (str (/ 1 zoom))}}]]]
[:rect {:x (:x vbox)
:y (:y vbox)

View file

@ -7,27 +7,38 @@
(ns app.render
"The main entry point for UI part needed by the exporter."
(:require
[app.common.logging :as log]
[app.common.logging :as l]
[app.common.math :as mth]
[app.common.spec :as us]
[app.common.uri :as u]
[app.config :as cf]
[app.main.ui.render :as render]
[app.main.data.fonts :as df]
[app.main.render :as render]
[app.main.repo :as repo]
[app.main.store :as st]
[app.util.dom :as dom]
[app.util.globals :as glob]
[beicon.core :as rx]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[garden.core :refer [css]]
[rumext.alpha :as mf]))
(log/initialize!)
(log/set-level! :root :warn)
(log/set-level! :app :info)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SETUP
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare reinit)
(l/initialize!)
(l/set-level! :root :warn)
(l/set-level! :app :info)
(declare ^:private render-object)
(declare ^:private render-single-object)
(declare ^:private render-components)
(declare ^:private render-objects)
(log/info :hint "Welcome to penpot (Export)"
:version (:full @cf/version)
:public-uri (str cf/public-uri))
(l/info :hint "Welcome to penpot (Export)"
:version (:full @cf/version)
:public-uri (str cf/public-uri))
(defn- parse-params
[loc]
@ -38,7 +49,8 @@
[]
(when-let [params (parse-params glob/location)]
(when-let [component (case (:route params)
"render-object" (render-object params)
"objects" (render-objects params)
"components" (render-components params)
nil)]
(mf/mount component (dom/get-element "app")))))
@ -55,23 +67,223 @@
[]
(reinit))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; COMPONENTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; ---- SINGLE OBJECT
(defn use-resource
"A general purpose hook for retrieve or subscribe to remote changes
using the reactive-streams mechanism mechanism.
It receives a function to execute for retrieve the stream that will
be used for creating the subscription. The function should be
stable, so is the responsability of the user of this hook to
properly memoize it.
TODO: this should be placed in some generic hooks namespace but his
right now is pending of refactor and it will be done later."
[f]
(let [[state ^js update-state!] (mf/useState {:loaded? false})]
(mf/with-effect [f]
(update-state! (fn [prev] (assoc prev :refreshing? true)))
(let [on-value (fn [data]
(update-state! #(-> %
(assoc :refreshing? false)
(assoc :loaded? true)
(merge data))))
subs (rx/subscribe (f) on-value)]
#(rx/dispose! subs)))
state))
(mf/defc object-svg
[{:keys [page-id file-id object-id render-embed? render-texts?]}]
(let [fetch-state (mf/use-fn
(mf/deps file-id page-id object-id)
(fn []
(->> (rx/zip
(repo/query! :font-variants {:file-id file-id})
(repo/query! :page {:file-id file-id
:page-id page-id
:object-id object-id}))
(rx/tap (fn [[fonts]]
(when (seq fonts)
(st/emit! (df/fonts-fetched fonts)))))
(rx/map (comp :objects second))
(rx/map (fn [objects]
(let [objects (render/adapt-objects-for-shape objects object-id)
bounds (render/get-object-bounds objects object-id)
object (get objects object-id)]
{:objects objects
:object (merge object bounds)}))))))
{:keys [objects object]} (use-resource fetch-state)]
;; Set the globa CSS to assign the page size, needed for PDF
;; exportation process.
(mf/with-effect [object]
(when object
(dom/set-page-style!
{:size (str/concat
(mth/ceil (:width object)) "px "
(mth/ceil (:height object)) "px")})))
(when objects
[:& render/object-svg
{:objects objects
:object object
:render-embed? render-embed?
:render-texts? render-texts?
:zoom 1}])))
(mf/defc objects-svg
[{:keys [page-id file-id object-ids render-embed? render-texts?]}]
(let [fetch-state (mf/use-fn
(mf/deps file-id page-id)
(fn []
(->> (rx/zip
(repo/query! :font-variants {:file-id file-id})
(repo/query! :page {:file-id file-id
:page-id page-id}))
(rx/tap (fn [[fonts]]
(when (seq fonts)
(st/emit! (df/fonts-fetched fonts)))))
(rx/map (comp :objects second)))))
objects (use-resource fetch-state)]
(when objects
(for [object-id object-ids]
(let [objects (render/adapt-objects-for-shape objects object-id)
bounds (render/get-object-bounds objects object-id)
object (merge (get objects object-id) bounds)]
[:& render/object-svg
{:objects objects
:key (str object-id)
:object object
:render-embed? render-embed?
:render-texts? render-texts?
:zoom 1}])))))
(s/def ::page-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::object-id ::us/uuid)
(s/def ::object-id
(s/or :single ::us/uuid
:multiple (s/coll-of ::us/uuid)))
(s/def ::render-text ::us/boolean)
(s/def ::embed ::us/boolean)
(s/def ::render-object-params
(s/def ::render-objects
(s/keys :req-un [::file-id ::page-id ::object-id]
:opt-un [::render-text ::embed]))
:opt-un [::render-text ::render-embed]))
(defn- render-object
(defn- render-objects
[params]
(let [{:keys [page-id file-id object-id render-texts embed]} (us/conform ::render-object-params params)]
(let [{:keys [file-id
page-id
render-embed
render-texts]
:as params}
(us/conform ::render-objects params)
[type object-id] (:object-id params)]
(case type
:single
(mf/html
[:& object-svg
{:file-id file-id
:page-id page-id
:object-id object-id
:render-embed? render-embed
:render-texts? render-texts}])
:multiple
(mf/html
[:& objects-svg
{:file-id file-id
:page-id page-id
:object-ids (into #{} object-id)
:render-embed? render-embed
:render-texts? render-texts}]))))
;; ---- COMPONENTS SPRITE
(mf/defc components-sprite-svg
[{:keys [file-id embed] :as props}]
(let [fetch (mf/use-fn
(mf/deps file-id)
(fn [] (repo/query! :file {:id file-id})))
file (use-resource fetch)
state (mf/use-state nil)]
(when file
[:*
[:style
(css [[:body
{:margin 0
:overflow "hidden"
:width "100vw"
:height "100vh"}]
[:main
{:overflow "auto"
:display "flex"
:justify-content "center"
:align-items "center"
:height "calc(100vh - 200px)"}
[:svg {:width "50%"
:height "50%"}]]
[:.nav
{:display "flex"
:margin 0
:padding "10px"
:flex-direction "column"
:flex-wrap "wrap"
:height "200px"
:list-style "none"
:overflow-x "scroll"
:border-bottom "1px dotted #e6e6e6"}
[:a {:cursor :pointer
:text-overflow "ellipsis"
:white-space "nowrap"
:overflow "hidden"
:text-decoration "underline"}]
[:li {:display "flex"
:width "150px"
:padding "5px"
:border "0px solid black"}]]])]
[:ul.nav
(for [[id data] (get-in file [:data :components])]
(let [on-click (fn [event]
(dom/prevent-default event)
(swap! state assoc :component-id id))]
[:li {:key (str id)}
[:a {:on-click on-click} (:name data)]]))]
[:main
[:& render/components-sprite-svg
{:data (:data file)
:embed embed}
(when-let [component-id (:component-id @state)]
[:use {:x 0 :y 0 :xlinkHref (str "#" component-id)}])]]
])))
(s/def ::component-id ::us/uuid)
(s/def ::render-components
(s/keys :req-un [::file-id]
:opt-un [::embed ::component-id]))
(defn render-components
[params]
(let [{:keys [file-id component-id embed]} (us/conform ::render-components params)]
(mf/html
[:& render/render-object
[:& components-sprite-svg
{:file-id file-id
:page-id page-id
:object-id object-id
:embed? embed
:render-texts? render-texts}])))
:component-id component-id
:embed embed}])))

View file

@ -1,169 +0,0 @@
;; 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.util.data
"A collection of data transformation utils."
(:require [cljs.reader :as r]
[cuerdas.core :as str]))
;; TODO: partially move to app.common.data
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data structure manipulation
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn index-by
"Return a indexed map of the collection
keyed by the result of executing the getter
over each element of the collection."
[getter coll]
(persistent!
(reduce #(assoc! %1 (getter %2) %2) (transient {}) coll)))
(def index-by-id #(index-by :id %))
(defn without-nils
"Given a map, return a map removing key-value
pairs when value is `nil`."
[data]
(into {} (remove (comp nil? second) data)))
(defn without-keys
"Return a map without the keys provided
in the `keys` parameter."
[data keys]
(persistent!
(reduce #(dissoc! %1 %2) (transient data) keys)))
(defn dissoc-in
[m [k & ks :as _keys]]
(if ks
(if-let [nextmap (get m k)]
(let [newmap (dissoc-in nextmap ks)]
(if (seq newmap)
(assoc m k newmap)
(dissoc m k)))
m)
(dissoc m k)))
(defn index-of
"Return the first index when appears the `v` value
in the `coll` collection."
[coll v]
(first (keep-indexed (fn [idx x]
(when (= v x) idx))
coll)))
(defn replace-by-id
([value]
(map (fn [item]
(if (= (:id item) (:id value))
value
item))))
([coll value]
(sequence (replace-by-id value) coll)))
(defn deep-merge
"Like merge, but merges maps recursively."
[& maps]
(if (every? map? maps)
(apply merge-with deep-merge maps)
(last maps)))
(defn conj-or-disj
"Given a set, and an element remove that element from set
if it exists or add it if it does not exists."
[s v]
(if (contains? s v)
(disj s v)
(conj s v)))
(defn enumerate
([items] (enumerate items 0))
([items start]
(loop [idx start
items items
res []]
(if (empty? items)
res
(recur (inc idx)
(rest items)
(conj res [idx (first items)]))))))
(defn concatv
[& colls]
(loop [colls colls
result []]
(if (seq colls)
(recur (rest colls) (reduce conj result (first colls)))
result)))
(defn seek
([pred coll]
(seek pred coll nil))
([pred coll not-found]
(reduce (fn [_ x]
(if (pred x)
(reduced x)
not-found))
not-found coll)))
(defn remove-equal-values [m1 m2]
(if (and (map? m1) (map? m2) (not (nil? m1)) (not (nil? m2)))
(->> m1
(remove (fn [[k v]] (= (k m2) v)))
(into {}))
m1))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Numbers Parsing
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn nan?
[v]
(js/isNaN v))
(defn read-string
[v]
(r/read-string v))
(defn parse-int
([v]
(parse-int v nil))
([v default]
(let [v (js/parseInt v 10)]
(if (or (not v) (nan? v))
default
v))))
(defn parse-float
([v]
(parse-float v nil))
([v default]
(let [v (js/parseFloat v)]
(if (or (not v) (nan? v))
default
v))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Other
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn normalize-props
[props]
(clj->js props :keyword-fn (fn [key]
(if (or (= key :class) (= key :class-name))
"className"
(str/camel (name key))))))
(defn matches-search
[name search-term]
(if (str/empty? search-term)
true
(let [st (str/trim (str/lower search-term))
nm (str/trim (str/lower name))]
(str/includes? nm st))))

View file

@ -6,6 +6,7 @@
(ns app.util.dom
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.point :as gpt]
[app.common.logging :as log]
@ -231,20 +232,20 @@
(.-innerText el)))
(defn query
([^string query]
(query globals/document query))
([^string selector]
(query globals/document selector))
([^js el ^string query]
([^js el ^string selector]
(when (some? el)
(.querySelector el query))))
(.querySelector el selector))))
(defn query-all
([^string query]
(query-all globals/document query))
([^string selector]
(query-all globals/document selector))
([^js el ^string query]
([^js el ^string selector]
(when (some? el)
(.querySelectorAll el query))))
(.querySelectorAll el selector))))
(defn get-client-position
[^js event]
@ -403,16 +404,16 @@
(defn mtype->extension [mtype]
;; https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
(case mtype
"image/apng" "apng"
"image/avif" "avif"
"image/gif" "gif"
"image/jpeg" "jpg"
"image/png" "png"
"image/svg+xml" "svg"
"image/webp" "webp"
"application/zip" "zip"
"application/penpot" "penpot"
"application/pdf" "pdf"
"image/apng" ".apng"
"image/avif" ".avif"
"image/gif" ".gif"
"image/jpeg" ".jpg"
"image/png" ".png"
"image/svg+xml" ".svg"
"image/webp" ".webp"
"application/zip" ".zip"
"application/penpot" ".penpot"
"application/pdf" ".pdf"
nil))
(defn set-attribute! [^js node ^string attr value]
@ -464,11 +465,11 @@
(defn trigger-download-uri
[filename mtype uri]
(let [link (create-element "a")
(let [link (create-element "a")
extension (mtype->extension mtype)
filename (if extension
(str filename "." extension)
filename)]
filename (if (and extension (not (str/ends-with? filename extension)))
(str/concat filename extension)
filename)]
(obj/set! link "href" uri)
(obj/set! link "download" filename)
(obj/set! (.-style ^js link) "display" "none")
@ -535,3 +536,13 @@
(and (some? node)
(some? candidate)
(.contains node candidate)))
(defn seq-nodes
[root-node]
(letfn [(branch? [node]
(d/not-empty? (get-children node)))
(get-children [node]
(seq (.-children node)))]
(->> root-node
(tree-seq branch? get-children))))

View file

@ -214,11 +214,13 @@
(= type :frame)
(let [;; The nodes with the "frame-background" class can have some anidation depending on the strokes they have
g-nodes (find-all-nodes node :g)
g-nodes (find-all-nodes node :g)
defs-nodes (flatten (map #(find-all-nodes % :defs) g-nodes))
gg-nodes (flatten (map #(find-all-nodes % :g) g-nodes))
rect-nodes (flatten [[(find-all-nodes node :rect)]
(map #(find-all-nodes % #{:rect :path}) defs-nodes)
(map #(find-all-nodes % #{:rect :path}) g-nodes)])
(map #(find-all-nodes % #{:rect :path}) g-nodes)
(map #(find-all-nodes % #{:rect :path}) gg-nodes)])
svg-node (d/seek #(= "frame-background" (get-in % [:attrs :class])) rect-nodes)]
(merge (add-attrs {} (:attrs svg-node)) node-attrs))
@ -405,17 +407,31 @@
[props node svg-data]
(let [fill (:fill svg-data)
hide-fill-on-export (get-meta node :hide-fill-on-export str->bool)
fill-color-ref-id (get-meta node :fill-color-ref-id uuid/uuid)
fill-color-ref-file (get-meta node :fill-color-ref-file uuid/uuid)
gradient (when (str/starts-with? fill "url")
(parse-gradient node fill))]
hide-fill-on-export (get-meta node :hide-fill-on-export str->bool)
fill-color-ref-id (get-meta node :fill-color-ref-id uuid/uuid)
fill-color-ref-file (get-meta node :fill-color-ref-file uuid/uuid)
meta-fill-color (get-meta node :fill-color)
meta-fill-opacity (get-meta node :fill-opacity)
meta-fill-color-gradient (if (str/starts-with? meta-fill-color "url")
(parse-gradient node meta-fill-color)
(get-meta node :fill-color-gradient))
gradient (when (str/starts-with? fill "url")
(parse-gradient node fill))]
(cond-> props
:always
(assoc :fill-color nil
:fill-opacity nil)
(some? meta-fill-color)
(assoc :fill-color meta-fill-color
:fill-opacity (d/parse-double meta-fill-opacity))
(some? meta-fill-color-gradient)
(assoc :fill-color-gradient meta-fill-color-gradient
:fill-color nil
:fill-opacity nil)
(some? gradient)
(assoc :fill-color-gradient gradient
:fill-color nil
@ -775,6 +791,7 @@
(-> node
(find-node :defs)
(find-node :pattern)
(find-node :g)
(find-node :image))]
(or (= type :image)
(some? pattern-image))))
@ -789,12 +806,51 @@
(-> node
(find-node :defs)
(find-node :pattern)
(find-node :g)
(find-node :image)
:attrs)
image-data (get-svg-data :image node)
svg-data (or image-data pattern-data)]
(:xlink:href svg-data)))
(defn get-image-fill
[node]
(let [linear-gradient-node (-> node
(find-node :defs)
(find-node :linearGradient))
radial-gradient-node (-> node
(find-node :defs)
(find-node :radialGradient))
gradient-node (or linear-gradient-node radial-gradient-node)
stops (parse-stops gradient-node)
gradient (cond-> {:stops stops}
(some? linear-gradient-node)
(assoc :type :linear
:start-x (-> linear-gradient-node :attrs :x1 d/parse-double)
:start-y (-> linear-gradient-node :attrs :y1 d/parse-double)
:end-x (-> linear-gradient-node :attrs :x2 d/parse-double)
:end-y (-> linear-gradient-node :attrs :y2 d/parse-double)
:width 1)
(some? radial-gradient-node)
(assoc :type :linear
:start-x (get-meta radial-gradient-node :start-x d/parse-double)
:start-y (get-meta radial-gradient-node :start-y d/parse-double)
:end-x (get-meta radial-gradient-node :end-x d/parse-double)
:end-y (get-meta radial-gradient-node :end-y d/parse-double)
:width (get-meta radial-gradient-node :width d/parse-double)))]
(if (some? (or linear-gradient-node radial-gradient-node))
{:fill-color-gradient gradient}
(-> node
(find-node :defs)
(find-node :pattern)
(find-node :g)
(find-node :rect)
:attrs
:style
parse-style))))
(defn parse-data
[type node]

View file

@ -302,16 +302,17 @@
(reduce simplify-command [[start] start-pos start-pos start-pos start-pos])
(first))))
(defn parse-path [path-str]
(let [clean-path-str
(-> path-str
(str/trim)
;; Change "commas" for spaces
(str/replace #"," " ")
;; Remove all consecutive spaces
(str/replace #"\s+" " "))
commands (re-seq commands-regex clean-path-str)]
(-> (mapcat parse-command commands)
(simplify-commands))))
(if (empty? path-str)
path-str
(let [clean-path-str
(-> path-str
(str/trim)
;; Change "commas" for spaces
(str/replace #"," " ")
;; Remove all consecutive spaces
(str/replace #"\s+" " "))
commands (re-seq commands-regex clean-path-str)]
(-> (mapcat parse-command commands)
(simplify-commands)))))

View file

@ -8,7 +8,6 @@
(:require
[cuerdas.core :as str]))
(def ^:const trail-zeros-regex-1 #"\.0+$")
(def ^:const trail-zeros-regex-2 #"(\.\d*[^0])0+$")
@ -36,3 +35,10 @@
(catch :default _
(str num))))
(defn matches-search
[name search-term]
(if (str/empty? search-term)
true
(let [st (str/trim (str/lower search-term))
nm (str/trim (str/lower name))]
(str/includes? nm st))))

View file

@ -54,9 +54,11 @@
(reply-error [cause]
(if (map? cause)
(post {:error cause})
(post {:error {:type :unexpected
:code :unhandled-error-on-worker
(post {:error {:type :worker-error
:code (or (:type cause) :wrapped)
:data cause}})
(post {:error {:type :worker-error
:code :unhandled-error
:hint (ex-message cause)
:data (ex-data cause)}})))

View file

@ -135,7 +135,7 @@
(rx/map #(assoc % :file-id file-id))
(rx/flat-map
(fn [media]
(let [file-path (str file-id "/media/" (:id media) "." (dom/mtype->extension (:mtype media)))]
(let [file-path (str/concat file-id "/media/" (:id media) (dom/mtype->extension (:mtype media)))]
(->> (http/send!
{:uri (cfg/resolve-file-media media)
:response-type :blob

View file

@ -31,6 +31,8 @@
;; Upload changes batches size
(def ^:const change-batch-size 100)
(def conjv (fnil conj []))
(defn get-file
"Resolves the file inside the context given its id and the data"
([context type]
@ -48,7 +50,7 @@
:typographies (str file-id "/typographies.json")
:media-list (str file-id "/media.json")
:media (let [ext (dom/mtype->extension (:mtype media))]
(str file-id "/media/" id "." ext))
(str/concat file-id "/media/" id ext))
:components (str file-id "/components.svg"))
parse-svg? (and (not= type :media) (str/ends-with? path "svg"))
@ -261,25 +263,29 @@
(cond-> (some? old-id)
(assoc :id (resolve old-id)))
(cond-> (< (:version context 1) 2)
(translate-frame type file)))
(translate-frame type file)))]
(try
(let [file (case type
:frame (fb/add-artboard file data)
:group (fb/add-group file data)
:bool (fb/add-bool file data)
:rect (fb/create-rect file data)
:circle (fb/create-circle file data)
:path (fb/create-path file data)
:text (fb/create-text file data)
:image (fb/create-image file data)
:svg-raw (fb/create-svg-raw file data)
#_default file)]
file (case type
:frame (fb/add-artboard file data)
:group (fb/add-group file data)
:bool (fb/add-bool file data)
:rect (fb/create-rect file data)
:circle (fb/create-circle file data)
:path (fb/create-path file data)
:text (fb/create-text file data)
:image (fb/create-image file data)
:svg-raw (fb/create-svg-raw file data)
#_default file)]
;; We store this data for post-processing after every shape has been
;; added
(cond-> file
(d/not-empty? interactions)
(assoc-in [:interactions (:id data)] interactions)))
;; We store this data for post-processing after every shape has been
;; added
(cond-> file
(d/not-empty? interactions)
(assoc-in [:interactions (:id data)] interactions))))))
(catch :default err
(log/error :hint (ex-message err) :cause err :js/data data)
(update file :errors conjv data)))))))
(defn setup-interactions
[file]
@ -300,8 +306,9 @@
(if (and (not (cip/close? node))
(cip/has-image? node))
(let [name (cip/get-image-name node)
data-uri (cip/get-image-data node)]
(->> (upload-media-files context file-id name data-uri)
image-data (cip/get-image-data node)
image-fill (cip/get-image-fill node)]
(->> (upload-media-files context file-id name image-data)
(rx/catch #(do (.error js/console "Error uploading media: " name)
(rx/of node)))
(rx/map
@ -310,7 +317,13 @@
(assoc-in [:attrs :penpot:media-id] (:id media))
(assoc-in [:attrs :penpot:media-width] (:width media))
(assoc-in [:attrs :penpot:media-height] (:height media))
(assoc-in [:attrs :penpot:media-mtype] (:mtype media)))))))
(assoc-in [:attrs :penpot:media-mtype] (:mtype media))
(assoc-in [:attrs :penpot:fill-color] (:fill image-fill))
(assoc-in [:attrs :penpot:fill-color-ref-file] (:fill-color-ref-file image-fill))
(assoc-in [:attrs :penpot:fill-color-ref-id] (:fill-color-ref-id image-fill))
(assoc-in [:attrs :penpot:fill-opacity] (:fill-opacity image-fill))
(assoc-in [:attrs :penpot:fill-color-gradient] (:fill-color-gradient image-fill)))))))
;; If the node is not an image just return the node
(->> (rx/of node)

View file

@ -16,22 +16,35 @@
[beicon.core :as rx]
[rumext.alpha :as mf]))
(defn- not-found?
[{:keys [type]}]
(= :not-found type))
(defn- handle-response
[response]
[{:keys [body status] :as response}]
(cond
(http/success? response)
(rx/of (:body response))
(http/client-error? response)
(rx/throw (:body response))
(= status 413)
(rx/throw {:type :validation
:code :request-body-too-large
:hint "request body too large"})
(and (http/client-error? response)
(map? body))
(rx/throw body)
:else
(rx/throw {:type :unexpected
:code (:error response)})))
(rx/throw {:type :unexpected-error
:code :unhandled-http-response
:http-status status
:http-body body})))
(defn- not-found?
[{:keys [type]}]
(= :not-found type))
(defn- body-too-large?
[{:keys [type code]}]
(and (= :validation type)
(= :request-body-too-large code)))
(defn- request-data-for-thumbnail
[file-id revn]
@ -56,16 +69,19 @@
:uri (u/join (cfg/get-public-uri) path)
:credentials "include"
:query params}]
(->> (http/send! request)
(rx/map http/conditional-decode-transit)
(rx/mapcat handle-response))))
(defn- render-thumbnail
[{:keys [data file-id revn] :as params}]
(let [elem (if-let [frame (:thumbnail-frame data)]
(mf/element render/frame-svg #js {:objects (:objects data) :frame frame})
(mf/element render/page-svg #js {:data data :width "290" :height "150" :thumbnails? true}))]
{:data (rds/renderToStaticMarkup elem)
[{:keys [page file-id revn] :as params}]
(let [objects (:objects page)
frame (some->> page :thumbnail-frame-id (get objects))
element (if frame
(mf/element render/frame-svg #js {:objects objects :frame frame})
(mf/element render/page-svg #js {:data page :thumbnails? true}))]
{:data (rds/renderToStaticMarkup element)
:fonts @fonts/loaded
:file-id file-id
:revn revn}))
@ -81,9 +97,11 @@
:uri (u/join (cfg/get-public-uri) path)
:credentials "include"
:body (http/transit-data params)}]
(->> (http/send! request)
(rx/map http/conditional-decode-transit)
(rx/mapcat handle-response)
(rx/catch body-too-large? (constantly nil))
(rx/map (constantly params)))))
(defmethod impl/handler :thumbnails/generate

View file

@ -7,6 +7,7 @@
(ns debug
(:require
[app.common.data :as d]
[app.common.logging :as l]
[app.common.pages.helpers :as cph]
[app.common.transit :as t]
[app.common.uuid :as uuid]
@ -17,6 +18,7 @@
[app.main.data.workspace.path.shortcuts]
[app.main.data.workspace.shortcuts]
[app.main.store :as st]
[app.util.dom :as dom]
[app.util.object :as obj]
[app.util.timers :as timers]
[beicon.core :as rx]
@ -25,6 +27,12 @@
[potok.core :as ptk]
[promesa.core :as p]))
(defn ^:export set-logging
([level]
(l/set-level! :app (keyword level)))
([ns level]
(l/set-level! (keyword ns) (keyword level))))
(def debug-options
#{;; Displays the bounding box for the shapes
:bounding-boxes
@ -333,3 +341,9 @@
(.log js/console "%c Viewer" style)
(print-shortcuts app.main.data.viewer.shortcuts/shortcuts)))
nil)
(defn ^:export nodeStats
[]
(let [root-node (dom/query ".viewport .render-shapes")
num-nodes (->> (dom/seq-nodes root-node) count)]
#js {:number num-nodes}))

View file

@ -484,14 +484,14 @@ atob@^2.1.2:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
autoprefixer@^10.4.2:
version "10.4.2"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.2.tgz#25e1df09a31a9fba5c40b578936b90d35c9d4d3b"
integrity sha512-9fOPpHKuDW1w/0EKfRmVnxTDt8166MAnLI3mgZ1JCnhNtYWxcJ6Ud5CO/AVOZi/AvFa8DY9RTy3h3+tFBlrrdQ==
autoprefixer@^10.4.4:
version "10.4.4"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.4.tgz#3e85a245b32da876a893d3ac2ea19f01e7ea5a1e"
integrity sha512-Tm8JxsB286VweiZ5F0anmbyGiNI3v3wGv3mz9W+cxEDYB/6jbnj6GM9H9mK3wIL8ftgl+C07Lcwb8PG5PCCPzA==
dependencies:
browserslist "^4.19.1"
caniuse-lite "^1.0.30001297"
fraction.js "^4.1.2"
browserslist "^4.20.2"
caniuse-lite "^1.0.30001317"
fraction.js "^4.2.0"
normalize-range "^0.1.2"
picocolors "^1.0.0"
postcss-value-parser "^4.2.0"
@ -709,15 +709,15 @@ browserify-zlib@^0.2.0:
dependencies:
pako "~1.0.5"
browserslist@^4.19.1:
version "4.19.1"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.19.1.tgz#4ac0435b35ab655896c31d53018b6dd5e9e4c9a3"
integrity sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==
browserslist@^4.20.2:
version "4.20.2"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.2.tgz#567b41508757ecd904dab4d1c646c612cd3d4f88"
integrity sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA==
dependencies:
caniuse-lite "^1.0.30001286"
electron-to-chromium "^1.4.17"
caniuse-lite "^1.0.30001317"
electron-to-chromium "^1.4.84"
escalade "^3.1.1"
node-releases "^2.0.1"
node-releases "^2.0.2"
picocolors "^1.0.0"
buffer-crc32@~0.2.3:
@ -823,10 +823,10 @@ camelcase@^6.2.0:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
caniuse-lite@^1.0.30001286, caniuse-lite@^1.0.30001297:
version "1.0.30001312"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz#e11eba4b87e24d22697dae05455d5aea28550d5f"
integrity sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==
caniuse-lite@^1.0.30001317:
version "1.0.30001322"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001322.tgz#2e4c09d11e1e8f852767dab287069a8d0c29d623"
integrity sha512-neRmrmIrCGuMnxGSoh+x7zYtQFFgnSY2jaomjU56sCkTA6JINqQrxutF459JpWcWRajvoyn95sOXq4Pqrnyjew==
caseless@~0.12.0:
version "0.12.0"
@ -1387,10 +1387,10 @@ cypress-file-upload@^5.0.8:
resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-5.0.8.tgz#d8824cbeaab798e44be8009769f9a6c9daa1b4a1"
integrity sha512-+8VzNabRk3zG6x8f8BWArF/xA/W0VK4IZNx3MV0jFWrJS/qKn8eHfa5nU73P9fOQAgwHFJx7zjg4lwOnljMO8g==
cypress@^9.5.0:
version "9.5.0"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.5.0.tgz#704a79f0d3d4e775f433334eb8f5ae065e3bea31"
integrity sha512-rC5QPolKsVjJ8QJZ7IeZ6HlKM4gswBGZc0XvoAJNL8urQCSL8zTX0A/ai/h35WfF47NQ0iSZnwIXBlHX3MOUIQ==
cypress@^9.5.3:
version "9.5.3"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.5.3.tgz#7c56b50fc1f1aa69ef10b271d895aeb4a1d7999e"
integrity sha512-ItelIVmqMTnKYbo1JrErhsGgQGjWOxCpHT1TfMvwnIXKXN/OSlPjEK7rbCLYDZhejQL99PmUqul7XORI24Ik0A==
dependencies:
"@cypress/request" "^2.88.10"
"@cypress/xvfb" "^1.2.4"
@ -1424,7 +1424,7 @@ cypress@^9.5.0:
listr2 "^3.8.3"
lodash "^4.17.21"
log-symbols "^4.0.0"
minimist "^1.2.5"
minimist "^1.2.6"
ospath "^1.2.2"
pretty-bytes "^5.6.0"
proxy-from-env "1.0.0"
@ -1686,10 +1686,10 @@ editorconfig@^0.15.3:
semver "^5.6.0"
sigmund "^1.0.1"
electron-to-chromium@^1.4.17:
version "1.4.71"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.71.tgz#17056914465da0890ce00351a3b946fd4cd51ff6"
integrity sha512-Hk61vXXKRb2cd3znPE9F+2pLWdIOmP7GjiTj45y6L3W/lO+hSnUSUhq+6lEaERWBdZOHbk2s3YV5c9xVl3boVw==
electron-to-chromium@^1.4.84:
version "1.4.98"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.98.tgz#1a9a0dec9792e78c5be1df052b6c74078d6b1b16"
integrity sha512-1IdsuSAnIGVxoYT1LkcUFb9MfjRxdHhCU9qiaDzhl1XvYgK9c8E2O9aJOPgGMQ68CSI8NxmLwrYhjvGauT8yuw==
elliptic@^6.5.3:
version "6.5.4"
@ -2164,10 +2164,10 @@ form-data@~2.3.2:
combined-stream "^1.0.6"
mime-types "^2.1.12"
fraction.js@^4.1.2:
version "4.1.3"
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.1.3.tgz#be65b0f20762ef27e1e793860bc2dfb716e99e65"
integrity sha512-pUHWWt6vHzZZiQJcM6S/0PXfS+g6FM4BF5rj9wZyreivhQPdsh5PpE25VtSNxq80wHS5RfY51Ii+8Z0Zl/pmzg==
fraction.js@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==
fragment-cache@^0.2.1:
version "0.2.1"
@ -3163,10 +3163,10 @@ isstream@~0.1.2:
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
js-beautify@^1.14.0:
version "1.14.0"
resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.14.0.tgz#2ce790c555d53ce1e3d7363227acf5dc69024c2d"
integrity sha512-yuck9KirNSCAwyNJbqW+BxJqJ0NLJ4PwBUzQQACl5O3qHMBXVkXb/rD0ilh/Lat/tn88zSZ+CAHOlk0DsY7GuQ==
js-beautify@^1.14.2:
version "1.14.2"
resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.14.2.tgz#8180514fd4c7789c4ac4bcc327b6dda634c55666"
integrity sha512-H85kX95a53os+q1OCqtYe8AXAmgy3BvtysA/V83S3fdhznm6WlUpGi14DqSPbKFsL3dXZFXYl7YQwW9U1+76ng==
dependencies:
config-chain "^1.1.12"
editorconfig "^0.15.3"
@ -3512,10 +3512,10 @@ lru-queue@^0.1.0:
dependencies:
es5-ext "~0.10.2"
luxon@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/luxon/-/luxon-2.3.0.tgz#bf16a7e642513c2a20a6230a6a41b0ab446d0045"
integrity sha512-gv6jZCV+gGIrVKhO90yrsn8qXPKD8HYZJtrUDSfEbow8Tkw84T9OnCyJhWvnJIaIF/tBuiAjZuQHUt1LddX2mg==
luxon@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/luxon/-/luxon-2.3.1.tgz#f276b1b53fd9a740a60e666a541a7f6dbed4155a"
integrity sha512-I8vnjOmhXsMSlNMZlMkSOvgrxKJl0uOsEzdGgGNZuZPaS9KlefpE9KV95QFftlJSC+1UyCC9/I69R02cz/zcCA==
make-dir@^3.0.0:
version "3.1.0"
@ -3677,6 +3677,11 @@ minimist@^1.2.0, minimist@^1.2.5:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
minimist@^1.2.6:
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
mixin-deep@^1.2.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
@ -3732,10 +3737,10 @@ nan@^2.12.1:
resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee"
integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==
nanoid@^3.2.0:
version "3.3.1"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35"
integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==
nanoid@^3.3.1:
version "3.3.2"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.2.tgz#c89622fafb4381cd221421c69ec58547a1eec557"
integrity sha512-CuHBogktKwpm5g2sRgv83jEy2ijFzBwMoYA60orPDR7ynsLijJDqgsi4RDGj3OJpy3Ieb+LYwiRmIOGyytgITA==
nanomatch@^1.2.9:
version "1.2.13"
@ -3805,7 +3810,7 @@ node-libs-browser@^2.2.1:
util "^0.11.0"
vm-browserify "^1.0.1"
node-releases@^2.0.1:
node-releases@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.2.tgz#7139fe71e2f4f11b47d4d2986aaf8c48699e0c01"
integrity sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==
@ -4360,12 +4365,12 @@ postcss@^7.0.16:
picocolors "^0.2.1"
source-map "^0.6.1"
postcss@^8.4.6:
version "8.4.6"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.6.tgz#c5ff3c3c457a23864f32cb45ac9b741498a09ae1"
integrity sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA==
postcss@^8.4.12:
version "8.4.12"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.12.tgz#1e7de78733b28970fa4743f7da6f3763648b1905"
integrity sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg==
dependencies:
nanoid "^3.2.0"
nanoid "^3.3.1"
picocolors "^1.0.0"
source-map-js "^1.0.2"
@ -4374,10 +4379,10 @@ prepend-http@^2.0.0:
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
prettier@^2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a"
integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==
prettier@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.1.tgz#d472797e0d7461605c1609808e27b80c0f9cfe17"
integrity sha512-8UVbTBYGwN37Bs9LERmxCPjdvPxlEowx2urIL6urHzdb3SDq4B/Z6xLFCblrSnE4iKWcS6ziJ3aOYrc1kz/E2A==
pretty-bytes@^5.6.0:
version "5.6.0"
@ -4876,13 +4881,20 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^3.0.0"
inherits "^2.0.1"
rxjs@^7.5.1, rxjs@~7.5.2:
rxjs@^7.5.1:
version "7.5.4"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.4.tgz#3d6bd407e6b7ce9a123e76b1e770dc5761aa368d"
integrity sha512-h5M3Hk78r6wAheJF0a5YahB1yRQKCsZ4MsGdZ5O9ETbVtjPcScGfrMmoOq7EBsCRzd4BDkvDJ7ogP8Sz5tTFiQ==
dependencies:
tslib "^2.1.0"
rxjs@~7.5.5:
version "7.5.5"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.5.tgz#2ebad89af0f560f460ad5cc4213219e1f7dd4e9f"
integrity sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==
dependencies:
tslib "^2.1.0"
safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
@ -4910,10 +4922,10 @@ safe-stable-stringify@^2.3.1:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sass@^1.49.7:
version "1.49.7"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.49.7.tgz#22a86a50552b9b11f71404dfad1b9ff44c6b0c49"
integrity sha512-13dml55EMIR2rS4d/RDHHP0sXMY3+30e1TKsyXaSz3iLWVoDWEoboY8WzJd5JMnxrRHffKO3wq2mpJ0jxRJiEQ==
sass@^1.49.9:
version "1.49.9"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.49.9.tgz#b15a189ecb0ca9e24634bae5d1ebc191809712f9"
integrity sha512-YlYWkkHP9fbwaFRZQRXgDi3mXZShslVmmo+FVK3kHLUELHHEYrCmL1x6IUjC7wLS6VuJSAFXRQS/DxdsC4xL1A==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"
@ -5001,10 +5013,10 @@ shadow-cljs-jar@1.3.2:
resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.2.tgz#97273afe1747b6a2311917c1c88d9e243c81957b"
integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg==
shadow-cljs@2.17.5:
version "2.17.5"
resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.17.5.tgz#ed2fa8b06ea62cb310f069b70314e555f5bf9d50"
integrity sha512-Xvev4OLxGkjxC5mT5jHZDpJuAKzSHn7bGa4RPBm+Jp2gBz4iNkNDNPDvkyqt0r9RD0SWaYJF8zGyxi5c18yJBw==
shadow-cljs@2.17.8:
version "2.17.8"
resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.17.8.tgz#7ee27ccf7585991f6c042f66f07f17582c0b70af"
integrity sha512-O39cLA7ukEh+OeH1yZlaWjGFinPOsDD87TetAWPe1QBD9TZQ0Ail+2ovaXeAyZpJ+6Z37joFfival+LNuCgsmQ==
dependencies:
node-libs-browser "^2.2.1"
readline-sync "^1.4.7"