diff --git a/common/src/app/common/file_builder.cljc b/common/src/app/common/file_builder.cljc index 64ae5dff4..c2cd12a07 100644 --- a/common/src/app/common/file_builder.cljc +++ b/common/src/app/common/file_builder.cljc @@ -87,15 +87,15 @@ :changes []}))) (defn add-page - [file name] - (let [page-id (uuid/next)] + [file data] + (let [page-id (uuid/next) + page (-> init/empty-page-data + (assoc :id page-id) + (d/deep-merge data))] (-> file (commit-change {:type :add-page - :id page-id - :name name - :page (-> init/empty-page-data - (assoc :name name))}) + :page page}) ;; Current page being edited (assoc :current-page-id page-id) diff --git a/frontend/src/app/libs/file_builder.cljs b/frontend/src/app/libs/file_builder.cljs index 4f1c6a207..2aa4bff3a 100644 --- a/frontend/src/app/libs/file_builder.cljs +++ b/frontend/src/app/libs/file_builder.cljs @@ -7,14 +7,65 @@ (ns app.libs.file-builder (:require [app.common.data :as d] - [app.common.file-builder :as fb])) + [app.common.file-builder :as fb] + [cuerdas.core :as str])) + +(defn parse-data [data] + (as-> data $ + (js->clj $ :keywordize-keys true) + ;; Transforms camelCase to kebab-case + (d/deep-mapm + (fn [[k v]] + [(-> k d/name str/kebab keyword) v]) $))) (deftype File [^:mutable file] Object - (addPage [self name] - (set! file (fb/add-page file name)) - (str (:current-page-id file)))) + (addPage + ([self name] + (addPage self name nil)) + + ([self name options] + (set! file (fb/add-page file {:name name :options options})) + (str (:current-page-id file)))) + + (closePage [self] + (set! file (fb/close-page file))) + + (addArtboard [self data] + (set! file (fb/add-artboard file (parse-data data))) + (str (:last-id file))) + + (closeArtboard [self data] + (set! file (fb/close-artboard file))) + + (addGroup [self data] + (set! file (fb/add-group file (parse-data data))) + (str (:last-id file))) + + (closeGroup [self] + (set! file (fb/close-group file))) + + (createRect [self data] + (set! file (fb/create-rect file (parse-data data)))) + + (createCircle [self data] + (set! file (fb/create-circle file (parse-data data)))) + + (createPath [self data] + (set! file (fb/create-path file (parse-data data)))) + + (createText [self data] + (set! file (fb/create-text file (parse-data data)))) + + (createImage [self data] + (set! file (fb/create-image file (parse-data data)))) + + (createSVG [self data] + (set! file (fb/create-svg-raw file (parse-data data)))) + + (closeSVG [self] + (set! file (fb/close-svg-raw file)))) (defn create-file-export [^string name] (File. (fb/create-file name))) diff --git a/frontend/src/app/main/exports.cljs b/frontend/src/app/main/exports.cljs index 2ee9d6db7..cb5a327d5 100644 --- a/frontend/src/app/main/exports.cljs +++ b/frontend/src/app/main/exports.cljs @@ -16,6 +16,7 @@ [app.common.uuid :as uuid] [app.main.ui.shapes.circle :as circle] [app.main.ui.shapes.embed :as embed] + [app.main.ui.shapes.export :as use] [app.main.ui.shapes.filters :as filters] [app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.group :as group] @@ -158,6 +159,9 @@ :style {:width "100%" :height "100%" :background background-color}} + + [:& use/export-page {:options (:options data)}] + (for [item shapes] (let [frame? (= (:type item) :frame)] (cond diff --git a/frontend/src/app/main/ui/shapes/export.cljs b/frontend/src/app/main/ui/shapes/export.cljs index 6cda48b11..5fcdb9153 100644 --- a/frontend/src/app/main/ui/shapes/export.cljs +++ b/frontend/src/app/main/ui/shapes/export.cljs @@ -83,6 +83,37 @@ (cond-> mask? (obj/set! "penpot:masked-group" "true")))))) +(defn prefix-keys [m] + (letfn [(prefix-entry [[k v]] + [(str "penpot:" (d/name k)) v])] + (into {} (map prefix-entry) m))) + +(mf/defc export-grid-data + [{:keys [grids]}] + [:> "penpot:grids" #js {} + (for [{:keys [type display params]} grids] + (let [props (->> (d/without-keys params [:color]) + (prefix-keys) + (clj->js))] + [:> "penpot:grid" + (-> props + (obj/set! "penpot:color" (get-in params [:color :color])) + (obj/set! "penpot:opacity" (get-in params [:color :opacity])) + (obj/set! "penpot:type" (d/name type)) + (cond-> (some? display) + (obj/set! "penpot:display" (str display))))]))]) + +(mf/defc export-page + [{:keys [options]}] + [:> "penpot:page" #js {} + (let [saved-grids (get options :saved-grids)] + (when-not (empty? saved-grids) + (let [parse-grid + (fn [[type params]] + {:type type :params params}) + grids (->> saved-grids (mapv parse-grid))] + [:& export-grid-data {:grids grids}])))]) + (mf/defc export-data [{:keys [shape]}] (let [props (-> (obj/new) @@ -135,5 +166,10 @@ (clj->js))))] [:> "penpot:svg-content" props (for [leaf (->> shape :content :content (filter string?))] - [:> "penpot:svg-child" {} leaf])]))])) + [:> "penpot:svg-child" {} leaf])])) + + + (when (and (= (:type shape) :frame) + (not (empty? (:grids shape)))) + [:& export-grid-data {:grids (:grids shape)}])])) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 8ead981ca..6a79f9c1e 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -14,6 +14,7 @@ [app.main.ui.context :as muc] [app.main.ui.measurements :as msr] [app.main.ui.shapes.embed :as embed] + [app.main.ui.shapes.export :as use] [app.main.ui.workspace.shapes :as shapes] [app.main.ui.workspace.shapes.text.editor :as editor] [app.main.ui.workspace.viewport.actions :as actions] @@ -188,6 +189,8 @@ :style {:background-color (get options :background "#E8E9EA") :pointer-events "none"}} + [:& use/export-page {:options options}] + [:& (mf/provider embed/context) {:value true} ;; Render root shape [:& shapes/root-shape {:key page-id diff --git a/frontend/src/app/util/import/parser.cljc b/frontend/src/app/util/import/parser.cljc index 97f811321..8609e3ed1 100644 --- a/frontend/src/app/util/import/parser.cljc +++ b/frontend/src/app/util/import/parser.cljc @@ -32,7 +32,8 @@ (defn get-data ([node] - (->> node :content (d/seek #(= :penpot:shape (:tag %))))) + (->> node :content (d/seek #(or (= :penpot:shape (:tag %)) + (= :penpot:page (:tag %)))))) ([node tag] (->> (get-data node) :content @@ -98,6 +99,41 @@ m attrs)) +(defn without-penpot-prefix + [m] + (let [no-penpot-prefix? + (fn [[k v]] + (not (str/starts-with? (d/name k) "penpot:")))] + (into {} (filter no-penpot-prefix?) m))) + +(defn remove-penpot-prefix + [m] + (into {} + (map (fn [[k v]] + (if (str/starts-with? (d/name k) "penpot:") + [(-> k d/name (str/replace "penpot:" "") keyword) v] + [k v]))) + m)) + +(defn camelize [[k v]] + [(-> k d/name str/camel keyword) v]) + +(defn camelize-keys + [m] + (assert (map? m) (str m)) + + (into {} (map camelize) m)) + +(defn fix-style-attr + [m] + (let [fix-style + (fn [[k v]] + (if (= k :style) + [k (-> v parse-style camelize-keys)] + [k v]))] + + (d/deep-mapm (comp camelize fix-style) m))) + (def search-data-node? #{:rect :image :path :text :circle}) (defn get-svg-data @@ -372,6 +408,26 @@ :suffix (get-meta node :suffix) :scale (get-meta node :scale d/parse-double)}) + +(defn parse-grid-node [node] + (let [attrs (-> node :attrs remove-penpot-prefix) + color {:color (:color attrs) + :opacity (-> attrs :opacity d/parse-double)} + + params (-> (d/without-keys attrs [:color :opacity :display :type]) + (d/update-when :size d/parse-double) + (d/update-when :item-length d/parse-double) + (d/update-when :gutter d/parse-double) + (d/update-when :margin d/parse-double) + (assoc :color color))] + {:type (-> attrs :type keyword) + :display (-> attrs :display str->bool) + :params params})) + +(defn parse-grids [node] + (let [grid-node (get-data node :penpot:grids)] + (->> grid-node :content (mapv parse-grid-node)))) + (defn extract-from-data ([node tag] (extract-from-data node tag identity)) @@ -477,32 +533,6 @@ props))) -(defn without-penpot-prefix - [m] - (let [no-penpot-prefix? - (fn [[k v]] - (not (str/starts-with? (d/name k) "penpot:")))] - (into {} (filter no-penpot-prefix?) m))) - -(defn camelize [[k v]] - [(-> k d/name str/camel keyword) v]) - -(defn camelize-keys - [m] - (assert (map? m) (str m)) - - (into {} (map camelize) m)) - -(defn fix-style-attr - [m] - (let [fix-style - (fn [[k v]] - (if (= k :style) - [k (-> v parse-style camelize-keys)] - [k v]))] - - (d/deep-mapm (comp camelize fix-style) m))) - (defn add-svg-content [props node] (let [svg-content (get-data node :penpot:svg-content) @@ -522,6 +552,12 @@ :tag tag :content node-content}))) +(defn add-frame-data [props node] + (let [grids (parse-grids node)] + (cond-> props + (not (empty? grids)) + (assoc :grids grids)))) + (defn get-image-name [node] (get-in node [:attrs :penpot:name])) @@ -550,6 +586,9 @@ (cond-> (= :svg-raw type) (add-svg-content node)) + (cond-> (= :frame type) + (add-frame-data node)) + (cond-> (= :group type) (add-group-data node)) @@ -561,3 +600,17 @@ (cond-> (= :text type) (add-text-data node)))))) + +(defn parse-page-data + [node] + (let [style (parse-style (get-in node [:attrs :style])) + background (:background style) + grids (->> (parse-grids node) + (group-by :type) + (d/mapm (fn [k v] (-> v first :params))))] + (cond-> {} + (some? background) + (assoc-in [:options :background] background) + + (not (empty? grids)) + (assoc-in [:options :saved-grids] grids)))) diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs index 6a1c338a9..ac01f53ee 100644 --- a/frontend/src/app/worker/import.cljs +++ b/frontend/src/app/worker/import.cljs @@ -137,11 +137,13 @@ [file [page-name content]] (if (cip/valid? content) (let [nodes (->> content cip/node-seq) - file-id (:id file)] + file-id (:id file) + page-data (-> (cip/parse-page-data content) + (assoc :name page-name))] (->> (rx/from nodes) (rx/filter cip/shape?) (rx/mapcat (partial resolve-images file-id)) - (rx/reduce add-shape-file (fb/add-page file page-name)) + (rx/reduce add-shape-file (fb/add-page file page-data)) (rx/map fb/close-page))) (rx/empty)))