Browse Source

fix: conflicts

charlie 4 years ago
parent
commit
1859479e42
48 changed files with 1080 additions and 408 deletions
  1. 4 7
      .github/workflows/build-desktop-release.yml
  2. 3 0
      externs.js
  3. 1 1
      package.json
  4. 2 2
      resources/forge.config.js
  5. 4 3
      resources/package.json
  6. 4 5
      src/electron/electron/configs.cljs
  7. 4 1
      src/electron/electron/core.cljs
  8. 115 0
      src/electron/electron/git.cljs
  9. 19 6
      src/electron/electron/handler.cljs
  10. 31 1
      src/electron/electron/state.cljs
  11. 13 1
      src/electron/electron/utils.cljs
  12. 22 18
      src/main/electron/listener.cljs
  13. 1 1
      src/main/frontend/commands.cljs
  14. 47 41
      src/main/frontend/components/block.cljs
  15. 21 4
      src/main/frontend/components/block.css
  16. 48 21
      src/main/frontend/components/content.cljs
  17. 7 13
      src/main/frontend/components/export.cljs
  18. 92 57
      src/main/frontend/components/settings.cljs
  19. 10 0
      src/main/frontend/components/settings.css
  20. 37 0
      src/main/frontend/components/shell.cljs
  21. 5 2
      src/main/frontend/components/svg.cljs
  22. 6 0
      src/main/frontend/db/model.cljs
  23. 16 14
      src/main/frontend/db/query_react.cljs
  24. 2 0
      src/main/frontend/dicts.cljs
  25. 5 1
      src/main/frontend/extensions/code.cljs
  26. 4 4
      src/main/frontend/extensions/pdf/assets.cljs
  27. 41 37
      src/main/frontend/extensions/srs.cljs
  28. 203 85
      src/main/frontend/extensions/zotero.cljs
  29. 4 1
      src/main/frontend/extensions/zotero/extractor.cljs
  30. 40 10
      src/main/frontend/extensions/zotero/setting.cljs
  31. 8 7
      src/main/frontend/format/block.cljs
  32. 8 7
      src/main/frontend/format/mldoc.cljs
  33. 28 7
      src/main/frontend/fs/watcher_handler.cljs
  34. 13 1
      src/main/frontend/handler/common.cljs
  35. 45 5
      src/main/frontend/handler/editor.cljs
  36. 5 0
      src/main/frontend/handler/events.cljs
  37. 4 0
      src/main/frontend/handler/extract.cljs
  38. 21 21
      src/main/frontend/handler/file.cljs
  39. 11 6
      src/main/frontend/handler/repo.cljs
  40. 30 0
      src/main/frontend/handler/shell.cljs
  41. 12 2
      src/main/frontend/modules/shortcut/config.cljs
  42. 28 7
      src/main/frontend/state.cljs
  43. 1 1
      src/main/frontend/ui.css
  44. 13 0
      src/main/frontend/util.cljc
  45. 7 3
      src/main/frontend/util/property.cljs
  46. 1 1
      src/main/frontend/version.cljs
  47. 30 0
      src/test/frontend/util/property_test.cljs
  48. 4 4
      yarn.lock

+ 4 - 7
.github/workflows/build-desktop-release.yml

@@ -58,12 +58,6 @@ jobs:
           sed -i 's/"version": "0.0.1"/"version": "${{ github.event.inputs.tag-version }}"/g' ./package.json
         working-directory: ./static
 
-      - name: Update OSX Packager Config
-        run: |
-          sed -i 's/appleId: "my-fake-apple-id"/appleId: "${{ secrets.APPLE_ID_EMAIL }}"/' ./forge.config.js
-          sed -i 's/appleIdPassword: "my-fake-apple-id-password"/appleIdPassword: "${{ secrets.APPLE_ID_PASSWORD }}"/' ./forge.config.js
-        working-directory: ./static
-
       - name: Display Package.json
         run: cat ./package.json
         working-directory: ./static
@@ -206,8 +200,11 @@ jobs:
           key: ${{ runner.os }}-node-modules
 
       - name: Build/Release Electron App
-        run: yarn install  && yarn electron:make
+        run: yarn install && yarn electron:make
         working-directory: ./static
