Compare commits

..

13 Commits

Author SHA1 Message Date
Andrey Antukh
2f8f1f0b9a 📎 Update changelog. 2021-05-28 08:49:27 +02:00
Andrey Antukh
d572fdac9b 🐛 Fix unexpected exception on duplicate project.
Related to files created out of order.
2021-05-28 08:39:04 +02:00
Andrey Antukh
ac41ed1af4 Add missing cause prop on error loging. 2021-05-28 08:32:30 +02:00
Andrey Antukh
f47bb6bcd0 Minor fix on previous commit. 2021-05-27 18:12:29 +02:00
Andrey Antukh
a3eb5e2928 🐛 Fix incorrect unicode code points handling on draft-to-penpot conversion. 2021-05-27 17:52:16 +02:00
Andrey Antukh
d4bf3ef6fd 📎 Remove mattermost mention-all workds from error report. 2021-05-27 13:29:29 +02:00
Andrey Antukh
ca5c374ecd 🐛 Fix empty font-family handling on custom fonts page. 2021-05-27 13:21:37 +02:00
Andrey Antukh
69ea8229ca :spakles: Minor improvements on svg uploading on libraries.
Mainly reject svgs that have doctype declaration for security reasons.
2021-05-27 13:00:13 +02:00
Andrey Antukh
4d19b87fff Improve error report on uploading invalid image to library. 2021-05-27 12:40:38 +02:00
Andrey Antukh
8847047fd1 🐛 Fix unexpected exception when user leaves typography name empty. 2021-05-27 12:21:40 +02:00
Andrey Antukh
6e8a5015c9 Add better auth module logging. 2021-05-27 11:52:01 +02:00
Andrey Antukh
e8919ee340 🐛 Add missing email scope to OIDC backend.
And additionaly emit a warn log message about the error.
2021-05-27 11:52:01 +02:00
alonso.torres
f8f506a8be 🐛 Fix some problems with paths 2021-05-27 11:10:30 +02:00
24 changed files with 346 additions and 265 deletions

View File

@@ -11,20 +11,37 @@
### :heart: Community contributions by (Thank you!)
## 1.6.2-alpha
### :bug: Bugs fixed
- Add better auth module logging.
- Add missing `email` scope to OIDC backend.
- Add missing cause prop on error loging.
- Fix empty font-family handling on custom fonts page.
- Fix incorrect unicode code points handling on draft-to-penpot conversion.
- Fix some problems with paths.
- Fix unexpected exception on duplicate project.
- Fix unexpected exception when user leaves typography name empty.
- Improve error report on uploading invalid image to library.
- Minor fix on previous commit.
- Minor improvements on svg uploading on libraries.
## 1.6.1-alpha
### :bug: Bugs fixed
- Make the navigation async by default.
- Improve editor lifecycle management.
- Fix problem when creating a component with empty data.
- Add safety check on reg-objects change impl.
- Fix custom fonts embbedding issue.
- Fix dashboard ordering issue.
- Fix problem when creating a component with empty data.
- Fix problem with moving shapes into frames.
- Fix problems with mov-objects.
- Fix wrong type usage on libraries changes.
- Fix custom fonts embbedding issue.
- Add safety check on reg-objects change impl.
- Fix unexpected excetion related to rounding integers.
- Fix wrong type usage on libraries changes.
- Improve editor lifecycle management.
- Make the navigation async by default.
## 1.6.0-alpha

View File

