花园

Powered by 🌱Roam Garden

{{roam/render:

(ns excalidraw.app.beta.v09
  (:require 
   [clojure.set :as s]
   [reagent.core :as r]
   [roam.datascript :as rd]
   [roam.block :as block]
   [clojure.string :as str]
   [clojure.edn :as edn]
   [roam.util :as util]
   [roam.datascript.reactive :as dr]
   [clojure.pprint :as pp]))

(def plugin-version 1)
(def app-page "roam/excalidraw")
(def app-settings-block "Settings")
(def app-setting-uid "Excal_SET")
(def default-app-settings {:mode "light"
                           :img  "SVG"
                           :full-screen-margin 0.015
                           :max-embed-width 600
                           :max-embed-height 400
                           :nested-text-rows 20
                           :nested-text-row-height 40
                           :nested-text-col-width 400
                           :nested-text-start-top 40
                           :nested-text-start-left 320
                           :nested-text-font-size 20
                           :nested-text-font-family 1})
(def app-settings (r/atom default-app-settings))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; util functions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def silent (r/atom false))
(defn debug [x]
  (if-not @silent (apply (.-log js/console) "<<< Roam-Excalidraw Main cljs >>>" x)))

(def embedded-view "ev")
(def full-screen-view "fs")
(defn is-full-screen [cs]  ;;component-state
  (not= (:position @cs) embedded-view))

(defn create-block [parent-uid order block-string]
  (.createBlock js/window.ExcalidrawWrapper parent-uid order block-string))

(defn block-update [x]
  (.updateBlock js/window.ExcalidrawWrapper x))

(defn pretty-settings [x]
  (let [y (into (sorted-map) (sort-by first (seq x)))]
    (-> (str y)
          (str/replace "{" "{\n")
          (str/replace ", " "\n")
          (str/replace "}" "\n}"))))

(defn get-next-block-order [x]
  (let [o (rd/q '[:find (max ?o) . 
                  :in $ ?uid
                  :where [?b :block/uid ?uid]
                         [?b :block/children ?c]
                         [?c :block/order ?o]]
                x)]
    (if (nil? o) 0 (+ 1 o))
))

(defn save-settings []
  ;;(debug ["(save-settings) Enter"])
  (let [settings-host (r/atom (rd/q '[:find ?uid .
                                     :in $ ?page ?block
                                     :where [?p :node/title ?page]
                                            [?p :block/children ?b]
                                            [?b :block/string ?block]
                                            [?b :block/uid ?uid]]
                                    app-page app-settings-block))]
    (if (nil? @settings-host)
      (do 
        ;;(debug ["(save-settings) settings host does not exist"])
        (reset! settings-host
          (create-block 
            (rd/q '[:find ?uid . 
                    :in $ ?page
                    :where [?p :node/title ?page]
                           [?p :block/uid ?uid]]
                  app-page)
            3 app-settings-block))))
    (let [settings-block (r/atom (rd/q '[:find ?uid .
                                         :in $ ?settings-host
                                         :where [?b :block/uid ?settings-host]
                                                [?b :block/children ?c]
                                                [?c :block/order 0]
                                                [?c :block/uid ?uid]]
                                        @settings-host))]
      (if (nil? @settings-block)
        (do 
          ;;(debug ["(save-settings) settings-block does not exist"])
          (create-block @settings-host 0 (pretty-settings @app-settings)))
        (do 
          ;;(debug ["(save-settings) settings-block exists, updating"])
          (block-update {:block {:uid @settings-block 
                                 :string (pretty-settings @app-settings)}}))))))