+        env:
+          APPLE_ID: ${{ secrets.APPLE_ID_EMAIL }}
+          APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
 
       - name: Change DMG Name
         run: mv static/out/make/*.dmg static/out/make/logseq-darwin-x64-${{ github.event.inputs.tag-version }}.dmg

+ 3 - 0
externs.js

@@ -115,6 +115,9 @@ dummy.getPageView = function() {};
 dummy.convertToPdfPoint = function() {};
 dummy.scrollPageIntoView = function() {};
 dummy.convertToViewportRectangle = function() {};
+dummy.init = function() {};
+dummy.commit = function() {};
+dummy.raw = function() {};
 
 /**
  * @typedef {{

+ 1 - 1
package.json

@@ -81,7 +81,7 @@
         "ignore": "^5.1.8",
         "is-svg": "4.2.2",
         "jszip": "^3.5.0",
-        "mldoc": "0.9.9",
+        "mldoc": "1.0.2",
         "path": "^0.12.7",
         "pixi-graph-fork": "^0.1.4",
         "posthog-js": "^1.10.2",

+ 2 - 2
resources/forge.config.js

@@ -12,8 +12,8 @@ module.exports = {
       'signature-flags': 'library'
     },
     osxNotarize: {
-      appleId: "my-fake-apple-id",
-      appleIdPassword: "my-fake-apple-id-password",
+      appleId: process.env['APPLE_ID'],
+      appleIdPassword: process.env['APPLE_ID_PASSWORD'],
     },
   },
   makers: [

+ 4 - 3
resources/package.json

@@ -1,6 +1,6 @@
 {
   "name": "Logseq",
-  "version": "0.3.3",
+  "version": "0.3.5",
   "main": "electron.js",
   "author": "Logseq",
   "description": "A privacy-first, open-source platform for knowledge management and collaboration.",
@@ -27,7 +27,8 @@
     "node-fetch": "^2.6.1",
     "open": "^7.3.1",
     "semver": "^7.3.5",
-    "update-electron-app": "^2.0.1"
+    "update-electron-app": "^2.0.1",
+    "simple-git": "2.44.0"
   },
   "devDependencies": {
     "@electron-forge/cli": "^6.0.0-beta.57",
@@ -36,7 +37,7 @@
     "@electron-forge/maker-rpm": "^6.0.0-beta.57",
     "@electron-forge/maker-squirrel": "^6.0.0-beta.57",
     "@electron-forge/maker-zip": "^6.0.0-beta.57",
-    "electron": "^13.1.9",
+    "electron": "13.1.8",
     "electron-builder": "^22.11.7",
     "electron-forge-maker-appimage": "trusktr/electron-forge-maker-appimage#patch-1",
     "electron-rebuild": "^2.3.5"

+ 4 - 5
src/electron/electron/configs.cljs

@@ -8,7 +8,6 @@
 (defonce cfg-root (.getPath app "userData"))
 (defonce cfg-path (.join path cfg-root "configs.edn"))
 
-
 (defn- ensure-cfg
   []
   (try
@@ -17,7 +16,8 @@
       (let [body (.toString (.readFileSync fs cfg-path))]
         (if (seq body) (reader/read-string body) {})))
     (catch js/Error e
-      (js/console.error :cfg-error e))))
+      (js/console.error :cfg-error e)
+      {})))
 
 (defn- write-cfg!
   [cfg]
@@ -43,7 +43,6 @@
   (when-let [cfg (and k (ensure-cfg))]
     (get cfg k)))
 
-
-(defn whole
+(defn get-config
   []
-  (ensure-cfg))
+  (ensure-cfg))

+ 4 - 1
src/electron/electron/core.cljs

@@ -13,7 +13,8 @@
             ["electron" :refer [BrowserWindow app protocol ipcMain dialog Menu MenuItem] :as electron]
             ["electron-window-state" :as windowStateKeeper]
             [clojure.core.async :as async]
-            [electron.state :as state]))
+            [electron.state :as state]
+            [electron.git :as git]))
 
 (defonce LSP_SCHEME "lsp")
 (defonce LSP_PROTOCOL (str LSP_SCHEME "://"))
@@ -260,6 +261,8 @@
 
                (search/open-dbs!)
 
+               (git/auto-commit-current-graph!)
+
                (vreset! *setup-fn
                         (fn []
                           (let [t1 (setup-updater! win)

+ 115 - 0
src/electron/electron/git.cljs

@@ -0,0 +1,115 @@
+(ns electron.git
+  (:require ["child_process" :as child-process]
+            ["simple-git" :as simple-git]
+            [goog.object :as gobj]
+            [electron.state :as state]
+            [electron.utils :as utils]
+            [promesa.core :as p]
+            [clojure.string :as string]))
+
+(def spawn-sync (gobj/get child-process "spawnSync"))
+
+(defonce gits
+  (atom {}))
+
+(defn installed?
+  []
+  (let [command (spawn-sync "git"
+                            #js ["--version"]
+                            #js {:stdio "ignore"})]
+    (if-let [error (gobj/get command "error")]
+      (do
+        (js/console.error error)
+        false)
+      true)))
+
+(defn get-git
+  []
+  (when (installed?)
+    (when-let [path (:graph/current @state/state)]
+      (if-let [result (get @gits path)]
+        result
+        (let [result (simple-git path)]
+          (swap! gits assoc path result)
+          result)))))
+
+(defn init!
+  []
+  (when-let [git ^js (get-git)]
+    (.init git false)))
+
+(defn add-all!
+  ([]
+   (add-all! (get-git)))
+  ([^js git]
+   (when git
+     (.add git "./*" (fn [error] (js/console.error error))))))
+
+(defn add-all-and-commit!
+  ([]
+   (add-all-and-commit! "Auto saved by Logseq"))
+  ([message]
+   (when-let [git ^js (get-git)]
+     (p/let [_ (add-all! git)]
+       (.commit git message)))))
+
+(defonce quotes-regex #"\"[^\"]+\"")
+(defn wrapped-by-quotes?
+  [v]
+  (and (string? v) (>= (count v) 2) (= "\"" (first v) (last v))))
+
+(defn unquote-string
+  [v]
+  (string/trim (subs v 1 (dec (count v)))))
+
+(defn- split-args
+  [s]
+  (let [quotes (re-seq quotes-regex s)
+        non-quotes (string/split s quotes-regex)
+        col (if (seq quotes)
+              (concat (interleave non-quotes quotes)
+                      (drop (count quotes) non-quotes))
+              non-quotes)]
+    (->> col
+         (map (fn [s]
+                (if (wrapped-by-quotes? s)
+                  [(unquote-string s)]
+                  (string/split s #"\s"))))
+         (flatten)
+         (remove string/blank?))))
+
+(defn raw!
+  [args & {:keys [ok-handler error-handler]}]
+  (when-let [git ^js (get-git)]
+    (let [args (if (string? args)
+                 (split-args args)
+                 args)
+          ok-handler (if ok-handler
+                       ok-handler
+                       (fn [result]
+                         (utils/send-to-renderer "notification" {:type "success"
+                                                                 :payload result})))
+          error-handler (if error-handler
+                          error-handler
+                          (fn [error]
+                            (js/console.dir error)
+                            (utils/send-to-renderer "notification" {:type "error"
+                                                                    :payload (.toString error)})))]
+      (p/let [_ (when (= (first args) "commit")
+                  (add-all!))]
+        (->
+         (p/let [result (.raw git (clj->js args))]
+           (when ok-handler
+             (ok-handler result)))
+         (p/catch error-handler))))))
+
+(defn auto-commit-current-graph!
+  []
+  (when (and (installed?)
+             (not (state/git-auto-commit-disabled?)))
+    (state/clear-git-commit-interval!)
+    (p/let [_ (add-all-and-commit!)]
+      (let [seconds (state/get-git-commit-seconds)]
+        (when (int? seconds)
+          (let [interval (js/setInterval add-all-and-commit! (* seconds 1000))]
+            (state/set-git-commit-interval! interval)))))))

+ 19 - 6
src/electron/electron/handler.cljs

@@ -13,7 +13,8 @@
             [electron.utils :as utils]
             [electron.state :as state]
             [clojure.core.async :as async]
-            [electron.search :as search]))
+            [electron.search :as search]
+            [electron.git :as git]))
 
 (defmulti handle (fn [_window args] (keyword (first args))))
 
@@ -189,15 +190,27 @@
   (.quit app))
 
 (defmethod handle :userAppCfgs [_window [_ k v]]
-  (if-not k
-    (cfgs/whole)
-    (if-not (nil? v)
-      (cfgs/set-item! (keyword k) v)
-      (cfgs/get-item (keyword k)))))
+  (let [config (cfgs/get-config)]
+    (if-not k
+      config
+      (if-not (nil? v)
+        (cfgs/set-item! (keyword k) v)
+        (cfgs/get-item (keyword k))))))
 
 (defmethod handle :getDirname [_]
   js/__dirname)
 
+(defmethod handle :setCurrentGraph [_ [_ path]]
+  (let [path (when path (string/replace path "logseq_local_" ""))]
+    (swap! state/state assoc :graph/current path)))
+
+(defmethod handle :runGit [_ [_ args]]
+  (when (seq args)
+    (git/raw! args)))
+
+(defmethod handle :gitCommitAll [_]
+  (git/add-all-and-commit!))
+
 (defmethod handle :default [args]
   (println "Error: no ipc handler for: " (bean/->js args)))
 

+ 31 - 1
src/electron/electron/state.cljs

@@ -1,4 +1,34 @@
 (ns electron.state
-  (:require [clojure.core.async :as async]))
+  (:require [clojure.core.async :as async]
+            [electron.configs :as config]))
 
 (defonce persistent-dbs-chan (async/chan 1))
+
+(defonce state
+  (atom {:graph/current nil
+         :git/auto-commit-interval nil
+
+         :config (config/get-config)}))
+
+(defn set-state!
+  [path value]
+  (if (vector? path)
+    (swap! state assoc-in path value)
+    (swap! state assoc path value)))
+
+(defn set-git-commit-interval!
+  [v]
+  (set-state! :git/auto-commit-interval v))
+
+(defn clear-git-commit-interval!
+  []
+  (when-let [interval (get @state :git/auto-commit-interval)]
+    (js/clearInterval interval)))
+
+(defn get-git-commit-seconds
+  []
+  (get-in @state [:config :git/auto-commit-seconds] 60))
+
+(defn git-auto-commit-disabled?
+  []
+  (get-in @state [:config :git/disable-auto-commit?]))

+ 13 - 1
src/electron/electron/utils.cljs

@@ -2,7 +2,9 @@
   (:require [clojure.string :as string]
             ["fs" :as fs]
             ["path" :as path]
-            [clojure.string :as string]))
+            [clojure.string :as string]
+            [cljs-bean.core :as bean]
+            ["electron" :refer [BrowserWindow]]))
 
 (defonce mac? (= (.-platform js/process) "darwin"))
 (defonce win32? (= (.-platform js/process) "win32"))
@@ -45,3 +47,13 @@
   [path]
   (when (fs/existsSync path)
     (.toString (fs/readFileSync path))))
+
+(defn get-focused-window
+  []
+  (.getFocusedWindow BrowserWindow))
+
+(defn send-to-renderer
+  [kind payload]
+  (when-let [window (get-focused-window)]
+    (.. ^js window -webContents
+       (send kind (bean/->js payload)))))

+ 22 - 18
src/main/electron/listener.cljs

@@ -11,22 +11,6 @@
             [frontend.handler.metadata :as metadata-handler]
             [frontend.ui :as ui]))
 
-(defn listen-to-open-dir!
-  []
-  (js/window.apis.on "open-dir-confirmed"
-                     (fn []
-                       (state/set-loading-files! true)
-                       (when-not (state/home?)
-                         (route-handler/redirect-to-home!)))))
-
-(defn run-dirs-watcher!
-  []
-  ;; TODO: move "file-watcher" to electron.ipc.channels
-  (js/window.apis.on "file-watcher"
-                     (fn [data]
-                       (let [{:keys [type payload]} (bean/->clj data)]
-                         (watcher-handler/handle-changed! type payload)))))
-
 (defn listen-persistent-dbs!
   []
   ;; TODO: move "file-watcher" to electron.ipc.channels
@@ -54,8 +38,28 @@
             100))
          (ipc/ipc "persistent-dbs-saved"))))))
 
+(defn listen-to-electron!
+  []
+  (js/window.apis.on "open-dir-confirmed"
+                     (fn []
+                       (state/set-loading-files! true)
+                       (when-not (state/home?)
+                         (route-handler/redirect-to-home!))))
+
+  ;; TODO: move "file-watcher" to electron.ipc.channels
+  (js/window.apis.on "file-watcher"
+                     (fn [data]
+                       (let [{:keys [type payload]} (bean/->clj data)]
+                         (watcher-handler/handle-changed! type payload))))
+
+  (js/window.apis.on "notification"
+                     (fn [data]
+                       (let [{:keys [type payload]} (bean/->clj data)
+                             type (keyword type)
+                             comp [:div (str payload)]]
+                         (notification/show! comp type false)))))
+
 (defn listen!
   []
-  (listen-to-open-dir!)
-  (run-dirs-watcher!)
+  (listen-to-electron!)
   (listen-persistent-dbs!))

+ 1 - 1
src/main/frontend/commands.cljs

@@ -149,7 +149,7 @@
                  (util/format "\n#+END_%s" (string/upper-case type)))
          template (str
                    left
-                   (if optional (str " " optional) "")
+                   (if optional (str (if (= format :markdown) "" " ") optional) "")
                    "\n"
                    right)
          backward-pos (if (= type "src")

+ 47 - 41
src/main/frontend/components/block.cljs

@@ -589,6 +589,7 @@
 (declare block-content)
 (declare block-container)
 (declare block-parents)
+
 (rum/defc block-reference < rum/reactive
   [config id label]
   (when (and
@@ -600,43 +601,48 @@
           hl-type (get-in block [:block/properties :hl-type])
           repo (state/get-current-repo)]
       (if block
-        [:div.block-ref-wrap.inline
-
-         {:data-type    (name (or block-type :default))
-          :data-hl-type hl-type
-          :on-mouse-down
-          (fn [^js/MouseEvent e]
-            (when (or (gobj/get e "shiftKey")
-                      (not (.. e -target (closest ".blank"))))
-              (util/stop e)
+        (let [title (let [title (:block/title block)
+                          block-content (block-content (assoc config :block-ref? true)
+                                                       block nil (:block/uuid block)
+                                                       (:slide? config))
+                          class (if (seq title) "block-ref" "block-ref-no-title")]
+                      [:span {:class class}
+                       block-content])
+              inner (if label
+                      (->elem
+                       :span.block-ref
+                       (map-inline config label))
+                      title)]
+          [:div.block-ref-wrap.inline
+           {:data-type    (name (or block-type :default))
+            :data-hl-type hl-type
+            :on-mouse-down
+            (fn [^js/MouseEvent e]
+              (if (util/right-click? e)
+                (state/set-state! :block-ref/context {:block (:block config)
+                                                      :block-ref block-id})
+
+                (when (and
+                       (or (gobj/get e "shiftKey")
+                           (not (.. e -target (closest ".blank"))))
+                       (not (util/right-click? e)))
+                  (util/stop e)
+
+                  (if (gobj/get e "shiftKey")
+                    (state/sidebar-add-block!
+                     (state/get-current-repo)
+                     (:db/id block)
+                     :block-ref
+                     {:block block})
+
+                    (match [block-type (util/electron?)]
+                      ;; pdf annotation
+                      [:annotation true] (pdf-assets/open-block-ref! block)
+
+                      ;; default open block page
+                      :else (route-handler/redirect! {:to          :page
+                                                      :path-params {:name id}}))))))}
 
-              (if (gobj/get e "shiftKey")
-                (state/sidebar-add-block!
-                  (state/get-current-repo)
-                  (:db/id block)
-                  :block-ref
-                  {:block block})
-
-                (match [block-type (util/electron?)]
-                       ;; pdf annotation
-                       [:annotation true] (pdf-assets/open-block-ref! block)
-
-                       ;; default open block page
-                       :else (route-handler/redirect! {:to          :page
-                                                       :path-params {:name id}})))))}
-
-         (let [title (let [title (:block/title block)
-                           block-content (block-content (assoc config :block-ref? true)
-                                                        block nil (:block/uuid block)
-                                                        (:slide? config))
-                           class (if (seq title) "block-ref" "block-ref-no-title")]
-                       [:span {:class class}
-                        block-content])
-               inner (if label
-                       (->elem
-                        :span.block-ref
-                        (map-inline config label))
-                       title)]
            (if (and (not (util/mobile?)) (not (:preview? config)) (nil? block-type))
              (ui/tippy {:html        (fn []
                                        [:div.tippy-wrapper.overflow-y-auto.p-4
@@ -649,7 +655,7 @@
                                           (assoc config :id (str id) :preview? true))]])
                         :interactive true
                         :delay       [1000, 100]} inner)
-             inner))]
+             inner)])
         [:span.warning.mr-1 {:title "Block ref invalid"}
          (util/format "((%s))" id)]))))
 
@@ -804,10 +810,6 @@
       [:a {:href (str "mainto:" address)}
        address])
 
-    ;; ["Block_reference" id]
-    ;; ;; FIXME: alert when self block reference
-    ;; (block-reference (assoc config :reference? true) id nil)
-
     ["Nested_link" link]
     (nested-link config html-export? link)
 
@@ -2450,6 +2452,10 @@
                    :markdown)]
     (try
       (match item
+        ;; ["Drawer" name lines]
+        ;; [:div.drawer {:data-drawer-name name}
+        ;;  (apply str lines)]
+
         ["Properties" m]
         [:div.properties
          (for [[k v] (dissoc m :roam_alias :roam_tags)]

+ 21 - 4
src/main/frontend/components/block.css

@@ -154,10 +154,6 @@
   }
 }
 
-.block-ref-no-title {
-    cursor: alias;
-}
-
 .block-ref {
   border-bottom: 0.5px solid;
   border-bottom-color: var(--ls-block-ref-link-text-color);
@@ -174,6 +170,22 @@
   }
 }
 
+.block-ref-no-title {
+    border-bottom: 0.5px solid;
+    border-bottom-color: var(--ls-block-ref-link-text-color);
+    cursor: alias;
+    padding: 2px 5px;
+    display: block;
+
+    &:hover {
+        color: var(--ls-link-text-hover-color);
+    }
+
+    .block-content {
+        cursor: inherit;
+    }
+}
+
 .page-ref {
   color: var(--ls-link-ref-text-color);
 
@@ -304,6 +316,11 @@
   border-bottom: none;
 }
 
+.block-ref :is(h1, h2, h3, h4, h5, h6) {
+    border-bottom: none;
+    font-size: 1rem;
+}
+
 .document-mode .ls-block h1,
 .document-mode .editor-inner .h1 {
   margin: 0.67em 0;

+ 48 - 21
src/main/frontend/components/content.cljs

@@ -24,6 +24,7 @@
             [frontend.context.i18n :as i18n]
             [frontend.text :as text]
             [frontend.handler.page :as page-handler]
+            [frontend.handler.common :as common-handler]
             [frontend.extensions.srs :as srs]))
 
 (defn- set-format-js-loading!
@@ -242,6 +243,38 @@
                                false)))}
                "(Dev) Show block data"))]])))))
 
+(rum/defc block-ref-custom-context-menu-content
+  [block block-ref-id]
+  (when (and block block-ref-id)
+    [:div#custom-context-menu
+     [:div.py-1.rounded-md.bg-base-3.shadow-xs
+      (ui/menu-link
+       {:key "open-in-sidebar"
+        :on-click (fn []
+                    (let [block (db/pull [:block/uuid block-ref-id])]
+                      (state/sidebar-add-block!
+                       (state/get-current-repo)
+                       block-ref-id
+                       :block-ref
+                       {:block block}))                    )}
+       "Open in sidebar")
+      (ui/menu-link
+       {:key "copy"
+        :on-click (fn [] (editor-handler/copy-current-ref block-ref-id))}
+       "Copy this reference")
+      (ui/menu-link
+       {:key "delete"
+        :on-click (fn [] (editor-handler/delete-current-ref! block block-ref-id))}
+       "Delete this reference")
+      (ui/menu-link
+       {:key "replace-with-text"
+        :on-click (fn [] (editor-handler/replace-ref-with-text! block block-ref-id))}
+       "Replace with text")
+      (ui/menu-link
+       {:key "replace-with-embed"
+        :on-click (fn [] (editor-handler/replace-ref-with-embed! block block-ref-id))}
+       "Replace with embed")]]))
+
 ;; TODO: content could be changed
 ;; Also, keyboard bindings should only be activated after
 ;; blocks were already selected.
@@ -264,31 +297,25 @@
      (mixins/listen state js/window "contextmenu"
                     (fn [e]
                       (let [target (gobj/get e "target")
-                            block-id (d/attr target "blockid")]
+                            block-id (d/attr target "blockid")
+                            {:keys [block block-ref]} (state/sub :block-ref/context)]
                         (cond
-                          (state/selection?)
+                          block-ref
                           (do
-                            (util/stop e)
-                            (let [client-x (gobj/get e "clientX")
-                                  client-y (gobj/get e "clientY")
-                                  scroll-y (util/cur-doc-top)]
-                              (state/show-custom-context-menu! (custom-context-menu-content))
-                              (when-let [context-menu (d/by-id "custom-context-menu")]
-                                (d/set-style! context-menu
-                                              :left (str client-x "px")
-                                              :top (str (+ scroll-y client-y) "px")))))
+                            (common-handler/show-custom-context-menu!
+                            e
+                            (block-ref-custom-context-menu-content block block-ref))
+                            (state/set-state! :block-ref/context nil))
+
+                          (state/selection?)
+                          (common-handler/show-custom-context-menu!
+                           e
+                           (custom-context-menu-content))
 
                           (and block-id (util/uuid-string? block-id))
-                          (do
-                            (util/stop e)
-                            (let [client-x (gobj/get e "clientX")
-                                  client-y (gobj/get e "clientY")
-                                  scroll-y (util/cur-doc-top)]
-                              (state/show-custom-context-menu! (block-context-menu-content target (cljs.core/uuid block-id)))
-                              (when-let [context-menu (d/by-id "custom-context-menu")]
-                                (d/set-style! context-menu
-                                              :left (str client-x "px")
-                                              :top (str (+ scroll-y client-y) "px")))))
+                          (common-handler/show-custom-context-menu!
+                           e
+                           (block-context-menu-content target (cljs.core/uuid block-id)))
 
                           :else
                           nil))))))

+ 7 - 13
src/main/frontend/components/export.cljs

@@ -76,8 +76,8 @@
   [state root-block-ids]
   (let [current-repo (state/get-current-repo)
         type (rum/react *export-block-type)
-        text-indent-style (rum/react (state/get-export-block-text-indent-style))
-        text-remove-options (rum/react (state/get-export-block-text-remove-options))
+        text-indent-style (state/sub :copy/export-block-text-indent-style)
+        text-remove-options (state/sub :copy/export-block-text-remove-options)
         copied? (::copied? state)
         content
         (case type
@@ -112,7 +112,7 @@
                             :visibility (if (= :text type) "visible" "hidden")}
                 :on-change (fn [e]
                              (let [value (util/evalue e)]
-                               (reset! (state/get-export-block-text-indent-style) value)))}
+                               (state/set-export-block-text-indent-style! value)))}
                (for [{:keys [label value selected]} options]
                  [:option (cond->
                            {:key   label
@@ -124,11 +124,8 @@
          (ui/checkbox {:style {:margin-right 6
                                :visibility (if (= :text type) "visible" "hidden")}
                        :checked (contains? text-remove-options :page-ref)
-                       :on-change (fn [e] (if (util/echecked? e)
-                                            (swap! (state/get-export-block-text-remove-options)
-                                                   #(conj % :page-ref))
-                                            (swap! (state/get-export-block-text-remove-options)
-                                                   #(disj % :page-ref))))})
+                       :on-change (fn [e]
+                                    (state/update-export-block-text-remove-options! e :page-ref))})
 
          [:div
           {:style {:visibility (if (= :text type) "visible" "hidden")}}
@@ -137,11 +134,8 @@
                                :margin-left 10
                                :visibility (if (= :text type) "visible" "hidden")}
                        :checked (contains? text-remove-options :emphasis)
-                       :on-change (fn [e] (if (util/echecked? e)
-                                            (swap! (state/get-export-block-text-remove-options)
-                                                   #(conj % :emphasis))
-                                            (swap! (state/get-export-block-text-remove-options)
-                                                   #(disj % :emphasis))))})
+                       :on-change (fn [e]
+                                    (state/update-export-block-text-remove-options! e :emphasis))})
 
          [:div
           {:style {:visibility (if (= :text type) "visible" "hidden")}}

+ 92 - 57
src/main/frontend/components/settings.cljs

@@ -128,21 +128,21 @@
 (rum/defc delete-account-confirm
   [close-fn]
   (rum/with-context [[t] i18n/*tongue-context*]
-                    [:div
-                     (ui/admonition
-                       :important
-                       [:p.text-gray-700 (t :user/delete-account-notice)])
-                     [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
-                      [:span.flex.w-full.rounded-md.sm:ml-3.sm:w-auto
-                       [:button.inline-flex.justify-center.w-full.rounded-md.border.border-transparent.px-4.py-2.bg-indigo-600.text-base.leading-6.font-medium.text-white.shadow-sm.hover:bg-indigo-500.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
-                        {:type     "button"
-                         :on-click user-handler/delete-account!}
-                        (t :user/delete-account)]]
-                      [:span.mt-3.flex.w-full.rounded-md.sm:mt-0.sm:w-auto
-                       [:button.inline-flex.justify-center.w-full.rounded-md.border.border-gray-300.px-4.py-2.bg-white.text-base.leading-6.font-medium.text-gray-700.shadow-sm.hover:text-gray-500.focus:outline-none.focus:border-blue-300.focus:shadow-outline-blue.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
-                        {:type     "button"
-                         :on-click close-fn}
-                        "Cancel"]]]]))
+    [:div
+     (ui/admonition
+      :important
+      [:p.text-gray-700 (t :user/delete-account-notice)])
+     [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
+      [:span.flex.w-full.rounded-md.sm:ml-3.sm:w-auto
+       [:button.inline-flex.justify-center.w-full.rounded-md.border.border-transparent.px-4.py-2.bg-indigo-600.text-base.leading-6.font-medium.text-white.shadow-sm.hover:bg-indigo-500.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
+        {:type     "button"
+         :on-click user-handler/delete-account!}
+        (t :user/delete-account)]]
+      [:span.mt-3.flex.w-full.rounded-md.sm:mt-0.sm:w-auto
+       [:button.inline-flex.justify-center.w-full.rounded-md.border.border-gray-300.px-4.py-2.bg-white.text-base.leading-6.font-medium.text-gray-700.shadow-sm.hover:text-gray-500.focus:outline-none.focus:border-blue-300.focus:shadow-outline-blue.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
+        {:type     "button"
+         :on-click close-fn}
+        "Cancel"]]]]))
 
 (rum/defc outdenting-hint
   []
@@ -160,10 +160,10 @@
 
 (defn edit-config-edn []
   (rum/with-context [[t] i18n/*tongue-context*]
-                    [:div.text-sm
-                     [:a.text-xs {:href     (rfe/href :file {:path (config/get-config-path)})
-                                  :on-click #(js/setTimeout (fn [] (ui-handler/toggle-settings-modal!)))}
-                      (t :settings-page/edit-config-edn)]]))
+    [:div.text-sm
+     [:a.text-xs {:href     (rfe/href :file {:path (config/get-config-path)})
+                  :on-click #(js/setTimeout (fn [] (ui-handler/toggle-settings-modal!)))}
+      (t :settings-page/edit-config-edn)]]))
 
 (defn show-brackets-row [t show-brackets?]
   [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
@@ -178,12 +178,7 @@
    [:div {:style {:text-align "right"}}
     (ui/keyboard-shortcut (shortcut-helper/gen-shortcut-seq :ui/toggle-brackets))]])
 
-(rum/defcs switch-spell-check-row
-  < {:will-mount
-     (fn [state]
-       (state/load-app-user-cfgs)
-       state)}
-    rum/reactive
+(rum/defcs switch-spell-check-row < rum/reactive
   [state t]
   (let [enabled? (state/sub [:electron/user-cfgs :spell-check])
         enabled? (if (nil? enabled?) true enabled?)]
@@ -194,21 +189,48 @@
      [:div
       [:div.rounded-md.sm:max-w-xs
        (ui/toggle
-         enabled?
-         (fn []
-           (state/set-state! [:electron/user-cfgs :spell-check] (not enabled?))
-           (p/then (ipc/ipc "userAppCfgs" :spell-check (not enabled?))
-                   #(if (js/confirm (t :relaunch-confirm-to-work))
-                      (js/logseq.api.relaunch))))
-         true)]]]))
-
-(rum/defcs app-auto-update-row
-  < {:will-mount
-     (fn [state]
-       (state/load-app-user-cfgs)
-       state)}
-    rum/reactive
+        enabled?
+        (fn []
+          (state/set-state! [:electron/user-cfgs :spell-check] (not enabled?))
+          (p/then (ipc/ipc "userAppCfgs" :spell-check (not enabled?))
+                  #(if (js/confirm (t :relaunch-confirm-to-work))
+                     (js/logseq.api.relaunch))))
+        true)]]]))
+
+(rum/defcs switch-git-auto-commit-row < rum/reactive
+  [state t]
+  (let [enabled? (not (state/sub [:electron/user-cfgs :git/disable-auto-commit?]))]
+    [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
+     [:label.block.text-sm.font-medium.leading-5.opacity-70
+      "Git auto commit"]
+     [:div
+      [:div.rounded-md.sm:max-w-xs
+       (ui/toggle
+        enabled?
+        (fn []
+          (state/set-state! [:electron/user-cfgs :git/disable-auto-commit?] enabled?)
+          (ipc/ipc "userAppCfgs" :git/disable-auto-commit? enabled?))
+        true)]]]))
+
+(rum/defcs git-auto-commit-seconds < rum/reactive
   [state t]
+  (let [secs (or (state/sub [:electron/user-cfgs :git/auto-commit-seconds]) 60)]
+    [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
+     [:label.block.text-sm.font-medium.leading-5.opacity-70
+      "Git auto commit seconds"]
+     [:div.mt-1.sm:mt-0.sm:col-span-2
+      [:div.max-w-lg.rounded-md.sm:max-w-xs
+       [:input#home-default-page.form-input.is-small.transition.duration-150.ease-in-out
+        {:default-value secs
+         :on-blur       (fn [event]
+                          (when-let [value (-> (util/evalue event)
+                                               util/safe-parse-int)]
+                            (when (< 0 value (inc 600))
+                              (state/set-state! [:electron/user-cfgs :git/auto-commit-seconds] value)
+                              (ipc/ipc "userAppCfgs" :git/auto-commit-seconds value))))}]]]]))
+
+(rum/defc app-auto-update-row < rum/reactive
+  [t]
   (let [enabled? (state/sub [:electron/user-cfgs :auto-update])
         enabled? (if (nil? enabled?) true enabled?)]
 
@@ -218,11 +240,11 @@
      [:div
       [:div.rounded-md.sm:max-w-xs
        (ui/toggle
-         enabled?
-         (fn []
-           (state/set-state! [:electron/user-cfgs :auto-update] (not enabled?))
-           (ipc/ipc "userAppCfgs" :auto-update (not enabled?)))
-         true)]]]))
+        enabled?
+        (fn []
+          (state/set-state! [:electron/user-cfgs :auto-update] (not enabled?))
+          (ipc/ipc "userAppCfgs" :auto-update (not enabled?)))
+        true)]]]))
 
 (rum/defcs current-graph
   [state t]
@@ -314,8 +336,8 @@
                       (when-not (string/blank? format)
                         (config-handler/set-config! :journal/page-title-format format)
                         (notification/show!
-                          [:div "You need to re-index your graph to make the change works"]
-                          :success)
+                         [:div "You need to re-index your graph to make the change works"]
+                         :success)
                         (state/close-modal!)
                         (route-handler/redirect! {:to :repos}))))}
       (for [format (sort (date/journal-title-formatters))]
@@ -478,7 +500,7 @@
           (t :settings-page/disable-sentry)
           (not instrument-disabled?)
           (fn [] (instrument/disable-instrument
-                   (not instrument-disabled?)))
+                 (not instrument-disabled?)))
           [:span.text-sm.opacity-50 "Logseq will never collect your local graph database or sell your data."]))
 
 (defn clear-cache-row [t]
@@ -516,9 +538,12 @@
 
 (rum/defcs settings
   < (rum/local :general ::active)
-    rum/reactive
+  {:will-mount
+   (fn [state]
+     (state/load-app-user-cfgs)
+     state)}
+  rum/reactive
   [state]
-
   (let [preferred-format (state/get-preferred-format)
         preferred-date-format (state/get-date-formatter)
         preferred-workflow (state/get-preferred-workflow)
@@ -558,6 +583,7 @@
           (for [[label text icon] [[:general (t :settings-page/tab-general) (svg/adjustments 16)]
                                    [:editor (t :settings-page/tab-editor) (svg/icon-editor 16)]
                                    [:shortcuts (t :settings-page/tab-shortcuts) (svg/icon-cmd 18)]
+                                   [:git (t :settings-page/tab-version-control) svg/git]
                                    [:advanced (t :settings-page/tab-advanced) (svg/icon-cli 16)]]]
 
             [:li
@@ -599,6 +625,15 @@
            [:div.panel-wrap
             (keyboard-shortcuts-row t)]
 
+           :git
+           [:div.panel-wrap
+            (switch-git-auto-commit-row t)
+            (git-auto-commit-seconds t)
+
+            (ui/admonition
+             :warning
+             [:p "You need to restart the app after updating the settings."])]
+
            :advanced
            [:div.panel-wrap.is-advanced
             (when (and util/mac? (util/electron?)) (app-auto-update-row t))
@@ -627,11 +662,11 @@
                                            (user-handler/set-cors! server)
                                            (notification/show! "Custom CORS proxy updated successfully!" :success)))))}]]]]
                (ui/admonition
-                 :important
-                 [:p (t :settings-page/dont-use-other-peoples-proxy-servers)
-                  [:a {:href   "https://github.com/isomorphic-git/cors-proxy"
-                       :target "_blank"}
-                   "https://github.com/isomorphic-git/cors-proxy"]])])
+                :important
+                [:p (t :settings-page/dont-use-other-peoples-proxy-servers)
+                 [:a {:href   "https://github.com/isomorphic-git/cors-proxy"
+                      :target "_blank"}
+                  "https://github.com/isomorphic-git/cors-proxy"]])])
 
             (when logged?
               [:div
@@ -643,8 +678,8 @@
                 [:div.mt-1.sm:mt-0.sm:col-span-2
                  [:div.max-w-lg.rounded-md.sm:max-w-xs
                   (ui/button (t :user/delete-your-account)
-                             :on-click (fn []
-                                         (ui-handler/toggle-settings-modal!)
-                                         (js/setTimeout #(state/set-modal! delete-account-confirm))))]]]])]
+                    :on-click (fn []
+                                (ui-handler/toggle-settings-modal!)
+                                (js/setTimeout #(state/set-modal! delete-account-confirm))))]]]])]
 
            nil)]]])))

+ 10 - 0
src/main/frontend/components/settings.css

@@ -242,3 +242,13 @@
     }
   }
 }
+
+
+svg.git {
+    margin-left: -4px;
+    transform: scale(0.9);
+}
+
+svg.cmd {
+    margin-left: -1px;
+}

+ 37 - 0
src/main/frontend/components/shell.cljs

@@ -0,0 +1,37 @@
+(ns frontend.components.shell
+  (:require [rum.core :as rum]
+            [frontend.ui :as ui]
+            [frontend.util :as util]
+            [frontend.handler.shell :as shell-handler]
+            [clojure.string :as string]
+            [frontend.mixins :as mixins]))
+
+(defn- run-command
+  [command]
+  (when-not (string/blank? @command)
+    (shell-handler/run-command! @command)))
+
+(defonce command (atom ""))
+
+
+(rum/defcs shell < rum/reactive
+  (mixins/event-mixin
+   (fn [state]
+     (mixins/on-enter state
+                      :on-enter (fn [state]
+                                  (run-command command)))))
+  [state]
+  [:div.flex.flex-col
+   [:div.w-full.mx-auto.sm:max-w-lg.sm:w-96
+    [:div
+     [:div
+      [:h1.title
+       "Input command"]
+      [:div.mt-4.mb-4.relative.rounded-md.shadow-sm.max-w-xs
+       [:input#run-command.form-input.block.w-full.sm:text-sm.sm:leading-5
+        {:autoFocus true
+         :on-key-down util/stop-propagation
+         :placeholder "git ..."
+         :on-change (fn [e]
+                      (reset! command (util/evalue e)))}]]]]
+    (ui/button "Run" :on-click #(run-command command))]])

+ 5 - 2
src/main/frontend/components/svg.cljs

@@ -567,6 +567,9 @@
             :height  "20"} opts)
     [:path {:d "M512 12.63616c-282.74688 0-512 229.21216-512 512 0 226.22208 146.69824 418.14016 350.12608 485.82656 25.57952 4.73088 35.00032-11.10016 35.00032-24.63744 0-12.20608-0.47104-52.55168-0.69632-95.31392-142.4384 30.96576-172.50304-60.416-172.50304-60.416-23.28576-59.16672-56.85248-74.91584-56.85248-74.91584-46.44864-31.78496 3.50208-31.1296 3.50208-31.1296 51.4048 3.60448 78.47936 52.75648 78.47936 52.75648 45.6704 78.27456 119.76704 55.64416 149.01248 42.55744 4.58752-33.09568 17.85856-55.68512 32.50176-68.46464-113.72544-12.94336-233.2672-56.85248-233.2672-253.0304 0-55.88992 20.00896-101.5808 52.75648-137.4208-5.3248-12.9024-22.85568-64.96256 4.95616-135.49568 0 0 43.008-13.74208 140.84096 52.49024 40.83712-11.34592 84.64384-17.03936 128.16384-17.24416 43.49952 0.2048 87.32672 5.87776 128.24576 17.24416 97.73056-66.2528 140.65664-52.49024 140.65664-52.49024 27.87328 70.53312 10.3424 122.59328 5.03808 135.49568 32.82944 35.86048 52.69504 81.53088 52.69504 137.4208 0 196.64896-119.78752 239.94368-233.79968 252.6208 18.37056 15.89248 34.73408 47.04256 34.73408 94.80192 0 68.5056-0.59392 123.63776-0.59392 140.51328 0 13.6192 9.216 29.5936 35.16416 24.576 203.32544-67.76832 349.83936-259.62496 349.83936-485.76512 0-282.78784-229.23264-512-512-512z"}]]))
 
+(def git
+  [:svg.icon.git {:width 24 :height 24 :viewbox "0 0 24 24" :stroke-width "2" :stroke "currentColor" :fill "none" :stroke-linecap "round" :stroke-linejoin "round"} [:path {:stroke "none" :d "M0 0h24v24H0z" :fill "none"}] [:circle {:cx "12" :cy "18" :r "2"}] [:circle {:cx "7" :cy "6" :r "2"}] [:circle {:cx "17" :cy "6" :r "2"}] [:path {:d "M7 8v2a2 2 0 0 0 2 2h6a2 2 0 0 0 2 -2v-2"}] [:line {:x1 "12" :y1 "12" :x2 "12" :y2 "16"}]])
+
 
 (defn info []
   [:svg {:class "info" :view-box "0 0 16 16" :width "16px" :height "16px"}
@@ -622,7 +625,7 @@
 (defn icon-cmd
   ([] (icon-cmd 16))
   ([size]
-   [:svg {:viewBox "0 0 1024 1024" :width size :height size}
+   [:svg.cmd {:viewBox "0 0 1024 1024" :width size :height size}
     [:path {:d "M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32z m-40 728H184V184h656v656z" :fill "currentColor"}]
     [:path {:d "M370.8 554.4c-54.6 0-98.8 44.2-98.8 98.8s44.2 98.8 98.8 98.8 98.8-44.2 98.8-98.8v-42.4h84.7v42.4c0 54.6 44.2 98.8 98.8 98.8s98.8-44.2 98.8-98.8-44.2-98.8-98.8-98.8h-42.4v-84.7h42.4c54.6 0 98.8-44.2 98.8-98.8 0-54.6-44.2-98.8-98.8-98.8s-98.8 44.2-98.8 98.8v42.4h-84.7v-42.4c0-54.6-44.2-98.8-98.8-98.8S272 316.2 272 370.8s44.2 98.8 98.8 98.8h42.4v84.7h-42.4z m42.4 98.8c0 23.4-19 42.4-42.4 42.4s-42.4-19-42.4-42.4 19-42.4 42.4-42.4h42.4v42.4z m197.6-282.4c0-23.4 19-42.4 42.4-42.4s42.4 19 42.4 42.4-19 42.4-42.4 42.4h-42.4v-42.4z m0 240h42.4c23.4 0 42.4 19 42.4 42.4s-19 42.4-42.4 42.4-42.4-19-42.4-42.4v-42.4zM469.6 469.6h84.7v84.7h-84.7v-84.7z m-98.8-56.4c-23.4 0-42.4-19-42.4-42.4s19-42.4 42.4-42.4 42.4 19 42.4 42.4v42.4h-42.4z" :fill "currentColor"}]]))
 
@@ -657,4 +660,4 @@
     [:path {:stroke-linecap "round" :stroke-linejoin "round" :stroke-width "2" :d "M5 13l4 4L19 7"}]]))
 
 (def arrow-expand
-  (hero-icon "M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"))
+  (hero-icon "M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"))

+ 6 - 0
src/main/frontend/db/model.cljs

@@ -1211,6 +1211,12 @@
          (reset! blocks-count-cache n)
          n)))))
 
+(defn get-all-block-uuids
+  []
+  (when-let [conn (conn/get-conn)]
+    (->> (d/datoms conn :avet :block/uuid)
+         (map :v))))
+
 ;; block/uuid and block/content
 (defn get-all-block-contents
   []

+ 16 - 14
src/main/frontend/db/query_react.cljs

@@ -14,6 +14,7 @@
             [frontend.util :as util]
             [frontend.db.react :as react]
             [frontend.text :as text]
+            [frontend.config :as config]
             [clojure.walk :as walk]))
 
 (defn- resolve-input
@@ -117,17 +118,18 @@
 
 (defn react-query
   [repo {:keys [query inputs] :as query'} query-opts]
-  (debug/pprint "================")
-  (debug/pprint "Use the following to debug your datalog queries:")
-  (debug/pprint query')
-  (try
-    (let [query (resolve-query query)
-          inputs (map resolve-input inputs)
-          repo (or repo (state/get-current-repo))
-          k [:custom query']]
-      (debug/pprint "inputs (post-resolution):" inputs)
-      (debug/pprint "query-opts:" query-opts)
-      (apply react/q repo k query-opts query inputs))
-    (catch js/Error e
-      (debug/pprint "Custom query failed: " {:query query'})
-      (js/console.dir e))))
+  (let [pprint (if config/dev? (fn [_] nil) debug/pprint)]
+    (pprint "================")
+    (pprint "Use the following to debug your datalog queries:")
+    (pprint query')
+    (try
+      (let [query (resolve-query query)
+            inputs (map resolve-input inputs)
+            repo (or repo (state/get-current-repo))
+            k [:custom query']]
+        (pprint "inputs (post-resolution):" inputs)
+        (pprint "query-opts:" query-opts)
+        (apply react/q repo k query-opts query inputs))
+      (catch js/Error e
+        (pprint "Custom query failed: " {:query query'})
+        (js/console.dir e)))))

+ 2 - 0
src/main/frontend/dicts.cljs

@@ -237,6 +237,7 @@
         :settings-page/tab-general "General"
         :settings-page/tab-editor "Editor"
         :settings-page/tab-shortcuts "Shortcuts"
+        :settings-page/tab-version-control "Git"
         :settings-page/tab-advanced "Advanced"
         :logseq "Logseq"
         :on "ON"
@@ -918,6 +919,7 @@
            :settings-page/tab-editor "编辑器"
            :settings-page/tab-shortcuts "快捷键"
            :settings-page/tab-advanced "高级设置"
+           :settings-page/tab-version-control "多版本控制"
            :logseq "Logseq"
            :on "已打开"
            :more-options "更多选项"

+ 5 - 1
src/main/frontend/extensions/code.cljs

@@ -44,7 +44,11 @@
             ["codemirror/mode/smalltalk/smalltalk"]
             ["codemirror/mode/sql/sql"]
             ["codemirror/mode/swift/swift"]
-            ["codemirror/mode/xml/xml"]))
+            ["codemirror/mode/xml/xml"]
+            ["codemirror/mode/sparql/sparql"]
+            ["codemirror/mode/turtle/turtle"]
+            ["codemirror/mode/r/r"]
+            ["codemirror/mode/julia/julia"]))
 
 ;; codemirror
 

+ 4 - 4
src/main/frontend/extensions/pdf/assets.cljs

@@ -38,10 +38,10 @@
               full-path
 
               :else
-              (util/node-path.join
-                "file://"                                   ;; TODO: bfs
-                (config/get-repo-dir (state/get-current-repo))
-                "assets" filename))]
+              (str "file://"                                ;; TODO: bfs
+                   (util/node-path.join
+                     (config/get-repo-dir (state/get-current-repo))
+                     "assets" filename)))]
     (when-let [key
                (if web-link?
                  (str (hash url))

+ 41 - 37
src/main/frontend/extensions/srs.cljs

@@ -14,6 +14,7 @@
             [frontend.ui :as ui]
             [frontend.date :as date]
             [frontend.commands :as commands]
+            [frontend.components.editor :as editor]
             [cljs-time.core :as t]
             [cljs-time.local :as tl]
             [cljs-time.coerce :as tc]
@@ -261,23 +262,21 @@
 
 (defn- query-scheduled
   "Return blocks scheduled to 'time' or before"
-  [repo {query-string :query-string query-result :query-result} time]
-  (when-let [*blocks (or query-result (and query-string (query repo query-string)))]
-    (when-let [blocks @*blocks]
-      (let [filtered-result (->>
-                             (flatten blocks)
-                             (filterv (fn [b]
-                                        (let [props (:block/properties b)
-                                              next-sched (get props card-next-schedule-property)
-                                              next-sched* (tc/from-string next-sched)
-                                              repeats (get props card-repeats-property)]
-                                          (or (nil? repeats)
-                                              (< repeats 1)
-                                              (nil? next-sched)
-                                              (nil? next-sched*)
-                                              (t/before? next-sched* time))))))]
-        {:total (count blocks)
-         :result filtered-result}))))
+  [repo blocks time]
+  (let [filtered-result (->>
+                         (flatten blocks)
+                         (filterv (fn [b]
+                                    (let [props (:block/properties b)
+                                          next-sched (get props card-next-schedule-property)
+                                          next-sched* (tc/from-string next-sched)
+                                          repeats (get props card-repeats-property)]
+                                      (or (nil? repeats)
+                                          (< repeats 1)
+                                          (nil? next-sched)
+                                          (nil? next-sched*)
+                                          (t/before? next-sched* time))))))]
+    {:total (count blocks)
+     :result filtered-result}))
 
 
 ;;; ================================================================
@@ -373,10 +372,10 @@
              :disabled false}
             (svg/info)))
 
-(defn- score-and-next-card [score card *card-index *cards *phase *review-records cb]
+(defn- score-and-next-card [score card *card-index cards *phase *review-records cb]
   (operation-score! card score)
   (swap! *review-records #(update % score (fn [ov] (conj ov card))))
-  (if (>= (inc @*card-index) (count @*cards))
+  (if (>= (inc @*card-index) (count cards))
     (when cb
       (swap! *card-index inc)
       (cb @*review-records))
@@ -384,10 +383,10 @@
       (swap! *card-index inc)
       (reset! *phase 1))))
 
-(defn- skip-card [card *card-index *cards *phase *review-records cb]
+(defn- skip-card [card *card-index cards *phase *review-records cb]
   (swap! *review-records #(update % "skip" (fn [ov] (conj ov card))))
   (swap! *card-index inc)
-  (if (>= (inc @*card-index) (count @*cards))
+  (if (>= (inc @*card-index) (count cards))
     (and cb (cb @*review-records))
     (reset! *phase 1)))
 
@@ -398,16 +397,14 @@
   < rum/reactive
   (rum/local 1 ::phase)
   (rum/local 0 ::card-index)
-  (rum/local nil ::cards)
   (rum/local {} ::review-records)
-  [state cards {preview? :preview?
+  [state blocks {preview? :preview?
                 modal? :modal?
                 cb :callback}]
-  (let [cards* (::cards state)
-        _ (when (nil? @cards*) (reset! cards* cards))
+  (let [cards (map ->card blocks)
         review-records (::review-records state)
         card-index (::card-index state)
-        card (util/nth-safe @cards* @card-index)]
+        card (util/nth-safe cards @card-index)]
     (if-not card
       review-finished
       (let [phase (::phase state)
@@ -417,10 +414,16 @@
         [:div.ls-card
          {:class (if (or preview? modal?)
                    (util/hiccup->class ".flex.flex-col.resize.overflow-y-auto.px-4"))}
+         (let [repo (state/get-current-repo)]
+           [:div.my-2.opacity-70.hover:opacity-100
+            (component-block/block-parents {} repo root-block-id
+                                           (:block/format root-block)
+                                           true)])
          (component-block/blocks-container
           blocks
           (merge (show-cycle-config card @phase)
-                 {:id (str root-block-id)}))
+                 {:id (str root-block-id)
+                  :editor-box editor/box}))
          (if (or preview? modal?)
            [:div.flex.my-4.justify-between
             [:div.flex-1
@@ -439,7 +442,7 @@
                  :id "card-next"
                  :small? true
                  :class "mr-2"
-                 :on-click #(skip-card card card-index cards* phase review-records cb)))
+                 :on-click #(skip-card card card-index cards phase review-records cb)))
 
              (when (and (not preview?) (= 1 next-phase))
                (let [interval-days-score-3 (get (get-next-interval card 3) card-last-interval-property)
@@ -450,20 +453,20 @@
                     :id "card-forgotten"
                     :small? true
                     :on-click (fn []
-                                (score-and-next-card 1 card card-index cards* phase review-records cb)
+                                (score-and-next-card 1 card card-index cards phase review-records cb)
                                 (let [tomorrow (tc/to-string (t/plus (t/today) (t/days 1)))]
                                   (editor-handler/set-block-property! root-block-id card-next-schedule-property tomorrow))))
 
                   (ui/button "Remembered(r)"
                     :id "card-remembered"
                     :small? true
-                    :on-click #(score-and-next-card 5 card card-index cards* phase review-records cb))
+                    :on-click #(score-and-next-card 5 card card-index cards phase review-records cb))
 
                   (ui/button "Take a while to recall(t)"
                     :id "card-recall"
                     :class (util/hiccup->class "opacity-60.hover:opacity-100")
                     :small? true
-                    :on-click #(score-and-next-card 3 card card-index cards* phase review-records cb))]))]
+                    :on-click #(score-and-next-card 3 card card-index cards phase review-records cb))]))]
 
             (when preview?
               (ui/tippy {:html [:div.text-sm
@@ -481,12 +484,12 @@
 
 (rum/defc view-modal <
   (shortcut/mixin :shortcut.handler/cards)
-  [cards option]
-  (view cards option))
+  [blocks option]
+  (view blocks option))
 
 (defn preview
   [blocks]
-  (state/set-modal! #(view (mapv ->card blocks) {:preview? true})))
+  (state/set-modal! #(view blocks {:preview? true})))
 
 
 ;;; ================================================================
@@ -519,8 +522,9 @@
   (let [repo (state/get-current-repo)
         query-string (string/join ", " (:arguments options))]
     (if-let [*query-result (query repo query-string)]
-      (let [{:keys [total result]} (query-scheduled repo {:query-result *query-result} (tl/local-now))
-            review-cards (mapv ->card result)
+      (let [blocks (rum/react *query-result)
+            {:keys [total result]} (query-scheduled repo blocks (tl/local-now))
+            review-cards result
             query-string (if (string/blank? query-string) "All" query-string)
             card-query-block (db/entity [:block/uuid (:block/uuid config)])
             filtered-total (count result)
@@ -553,7 +557,7 @@
                {:on-click (fn [_]
                             (let [all-blocks (flatten @(query (state/get-current-repo) query-string))]
                               (when (> (count all-blocks) 0)
-                                (let [review-cards (mapv ->card all-blocks)]
+                                (let [review-cards all-blocks]
                                   (state/set-modal! #(view-modal
                                                       review-cards
                                                       {:preview? true

+ 203 - 85
src/main/frontend/extensions/zotero.cljs

@@ -1,5 +1,5 @@
 (ns frontend.extensions.zotero
-  (:require [cljs.core.async :refer [<! >! go chan go-loop] :as a]
+  (:require [cljs.core.async :refer [<! >! chan go go-loop] :as a]
             [clojure.edn :refer [read-string]]
             [clojure.string :as str]
             [frontend.components.svg :as svg]
@@ -13,6 +13,7 @@
             [frontend.ui :as ui]
             [frontend.util :as util]
             [goog.dom :as gdom]
+            [promesa.core :as p]
             [rum.core :as rum]))
 
 (def term-chan (chan))
@@ -116,29 +117,11 @@
            (set! (.-scrollTop (.-parentNode (gdom/getElement "zotero-search"))) 0)
            (go (<! (search-fn prev-search-term next-page))))))]]))
 
-
-(rum/defcs settings
-  <
+(rum/defcs user-or-group-setting <
   (rum/local (setting/setting :type-id) ::type-id)
-  (rum/local nil ::progress)
-  (rum/local false ::total)
-  (rum/local "Add all" ::fetching-button)
   rum/reactive
   [state]
-  [:div.zotero-settings
-   [:h1.mb-4.text-4xl.font-bold.mb-8 "Zotero Settings"]
-
-   [:div.row
-    [:label.title
-     {:for "zotero_api_key"}
-     "Zotero API key"]
-    [:div.mt-1.sm:mt-0.sm:col-span-2
-     [:div.max-w-lg.rounded-md
-      [:input.form-input.block
-       {:default-value (setting/api-key)
-        :placeholder   "Please enter your Zotero API key"
-        :on-blur       (fn [e] (setting/set-api-key (util/evalue e)))}]]]]
-
+  [:div
    [:div.row
     [:label.title
      {:for "zotero_type"}
@@ -176,19 +159,12 @@
        "User ID is different from username and can be found on the "
        [:a {:href "https://www.zotero.org/settings/keys" :target "_blank"}
         "https://www.zotero.org/settings/keys"]
-       " page, it's a number of digits"]))
-
-   [:div.row
-    [:label.title
-     {:for "zotero_prefer_citekey"
-      :title "Make sure to install Better BibTeX and pin your item first"}
-     "Always prefer citekey as your page title?"]
-    [:div
-     [:div.rounded-md.sm:max-w-xs
-      (ui/toggle (setting/setting :prefer-citekey?)
-                 (fn [] (setting/set-setting! :prefer-citekey? (not (setting/setting :prefer-citekey?))))
-                 true)]]]
+       " page, it's a number of digits"]))])
 
+(rum/defc attachment-setting <
+  rum/reactive
+  []
+  [:div
    [:div.row
     [:label.title
      {:for "zotero_include_attachment_links"}
@@ -198,7 +174,6 @@
       (ui/toggle (setting/setting :include-attachments?)
                  (fn [] (setting/set-setting! :include-attachments? (not (setting/setting :include-attachments?))))
                  true)]]]
-
    (when (setting/setting :include-attachments?)
      [:div.row
       [:label.title
@@ -209,15 +184,14 @@
         [:input.form-input.block
          {:default-value (setting/setting :attachments-block-text)
           :on-blur       (fn [e] (setting/set-setting! :attachments-block-text (util/evalue e)))}]]]])
-
    (when (setting/setting :include-attachments?)
      [:div.row
       [:label.title
        {:for "zotero_linked_attachment_base_directory"}
        "Zotero linked attachment base directory"
        [:a.ml-2
-        {:title "If you store attached files in Zotero — the default — this setting does not affect you. It only applies to linked files. If you're using the ZotFile plugin to help with a linked-file workflow, you should configure it to store linked files within the base directory you've configured. Click to learn more."
-         :href "https://www.zotero.org/support/preferences/advanced#linked_attachment_base_directory"
+        {:title  "If you store attached files in Zotero — the default — this setting does not affect you. It only applies to linked files. If you're using the ZotFile plugin to help with a linked-file workflow, you should configure it to store linked files within the base directory you've configured. Click to learn more."
+         :href   "https://www.zotero.org/support/preferences/advanced#linked_attachment_base_directory"
          :target "_blank"}
         (svg/info)]]
       [:div.mt-1.sm:mt-0.sm:col-span-2
@@ -225,8 +199,38 @@
         [:input.form-input.block
          {:default-value (setting/setting :zotero-linked-attachment-base-directory)
           :placeholder   "/Users/Sarah/Dropbox"
-          :on-blur       (fn [e] (setting/set-setting! :zotero-linked-attachment-base-directory (util/evalue e)))}]]]])
+          :on-blur       (fn [e] (setting/set-setting! :zotero-linked-attachment-base-directory (util/evalue e)))}]]]])])
 
+(rum/defc prefer-citekey-setting <
+  rum/reactive
+  []
+  [:div.row
+   [:label.title
+    {:for   "zotero_prefer_citekey"
+     :title "Make sure to install Better BibTeX and pin your item first"}
+    "Always prefer citekey as your page title?"]
+   [:div
+    [:div.rounded-md.sm:max-w-xs
+     (ui/toggle (setting/setting :prefer-citekey?)
+                (fn [] (setting/set-setting! :prefer-citekey? (not (setting/setting :prefer-citekey?))))
+                true)]]])
+
+(rum/defc api-key-setting []
+  [:div.row
+   [:label.title
+    {:for "zotero_api_key"}
+    "Zotero API key"]
+   [:div.mt-1.sm:mt-0.sm:col-span-2
+    [:div.max-w-lg.rounded-md
+     [:input.form-input.block
+      {:default-value (setting/api-key)
+       :placeholder   "Please enter your Zotero API key"
+       :on-blur       (fn [e] (setting/set-api-key (util/evalue e)))}]]]])
+
+(rum/defc notes-setting <
+  rum/reactive
+  []
+  [:div
    [:div.row
     [:label.title
      {:for "zotero_include_notes"}
@@ -237,7 +241,6 @@
                  (fn [] (setting/set-setting! :include-notes?
                                               (not (setting/setting :include-notes?))))
                  true)]]]
-
    (when (setting/setting :include-notes?)
      [:div.row
       [:label.title
@@ -247,47 +250,125 @@
        [:div.max-w-lg.rounded-md
         [:input.form-input.block
          {:default-value (setting/setting :notes-block-text)
-          :on-blur       (fn [e] (setting/set-setting! :notes-block-text (util/evalue e)))}]]]])
-
-   [:div.row
-    [:label.title
-     {:for "zotero_page_prefix"}
-     "Insert page name with prefix:"]
-    [:div.mt-1.sm:mt-0.sm:col-span-2
-     [:div.max-w-lg.rounded-md
-      [:input.form-input.block
-       {:default-value (setting/setting :page-insert-prefix)
-        :on-blur       (fn [e] (setting/set-setting! :page-insert-prefix (util/evalue e)))}]]]]
-
-   [:div.row
-    [:label.title
-     {:for "zotero_extra_tags"
-      :title "Extra tags to add for every imported page. Separate by comma, or leave it empty."}
-     "Extra tags to add:"]
-    [:div.mt-1.sm:mt-0.sm:col-span-2
-     [:div.max-w-lg.rounded-md
-      [:input.form-input.block
-       {:default-value (setting/setting :extra-tags)
-        :placeholder   "tag1,tag2,tag3"
-        :on-blur       (fn [e] (setting/set-setting! :extra-tags (util/evalue e)))}]]]]
-
-   (when (util/electron?)
-     [:div.row
-      [:label.title
-       {:for "zotero_data_directory"}
-       "Zotero data directory"
-       [:a.ml-2
-        {:title "Set Zotero data directory to open pdf attachment in Logseq. Click to learn more."
-         :href "https://www.zotero.org/support/zotero_data"
-         :target "_blank"}
-        (svg/info)]]
-      [:div.mt-1.sm:mt-0.sm:col-span-2
-       [:div.max-w-lg.rounded-md
-        [:input.form-input.block
-         {:default-value (setting/setting :zotero-data-directory)
-          :placeholder   "/Users/<username>/Zotero"
-          :on-blur       (fn [e] (setting/set-setting! :zotero-data-directory (util/evalue e)))}]]]])
-
+          :on-blur       (fn [e] (setting/set-setting! :notes-block-text (util/evalue e)))}]]]])])
+
+(rum/defc page-prefix-setting []
+  [:div.row
+   [:label.title
+    {:for "zotero_page_prefix"}
+    "Insert page name with prefix:"]
+   [:div.mt-1.sm:mt-0.sm:col-span-2
+    [:div.max-w-lg.rounded-md
+     [:input.form-input.block
+      {:default-value (setting/setting :page-insert-prefix)
+       :on-blur       (fn [e] (setting/set-setting! :page-insert-prefix (util/evalue e)))}]]]])
+
+(rum/defc extra-tags-setting []
+  [:div.row
+   [:label.title
+    {:for "zotero_extra_tags"
+     :title "Extra tags to add for every imported page. Separate by comma, or leave it empty."}
+    "Extra tags to add:"]
+   [:div.mt-1.sm:mt-0.sm:col-span-2
+    [:div.max-w-lg.rounded-md
+     [:input.form-input.block
+      {:default-value (setting/setting :extra-tags)
+       :placeholder   "tag1,tag2,tag3"
+       :on-blur       (fn [e] (setting/set-setting! :extra-tags (util/evalue e)))}]]]])
+
+(rum/defc data-directory-setting []
+  [:div.row
+   [:label.title
+    {:for "zotero_data_directory"}
+    "Zotero data directory"
+    [:a.ml-2
+     {:title "Set Zotero data directory to open pdf attachment in Logseq. Click to learn more."
+      :href "https://www.zotero.org/support/zotero_data"
+      :target "_blank"}
+     (svg/info)]]
+   [:div.mt-1.sm:mt-0.sm:col-span-2
+    [:div.max-w-lg.rounded-md
+     [:input.form-input.block
+      {:default-value (setting/setting :zotero-data-directory)
+       :placeholder   "/Users/<username>/Zotero"
+       :on-blur       (fn [e] (setting/set-setting! :zotero-data-directory (util/evalue e)))}]]]])
+
+(rum/defcs profile-name-dialog-inner <
+  (rum/local "" ::input)
+  [state profile* close-fn]
+  (let [input (get state ::input)]
+    [:div.w-full.sm:max-w-lg.sm:w-96
+     [:div.sm:flex.sm:items-start
+      [:div.mt-3.text-center.sm:mt-0.sm:text-left
+       [:h3#modal-headline.text-lg.leading-6.font-medium
+        "Please enter your profile name"]]]
+
+     [:input.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2
+      {:auto-focus true
+       :default-value ""
+       :on-change (fn [e] (reset! input (util/evalue e)))}]
+
+     [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
+      [:span.flex.w-full.rounded-md.shadow-sm.sm:ml-3.sm:w-auto
+       [:button.inline-flex.justify-center.w-full.rounded-md.border.border-transparent.px-4.py-2.bg-indigo-600.text-base.leading-6.font-medium.text-white.shadow-sm.hover:bg-indigo-500.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
+        {:type "button"
+         :class "ui__modal-enter"
+         :on-click (fn []
+                     (let [profile-name (str/trim @input)]
+                       (when-not (str/blank? profile-name)
+                         (p/let [_ (setting/add-profile profile-name)
+                                 _ (setting/set-profile profile-name)]
+                           (reset! profile* profile-name)))
+                       (state/close-modal!)))}
+        "Submit"]]
+      [:span.mt-3.flex.w-full.rounded-md.shadow-sm.sm:mt-0.sm:w-auto
+       [:button.inline-flex.justify-center.w-full.rounded-md.border.border-gray-300.px-4.py-2.bg-white.text-base.leading-6.font-medium.text-gray-700.shadow-sm.hover:text-gray-500.focus:outline-none.focus:border-blue-300.focus:shadow-outline-blue.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
+        {:type "button"
+         :on-click close-fn}
+        "Cancel"]]]]))
+
+(rum/defc zotero-profile-selector <
+  rum/reactive
+  [profile*]
+  [:div.zotero-profile-selector.my-4
+   [:label.mr-32 {:for "profile-select"} "Choose a profile:"]
+   [:select
+    {:value @profile*
+     :on-change
+     (fn [e]
+       (when-let [profile (util/evalue e)]
+         (p/let [_ (setting/set-profile profile)]
+           (reset! profile* profile))))}
+    (map-indexed (fn [i x] [:option
+                            {:key      i
+                             :value    x}
+                            x]) (setting/all-profiles))]
+   (ui/button
+    "New profile"
+    :small? true
+    :class "ml-4"
+    :on-click
+    (fn []
+      (state/set-modal!
+        (fn [close-fn]
+          (profile-name-dialog-inner profile* close-fn)))))
+   (ui/button
+    "Delete profile!"
+    :small? true
+    :background "red"
+    :class "ml-4"
+    :on-click
+    (fn []
+      (p/let [_ (setting/remove-profile @profile*)]
+        (reset! profile* (setting/profile)))))])
+
+(rum/defcs add-all-items <
+  (rum/local nil ::progress)
+  (rum/local false ::total)
+  (rum/local "Add all" ::fetching-button)
+  rum/reactive
+  [state]
+  [:div
    [:div.row
     [:label.title
      {:for "zotero_import_all"}
@@ -304,12 +385,11 @@
             (when (.confirm
                    js/window
                    (str "This will import all your zotero items and add total number of " total " pages. Do you wish to continue?"))
-              (do
-                (reset! (::total state) total)
-                (<! (zotero-handler/add-all (::progress state)))
-                (reset! (::total state) false)
-                (notification/show! "Successfully added all items!" :success)))))))]]
 
+              (reset! (::total state) total)
+              (<! (zotero-handler/add-all (::progress state)))
+              (reset! (::total state) false)
+              (notification/show! "Successfully added all items!" :success))))))]]
    (ui/admonition
     :warning
     "If you have a lot of items in Zotero, adding them all can slow down Logseq. You can type /zotero to import specific item on demand instead.")
@@ -319,6 +399,44 @@
       [:div.bg-greenred-200.py-3.rounded-lg.col-span-full
        [:progress.w-full {:max (+ @(::total state) 30) :value @(::progress state)}] "Importing items from Zotero....Please wait..."]])])
 
+(rum/defc setting-rows
+  []
+  [:div
+   (api-key-setting)
+
+   (user-or-group-setting)
+
+   (prefer-citekey-setting)
+
+   (attachment-setting)
+
+   (notes-setting)
+
+   (page-prefix-setting)
+
+   (extra-tags-setting)
+
+   (data-directory-setting)])
+
+(rum/defcs settings
+  <
+  (rum/local (setting/all-profiles) ::all-profiles)
+  (rum/local (setting/profile) ::profile)
+  rum/reactive
+  {:should-update
+   (fn [old-state new-state]
+     (let [all-profiles (setting/all-profiles)]
+       (not= all-profiles @(::all-profiles old-state))))}
+  [state]
+  [:div.zotero-settings
+   [:h1.mb-4.text-4xl.font-bold.mb-8 "Zotero Settings"]
+
+   (zotero-profile-selector (::profile state))
+
+   (rum/with-key (setting-rows) @(::profile state))
+
+   (add-all-items)])
+
 (defn open-button [full-path]
   (if (str/ends-with? (str/lower-case full-path) "pdf")
     (ui/button

+ 4 - 1
src/main/frontend/extensions/zotero/extractor.cljs

@@ -170,7 +170,10 @@
             (markdown-link title (str "file://" path) true)
             (markdown-link title (str "file://" path)))))
       "imported_url"
-      (markdown-link title url)
+      (str
+       (markdown-link title url)
+       " "
+       (zotero-imported-file-macro (item-key item) filename))
       "linked_url"
       (markdown-link title url))))
 

+ 40 - 10
src/main/frontend/extensions/zotero/setting.cljs

@@ -16,24 +16,54 @@
    :extra-tags             ""
    :page-insert-prefix     "@"})
 
+(defn sub-zotero-config
+  []
+  (:zotero/settings-v2 (get (state/sub-config) (state/get-current-repo))))
+
+(defn all-profiles []
+  (let [profiles (-> (sub-zotero-config) keys set)
+        default #{"default"}]
+    (if (empty? profiles) default profiles)))
+
+(defn profile []
+  (let [profile (storage/get :zotero/setting-profile)]
+    (if (and profile (contains? (all-profiles) profile))
+      profile
+      (first (all-profiles)))))
+
 (defn api-key []
-  (storage/get :zotero/api-key))
+  (get (storage/get :zotero/api-key-v2) (profile)))
 
 (defn set-api-key [key]
-  (storage/set :zotero/api-key key))
+  (let [profile (profile)
+        api-key-map (storage/get :zotero/api-key-v2)]
+    (storage/set :zotero/api-key-v2 (assoc api-key-map profile key))))
 
-(defn sub-zotero-config
-  []
-  (:zotero/settings (get (state/sub-config) (state/get-current-repo))))
+(defn add-profile [profile]
+  (let [settings (assoc (sub-zotero-config) profile {})]
+    (config-handler/set-config! :zotero/settings-v2 settings)))
+
+(defn set-profile [profile]
+  (storage/set :zotero/setting-profile profile)
+  (when-not (contains? (all-profiles) profile)
+    (add-profile name)))
+
+(defn remove-profile [profile]
+  (let [settings (dissoc (sub-zotero-config) profile)]
+    (config-handler/set-config! :zotero/settings-v2 settings)))
 
 (defn set-setting! [k v]
-  (let [new-settings (assoc (sub-zotero-config) k v)]
-    (config-handler/set-config! :zotero/settings new-settings)))
+  (let [profile (profile)
+        new-settings (update (sub-zotero-config)
+                             profile
+                             #(assoc % k v))]
+    (config-handler/set-config! :zotero/settings-v2 new-settings)))
 
 (defn setting [k]
-  (get (sub-zotero-config)
-       k
-       (get default-settings k)))
+  (let [profile (profile)]
+    (-> (sub-zotero-config)
+        (get profile)
+        (get k (get default-settings k)))))
 
 (defn valid? []
   (and

+ 8 - 7
src/main/frontend/format/block.cljs

@@ -269,16 +269,17 @@
 (defn convert-page-if-journal
   "Convert journal file name to user' custom date format"
   [original-page-name]
-  (let [page-name (string/lower-case original-page-name)
-        day (date/journal-title->int page-name)]
-    (if day
-      (let [original-page-name (date/int->journal-title day)]
-        [original-page-name (string/lower-case original-page-name) day])
-      [original-page-name page-name day])))
+  (when original-page-name
+    (let [page-name (string/lower-case original-page-name)
+         day (date/journal-title->int page-name)]
+     (if day
+       (let [original-page-name (date/int->journal-title day)]
+         [original-page-name (string/lower-case original-page-name) day])
+       [original-page-name page-name day]))))
 
 (defn page-name->map
   [original-page-name with-id?]
-  (when original-page-name
+  (when (and original-page-name (string? original-page-name))
     (let [original-page-name (util/remove-boundary-slashes original-page-name)
           [original-page-name page-name journal-day] (convert-page-if-journal original-page-name)
           namespace? (and (string/includes? original-page-name "/")

+ 8 - 7
src/main/frontend/format/mldoc.cljs

@@ -17,7 +17,7 @@
 (defonce parseJson (gobj/get Mldoc "parseJson"))
 (defonce parseInlineJson (gobj/get Mldoc "parseInlineJson"))
 (defonce parseOPML (gobj/get Mldoc "parseOPML"))
-(defonce exportToHtml (gobj/get Mldoc "exportToHtml"))
+(defonce export (gobj/get Mldoc "export"))
 (defonce anchorLink (gobj/get Mldoc "anchorLink"))
 (defonce parseAndExportMarkdown (gobj/get Mldoc "parseAndExportMarkdown"))
 (defonce parseAndExportOPML (gobj/get Mldoc "parseAndExportOPML"))
@@ -152,7 +152,7 @@
                       properties-ast
                       (map (fn [[k v]]
                              (let [k (keyword (string/lower-case k))
-                                   v (if (contains? #{:title :description :filters :roam_tags} k)
+                                   v (if (contains? #{:title :description :filters :roam_tags :macro} k)
                                        v
                                        (text/split-page-refs-without-brackets v))]
                                [k v]))))
@@ -162,10 +162,11 @@
                    (->>
                     (map
                      (fn [[_ v]]
-                       (let [[k v] (util/split-first " " v)]
-                         (mapv
-                          string/trim
-                          [k v])))
+                       (do
+                         (let [[k v] (util/split-first " " v)]
+                          (mapv
+                           string/trim
+                           [k v]))))
                      macro-properties)
                     (into {}))
                    {})
@@ -285,7 +286,7 @@
   (toEdn [this content config]
     (->edn content config))
   (toHtml [this content config references]
-    (exportToHtml content config references))
+    (export "html" content config references))
   (loaded? [this]
     true)
   (lazyLoad [this ok-handler]

+ 28 - 7
src/main/frontend/fs/watcher_handler.cljs

@@ -10,7 +10,24 @@
             [frontend.db :as db]
             [frontend.state :as state]
             [clojure.string :as string]
-            [frontend.encrypt :as encrypt]))
+            [frontend.encrypt :as encrypt]
+            [frontend.db.model :as model]
+            [frontend.handler.editor :as editor]
+            [frontend.handler.extract :as extract]
+            [promesa.core :as p]
+            [electron.ipc :as ipc]))
+
+(defn- set-missing-block-ids!
+  [content]
+  (when (string? content)
+    (doseq [block-id (extract/extract-all-block-refs content)]
+      (when-let [block (try
+                         (model/get-block-by-uuid block-id)
+                         (catch js/Error _e
+                           nil))]
+        (let [id-property (:id (:block/properties block))]
+          (when-not (= (str id-property) (str block-id))
+            (editor/set-block-property! block-id "id" block-id)))))))
 
 (defn handle-changed!
   [type {:keys [dir path content stat] :as payload}]
@@ -21,8 +38,9 @@
         (cond
           (= "add" type)
           (when-not (db/file-exists? repo path)
-            (let [_ (file-handler/alter-file repo path content {:re-render-root? true
-                                                                :from-disk? true})]
+            (p/let [_ (file-handler/alter-file repo path content {:re-render-root? true
+                                                                  :from-disk? true})]
+              (set-missing-block-ids! content)
               (db/set-file-last-modified-at! repo path mtime)
               ;; return nil, otherwise the entire db will be transfered by ipc
               nil))
@@ -34,10 +52,13 @@
           (and (= "change" type)
                (when-let [last-modified-at (db/get-file-last-modified-at repo path)]
                  (> mtime last-modified-at)))
-          (let [_ (file-handler/alter-file repo path content {:re-render-root? true
-                                                              :from-disk? true})]
-            (db/set-file-last-modified-at! repo path mtime)
-            nil)
+          (when-not (string/blank? content)
+            (p/let [result (ipc/ipc "gitCommitAll")
+                    _ (file-handler/alter-file repo path content {:re-render-root? true
+                                                                  :from-disk? true})]
+              (set-missing-block-ids! content)
+              (db/set-file-last-modified-at! repo path mtime)
+              nil))
 
           (contains? #{"add" "change" "unlink"} type)
           nil

+ 13 - 1
src/main/frontend/handler/common.cljs

@@ -17,7 +17,8 @@
             ["ignore" :as Ignore]
             ["/frontend/utils" :as utils]
             [frontend.date :as date]
-            [clojure.string :as string]))
+            [clojure.string :as string]
+            [dommy.core :as d]))
 
 (defn get-ref
   [repo-url]
@@ -213,3 +214,14 @@
                     (date/journal-day->ts journal-day)
                     (util/time-ms)))))
     pages))
+
+(defn show-custom-context-menu! [e context-menu-content]
+  (util/stop e)
+  (let [client-x (gobj/get e "clientX")
+        client-y (gobj/get e "clientY")
+        scroll-y (util/cur-doc-top)]
+    (state/show-custom-context-menu! context-menu-content)
+    (when-let [context-menu (d/by-id "custom-context-menu")]
+      (d/set-style! context-menu
+                    :left (str client-x "px")
+                    :top (str (+ scroll-y client-y) "px")))))

+ 45 - 5
src/main/frontend/handler/editor.cljs

@@ -990,9 +990,10 @@
             new-line (str (string/upper-case key) ": " value)
             new-content (let [lines (string/split-lines content)
                               new-lines (map (fn [line]
-                                               (if (string/starts-with? (string/lower-case line) key)
-                                                 new-line
-                                                 line))
+                                               (string/trim
+                                                (if (string/starts-with? (string/lower-case line) key)
+                                                  new-line
+                                                  line)))
                                              lines)
                               new-lines (if (not= lines new-lines)
                                           new-lines
@@ -1087,8 +1088,8 @@
         top-level-block-uuids (mapv :block/uuid (filterv #(not (vector? %)) tree))
         exported-md-contents (export/export-blocks-as-markdown
                                      repo top-level-block-uuids
-                                     @(state/get-export-block-text-indent-style)
-                                     (into [] @(state/get-export-block-text-remove-options)))]
+                                     (state/get-export-block-text-indent-style)
+                                     (into [] (state/get-export-block-text-remove-options)))]
     [exported-md-contents tree]))
 
 (defn copy-selection-blocks
@@ -3160,3 +3161,42 @@
        (state/append-current-edit-content! clipboard-data)))
    (fn [error]
      (js/console.error error))))
+
+(defn copy-current-ref
+  [block-id]
+  (when block-id
+    (util/copy-to-clipboard! (util/format "((%s))" (str block-id)))))
+
+(defn delete-current-ref!
+  [block ref-id]
+  (when (and block ref-id)
+    (let [match (re-pattern (str "\\s?" (util/format "\\(\\(%s\\)\\)" (str ref-id))))
+          content (string/replace-first (:block/content block) match "")]
+      (save-block! (state/get-current-repo)
+                   (:block/uuid block)
+                   content))))
+
+(defn replace-ref-with-text!
+  [block ref-id]
+  (when (and block ref-id)
+    (let [match (util/format "((%s))" (str ref-id))
+          ref-block (db/entity [:block/uuid ref-id])
+          block-ref-content (->> (or (:block/content ref-block)
+                                    "")
+                                 (property/remove-built-in-properties (:block/format ref-block)))
+          content (string/replace-first (:block/content block) match
+                                        block-ref-content)]
+      (save-block! (state/get-current-repo)
+                   (:block/uuid block)
+                   content))))
+
+(defn replace-ref-with-embed!
+  [block ref-id]
+  (when (and block ref-id)
+    (let [match (util/format "((%s))" (str ref-id))
+          content (string/replace-first (:block/content block) match
+                                        (util/format "{{embed ((%s))}}"
+                                                     (str ref-id)))]
+      (save-block! (state/get-current-repo)
+                   (:block/uuid block)
+                   content))))

+ 5 - 0
src/main/frontend/handler/events.cljs

@@ -11,6 +11,7 @@
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.page :as page-handler]
             [frontend.components.encryption :as encryption]
+            [frontend.components.shell :as shell]
             [frontend.fs.nfs :as nfs]
             [frontend.db.conn :as conn]
             [frontend.extensions.srs :as srs]
@@ -155,6 +156,10 @@
                false))))
         repos))
 
+(defmethod handle :command/run [_]
+  (when (util/electron?)
+    (state/set-modal! shell/shell)))
+
 (defn run!
   []
   (let [chan (state/get-events-chan)]

+ 4 - 0
src/main/frontend/handler/extract.cljs

@@ -237,3 +237,7 @@
                             block-ids-set (set (map (fn [{:block/keys [uuid]}] [:block/uuid uuid]) block-ids))
                             blocks (map #(remove-illegal-refs % block-ids-set refresh?) blocks)]
                         (apply concat [pages-index pages block-ids blocks])))))))))
+
+(defn extract-all-block-refs
+  [content]
+  (map second (re-seq #"\(\(([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})\)\)" content)))

+ 21 - 21
src/main/frontend/handler/file.cljs

@@ -169,27 +169,27 @@
         write-file! (if from-disk?
                       #(p/resolved nil)
                       #(fs/write-file! repo (config/get-repo-dir repo) path content (when original-content {:old-content original-content})))]
-    (if reset?
-      (do
-        (when-let [page-id (db/get-file-page-id path)]
-          (db/transact! repo
-            [[:db/retract page-id :block/alias]
-             [:db/retract page-id :block/tags]]))
-        (reset-file! repo path content))
-      (db/set-file-content! repo path content))
-    (util/p-handle (write-file!)
-                   (fn [_]
-                     (when (= path (config/get-config-path repo))
-                       (restore-config! repo true))
-                     (when (= path (config/get-custom-css-path repo))
-                       (ui-handler/add-style-if-exists!))
-                     (when re-render-root? (ui-handler/re-render-root!))
-                     ;; (when (and add-history? original-content)
-                     ;;   (history/add-history! repo [[path original-content content]]))
-                     )
-                   (fn [error]
-                     (println "Write file failed, path: " path ", content: " content)
-                     (log/error :write/failed error)))))
+    (p/let [_ (if reset?
+                (do
+                  (when-let [page-id (db/get-file-page-id path)]
+                    (db/transact! repo
+                      [[:db/retract page-id :block/alias]
+                       [:db/retract page-id :block/tags]]))
+                  (reset-file! repo path content))
+                (db/set-file-content! repo path content))]
+      (util/p-handle (write-file!)
+                     (fn [_]
+                       (when (= path (config/get-config-path repo))
+                         (restore-config! repo true))
+                       (when (= path (config/get-custom-css-path repo))
+                         (ui-handler/add-style-if-exists!))
+                       (when re-render-root? (ui-handler/re-render-root!))
+                       ;; (when (and add-history? original-content)
+                       ;;   (history/add-history! repo [[path original-content content]]))
+                       )
+                     (fn [error]
+                       (println "Write file failed, path: " path ", content: " content)
+                       (log/error :write/failed error))))))
 
 (defn set-file-content!
   [repo path new-content]

+ 11 - 6
src/main/frontend/handler/repo.cljs

@@ -5,6 +5,7 @@
             [frontend.config :as config]
             [frontend.date :as date]
             [frontend.db :as db]
+            [frontend.db.model :as db-model]
             [frontend.dicts :as dicts]
             [frontend.encrypt :as encrypt]
             [frontend.format :as format]
@@ -26,7 +27,8 @@
             [frontend.util :as util]
             [lambdaisland.glogi :as log]
             [promesa.core :as p]
-            [shadow.resource :as rc]))
+            [shadow.resource :as rc]
+            [clojure.set :as set]))
 
 ;; Project settings should be checked in two situations:
 ;; 1. User changes the config.edn directly in logseq.com (fn: alter-file)
@@ -147,10 +149,11 @@
        (state/pub-event! [:page/create-today-journal repo-url])))))
 
 (defn- remove-non-exists-refs!
-  [data]
-  (let [block-ids (->> (map :block/uuid data)
-                       (remove nil?)
-                       (set))
+  [data all-block-ids]
+  (let [block-ids (->> (->> (map :block/uuid data)
+                        (remove nil?)
+                        (set))
+                       (set/union (set all-block-ids)))
         keep-block-ref-f (fn [refs]
                            (filter (fn [ref]
                                      (if (and (vector? ref)
@@ -169,7 +172,9 @@
   (let [files (map #(select-keys % [:file/path :file/last-modified-at]) files)
         all-data (-> (concat delete-files delete-blocks files blocks-pages)
                      (util/remove-nils))
-        all-data (if refresh? all-data (remove-non-exists-refs! all-data))]
+        all-data (if refresh?
+                   (remove-non-exists-refs! all-data (db-model/get-all-block-uuids))
+                   (remove-non-exists-refs! all-data nil))]
     (db/transact! repo-url all-data)))
 
 (defn- load-pages-metadata!

+ 30 - 0
src/main/frontend/handler/shell.cljs

@@ -0,0 +1,30 @@
+(ns frontend.handler.shell
+  (:require [electron.ipc :as ipc]
+            [clojure.string :as string]
+            [frontend.util :as util]
+            [frontend.handler.notification :as notification]))
+
+(defn run-git-command!
+  [command]
+  (ipc/ipc "runGit" command))
+
+(defn run-pandoc-command!
+  [command]
+  (ipc/ipc "runPandoc" command))
+
+(defn run-command!
+  [command]
+  (let [[command args] (util/split-first " " command)
+        command (and command (string/lower-case command))]
+    (when (and (not (string/blank? command)) (not (string/blank? args)))
+      (let [args (string/trim args)]
+        (case (keyword command)
+         :git
+         (run-git-command! args)
+
+         :pandoc
+         (run-pandoc-command! args)
+
+         (notification/show!
+          [:div (str command " is not supported yet!")]
+          :error))))))

+ 12 - 2
src/main/frontend/modules/shortcut/config.cljs

@@ -306,7 +306,13 @@
      :binding "mod+c mod+r"
      :fn      #(repo-handler/re-index!
                 nfs-handler/rebuild-index!
-                page-handler/create-today-journal!)}}
+                page-handler/create-today-journal!)}
+    :sidebar/clear
+    {:desc    "Clear all in the right sidebar"
+     :binding "mod+c mod+c"
+     :fn      #(do
+                 (state/clear-sidebar-blocks!)
+                 (state/hide-right-sidebar!))}}
 
    :shortcut.handler/misc
    ;; always overrides the copy due to "mod+c mod+s"
@@ -316,7 +322,11 @@
 
    :shortcut.handler/global-non-editing-only
    ^{:before m/enable-when-not-editing-mode!}
-   {:ui/toggle-document-mode
+   {:command/run
+    {:desc    "Run git/pandoc/others commands"
+     :binding "r"
+     :fn      #(state/pub-event! [:command/run])}
+    :ui/toggle-document-mode
     {:desc    "Toggle document mode"
      :binding "t d"
      :fn      state/toggle-document-mode!}

+ 28 - 7
src/main/frontend/state.cljs

@@ -17,8 +17,11 @@
             [cljs-time.format :as tf]))
 
 (defonce ^:private state
-  (atom
-   (let [document-mode? (or (storage/get :document/mode?) false)]
+  (let [document-mode? (or (storage/get :document/mode?) false)
+        current-graph (let [graph (storage/get :git/current-repo)]
+                        (when graph (ipc/ipc "setCurrentGraph" graph))
+                        graph)]
+    (atom
      {:route-match nil
       :today nil
       :system/events (async/chan 100)
@@ -38,7 +41,7 @@
       :network/online? true
       :indexeddb/support? true
       :me nil
-      :git/current-repo (storage/get :git/current-repo)
+      :git/current-repo current-graph
       :git/status {}
       :format/loading {}
       :draw? false
@@ -147,9 +150,10 @@
       ;; copied blocks
       :copy/blocks {:copy/content nil :copy/block-tree nil}
 
-      :copy/export-block-text-indent-style  (atom "dashes")
-      :copy/export-block-text-remove-options (atom #{})
-
+      :copy/export-block-text-indent-style  (or (storage/get :copy/export-block-text-indent-style)
+                                                "dashes")
+      :copy/export-block-text-remove-options (or (storage/get :copy/export-block-text-remove-options)
+                                                 #{})
       :date-picker/date nil
 
       :view/components {}})))
@@ -391,7 +395,8 @@
   (swap! state assoc :git/current-repo repo)
   (if repo
     (storage/set :git/current-repo repo)
-    (storage/remove :git/current-repo)))
+    (storage/remove :git/current-repo))
+  (ipc/ipc "setCurrentGraph" repo))
 
 (defn set-preferred-format!
   [format]
@@ -741,6 +746,10 @@
   []
   (:sidebar/blocks @state))
 
+(defn clear-sidebar-blocks!
+  []
+  (set-state! :sidebar/blocks '()))
+
 (defn sidebar-block-toggle-collapse!
   [db-id]
   (when db-id
@@ -1377,9 +1386,21 @@
 (defn get-export-block-text-indent-style []
   (:copy/export-block-text-indent-style @state))
 
+(defn set-export-block-text-indent-style!
+  [v]
+  (set-state! :copy/export-block-text-indent-style v)
+  (storage/set :copy/export-block-text-indent-style v))
+
 (defn get-export-block-text-remove-options []
   (:copy/export-block-text-remove-options @state))
 
+(defn update-export-block-text-remove-options!
+  [e k]
+  (let [f (if (util/echecked? e) conj disj)]
+    (update-state! :copy/export-block-text-remove-options
+                   #(f % k))
+    (storage/set :copy/export-block-text-remove-options
+                 (get-export-block-text-remove-options))))
 
 (defn set-editor-args!
   [args]

+ 1 - 1
src/main/frontend/ui.css

@@ -65,7 +65,7 @@
 
   @screen sm {
     & {
-      @apply inset-0 flex items-center justify-center;
+      @apply inset-0 flex items-baseline justify-center top-24;
     }
   }
 

+ 13 - 0
src/main/frontend/util.cljc

@@ -370,6 +370,11 @@
    (defn stop [e]
      (when e (doto e (.preventDefault) (.stopPropagation)))))
 
+#?(:cljs
+   (defn stop-propagation [e]
+     (when e (.stopPropagation e))))
+
+
 (def speed 500)
 (def moving-frequency 15)
 
@@ -1389,3 +1394,11 @@
   (if (wrapped-by-quotes? v)
     (unquote-string v)
     v))
+
+#?(:cljs
+   (defn right-click?
+     [e]
+     (let [which (gobj/get e "which")
+           button (gobj/get e "button")]
+       (or (= which 3)
+           (= button 2)))))

+ 7 - 3
src/main/frontend/util/property.cljs

@@ -117,7 +117,11 @@
   (let [org? (= format :org)
         properties (filter (fn [[k v]] ((built-in-properties) k)) properties)]
     (if (seq properties)
-      (let [[title & body] (string/split-lines content)
+      (let [lines (string/split-lines content)
+            ast (mldoc/->edn content (mldoc/default-config format))
+            [title body] (if (mldoc/block-with-title? (first (ffirst ast)))
+                           [(first lines) (rest lines)]
+                           [nil lines])
             properties-in-content? (and title (= (string/upper-case title) properties-start))
             no-title? (or (simplified-property? title) properties-in-content?)
             properties-and-body (concat
@@ -146,7 +150,7 @@
                          (when org?
                            [properties-end])
                          body)]
-        (string/join "\n" body))
+        (string/triml (string/join "\n" body)))
       content)))
 
 ;; FIXME:
@@ -284,7 +288,7 @@
         content (reduce (fn [content key]
                           (remove-property format key content)) content built-in-properties*)]
     (if (= format :org)
-      (string/replace-first content ":PROPERTIES:\n:END:" "")
+      (string/replace-first content (re-pattern ":PROPERTIES:\n:END:\n*") "")
       content)))
 
 (defn ->new-properties

+ 1 - 1
src/main/frontend/version.cljs

@@ -1,3 +1,3 @@
 (ns frontend.version)
 
-(defonce version "0.3.3")
+(defonce version "0.3.5")

+ 30 - 0
src/test/frontend/util/property_test.cljs

@@ -147,4 +147,34 @@
     {:title "a/b/c" :tags "d,e"}
     ":PROPERTIES:\n:title: a/b/c\n:tags: d,e\n:END:\n"))
 
+(deftest test-with-built-in-properties
+  (let [content "#+BEGIN_QUERY\n{:title      \"cool NEXT\"\n    :query      [:find (pull ?h [*])\n                 :in $ ?start ?next\n                 :where\n                 [?h :block/marker ?marker]\n                 [(contains? #{\"NOW\" \"LATER\" \"TODO\"} ?marker)]\n                 [?h :block/ref-pages ?p]\n                 [?p :block/journal? true]\n                 [?p :block/journal-day ?d]\n                 [(> ?d ?start)]\n                 [(< ?d ?next)]]\n    :inputs     [:today :7d-after]\n    :collapsed? false}\n#+END_QUERY"]
+    (let [md-property "query-table:: true"]
+      (are [x y] (= (property/with-built-in-properties {:query-table true} x :markdown) y)
+       content
+       (str md-property "\n" content)
+
+       "title"
+       (str "title\n" md-property)
+
+       "title\nbody"
+       (str "title\n" md-property "\nbody")
+
+       "1. list"
+       (str md-property "\n1. list")))
+
+    (let [org-property ":PROPERTIES:\n:query-table: true\n:END:"]
+      (are [x y] (= (property/with-built-in-properties {:query-table true} x :org) y)
+       content
+       (str org-property "\n" content)
+
+       "title"
+       (str "title\n" org-property)
+
+       "title\nbody"
+       (str "title\n" org-property "\nbody")
+
+       "1. list"
+       (str org-property "\n1. list")))))
+
 #_(cljs.test/run-tests)

+ 4 - 4
yarn.lock

@@ -6175,10 +6175,10 @@ mkdirp@^0.5.0, mkdirp@^0.5.4, mkdirp@~0.5.1:
   dependencies:
     minimist "^1.2.5"
 
-mldoc@0.9.8:
-  version "0.9.8"
-  resolved "https://registry.yarnpkg.com/mldoc/-/mldoc-0.9.8.tgz#176cd5383002eadd2931a6e6ad54034ff67a8608"
-  integrity sha512-qj4issvoDV0hKR6zS3BLOZQkqaBb0oh4nzI/rxdfsxVf5KRBRdka0HVZ3ayLXaozWv9ozhd3qSYIM5m57VNB8w==
+mldoc@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/mldoc/-/mldoc-1.0.1.tgz#b6633b9797ff491227e1b78918f8aa16d5150b9c"
+  integrity sha512-uf7sdA4HFAuqSlWfHDLAtQkJHK7Jgnj+WBfUwElXqvzAiP4VFmO7XVJ7aI+U224RbBJK76UQuFinyTGQBYp2JQ==
   dependencies:
     yargs "^12.0.2"