@@ -117,7 +117,8 @@
(l/error :hint "psql exception"
:error-message (ex-message error)
:error-id (str (:id cdata))
:sql-state state)
:sql-state state
:cause error)
(cond
(= state "57014")

View File

@@ -109,6 +109,17 @@
:cause e)
nil)))
(s/def ::backend ::us/not-empty-string)
(s/def ::email ::us/not-empty-string)
(s/def ::fullname ::us/not-empty-string)
(s/def ::props (s/map-of ::us/keyword any?))
(s/def ::info
(s/keys :req-un [::backend
::email
::fullname
::props]))
(defn retrieve-info
[{:keys [tokens provider] :as cfg} request]
(let [state (get-in request [:params :state])
@@ -116,7 +127,10 @@
info (some->> (get-in request [:params :code])
(retrieve-access-token cfg)
(retrieve-user-info cfg))]
(when-not info
(when-not (s/valid? ::info info)
(l/warn :hint "received incomplete profile info object (please set correct scopes)"
:info (pr-str info))
(ex/raise :type :internal
:code :unable-to-auth
:hint "no user info"))
@@ -228,6 +242,13 @@
:auth-uri (get data "authorization_endpoint")
:user-uri (get data "userinfo_endpoint"))))))
(defn- obfuscate-string
[s]
(if (< (count s) 10)
(apply str (take (count s) (repeat "*")))
(str (subs s 0 5)
(apply str (take (- (count s) 5) (repeat "*"))))))
(defn- initialize-oidc-provider
[cfg]
(let [opts {:base-uri (cf/get :oidc-base-uri)
@@ -236,7 +257,7 @@
:token-uri (cf/get :oidc-token-uri)
:auth-uri (cf/get :oidc-auth-uri)
:user-uri (cf/get :oidc-user-uri)
:scopes (cf/get :oidc-scopes #{"openid" "profile"})
:scopes (cf/get :oidc-scopes #{"openid" "profile" "email"})
:roles-attr (cf/get :oidc-roles-attr)
:roles (cf/get :oidc-roles)
:name "oidc"}]
@@ -247,10 +268,12 @@
(string? (:user-uri opts))
(string? (:auth-uri opts)))
(do
(l/info :action "initialize" :provider "oid" :method "static")
(l/info :action "initialize" :provider "oidc" :method "static"
:opts (pr-str (update opts :client-secret obfuscate-string)))
(assoc-in cfg [:providers "oidc"] opts))
(let [opts (discover-oidc-config opts)]
(l/info :action "initialize" :provider "oid" :method "discover")
(l/info :action "initialize" :provider "oidc" :method "discover"
:opts (pr-str (update opts :client-secret obfuscate-string)))
(assoc-in cfg [:providers "oidc"] opts)))
cfg)))
@@ -266,7 +289,8 @@
(if (and (string? (:client-id opts))
(string? (:client-secret opts)))
(do
(l/info :action "initialize" :provider "google")
(l/info :action "initialize" :provider "google"
:opts (pr-str (update opts :client-secret obfuscate-string)))
(assoc-in cfg [:providers "google"] opts))
cfg)))
@@ -282,7 +306,8 @@
(if (and (string? (:client-id opts))
(string? (:client-secret opts)))
(do
(l/info :action "initialize" :provider "github")
(l/info :action "initialize" :provider "github"
:opts (pr-str (update opts :client-secret obfuscate-string)))
(assoc-in cfg [:providers "github"] opts))
cfg)))
@@ -301,7 +326,8 @@
(if (and (string? (:client-id opts))
(string? (:client-secret opts)))
(do
(l/info :action "initialize" :provider "gitlab")
(l/info :action "initialize" :provider "gitlab"
:opts (pr-str (update opts :client-secret obfuscate-string)))
(assoc-in cfg [:providers "gitlab"] opts))
cfg)))

View File