(defn js-to-clj-str [& x]
  ;;(debug ["(js-to-clj-str): x: " x (str x)])
  (let [res (-> x
              (str)
              (str/replace #"\(#js"  "")
              (str/replace #"#js" "")
              (str/replace #"\}\}\)" "}}"))]
    res))

(defn fix-double-bracket [x]
  (str/replace x #"\[{2}" "[ ["))

(defn get-data-block-uid [x]
  (rd/q '[:find ?drawing-uid .
          :in $ ?uid
          :where [?e :block/uid ?uid]
                  [?e :block/children ?c]
                  [?c :block/order 0]
                  [?c :block/string ?s]
                  [(clojure.string/includes? ?s "((ExcalDATA))")]
                  [?c :block/uid ?drawing-uid]]
        x))

(defn pull-children [block-uid order]
  (rd/q '[:find (pull ?b [:block/uid :block/string {:block/children [:block/string :block/order :block/uid {:block/children ...}]}])
                                         :in $ ?block-uid ?order
                                         :where [?e :block/uid ?block-uid]
                                                [?e :block/children ?b]
                                                [?b :block/order ?order]]
                                         block-uid order))

(defn flatten-nested-text [nested-text level] 
  (let [result (atom nil)]
    (doseq [y nested-text]
      (let [order (str/join [level (pp/cl-format nil "~2,'0d" (:block/order y))])]
        (reset! result (conj @result {:block/string (:block/string y)
                                       :block/order order
                                       :block/uid (:block/uid y)}))
        (if-not (nil? (:block/children y)) 
          (reset! result (concat @result (flatten-nested-text (:block/children y) order))))
    ))   
    ;;(debug ["flat-nest " (str @result)])
     (into [] (sort-by :block/order @result))
))

(defn get-text-blocks [x]
  (-> x
    (pull-children 1)
    (first)
    (get-in [0 :block/children])
    (flatten-nested-text "")))

(defn get-text-elements [x]
  (filter (comp #{"text"} :type) x)
)

(defn get-block-uid-from-text-element [x]
  (second (re-find #"ROAM_(.*)_ROAM" (:id x)))
)

;;updates the :elements value of the drawing with nested text and updated object groups
(defn update-elements-with-parts [x] {:raw-elements [] :text-elements [] :groups []}
  (into [] (concat (into [] (remove (comp #{"text"} :type) (:raw-elements x)))  (:text-elements x)))
)

;;{:block-uid "BlockUID" :map-string "String" :cs atom :drawing atom}
(defn save-component [x] 
  ;;Disable the pullWatch while blocks are edited
  (reset! (:saving-flag x) true) 
  ;;(debug ["(save-component) Enter"])

  (let [data-block-uid (get-data-block-uid (:block-uid x))
        edn-map (edn/read-string (:map-string x))
        text-elements (r/atom nil)
        ;;get text blocks nested under title
        nestedtext-parent-block-uid (get-in @(:drawing x) [:nestedtext-parent :block-uid])
        nested-text-blocks (get-text-blocks (:block-uid x)) 
        app-state (into {} (filter (comp some? val) (:appState edn-map)))] ;;remove nil elements from appState
    
    ;;process text on drawing
    ;;(debug ["(save-component) start processing text"])
    (doseq [y (get-text-elements (:elements edn-map))]
      (if (:isDeleted y)
        (if (str/starts-with? (:id y) "ROAM_")
          (block/delete {:block {:uid (get-block-uid-from-text-element y)}})
        )
        (if (str/starts-with? (:id y) "ROAM_")
          (do ;;block with text should already exist, update text, but double check that the block is there...
            ;;(debug ["(save-component) nested block should exist text:" (:text y) "block-id" (get-block-uid-from-text-element y)])
            (let [text-block-uid (get-block-uid-from-text-element y)
                  nested-block (filter (comp #{text-block-uid} :block/uid) nested-text-blocks)]
              (if-not (= 0 (count nested-block))
                (do ;;block exists
                  ;;(debug ["(save-component) block exists, updateing"])
                  (if-not (= (:block/string (first nested-block)) (:text y))
                    (block-update {:block {:uid text-block-uid :string (:text y)}}))
                  (reset! text-elements (conj @text-elements y))
                )
                (do ;block no-longer exists, create new one
                  ;;(debug ["(save-component) block should, but does not exist, creating..."])
                  (let [new-block-uid (.createBlock js/ExcalidrawWrapper nestedtext-parent-block-uid (get-next-block-order nestedtext-parent-block-uid) (:text y))]
                    (reset! text-elements (conj @text-elements (assoc-in y [:id] (str/join ["ROAM_" new-block-uid "_ROAM"]))))
          )))))
          (do ;;block with text does not exist as nested block, create new
            ;;(debug ["(save-component) block does not exists, creating"])
            (let [new-block-uid (.createBlock js/ExcalidrawWrapper nestedtext-parent-block-uid (get-next-block-order nestedtext-parent-block-uid) (:text y))]
              (reset! text-elements (conj @text-elements (assoc-in y [:id] (str/join ["ROAM_" new-block-uid "_ROAM"])))) 
              (reset! text-elements (conj @text-elements (assoc-in y [:isDeleted] true))) 
              )))))

    ;;(debug ["(save-component) text-blocks with updated IDs" (str @text-elements)])
    ;;updating the data block is the final piece in saving the component
    (let [elements (update-elements-with-parts {:raw-elements (:elements edn-map) :text-elements @text-elements})  
          out-string (fix-double-bracket (str {:elements elements :appState app-state :roamExcalidraw {:version plugin-version}}))
          render-string (str/join ["{{roam/render: ((ExcalDATA)) " out-string " }}"])]
      (block-update
        {:block {:uid data-block-uid
                :string render-string}}) 
      (swap! app-settings assoc-in [:mode] (get-in app-state [:theme]))
      (save-settings)
      (reset! (:saving-flag x) false)
      {:elements elements :appState app-state :roamExcalidraw {:version plugin-version}}                                      
)))

(defn load-settings []
  ;;(debug ["(load-settings) Enter"])
  (let [settings-block (rd/q '[:find ?settings .
                              :in $ ?page ?block
                              :where [?p :node/title ?page]
                                     [?p :block/children ?b]
                                     [?b :block/string ?block]
                                     [?b :block/children ?c]
                                     [?c :block/order 0]
                                     [?c :block/string ?settings]]
                            app-page app-settings-block)]
    (if-not (nil? settings-block)
      (do 
        ;;(debug ["(load-settings) settings: " settings-block])
        (reset! app-settings (edn/read-string settings-block))
        (if (nil? @app-settings)
          (reset! app-settings default-app-settings))
        (doseq [key (keys default-app-settings)]
          (if (nil? (key @app-settings))
            (swap! app-settings assoc-in [key] (key default-app-settings))))))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Load data from nested block(s)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;finds the json enclosed in double parentheses and returns it
(defn get-data-from-block-string [x]
  ;;(debug ["(get-data-from-block-string)" (count x)])
  (if (= (count x) 0)
    (do 
      ;;(debug ["(get-data-from-block-string) returning nil"])
      nil)
    (do
      (let [data-string (get-in (first x) [0 :block/string])
            return-string (second (re-find #"ExcalDATA\){2}\s*(\{.*\})\s*\}{2}" data-string))]
        ;;;(debug ["(get-data-from-block-string) returning: " retrun-string])
        (edn/read-string return-string)))))

(defn create-nested-blocks [x]; {:block-uid "BlockUID" :drawing atom :empty-block-uid "BlockUID"}
;;block uid is the block of the roam/render component
;;empty block is the block created by the user by trying to nest text under 
;;a new drawing that hasn't been edited yet (i.e. the data and title children
;;are missing)
  ;;(debug ["(create-nested-blocks)"])
  (let [default-data {:appState {:theme (:mode @app-settings)}
                      :roamExcalidraw {:version 1}}]
    (create-block (:block-uid x) 0 (str/join ["{{roam/render: ((ExcalDATA)) "
                                (str default-data) " }}"]))
    (reset! (:drawing x) {:drawing default-data 
                          :nestedtext-parent {:block-uid (create-block (:block-uid x) 1 "**Text nested here will appear on your drawing:**")}})
                                                              
    (if (nil? (:empty-block-uid x)) 
      (block-update {:block {:uid (:block-uid x) :open false}}) ;;fold up the drawing block to hide children
      (block/move {:location {:parent-uid (get-in @(:drawing x) [:nestedtext-parent :block-uid]) ;;move new block under nested text
                            :order 0}
                 :block {:uid (:empty-block-uid x)}}))
))



(defn load-drawing [x] ;{:block-uid "BlockUID" :drawing atom :data objects :text "text"} 
;drawing is the atom holding the drawing map
;block uid is the block with the roam/render component
;data are the drawing objects
;text are the nested text blocks
  ;;(debug ["(load-drawing) enter"])
  (if (= (count (:data x)) 0)
      (do
        ;;(debug ["(load-drawing) no children - creating dummy data"])
        (let [default-data {:appState {:theme (:mode @app-settings)}
                            :roamExcalidraw {:version 1}}]
          (reset! (:drawing x) {:drawing default-data 
                                :nestedtext-parent {:block-uid nil}})
      ))
      (if (= (count (:text x)) 0)
        (do
          ;;(debug ["(load-drawing) create title only"])
          (reset! (:drawing x) {:drawing (:data x)
                                :nestedtext-parent {:block-uid (create-block (:block-uid x) 1 "**Text nested here will appear on your drawing:**")}})
          (block-update {:block {:uid (:block-uid x) :open false}})
        )
        (do
          ;;(debug ["(load-drawing) ExcalDATA & title already exist"])
          (reset! (:drawing x) {:drawing (:data x)
                                :nestedtext-parent {:block-uid  (get-in (:text x) [0 :block/uid])}
                                :text (get-in (:text x) [0 :block/children])})
  )))
  ;;(debug ["(load-drawing) drawing: " @(:drawing x) " data: " (:data x) " text: " (str (:text x)) "theme " (get-in (:data x) [:appState :theme])])
)

;;check if text in nested block has changed compared to drawing and updated text in drawing element including size
(defn update-drawing-based-on-nested-blocks [x] ;{:elements [] :appState {} :nested-text [:block/uid "BlockUID" :block/string "text"]}
  ;;(debug ["(update-drawing-based-on-nested-blocks) Enter x:" x])
  (if-not (nil? (:nested-text x)) 
    (do
      (let [text-elements (r/atom nil)
            nested-text (flatten-nested-text (:nested-text x) "")]   
      ;;(debug ["(update-drawing-based-on-nested-blocks) processing nested text - apply changes to existing text elements, omit deleted ones"])
        ;;update elements on drawing based on changes to nested text
        (doseq [y (get-text-elements (:elements x))]
          (let [block-uid (get-block-uid-from-text-element y)
                block-text (:block/string (first (filter (comp #{block-uid} :block/uid) nested-text)))
                text-element-has-nested-block-pair (= 0 (count (filter (comp #{block-uid} :block/uid) nested-text)))]
            ;;add text to drawing if text element has a nested block pair, 
            (if (not text-element-has-nested-block-pair)  
              (do
                ;if text has changed, update measures
                (if-not (= block-text (:text y)) 
                  (let [text-measures (js->clj (.measureText js/ExcalidrawWrapper block-text y))]
                    (reset! text-elements 
                              (conj @text-elements 
                                      (-> y 
                                        (assoc-in [:text] block-text)
                                        (assoc-in [:baseline] (get text-measures "baseline"))
                                        (assoc-in [:width] (get text-measures "width"))
                                        (assoc-in [:height] (get text-measures "height"))
                    ))))
                  ;;else add to list as-is 
                  (reset! text-elements (conj @text-elements y))))
              ;;else it should be removed from the drawing
              ;;unless the drawing is from an old version of the plugin when nested blocks weren't handled
              (if (< (get-in x [:roamExcalidraw :version]) 1) 
                (reset! text-elements (conj @text-elements y)))
        )))
        
        ;;(debug ["(update-drawing-based-on-nested-blocks) processing nested text - add new nested blocks"])
        ;;add text for newly nested blocks
        (let [counter (atom -1)]
          (doseq [nt nested-text]
            (swap! counter inc)
            (let [text (:block/string nt)
                  dummy {:fontFamily (:nested-text-font-family @app-settings) 
                        :fontSize (:nested-text-font-size @app-settings)}
                  order (:block/order nt)
                  id (:block/uid nt)]
              (if (= 0 (count (filter (comp #{(str/join ["ROAM_" (:block/uid nt) "_ROAM"])} :id) @text-elements)))
                (let [col (int (/ @counter (:nested-text-rows @app-settings)))
                      row (mod @counter (:nested-text-rows @app-settings))
                      x (+ (:nested-text-start-left @app-settings) (* col (:nested-text-col-width @app-settings)))
                      y (+ (:nested-text-start-top @app-settings) (* row (:nested-text-row-height @app-settings)))  
                      text-measures (js->clj (.measureText js/ExcalidrawWrapper text dummy))]
                  ;;(debug ["(update-drawing-based-on-nested-blocks) add new: text" text "id" id])
                  (reset! text-elements 
                            (conj @text-elements 
                                  {:y y
                                    :baseline (get text-measures "baseline")
                                    :isDeleted false
                                    :strokeStyle "solid":roughness 1
                                    :width (get text-measures "width")
                                    :type "text"
                                    :strokeSharpness "sharp"
                                    :fillStyle "hachure"
                                    :angle 0
                                    :groupIds []
                                    :seed 1
                                    :fontFamily (:nested-text-font-family @app-settings)
                                    :boundElementIds []
                                    :strokeWidth 1
                                    :opacity 100
                                    :id (str/join ["ROAM_" id "_ROAM"])
                                    :verticalAlign "top"
                                    :strokeColor "#000000"
                                    :textAlign "left"
                                    :x (+ x (* 5 (count order)))
                                    :fontSize (:nested-text-font-size @app-settings)
                                    :version 1
                                    :backgroundColor "transparent"
                                    :versionNonce 1
                                    :height (get text-measures "height")
                                    :text text}))                                
          )))))


        {:elements (update-elements-with-parts {:raw-elements (:elements x) :text-elements @text-elements})
        :appState (:appState x)}
    ))
    {:elements (:elements x) :appState (:appState x)}
))

(defn generate-scene [x] ;{:drawing atom}]
  ;;(debug ["(generate-scene) enter" x])
  (update-drawing-based-on-nested-blocks {:elements (:elements (:drawing @(:drawing x)))
                                                      :appState (:appState (:drawing @(:drawing x)))
                                                      :nested-text (:text @(:drawing x))
                                                      :roamExcalidraw (:roamExcalidraw (:drawing @(:drawing x)))}))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Main Function Form-3
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn resize [ew]
  ;;(debug ["(resize)"])
  (if-not (nil? @ew) (.onResize @ew)))

(defn update-scene [ew scene]
  ;;(debug ["(update-scene) scene: " scene])
  (if-not (nil? @ew) (.updateScene @ew scene)))

(defn get-drawing [ew]
  ;;(debug ["(get-drawing): " (.getDrawing js/window.ExcalidrawWrapper @ew)])
  (.getDrawing js/window.ExcalidrawWrapper @ew))

(defn host-div-style [cs]
  (let [width    (.-innerWidth js/window)
        height   (.-innerHeight js/window)
        top      (int (* height (:full-screen-margin @app-settings)))
        left     (int (* width (:full-screen-margin @app-settings)))
        host-div-width (if (nil? (:this-dom-node @cs)) (:max-embed-width @app-settings)
                         (.getHostDIVWidth js/ExcalidrawWrapper (:this-dom-node @cs)))
        embed-width (if (> host-div-width (:max-embed-width @app-settings)) 
                      (:max-embed-width @app-settings) host-div-width)
        embed-height (* (:max-embed-height @app-settings) (/ embed-width (:max-embed-width @app-settings)))
        ar (:aspect-ratio @cs)
        w (if (nil? ar) embed-width 
            (if (> ar 1.0) embed-width
              (* ar embed-height)))
        h (if (nil? ar @cs) "100%" 
            (if (> ar 1.0) "100%" 
              embed-height  ))]
    (if (is-full-screen cs)
      {:position "fixed"
      :z-index 1000
      :top top
      :left left
      :width  (str/join ["calc(100% - " (* left 2) "px)"]) 
      :height (- height (* top 2))
      :resize "none"}
      {:position "relative"
      :width w
      :height h
      :resize "both"
      :overflow "hidden"}
)))

(defn going-full-screen? [x cs style]
  (if (= x true)
    (do
      (load-settings)
      (.fullScreenKeyboardEventRedirect js/window.ExcalidrawWrapper true)
      (swap! cs assoc-in [:position] full-screen-view)
      (swap! style assoc-in [:host-div] (host-div-style cs))
      (swap! cs assoc-in [:mouseover] false))
    (do
      (.fullScreenKeyboardEventRedirect js/window.ExcalidrawWrapper false)
      (swap! cs assoc-in [:position] embedded-view)
      (swap! style assoc-in [:host-div] (host-div-style cs)))))


;;state to capture when callback confirms React libraries have loaded
(def deps-available (r/atom false))

(defn check-js-dependencies []
  (if (and 
       (not= (str (type js/Excalidraw)) "")
       (not= (str (type js/ReactDOM)) "")
       (not= (str (type js/React)) "")
       (not= (str (type js/ExcalidrawConfig)) "")
       (not= (str (type js/ExcalidrawWrapper)) ""))
    (do (reset! silent (not (.-DEBUG js/ExcalidrawConfig)))
      (reset! deps-available true))
    (js/setTimeout check-js-dependencies 1000)
  ))
                                  
(defn get-embed-image [drawing dom-node app-name]
  (if (= (:img @app-settings) "PNG")
    (.getPNG js/window.ExcalidrawWrapper drawing dom-node app-name)
    (.getSVG js/window.ExcalidrawWrapper drawing dom-node app-name)
  ))

(defn main [{:keys [block-uid]} & args]
  ;;(debug ["(main) component starting..."])
  (check-js-dependencies)
  (let [drawing (r/atom nil)
        cs (r/atom {:position embedded-view  ;;component-state
                    :this-dom-node nil
                    :aspect-ratio nil
                    :mouseover false
                    :prev-empty-block nil}) ;; this is a semaphore system to avoid creating double nested blocks when manually creating the first nested element
        saving-flag (atom false)
        pull-watch-active (atom false)
        ew (r/atom nil) ;;excalidraw-wrapper
        app-name (str/join ["excalidraw-app-" block-uid])
        style (r/atom {:host-div (host-div-style cs)})
        resize-handler (fn [] (if (is-full-screen cs) 
                                (swap! style assoc-in [:host-div] (host-div-style cs))  
                                (if-not (nil? (:this-dom-node @cs)) 
                                  (swap! style assoc-in [:host-div] (host-div-style cs)))))
        ;changed-drawing (atom nil)
        drawing-on-change-callback (fn [x] (if-not @saving-flag
                                             (.updateScene 
                                              @ew 
                                              (save-component 
                                               {:block-uid block-uid 
                                                :map-string (js-to-clj-str x) 
                                                :cs cs
                                                :drawing drawing
                                                :saving-flag saving-flag}))))
        pull-watch-callback (fn [before after]
                              ;;(debug ["(pull-watch-callback) after:" (js-to-clj-str after)])
                              (if-not (or @saving-flag (is-full-screen cs) @pull-watch-active)
                                (do 
                                  (reset! pull-watch-active true)
                                  (let [drawing-data (pull-children block-uid 0)
                                        drawing-text (pull-children block-uid 1)
                                        empty-block-uid (re-find #":block/uid \"(.*)\", (:block/string \"\")" (str drawing-data))] ;check if user has nested a block under a new drawing
                                    (if-not (nil? empty-block-uid)
                                      (if-not (= (second empty-block-uid) (:prev-empty-block @cs))
                                        (do
                                          (swap! cs assoc-in [:prev-empty-block] (second empty-block-uid)) ;;semaphore to avoid double creation of blocks
                                          (create-nested-blocks {:block-uid block-uid 
                                                                  :drawing drawing 
                                                                  :empty-block-uid (second empty-block-uid)})
                                          ))) 
                                    (load-drawing {:block-uid block-uid 
                                                  :drawing drawing 
                                                  :data (get-data-from-block-string drawing-data) 
                                                  :text (first drawing-text)})
                                    (if-not (is-full-screen cs)
                                      (do
                                        (swap! cs assoc-in [:aspect-ratio] (get-embed-image (generate-scene {:drawing drawing}) (:this-dom-node @cs) app-name))
                                        (swap! style assoc-in [:host-div] (host-div-style cs)))))
                                  (reset! pull-watch-active false)
  )))]
    (if (= @deps-available false)
    [:div "Libraries have not yet loaded. Please refresh the block in a moment."]
    (fn []
      ;;(debug ["(main) fn[] starting..."])
      (r/create-class
      { :display-name "Excalidraw Roam Beta"
        ;; Constructor
;           :constructor (fn [this props])
;           :get-initial-state (fn [this] )
        ;; Static methods
;           :get-derived-state-from-props (fn [props state] )
;           :get-derived-state-from-error (fn [error] )
        ;; Methods
;          :get-snapshot-before-update (fn [this old-argv new-argv] )
;          :should-component-update (fn [this old-argv new-argv])
        :component-did-mount (fn [this]
                                ;;(debug ["(main) :component-did-mount"])
                                (load-settings)
                                (swap! cs assoc-in [:this-dom-node] (r/dom-node this))
                                ;;(debug ["(main) :component-did-mount addPullWatch"])
                                (.addPullWatch js/ExcalidrawWrapper block-uid pull-watch-callback)
                                (pull-watch-callback nil nil)
                                (swap! style assoc-in [:host-div] (host-div-style cs))
                                (.addEventListener js/window "resize" resize-handler)
                                ;;(debug ["(main) :component-did-mount Exalidraw mount initiated"])
                              )
        :component-did-update (fn [this old-argv old-state snapshot]
                                ;;(debug ["(main) :component-did-update"])
                                (if (is-full-screen cs)
                                  (resize ew)))
        :component-will-unmount (fn [this]
                                  ;;(debug ["(main) :component-will-unmount"])
                                  (.removePullWatch js/ExcalidrawWrapper block-uid pull-watch-callback)
                                  (.removeEventListener js/window "resize" resize-handler))
;           :component-did-catch (fn [this error info])
        :reagent-render (fn [{:keys [block-uid]} & args]
                          ;;(debug ["(main) :reagent-render"])
                          [:div
                            {:class "excalidraw-host"
                              :style (:host-div @style)
                              :on-mouse-over (fn[e] (swap! cs assoc-in [:mouseover] true))
                              :on-mouse-leave (fn[e] (swap! cs assoc-in [:mouseover] false)) }
                            (if-not (is-full-screen cs)
                              [:button
                                {:class "ex-embed-button"
                                :style {:display (if (:mouseover @cs) "block" "none")
                                        :left (if-not (nil? (:this-dom-node @cs)) 
                                                (- (.-clientWidth (:this-dom-node @cs)) 32) 
                                                0)}
                                :draggable true
                                :on-click (fn [e]
                                            (load-settings)
                                            (going-full-screen? true cs style)
                                            (if (nil? (get-in @drawing [:nestedtext-parent :block-uid])) 
                                              (create-nested-blocks {:block-uid block-uid 
                                                                      :drawing drawing 
                                                                      :empty-block-uid nil}))
                                            (reset! ew (js/ExcalidrawWrapper.
                                                        app-name
                                                        (generate-scene {:drawing drawing})
                                                        (:this-dom-node @cs)
                                                        drawing-on-change-callback ))
                                                        ;(js/setTimeout autosave 10000)
                                                        )}
                                "🖋"]
                              [:button
                                {:class "ex-fullscreen-button"
                                :style {:left (- (.-clientWidth (:this-dom-node @cs)) 32)}
                                :draggable true
                                :on-click (fn [e]
                                            (.svgClipboard js/ExcalidrawWrapper)
                                            (going-full-screen? false cs style)
                                            (save-component {:block-uid block-uid 
                                                              :map-string (js-to-clj-str (get-drawing ew))
                                                              :cs cs
                                                              :drawing drawing
                                                              :saving-flag saving-flag})
                                            (swap! cs assoc-in [:aspect-ratio] (get-embed-image (get-drawing ew) (:this-dom-node @cs) app-name))
                                )}
                                "❌"])
                            [:div
                              {:id app-name
                              :style {:position "relative" :width "100%" :height "100%"}}
]])})))))
}}

{{roam/render:

{:appState {:currentItemFontFamily 1, :isBindingEnabled true, :currentItemRoughness 1, :zoom {:value 0.8, :translation {:x 152.5999999999999, :y 79.19999999999993}}, :zenModeEnabled false, :lastPointerDownWith "mouse", :isLibraryOpen false, :scrollX 0, :scrolledOutside false, :scrollY -25, :exportBackground true, :showStats false, :suggestedBindings [], :name "Untitled-2021-05-27-2054", :viewBackgroundColor "ffffff", :currentItemFillStyle "hachure", :width 1164, :shouldCacheIgnoreZoom false, :currentItemStrokeSharpness "sharp", :selectedGroupIds {}, :isRotating false, :pasteDialog {:shown false, :data nil}, :offsetLeft 18, :currentItemStrokeWidth 1, :currentItemBackgroundColor "transparent", :elementType "selection", :offsetTop 10, :theme "light", :exportWithDarkMode false, :currentItemTextAlign "left", :currentItemLinearStrokeSharpness "round", :currentItemOpacity 100, :exportEmbedScene false, :currentItemStrokeColor "000000", :isLoading false, :currentItemFontSize 20, :elementLocked false, :currentChartType "bar", :shouldAddWatermark false, :currentItemEndArrowhead "arrow", :previousSelectedElementIds {:HP34wObisnv9HLwtFMTtV true}, :viewModeEnabled false, :isResizing false, :showHelpDialog false, :height 647, :currentItemStrokeStyle "solid", :cursorButton "up"}, :roamExcalidraw {:version 1}, :elements [{:y 104.96034240722656, :isDeleted false, :strokeStyle "solid", :roughness 1, :width 196.51910400390625, :type "rectangle", :strokeSharpness "sharp", :fillStyle "hachure", :angle 0, :groupIds [], :seed 1961152207, :boundElementIds [], :strokeWidth 1, :opacity 100, :id "0d41hkWBerTDnB4MlgQGQ", :strokeColor "000000", :x 597.7858123779296, :version 199, :backgroundColor "transparent", :versionNonce 401170472, :height 35.35868835449219} {:y 142.71205139160156, :isDeleted false, :strokeStyle "solid", :roughness 1, :width 231.62469482421875, :type "rectangle", :strokeSharpness "sharp", :fillStyle "hachure", :angle 0, :groupIds [], :seed 873844783, :boundElementIds [], :strokeWidth 1, :opacity 100, :id "RKcwPNILcJjnUaXUqBJvE", :strokeColor "000000", :x 562.394805908203, :version 179, :backgroundColor "transparent", :versionNonce 1039799896, :height 48.95811462402344} {:y 194.0578269958496, :isDeleted false, :strokeStyle "solid", :roughness 1, :width 258.6627197265625, :type "rectangle", :strokeSharpness "sharp", :fillStyle "hachure", :angle 0, :groupIds [], :seed 967910319, :boundElementIds [], :strokeWidth 1, :opacity 100, :id "2fL20lpRiHvmzfq3wdPP4", :strokeColor "000000", :x 536.7185974121093, :version 196, :backgroundColor "transparent", :versionNonce 1192958760, :height 47.177459716796875} {:y 240.89492416381836, :isDeleted false, :strokeStyle "solid", :roughness 1, :width 307.20965576171875, :type "rectangle", :strokeSharpness "sharp", :fillStyle "hachure", :angle 0, :groupIds [], :seed 1793073057, :boundElementIds [], :strokeWidth 1, :opacity 100, :id "nNDP6Im3jxttX4ZYigZVp", :strokeColor "000000", :x 487.2685546874999, :version 160, :backgroundColor "transparent", :versionNonce 538282840, :height 38.4932861328125} {:y 280.4719886779785, :isDeleted false, :strokeStyle "solid", :roughness 1, :width 384.31011962890625, :type "rectangle", :strokeSharpness "sharp", :fillStyle "hachure", :angle 0, :groupIds [], :seed 1529276847, :boundElementIds [], :strokeWidth 1, :opacity 100, :id "d3e6o_D7suy4jLUtb48sL", :strokeColor "000000", :x 410.70492553710926, :version 155, :backgroundColor "transparent", :versionNonce 1046312488, :height 40.593475341796875} {:y 324.7212715148926, :isDeleted false, :strokeStyle "solid", :roughness 1, :width 464.79766845703125, :type "rectangle", :strokeSharpness "sharp", :fillStyle "hachure", :angle 0, :groupIds [], :seed 16640673, :boundElementIds [], :strokeWidth 1, :opacity 100, :id "HWAK2-V_SUY64nX2oV5-i", :strokeColor "000000", :x 332.8465118408202, :version 147, :backgroundColor "transparent", :versionNonce 2029674584, :height 40.19110107421875} {:y 366.5802803039551, :isDeleted false, :strokeStyle "solid", :roughness 1, :width 543.7197875976562, :type "rectangle", :strokeSharpness "sharp", :fillStyle "hachure", :angle 0, :groupIds [], :seed 1256555521, :boundElementIds [], :strokeWidth 1, :opacity 100, :id "jCYiVpek47An7k4OQAre4", :strokeColor "000000", :x 254.79930114746094, :version 168, :backgroundColor "transparent", :versionNonce 1922855208, :height 32.08953857421875} {:y 400.7959785461426, :isDeleted false, :strokeStyle "solid", :roughness 1, :width 621.2939758300781, :type "rectangle", :strokeSharpness "sharp", :fillStyle "hachure", :angle 0, :groupIds [], :seed 1224060225, :boundElementIds [], :strokeWidth 1, :opacity 100, :id "i_sr5BlyZvUT7Dx2FYOjo", :strokeColor "000000", :x 173.82098388671875, :version 226, :backgroundColor "transparent", :versionNonce 2089161048, :height 45.641204833984375} {:y 446.6923294067383, :isDeleted false, :strokeStyle "solid", :roughness 1, :width 654.0537872314453, :type "rectangle", :strokeSharpness "sharp", :fillStyle "hachure", :angle 0, :groupIds [], :seed 1431022191, :boundElementIds [], :strokeWidth 1, :opacity 100, :id "mLgru1vOHVWPtV3RMox-y", :strokeColor "000000", :x 140.33084106445312, :version 276, :backgroundColor "transparent", :versionNonce 1696852568, :height 52.36700439453125} {:y 499.61326599121094, :isDeleted false, :strokeStyle "solid", :roughness 1, :width 685.0969848632812, :type "rectangle", :strokeSharpness "sharp", :fillStyle "hachure", :angle 0, :groupIds [], :seed 1003129871, :boundElementIds [], :strokeWidth 1, :opacity 100, :id "7zpQa2H-kGFyoAORxZJLZ", :strokeColor "000000", :x 108.60432434082031, :version 165, :backgroundColor "transparent", :versionNonce 328243544, :height 44.18501281738281} {:y 104.98355102539062, :isDeleted false, :strokeStyle "solid", :roughness 1, :width 251.12469482421875, :type "rectangle", :strokeSharpness "sharp", :fillStyle "hachure", :angle 0, :groupIds [], :seed 871321313, :boundElementIds [], :strokeWidth 1, :opacity 100, :id "1hBZ88gry4BBxTHau54JZ", :strokeColor "000000", :x 794.7753601074219, :version 157, :backgroundColor "transparent", :versionNonce 108457000, :height 445.94377136230463} {:y 512.943775177002, :baseline 21, :isDeleted false, :strokeStyle "solid", :roughness 1, :width 140, :type "text", :strokeSharpness "sharp", :fillStyle "hachure", :angle 0, :groupIds [], :seed 1, :fontFamily 1, :boundElementIds [], :strokeWidth 1, :opacity 100, :id "ROAM_DrJ443mKW_ROAM", :verticalAlign "top", :strokeColor "000000", :textAlign "left", :x 635.1720886230469, :fontSize 20, :version 303, :backgroundColor "transparent", :versionNonce 71056984, :height 28, :text "第十章期间计算"} {:y 462.72856521606445, :baseline 21, :isDeleted false, :strokeStyle "solid", :roughness 1, :width 140, :type "text", :strokeSharpness "sharp", :fillStyle "hachure", :angle 0, :groupIds [], :seed 1, :fontFamily 1, :boundElementIds [], :strokeWidth 1, :opacity 100, :id "ROAM_9EkMMoVYC_ROAM", :verticalAlign "top", :strokeColor "000000", :textAlign "left", :x 626.9279479980469, :fontSize 20, :version 291, :backgroundColor "transparent", :versionNonce 904443944, :height 28, :text "第九章诉讼时效"} {:y 414.63098526000977, :baseline 21, :isDeleted false, :strokeStyle "solid", :roughness 1, :width 140, :type "text", :strokeSharpness "sharp", :fillStyle "hachure", :angle 0, :groupIds [], :seed 1, :fontFamily 1, :boundElementIds [], :strokeWidth 1, :opacity 100, :id "ROAM_Se-H_lYE6_ROAM", :verticalAlign "top", :strokeColor "000000", :textAlign "left", :x 612.7770538330078, :fontSize 20, :version 308, :backgroundColor "transparent", :versionNonce 1975200088, :height 28, :text "第八章民事责任"} {:y 369.5413246154785, :baseline 21, :isDeleted false, :strokeStyle "solid", :roughness 1, :width 100, :type "text", :strokeSharpness "sharp", :fillStyle "hachure", :angle 0, :groupIds [], :seed 1, :fontFamily 1, :boundElementIds [], :strokeWidth 1, :opacity 100, :id "ROAM_nUumZy81O_ROAM", :verticalAlign "top", :strokeColor "000000", :textAlign "left", :x 650.7830810546875, :fontSize 20, :version 239, :backgroundColor "transparent", :versionNonce 258383144, :height 28, :text "第七章代理"} {:y 330.3670234680176, :baseline 21, :isDeleted false, :strokeStyle "solid", :roughness 1, :width 180, :type "text", :strokeSharpness "sharp", :fillStyle "hachure", :angle 0, :groupIds [], :seed 1, :fontFamily 1, :boundElementIds [], :strokeWidth 1, :opacity 100, :id "ROAM_SjOsmZrPV_ROAM", :verticalAlign "top", :strokeColor "000000", :textAlign "left", :x 595.5289611816405, :fontSize 20, :version 255, :backgroundColor "transparent", :versionNonce 1333490776, :height 28, :text "第六章民事法律行为"} {:y 290.9044380187988, :baseline 21, :isDeleted false, :strokeStyle "solid", :roughness 1, :width 140, :type "text", :strokeSharpness "sharp", :fillStyle "hachure", :angle 0, :groupIds [], :seed 1, :fontFamily 1, :boundElementIds [], :strokeWidth 1, :opacity 100, :id "ROAM_wjfjOuB1Y_ROAM", :verticalAlign "top", :strokeColor "000000", :textAlign "left", :x 632.9170074462891, :fontSize 20, :version 236, :backgroundColor "transparent", :versionNonce 1409098280, :height 28, :text "第五章民事权利"} {:y 248.89802169799805, :baseline 21, :isDeleted false, :strokeStyle "solid", :roughness 1, :width 160, :type "text", :strokeSharpness "sharp", :fillStyle "hachure", :angle 0, :groupIds [], :seed 1, :fontFamily 1, :boundElementIds [], :strokeWidth 1, :opacity 100, :id "ROAM_yLlqJhRFy_ROAM", :verticalAlign "top", :strokeColor "000000", :textAlign "left", :x 619.4634246826172, :fontSize 20, :version 229, :backgroundColor "transparent", :versionNonce 1445535576, :height 28, :text "第四章非法人组织"} {:y 204.60577011108398, :baseline 21, :isDeleted false, :strokeStyle "solid", :roughness 1, :width 100, :type "text", :strokeSharpness "sharp", :fillStyle "hachure", :angle 0, :groupIds [], :seed 1, :fontFamily 1, :boundElementIds [], :strokeWidth 1, :opacity 100, :id "ROAM__0Or9kaKI_ROAM", :verticalAlign "top", :strokeColor "000000", :textAlign "left", :x 654.0269165039062, :fontSize 20, :version 227, :backgroundColor "transparent", :versionNonce 574637864, :height 28, :text "第三章法人"} {:y 152.8798484802246, :baseline 21, :isDeleted false, :strokeStyle "solid", :roughness 1, :width 120, :type "text", :strokeSharpness "sharp", :fillStyle "hachure", :angle 0, :groupIds [], :seed 1, :fontFamily 1, :boundElementIds [], :strokeWidth 1, :opacity 100, :id "ROAM_7kjHwZ4Wj_ROAM", :verticalAlign "top", :strokeColor "000000", :textAlign "left", :x 642.4361419677734, :fontSize 20, :version 213, :backgroundColor "transparent", :versionNonce 1945119320, :height 28, :text "第二章自然人"} {:y 106.3178939819336, :baseline 21, :isDeleted false, :strokeStyle "solid", :roughness 1, :width 140, :type "text", :strokeSharpness "sharp", :fillStyle "hachure", :angle 0, :groupIds [], :seed 1, :fontFamily 1, :boundElementIds [], :strokeWidth 1, :opacity 100, :id "ROAM_TJCHU-9y9_ROAM", :verticalAlign "top", :strokeColor "000000", :textAlign "left", :x 644.2774047851562, :fontSize 20, :version 238, :backgroundColor "transparent", :versionNonce 1531186216, :height 28, :text "第一章基本规定"} {:y 290.4602584838867, :baseline 21, :isDeleted false, :strokeStyle "solid", :roughness 1, :width 160, :type "text", :strokeSharpness "sharp", :fillStyle "hachure", :angle 0, :groupIds [], :seed 130728929, :fontFamily 1, :boundElementIds [], :strokeWidth 1, :opacity 100, :id "ROAM_OsCs_bb8D_ROAM", :verticalAlign "top", :strokeColor "000000", :textAlign "left", :x 841.7460174560547, :fontSize 20, :version 280, :backgroundColor "transparent", :versionNonce 1210887000, :height 28, :text "民法典第一编总则"}]} }}

Text nested here will appear on your drawing:

第一章基本规定

第二章自然人

第三章法人

第四章非法人组织

第五章民事权利

第六章民事法律行为

第七章代理

第八章民事责任

第九章诉讼时效

第十章期间计算

民法典第一编总则