@@ -61,7 +61,7 @@
[cfg {:keys [host version id] :as cdata}]
(try
(let [uri (:uri cfg)
text (str "Unhandled exception (@channel):\n"
text (str "Unhandled exception:\n"
"- detail: " (cfg/get :public-uri) "/dbg/error-by-id/" id "\n"
"- profile-id: `" (:profile-id cdata) "`\n"
"- host: `" host "`\n"

View File

@@ -66,8 +66,7 @@
(defmethod process-error :default
[error]
(ex/raise :type :internal
:cause error))
(throw error))
(defn run
[{:keys [rlimits] :as cfg} {:keys [rlimit] :or {rlimit :image} :as params}]
@@ -184,11 +183,10 @@
(us/assert ::input input)
(let [{:keys [path mtype]} input]
(if (= mtype "image/svg+xml")
(let [data (svg/parse (slurp path))
info (get-basic-info-from-svg data)]
(let [info (some-> path slurp svg/parse get-basic-info-from-svg)]
(when-not info
(ex/raise :type :validation
:code :unable-to-retrieve-dimensions
:code :invalid-svg-file
:hint "uploaded svg does not provides dimensions"))
(assoc info :mtype mtype))
@@ -211,6 +209,7 @@
[error]
(ex/raise :type :validation
:code :invalid-image
:hint "invalid image"
:cause error))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@@ -173,7 +173,7 @@
index {file-id (uuid/next)}
params (assoc params :index index :file file)]
(proj/check-edition-permissions! conn profile-id (:project-id file))
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
(-> (duplicate-file conn params {:reset-shared-flag true})
(update :data blob/decode)))))
@@ -191,6 +191,7 @@
(db/with-atomic [conn pool]
(let [project (db/get-by-id conn :project project-id)]
(teams/check-edition-permissions! conn profile-id (:team-id project))
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
(duplicate-project conn (assoc params :project project)))))
(defn duplicate-project

View File

@@ -36,6 +36,7 @@
:message (ex-message e))
(ex/raise :type :validation
:code :invalid-svg-file
:hint "invalid svg file"
:cause e))))
(declare pre-process)
@@ -53,6 +54,6 @@
[data]
(cond-> data
(str/includes? data "<!DOCTYPE")
(str/replace #"<\!DOCTYPE[^>]+>" "")))
(str/replace #"<\!DOCTYPE[^>]*>" "")))
(def pre-process strip-doctype)

View File

@@ -7,6 +7,7 @@
(ns app.common.text
(:require
[app.common.attrs :as attrs]
[app.common.uuid :as uuid]
[app.common.data :as d]
[app.util.transit :as t]
[clojure.walk :as walk]
@@ -74,3 +75,181 @@
(defn ^boolean is-root-node?
[node]
(= "root" (:type node)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; DraftJS <-> Penpot Conversion
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn encode-style-value
[v]
#?(:cljs (t/encode v)
:clj (t/encode-str v)))
(defn decode-style-value
[v]
#?(:cljs (t/decode v)
:clj (t/decode-str v)))
(defn encode-style
[key val]
(let [k (d/name key)
v (encode-style-value val)]
(str "PENPOT$$$" k "$$$" v)))
(defn decode-style
[style]
(let [[_ k v] (str/split style "$$$" 3)]
[(keyword k) (decode-style-value v)]))
(defn attrs-to-styles
[attrs]
(reduce-kv (fn [res k v]
(conj res (encode-style k v)))
#{}
attrs))
(defn styles-to-attrs
[styles]
(persistent!
(reduce (fn [result style]
(if (str/starts-with? style "PENPOT")
(if (= style "PENPOT_SELECTION")
(assoc! result :penpot-selection true)
(let [[_ k v] (str/split style "$$$" 3)]
(assoc! result (keyword k) (decode-style-value v))))
result))
(transient {})
(seq styles))))
(defn- parse-draft-styles
"Parses draft-js style ranges, converting encoded style name into a
key/val pair of data."
[styles]
(->> styles
(filter #(str/starts-with? (get % :style) "PENPOT$$$"))
(map (fn [item]
(let [[_ k v] (-> (get item :style)
(str/split "$$$" 3))]
{:key (keyword k)
:val (decode-style-value v)
:offset (get item :offset)
:length (get item :length)})))))
(defn- build-style-index
"Generates a character based index with associated styles map."
[length ranges]
(loop [result (->> (range length)
(mapv (constantly {}))
(transient))
ranges (seq ranges)]
(if-let [{:keys [offset length] :as item} (first ranges)]
(recur (reduce (fn [result index]
(let [prev (get result index)]
(assoc! result index (assoc prev (:key item) (:val item)))))
result
(range offset (+ offset length)))
(rest ranges))
(persistent! result))))
(defn- text->code-points
[text]
#?(:cljs (into [] (js/Array.from text))
:clj (into [] (iterator-seq (.iterator (.codePoints ^String text))))))
(defn- code-points->text
[cpoints start end]
#?(:cljs (apply str (subvec cpoints start end))
:clj (let [sb (StringBuilder. (- end start))]
(run! #(.appendCodePoint sb (int %)) (subvec cpoints start end))
(.toString sb))))
(defn convert-from-draft
[content]
(letfn [(extract-text [cpoints part]
(let [start (ffirst part)
end (inc (first (last part)))
text (code-points->text cpoints start end)
attrs (second (first part))]
(assoc attrs :text text)))
(split-texts [text styles]
(let [cpoints (text->code-points text)
children (->> (parse-draft-styles styles)
(build-style-index (count cpoints))
(d/enumerate)
(partition-by second)
(mapv #(extract-text cpoints %)))]
(cond-> children
(empty? children)
(conj {:text ""}))))
(build-paragraph [block]
(let [key (get block :key)
text (get block :text)
styles (get block :inlineStyleRanges)
data (get block :data)]
(-> data
(assoc :key key)
(assoc :type "paragraph")
(assoc :children (split-texts text styles)))))]
{:type "root"
:children
[{:type "paragraph-set"
:children (->> (get content :blocks)
(mapv build-paragraph))}]}))
(defn convert-to-draft
[root]
(letfn [(process-attr [children ranges [k v]]
(loop [children (seq children)
start nil
offset 0
ranges ranges]
(if-let [{:keys [text] :as item} (first children)]
(let [cpoints (text->code-points text)]
(if (= v (get item k ::novalue))
(recur (rest children)
(if (nil? start) offset start)
(+ offset (count cpoints))
ranges)
(if (some? start)
(recur (rest children)
nil
(+ offset (count cpoints))
(conj! ranges {:offset start
:length (- offset start)
:style (encode-style k v)}))
(recur (rest children)
start
(+ offset (count cpoints))
ranges))))
(cond-> ranges
(some? start)
(conj! {:offset start
:length (- offset start)
:style (encode-style k v)})))))
(calc-ranges [{:keys [children] :as blok}]
(let [xform (comp (map #(dissoc % :key :text))
(remove empty?)
(mapcat vec)
(distinct))
proc #(process-attr children %1 %2)]
(persistent!
(transduce xform proc (transient []) children))))
(build-block [{:keys [key children] :as paragraph}]
{:key key
:depth 0
:text (apply str (map :text children))
:data (dissoc paragraph :key :children :type)
:type "unstyled"
:entityRanges []
:inlineStyleRanges (calc-ranges paragraph)})]
{:blocks (reduce #(conj %1 (build-block %2)) [] (node-seq #(= (:type %) "paragraph") root))
:entityMap {}}))

View File

@@ -42,7 +42,7 @@ PENPOT_REGISTRATION_ENABLED=true
# Comma separated list of allowed domains to register. Empty for allow
# all.
PENPOT_REGISTRATION_DOMAIN_WHITELIST=""
# PENPOT_REGISTRATION_DOMAIN_WHITELIST=""
# Penpot comes with the facility to create quick demo users that are
# automatically deleted after some time. This settings enables or

View File

@@ -97,7 +97,7 @@ update_registration_enabled() {
fi
}
update_registration_enabled() {
update_analytics_enabled() {
if [ -n "$PENPOT_ANALYTICS_ENABLED" ]; then
sed -i \
-e "s|^//var penpotAnalyticsEnabled = .*;|var penpotAnalyticsEnabled = $PENPOT_ANALYTICS_ENABLED;|g" \

View File

@@ -89,7 +89,7 @@
{:content {:data (js/Uint8Array. data)
:name name
:type type}
:font-family family
:font-family (or family "")
:font-weight (cm/parse-font-weight variant)
:font-style (cm/parse-font-style variant)}))

View File

@@ -1238,7 +1238,6 @@
qparams {:page-id page-id}]
(rx/of (rt/nav :workspace pparams qparams))))))
(defn go-to-viewer
([] (go-to-viewer {}))
([{:keys [file-id page-id]}]

View File

@@ -228,7 +228,7 @@
ptk/WatchEvent
(watch [it state stream]
(let [[path name] (cp/parse-path-name (:name typography))
typography (assoc typography :path path :name name)
typography (assoc typography :path path :name name)
prev (get-in state [:workspace-data :typographies (:id typography)])
rchg {:type :mod-typography
:typography typography}

View File

@@ -88,10 +88,10 @@
(let [objects (wsh/lookup-page-objects state)
page-id (:current-page-id state)
id (get-in state [:workspace-local :edition])
old-content (get-in state [:workspace-local :edit-path id :old-content])]
(if (some? old-content)
(let [shape (get-in state (st/get-path state))
[rch uch] (generate-path-changes objects page-id shape old-content (:content shape))]
old-content (get-in state [:workspace-local :edit-path id :old-content])
shape (get-in state (st/get-path state))]
(if (and (some? old-content) (some? shape))
(let [[rch uch] (generate-path-changes objects page-id shape old-content (:content shape))]
(rx/of (dch/commit-changes {:redo-changes rch
:undo-changes uch
:origin it})))

View File

@@ -60,20 +60,20 @@
old-points (->> content upg/content->points)
new-points (->> new-content upg/content->points)
point-change (->> (map hash-map old-points new-points) (reduce merge))
point-change (->> (map hash-map old-points new-points) (reduce merge))]
[rch uch] (changes/generate-path-changes objects page-id shape (:content shape) new-content)]
(if (empty? new-content)
(rx/of (dch/commit-changes {:redo-changes rch
:undo-changes uch
:origin it})
dwc/clear-edition-mode)
(rx/of (dch/commit-changes {:redo-changes rch
:undo-changes uch
:origin it})
(selection/update-selection point-change)
(fn [state] (update-in state [:workspace-local :edit-path id] dissoc :content-modifiers :moving-nodes :moving-handler))))))))
(when (and (some? new-content) (some? shape))
(let [[rch uch] (changes/generate-path-changes objects page-id shape (:content shape) new-content)]
(if (empty? new-content)
(rx/of (dch/commit-changes {:redo-changes rch
:undo-changes uch
:origin it})
dwc/clear-edition-mode)
(rx/of (dch/commit-changes {:redo-changes rch
:undo-changes uch
:origin it})
(selection/update-selection point-change)
(fn [state] (update-in state [:workspace-local :edit-path id] dissoc :content-modifiers :moving-nodes :moving-handler))))))))))
(defn modify-content-point
[content {dx :x dy :y} modifiers point]
@@ -263,7 +263,8 @@
(streams/drag-stream
(rx/concat
(->> (streams/move-handler-stream snap-toggled start-point point handler opposite points)
(rx/take-until (->> stream (rx/filter ms/mouse-up?)))
(rx/take-until (->> stream (rx/filter #(or (ms/mouse-up? %)
(streams/finish-edition? %)))))
(rx/map
(fn [{:keys [x y alt? shift?]}]
(let [pos (cond-> (gpt/point x y)

View File

@@ -24,6 +24,9 @@
(fn [current]
(>= (gpt/distance start current) (/ drag-threshold zoom))))
(defn finish-edition? [event]
(= (ptk/type event) :app.main.data.workspace.common/clear-edition-mode))
(defn drag-stream
([to-stream]
(drag-stream to-stream (rx/empty)))
@@ -31,7 +34,8 @@
([to-stream not-drag-stream]
(let [start @ms/mouse-position
zoom (get-in @st/state [:workspace-local :zoom] 1)
mouse-up (->> st/stream (rx/filter #(ms/mouse-up? %)))
mouse-up (->> st/stream (rx/filter #(or (finish-edition? %)
(ms/mouse-up? %))))
position-stream
(->> ms/mouse-position

View File

@@ -33,7 +33,7 @@
selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{})
points (or points selected-points)]
(when-not (empty? points)
(when (and (not (empty? points)) (some? shape))
(let [new-content (-> (tool-fn (:content shape) points)
(ups/close-subpaths))
[rch uch] (changes/generate-path-changes objects page-id shape (:content shape) new-content)]

View File

@@ -192,9 +192,10 @@
on-save
(fn [event]
(let [font-family @state]
(st/emit! (df/update-font
{:id font-id
:name font-family}))
(when-not (str/blank? font-family)
(st/emit! (df/update-font
{:id font-id
:name font-family})))
(reset! edit? false)))
on-key-down

View File

@@ -9,6 +9,7 @@
["draft-js" :as draft]
[app.common.data :as d]
[app.common.geom.shapes :as gsh]
[app.common.text :as txt]
[app.main.data.workspace :as dw]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.selection :as dws]
@@ -154,7 +155,7 @@
:handle-return handle-return
:strip-pasted-styles true
:custom-style-fn (fn [styles _]
(-> (ted/styles-to-attrs styles)
(-> (txt/styles-to-attrs styles)
(sts/generate-text-styles)))
:block-renderer-fn #(render-block % shape)
:ref on-editor

View File

@@ -428,26 +428,27 @@
[:> text-transform-options opts]]]))
;; TODO: this need to be refactored, right now combines too much logic
;; and has a dropdown that behaves like a modal but is not a modal.
;; In summary, this need to a good UX/UI/IMPL rework.
(mf/defc typography-entry
[{:keys [typography read-only? selected? on-click on-change on-detach on-context-menu editting? focus-name? file]}]
(let [open? (mf/use-state editting?)
hover-detach (mf/use-state false)
name-input-ref (mf/use-ref nil)
value (mf/use-state (cp/merge-path-item (:path typography) (:name typography)))
(let [open? (mf/use-state editting?)
hover-detach (mf/use-state false)
name-input-ref (mf/use-ref)
#_(rt/resolve router :workspace
{:project-id (:project-id file)
:file-id (:id file)}
{:page-id (get-in file [:data :pages 0])})
handle-change
on-name-blur
(fn [event]
(reset! value (dom/get-target-val event)))
(let [content (dom/get-target-val event)]
(when-not (str/blank? content)
(on-change {:name content}))))
handle-go-to-edit
(fn [] (st/emit! (rt/nav :workspace {:project-id (:project-id file)
:file-id (:id file)}
{:page-id (get-in file [:data :pages 0])})))]
(fn []
(let [pparams {:project-id (:project-id file)
:file-id (:id file)}]
(st/emit! (rt/nav :workspace pparams))))]
(mf/use-effect
(mf/deps editting?)
@@ -459,7 +460,7 @@
(mf/deps focus-name?)
(fn []
(when focus-name?
(ts/schedule 100
(ts/schedule
#(when-let [node (mf/ref-val name-input-ref)]
(dom/focus! node)
(dom/select-text! node))))))
@@ -530,8 +531,7 @@
[:input.element-name.adv-typography-name
{:type "text"
:ref name-input-ref
:value @value
:on-change handle-change
:on-blur #(on-change {:name @value})}]]]
:default-value (cp/merge-path-item (:path typography) (:name typography))
:on-blur on-name-blur}]]]
[:& typography-options {:values typography
:on-change on-change}]])]]))

View File

@@ -19,190 +19,12 @@
[clojure.walk :as walk]
[cuerdas.core :as str]))
;; --- INLINE STYLES ENCODING
(defn encode-style-value
[v]
(cond
(uuid? v) (str "u:" v)
(string? v) (str "s:" v)
(number? v) (str "n:" v)
(keyword? v) (str "k:" (name v))
(map? v) (str "m:" (t/encode v))
(nil? v) (str "z:null")
:else (str "o:" v)))
(defn decode-style-value
[v]
(let [prefix (subs v 0 2)]
(case prefix
"s:" (subs v 2)
"n:" (js/Number (subs v 2))
"k:" (keyword (subs v 2))
"m:" (t/decode (subs v 2))
"u:" (uuid/uuid (subs v 2))
"z:" nil
"o:" (subs v 2)
v)))
(defn encode-style
[key val]
(let [k (d/name key)
v (encode-style-value val)]
(str "PENPOT$$$" k "$$$" v)))
(defn encode-style-prefix
[key]
(let [k (d/name key)]
(str "PENPOT$$$" k "$$$")))
(defn decode-style
[style]
(let [[_ k v] (str/split style "$$$" 3)]
[(keyword k) (decode-style-value v)]))
(defn attrs-to-styles
[attrs]
(reduce-kv (fn [res k v]
(conj res (encode-style k v)))
#{}
attrs))
(defn styles-to-attrs
[styles]
(persistent!
(reduce (fn [result style]
(if (str/starts-with? style "PENPOT")
(if (= style "PENPOT_SELECTION")
(assoc! result :penpot-selection true)
(let [[_ k v] (str/split style "$$$" 3)]
(assoc! result (keyword k) (decode-style-value v))))
result))
(transient {})
(seq styles))))
;; --- CONVERSION
(defn- parse-draft-styles
"Parses draft-js style ranges, converting encoded style name into a
key/val pair of data."
[styles]
(->> styles
(filter #(str/starts-with? (obj/get % "style") "PENPOT$$$"))
(map (fn [item]
(let [[_ k v] (-> (obj/get item "style")
(str/split "$$$" 3))]
{:key (keyword k)
:val (decode-style-value v)
:offset (obj/get item "offset")
:length (obj/get item "length")})))))
(defn- build-style-index
"Generates a character based index with associated styles map."
[text ranges]
(loop [result (->> (range (count text))
(mapv (constantly {}))
(transient))
ranges (seq ranges)]
(if-let [{:keys [offset length] :as item} (first ranges)]
(recur (reduce (fn [result index]
(let [prev (get result index)]
(assoc! result index (assoc prev (:key item) (:val item)))))
result
(range offset (+ offset length)))
(rest ranges))
(persistent! result))))
(defn- convert-from-draft
[content]
(letfn [(build-text [text part]
(let [start (ffirst part)
end (inc (first (last part)))]
(-> (second (first part))
(assoc :text (subs text start end)))))
(split-texts [text styles]
(let [children (->> (parse-draft-styles styles)
(build-style-index text)
(d/enumerate)
(partition-by second)
(mapv #(build-text text %)))]
(cond-> children
(empty? children)
(conj {:text ""}))))
(build-paragraph [block]
(let [key (obj/get block "key")
text (obj/get block "text")
styles (obj/get block "inlineStyleRanges")
data (obj/get block "data")]
(-> (js->clj data :keywordize-keys true)
(assoc :key key)
(assoc :type "paragraph")
(assoc :children (split-texts text styles)))))]
{:type "root"
:children
[{:type "paragraph-set"
:children (->> (obj/get content "blocks")
(mapv build-paragraph))}]}))
(defn- convert-to-draft
[root]
(letfn [(process-attr [children ranges [k v]]
(loop [children (seq children)
start nil
offset 0
ranges ranges]
(if-let [{:keys [text] :as item} (first children)]
(if (= v (get item k ::novalue))
(recur (rest children)
(if (nil? start) offset start)
(+ offset (alength text))
ranges)
(if (some? start)
(recur (rest children)
nil
(+ offset (alength text))
(arr/conj! ranges #js {:offset start
:length (- offset start)
:style (encode-style k v)}))
(recur (rest children)
start
(+ offset (alength text))
ranges)))
(cond-> ranges
(some? start)
(arr/conj! #js {:offset start
:length (- offset start)
:style (encode-style k v)})))))
(calc-ranges [{:keys [children] :as blok}]
(let [xform (comp (map #(dissoc % :key :text))
(remove empty?)
(mapcat vec)
(distinct))
proc #(process-attr children %1 %2)]
(transduce xform proc #js [] children)))
(build-block [result {:keys [key children] :as paragraph}]
(->> #js {:key key
:depth 0
:text (apply str (map :text children))
:data (-> (dissoc paragraph :key :children :type)
(clj->js))
:type "unstyled"
:entityRanges #js []
:inlineStyleRanges (calc-ranges paragraph)}
(arr/conj! result)))]
#js {:blocks (reduce build-block #js [] (txt/node-seq #(= (:type %) "paragraph") root))
:entityMap #js {}}))
(defn immutable-map->map
[obj]
(into {} (map (fn [[k v]] [(keyword k) v])) (seq obj)))
;; --- DRAFT-JS HELPERS
(defn create-editor-state
@@ -219,13 +41,14 @@
(defn import-content
[content]
(-> content convert-to-draft draft/convertFromRaw))
(-> content txt/convert-to-draft clj->js draft/convertFromRaw))
(defn export-content
[content]
(-> content
(draft/convertToRaw)
(convert-from-draft)))
(js->clj :keywordize-keys true)
(txt/convert-from-draft)))
(defn get-editor-current-content
[state]
@@ -256,7 +79,7 @@
(defn get-editor-current-inline-styles
[state]
(-> (.getCurrentInlineStyle ^js state)
(styles-to-attrs)))
(txt/styles-to-attrs)))
(defn update-editor-current-block-data
[state attrs]
@@ -264,7 +87,7 @@
(defn update-editor-current-inline-styles
[state attrs]
(impl/applyInlineStyle state (attrs-to-styles attrs)))
(impl/applyInlineStyle state (txt/attrs-to-styles attrs)))
(defn editor-split-block
[state]

View File

@@ -0,0 +1,27 @@
(ns app.test-draft-conversion
(:require
[app.common.data :as d]
[app.common.text :as txt]
[cljs.test :as t :include-macros true]
[cljs.pprint :refer [pprint]]))
(t/deftest test-basic-conversion-roundtrip
(let [text "qwqw 🠒"
content {:type "root",
:children
[{:type "paragraph-set",
:children
[{:key "cfjh",
:type "paragraph",
:children
[{:font-id "gfont-roboto",
:font-family "Roboto",
:font-variant-id "regular",
:font-weight "400",
:font-style "normal",
:text text}]}]}]}]
;; (cljs.pprint/pprint (txt/convert-to-draft content))
;; (cljs.pprint/pprint (txt/convert-from-draft (txt/convert-to-draft content)))
(t/is (= (txt/convert-from-draft (txt/convert-to-draft content))
content))))

View File

@@ -1,16 +1,17 @@
(ns app.test-helpers.pages
(:require [cljs.test :as t :include-macros true]
[cljs.pprint :refer [pprint]]
[beicon.core :as rx]
[potok.core :as ptk]
[app.common.uuid :as uuid]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.pages :as cp]
[app.common.pages.helpers :as cph]
[app.main.data.workspace :as dw]
[app.main.data.workspace.groups :as dwg]
[app.main.data.workspace.libraries-helpers :as dwlh]))
(:require
[cljs.test :as t :include-macros true]
[cljs.pprint :refer [pprint]]
[beicon.core :as rx]
[potok.core :as ptk]
[app.common.uuid :as uuid]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.pages :as cp]
[app.common.pages.helpers :as cph]
[app.main.data.workspace :as dw]
[app.main.data.workspace.groups :as dwg]
[app.main.data.workspace.libraries-helpers :as dwlh]))
;; ---- Helpers to manage pages and objects
@@ -84,7 +85,7 @@
shapes (dwg/shapes-for-grouping (:objects page) ids)
[group rchanges uchanges]
(dwg/prepare-create-group (:id page) shapes prefix true)]
(dwg/prepare-create-group (:objects page) (:id page) shapes prefix true)]
(swap! idmap assoc label (:id group))
(update state :workspace-data

View File

@@ -1 +1 @@
1.6.1-alpha
1.6.2-alpha