Prechádzať zdrojové kódy

Merge branch 'master' into feat/integration-plugins-core

charlie 4 rokov pred
rodič
commit
f4e6d7abba
56 zmenil súbory, kde vykonal 1362 pridanie a 766 odobranie
  1. 1 1
      package.json
  2. 9 1
      resources/css/common.css
  3. 0 0
      resources/css/tooltip.css
  4. 15 7
      resources/forge.config.js
  5. 2 2
      resources/package.json
  6. 0 2
      shadow-cljs.edn
  7. 1 2
      src/electron/electron/core.cljs
  8. 62 0
      src/electron/electron/fs_watcher.cljs
  9. 9 80
      src/electron/electron/handler.cljs
  10. 27 1
      src/electron/electron/utils.cljs
  11. 4 4
      src/main/frontend/commands.cljs
  12. 115 88
      src/main/frontend/components/block.cljs
  13. 16 6
      src/main/frontend/components/block.css
  14. 6 0
      src/main/frontend/components/editor.cljs
  15. 3 3
      src/main/frontend/components/export.cljs
  16. 167 171
      src/main/frontend/components/page.cljs
  17. 497 0
      src/main/frontend/components/page.cljs.~e600c29046589a778b9a28a2122606358d7ab458~
  18. 1 1
      src/main/frontend/components/search.cljs
  19. 8 3
      src/main/frontend/components/settings.cljs
  20. 10 1
      src/main/frontend/components/svg.cljs
  21. 0 113
      src/main/frontend/components/theme.css
  22. 2 2
      src/main/frontend/config.cljs
  23. 3 3
      src/main/frontend/db.cljs
  24. 14 14
      src/main/frontend/db/model.cljs
  25. 6 3
      src/main/frontend/db/query_dsl.cljs
  26. 2 2
      src/main/frontend/db/query_react.cljs
  27. 1 1
      src/main/frontend/dicts.cljs
  28. 1 1
      src/main/frontend/extensions/html_parser.cljs
  29. 38 32
      src/main/frontend/format/block.cljs
  30. 21 9
      src/main/frontend/format/mldoc.cljs
  31. 5 2
      src/main/frontend/fs/watcher_handler.cljs
  32. 9 1
      src/main/frontend/handler.cljs
  33. 1 1
      src/main/frontend/handler/block.cljs
  34. 22 16
      src/main/frontend/handler/editor.cljs
  35. 1 5
      src/main/frontend/handler/editor/keyboards.cljs
  36. 0 1
      src/main/frontend/handler/editor/lifecycle.cljs
  37. 2 1
      src/main/frontend/handler/export.cljs
  38. 3 3
      src/main/frontend/handler/extract.cljs
  39. 0 1
      src/main/frontend/handler/history.cljs
  40. 12 7
      src/main/frontend/handler/page.cljs
  41. 23 21
      src/main/frontend/handler/web/nfs.cljs
  42. 46 44
      src/main/frontend/modules/file/core.cljs
  43. 2 2
      src/main/frontend/modules/outliner/tree.cljs
  44. 5 5
      src/main/frontend/modules/shortcut/dict.cljs
  45. 3 2
      src/main/frontend/security.cljs
  46. 21 3
      src/main/frontend/state.cljs
  47. 12 16
      src/main/frontend/text.cljs
  48. 6 6
      src/main/frontend/ui.cljs
  49. 43 34
      src/main/frontend/util.cljc
  50. 1 1
      src/main/frontend/util/marker.cljs
  51. 1 1
      src/main/frontend/util/priority.cljs
  52. 30 16
      src/main/frontend/util/property.cljs
  53. 36 0
      src/test/frontend/format/block_test.cljs
  54. 0 12
      src/test/frontend/text_test.cljs
  55. 33 8
      src/test/frontend/util/property_test.cljs
  56. 4 4
      yarn.lock

+ 1 - 1
package.json

@@ -74,7 +74,7 @@
         "ignore": "^5.1.8",
         "is-svg": "4.2.2",
         "jszip": "^3.5.0",
-        "mldoc": "0.6.16",
+        "mldoc": "0.6.18",
         "path": "^0.12.7",
         "react": "^17.0.2",
         "react-dom": "^17.0.2",

+ 9 - 1
resources/css/common.css

@@ -4,7 +4,7 @@
   --ls-page-text-size: 1em;
   --ls-page-title-size: 36px;
   --ls-font-family: 'Inter';
-  --ls-main-content-max-width: 700px;
+  --ls-main-content-max-width: 740px;
   --ls-border-radius-low: 4px;
   --ls-border-radius-medium: 8px;
 }
@@ -35,6 +35,7 @@ html[data-theme='dark'] {
   --ls-active-primary-color: #8ec2c2;
   --ls-active-secondary-color: #d0e8e8;
   --ls-block-properties-background-color: #06323e;
+  --ls-page-properties-background-color: #02171d;
   --ls-block-ref-link-text-color: #1a6376;
   --ls-search-background-color: linear-gradient(
     to right,
@@ -93,6 +94,7 @@ html[data-theme='light'] {
   --ls-active-primary-color: rgb(4, 85, 145);
   --ls-active-secondary-color: #003761;
   --ls-block-properties-background-color: #f7f6f4;
+  --ls-page-properties-background-color: #eae7e1;
   --ls-block-ref-link-text-color: #d8e1e8;
   --ls-search-background-color: var(--ls-primary-background-color);
   --ls-border-color: #ccc;
@@ -725,6 +727,11 @@ a.fade-link:hover {
   height: 20px;
 }
 
+.svg-small svg {
+    transform: scale(0.6);
+    display: inline;
+}
+
 /* < > buttons */
 
 a.navigation {
@@ -757,6 +764,7 @@ mark {
 
 .block-ref {
   padding: 2px 5px;
+  display: inline;
 }
 
 .block-ref .block-ref {

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
resources/css/tooltip.css


+ 15 - 7
resources/forge.config.js

@@ -2,24 +2,32 @@ const path = require('path')
 
 module.exports = {
   packagerConfig: {
-    icon: './icons/canary/logseq_big_sur.icns',
-    name: 'Logseq Canary',
+    icon: './icons/logseq_big_sur.icns',
+    osxSign: {
+      identity: 'Developer ID Application: Tiansheng Qin',
+      'hardened-runtime': true,
+      entitlements: 'entitlements.plist',
+      'entitlements-inherit': 'entitlements.plist',
+      'signature-flags': 'library'
+    },
+    osxNotarize: {
+      appleId: "my-fake-apple-id",
+      appleIdPassword: "my-fake-apple-id-password",
+    },
   },
   makers: [
     {
       'name': '@electron-forge/maker-squirrel',
       'config': {
-        title: 'Logseq Canary',
-        name: 'LogseqCanary', // ID name
-        setupIcon: './icons/canary/logseq.ico'
+        'name': 'Logseq'
       }
     },
     {
       name: '@electron-forge/maker-dmg',
       config: {
         format: 'ULFO',
-        icon: './icons/canary/logseq_big_sur.icns',
-        name: 'Logseq Canary'
+        icon: './icons/logseq_big_sur.icns',
+        name: 'Logseq'
       }
     },
     {

+ 2 - 2
resources/package.json

@@ -1,6 +1,6 @@
 {
-  "name": "Logseq-Canary",
-  "version": "0.0.1",
+  "name": "Logseq",
+  "version": "0.1.0",
   "main": "electron.js",
   "author": "Logseq",
   "description": "A privacy-first, open-source platform for knowledge management and collaboration.",

+ 0 - 2
shadow-cljs.edn

@@ -2,8 +2,6 @@
 {:deps     true
  :nrepl        {:port 8701}
 
- :http {:host "192.168.2.227"}
-
  :builds
  {:app
   {:target :browser

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

@@ -28,9 +28,8 @@
         win-opts (cond->
                   {:width         (.-width win-state)
                    :height        (.-height win-state)
-                   :frame         (not mac?)
+                   :frame         true
                    :autoHideMenuBar (not mac?)
-                   :titleBarStyle (if mac? "hidden" nil)
                    :webPreferences
                    {:plugins                 true ; pdf
                     :nodeIntegration         false

+ 62 - 0
src/electron/electron/fs_watcher.cljs

@@ -0,0 +1,62 @@
+(ns electron.fs-watcher
+  (:require [cljs-bean.core :as bean]
+            ["fs" :as fs]
+            ["chokidar" :as watcher]
+            [promesa.core :as p]
+            [clojure.string :as string]
+            [electron.utils :as utils]
+            ["electron" :refer [app]]))
+
+;; TODO: explore different solutions for different platforms
+;; 1. https://github.com/Axosoft/nsfw
+
+(defonce polling-interval 5000)
+
+(defonce file-watcher-chan "file-watcher")
+(defn send-file-watcher! [^js win type payload]
+  (.. win -webContents
+      (send file-watcher-chan
+            (bean/->js {:type type :payload payload}))))
+
+(defn watch-dir!
+  [win dir]
+  (when (fs/existsSync dir)
+    (let [watcher (.watch watcher dir
+                          (clj->js
+                           {:ignored (partial utils/ignored-path? dir)
+                            :ignoreInitial true
+                            :ignorePermissionErrors true
+                            :interval polling-interval
+                            :binaryInterval polling-interval
+                            :persistent true
+                            :disableGlobbing true
+
+                            :awaitWriteFinish true}))]
+      ;; TODO: batch sender
+      (.on watcher "add"
+           (fn [path]
+             (send-file-watcher! win "add"
+                                 {:dir (utils/fix-win-path! dir)
+                                  :path (utils/fix-win-path! path)
+                                  :content (utils/read-file path)
+                                  :stat (fs/statSync path)})))
+      (.on watcher "change"
+           (fn [path]
+             (send-file-watcher! win "change"
+                                 {:dir (utils/fix-win-path! dir)
+                                  :path (utils/fix-win-path! path)
+                                  :content (utils/read-file path)
+                                  :stat (fs/statSync path)})))
+      ;; (.on watcher "unlink"
+      ;;      (fn [path]
+      ;;        (send-file-watcher! win "unlink"
+      ;;                            {:dir (utils/fix-win-path! dir)
+      ;;                             :path (utils/fix-win-path! path)})))
+      (.on watcher "error"
+           (fn [path]
+             (println "Watch error happened: "
+                      {:path path})))
+
+      (.on app "quit" #(.close watcher))
+
+      true)))

+ 9 - 80
src/electron/electron/handler.cljs

@@ -4,7 +4,7 @@
             ["fs" :as fs]
             ["fs-extra" :as fs-extra]
             ["path" :as path]
-            ["chokidar" :as watcher]
+            [electron.fs-watcher :as watcher]
             [promesa.core :as p]
             [goog.object :as gobj]
             [clojure.string :as string]
@@ -33,11 +33,8 @@
 (defmethod handle :unlink [_window [_ path]]
   (fs/unlinkSync path))
 
-(defn- read-file
-  [path]
-  (.toString (fs/readFileSync path)))
 (defmethod handle :readFile [_window [_ path]]
-  (read-file path))
+  (utils/read-file path))
 
 (defmethod handle :writeFile [_window [_ path content]]
   ;; TODO: handle error
@@ -50,21 +47,7 @@
 (defmethod handle :stat [_window [_ path]]
   (fs/statSync path))
 
-(defn- fix-win-path!
-  [path]
-  (when path
-    (if utils/win32?
-      (string/replace path "\\" "/")
-      path)))
-
-;; TODO: ignore according to mime types
-(defn ignored-path?
-  [dir path]
-  (or
-   (some #(string/starts-with? path (str dir "/" %))
-         ["." "assets" "node_modules"])
-   (some #(string/ends-with? path %)
-         [".swap" ".crswap" ".tmp" ".DS_Store"])))
+
 
 (defonce allowed-formats
   #{:org :markdown :md :edn :json :css :excalidraw})
@@ -79,16 +62,16 @@
   [path]
   (let [result (->>
                 (readdir path)
-                (remove (partial ignored-path? path))
+                (remove (partial utils/ignored-path? path))
                 (filter #(contains? allowed-formats (get-ext %)))
                 (map (fn [path]
                        (let [stat (fs/statSync path)]
                          (when-not (.isDirectory stat)
-                           {:path (fix-win-path! path)
-                            :content (read-file path)
+                           {:path (utils/fix-win-path! path)
+                            :content (utils/read-file path)
                             :stat stat}))))
                 (remove nil?))]
-    (vec (cons {:path (fix-win-path! path)} result))))
+    (vec (cons {:path (utils/fix-win-path! path)} result))))
 
 (defn- get-ls-dotdir-root
   []
@@ -150,7 +133,7 @@
 (defn clear-cache!
   []
   (let [path (.getPath ^object app "userData")]
-    (doseq [dir ["search" "IndexedDB" "Local Storage" "databases" "cache"]]
+    (doseq [dir ["search" "IndexedDB"]]
       (let [path (path/join path dir)]
         (try
           (fs-extra/removeSync path)
@@ -162,63 +145,9 @@
   (clear-cache!)
   (search/ensure-search-dir!))
 
-(defn- get-file-ext
-  [file]
-  (last (string/split file #"\.")))
-
-(defonce file-watcher-chan "file-watcher")
-(defn send-file-watcher! [^js win type payload]
-  (.. win -webContents
-      (send file-watcher-chan
-            (bean/->js {:type type :payload payload}))))
-
-(defonce polling-interval 5000)
-(defn watch-dir!
-  [win dir]
-  (when (fs/existsSync dir)
-    (let [watcher (.watch watcher dir
-                          (clj->js
-                           {:ignored (partial ignored-path? dir)
-                            :ignoreInitial true
-                            :ignorePermissionErrors true
-                            :interval polling-interval
-                            :binaryInterval polling-interval
-                            :persistent true
-                            :disableGlobbing true
-
-                            :awaitWriteFinish true}))]
-      ;; TODO: batch sender
-      (.on watcher "add"
-           (fn [path]
-             (send-file-watcher! win "add"
-                                 {:dir (fix-win-path! dir)
-                                  :path (fix-win-path! path)
-                                  :content (read-file path)
-                                  :stat (fs/statSync path)})))
-      (.on watcher "change"
-           (fn [path]
-             (send-file-watcher! win "change"
-                                 {:dir (fix-win-path! dir)
-                                  :path (fix-win-path! path)
-                                  :content (read-file path)
-                                  :stat (fs/statSync path)})))
-      ;; (.on watcher "unlink"
-      ;;      (fn [path]
-      ;;        (send-file-watcher! win "unlink"
-      ;;                            {:dir (fix-win-path! dir)
-      ;;                             :path (fix-win-path! path)})))
-      (.on watcher "error"
-           (fn [path]
-             (println "Watch error happened: "
-                      {:path path})))
-
-      (.on app "quit" #(.close watcher))
-
-      true)))
-
 (defmethod handle :addDirWatcher [window [_ dir]]
   (when dir
-    (watch-dir! window dir)))
+    (watcher/watch-dir! window dir)))
 
 (defmethod handle :openDialogSync [^js window _messages]
   (let [result (.showOpenDialogSync dialog (bean/->js

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

@@ -1,4 +1,6 @@
-(ns electron.utils)
+(ns electron.utils
+  (:require [clojure.string :as string]
+            ["fs" :as fs]))
 
 (defonce mac? (= (.-platform js/process) "darwin"))
 (defonce win32? (= (.-platform js/process) "win32"))
@@ -10,3 +12,27 @@
 
 (defonce open (js/require "open"))
 (defonce fetch (js/require "node-fetch"))
+
+(defn get-file-ext
+  [file]
+  (last (string/split file #"\.")))
+
+;; TODO: ignore according to mime types
+(defn ignored-path?
+  [dir path]
+  (or
+   (some #(string/starts-with? path (str dir "/" %))
+         ["." "assets" "node_modules"])
+   (some #(string/ends-with? path %)
+         [".swap" ".crswap" ".tmp" ".DS_Store"])))
+
+(defn fix-win-path!
+  [path]
+  (when path
+    (if win32?
+      (string/replace path "\\" "/")
+      path)))
+
+(defn read-file
+  [path]
+  (.toString (fs/readFileSync path)))

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

@@ -441,7 +441,7 @@
 
 (defn compute-pos-delta-when-change-marker
   [current-input edit-content new-value marker pos]
-  (let [old-marker (some->> (first (re-find marker/bare-marker-pattern edit-content))
+  (let [old-marker (some->> (first (util/safe-re-find marker/bare-marker-pattern edit-content))
                             (string/trim))
         old-marker (if old-marker old-marker "")
         pos-delta (- (count marker)
@@ -463,7 +463,7 @@
                   (if-let [matches (seq (util/re-pos new-line-re-pattern prefix))]
                     (let [[start-pos content] (last matches)]
                       (+ start-pos (count content)))
-                    (count (re-find re-pattern prefix))))
+                    (count (util/safe-re-find re-pattern prefix))))
             new-value (str (subs edit-content 0 pos)
                            (string/replace-first (subs edit-content pos)
                                                  marker/marker-pattern
@@ -490,9 +490,9 @@
             slash-pos (:pos @*slash-caret-pos)
             heading-pattern  #"^#\+"
             prefix (subs edit-content 0 (dec slash-pos))
-            pos (count (re-find heading-pattern prefix))
+            pos (count (util/safe-re-find heading-pattern prefix))
             new-value (cond
-                        (re-find heading-pattern prefix)
+                        (util/safe-re-find heading-pattern prefix)
                         (str (subs edit-content 0 pos)
                              (string/replace-first (subs edit-content pos)
                                                    heading-pattern

+ 115 - 88
src/main/frontend/components/block.cljs

@@ -249,11 +249,15 @@
     (if (and (config/local-asset? href)
              (config/local-db? (state/get-current-repo)))
       (asset-link config title href label metadata full_text)
-      (let [href (if config/publishing?
+      (let [href (cond
+                   (util/starts-with? href "http")
+                   href
+
+                   config/publishing?
                    (subs href 1)
-                   (if (util/starts-with? href "http")
-                     href
-                     (get-file-absolute-path config href)))]
+
+                   :else
+                   (get-file-absolute-path config href))]
         (resizable-image config title href metadata full_text false)))))
 
 (defn repetition-to-string
@@ -365,10 +369,10 @@
        (get page-entity :block/original-name page-name)))])
 
 (rum/defc page-cp
-  [{:keys [html-export? label children contents-page?] :as config} page]
+  [{:keys [html-export? label children contents-page? sidebar?] :as config} page]
   (when-let [page-name (:block/name page)]
-    (let [page-entity page
-          page (string/lower-case page-name)
+    (let [page (string/lower-case page-name)
+          page-entity (db/entity [:block/name page])
           redirect-page-name (cond
                                (:block/alias? config)
                                page
@@ -385,12 +389,18 @@
                  (util/encode-str page)
                  (rfe/href :page {:name redirect-page-name}))
           inner (page-inner config page-name href redirect-page-name page-entity contents-page? children html-export? label)]
-      inner
-      ;; (ui/tippy
-      ;;  {:interactive true
-      ;;   :html (page-preview page-name)}
-      ;;  inner)
-      )))
+      (ui/tippy {:html [:div.tippy-wrapper.overflow-y-auto
+                        {:style {:width 735
+                                 :text-align "left"
+                                 :font-weight 500
+                                 :max-height 600
+                                 :padding-bottom 200}}
+                        [:h2.font-bold.text-lg page-name]
+                        (let [page (db/entity [:block/name (string/lower-case page-name)])]
+                          ((state/get-page-blocks-cp) (state/get-current-repo) page (:sidebar? config)))]
+                 :interactive true
+                 :delay 1000}
+                inner))))
 
 (rum/defc asset-reference
   [title path]
@@ -487,6 +497,7 @@
          (blocks-container blocks (assoc config
                                          :id page-name
                                          :embed? true
+                                         :page-embed? true
                                          :ref? false))))]))
 
 (defn- get-label-text
@@ -528,25 +539,23 @@
                {:block block})
               (route-handler/redirect! {:to          :page
                                         :path-params {:name id}})))}
-
          (let [title (let [title (:block/title block)]
-                       (if (empty? title)
-                         ;; display the content
-                         [:div.block-ref
-                          (block-content config block nil (:block/uuid block) (:slide? config))]
-                         (->elem
-                          :span.block-ref
-                          (map-inline config title))))]
-           (ui/tippy {:html [:div.tippy-wrapper
-                             {:style {:width 780
-                                      :text-align "left"}}
+                       [:span.block-ref
+                        (block-content (assoc config :block-ref? true)
+                                       block nil (:block/uuid block)
+                                       (:slide? config))])]
+           (ui/tippy {:html [:div.tippy-wrapper.overflowy-y-auto
+                             {:style {:width 735
+                                      :text-align "left"
+                                      :max-height 600}}
                              (block-container config block)]
-                      :interactive true}
-            (if label
-              (->elem
-               :span.block-ref
-               (map-inline config label))
-              title)))]
+                      :interactive true
+                      :delay 1000}
+                     (if label
+                       (->elem
+                        :span.block-ref
+                        (map-inline config label))
+                       title)))]
         [:span.warning.mr-1 {:title "Block ref invalid"}
          (util/format "((%s))" id)]))))
 
@@ -568,10 +577,7 @@
   (if macro-content
     (let [ast (->> (mldoc/->edn macro-content (mldoc/default-config format))
                    (map first))
-          block? (contains? #{"Paragraph"
-                              "Raw_Html"
-                              "Hiccup"}
-                            (ffirst ast))]
+          block? (mldoc/block-with-title? (ffirst ast))]
       (if block?
         [:div
          (markup-elements-cp (assoc config :block/format format) ast)]
@@ -697,7 +703,7 @@
                (not= \* (last s)))
           (->elem :a {:on-click #(route-handler/jump-to-anchor! (mldoc/anchorLink (subs s 1)))} (subs s 1))
 
-          (re-find #"(?i)^http[s]?://" s)
+          (util/safe-re-find #"(?i)^http[s]?://" s)
           (->elem :a {:href s
                       :data-href s
                       :target "_blank"}
@@ -850,7 +856,7 @@
             (when-let [youtube-id (cond
                                     (== 11 (count url)) url
                                     :else
-                                    (nth (re-find YouTube-regex url) 5))]
+                                    (nth (util/safe-re-find YouTube-regex url) 5))]
               (when-not (string/blank? youtube-id)
                 (let [width (min (- (util/get-width) 96)
                                  560)
@@ -867,7 +873,7 @@
         (= name "vimeo")
         (when-let [url (first arguments)]
           (let [Vimeo-regex #"^((?:https?:)?//)?((?:www).)?((?:player.vimeo.com|vimeo.com)?)((?:/video/)?)([\w-]+)(\S+)?$"]
-            (when-let [vimeo-id (nth (re-find Vimeo-regex url) 5)]
+            (when-let [vimeo-id (nth (util/safe-re-find Vimeo-regex url) 5)]
               (when-not (string/blank? vimeo-id)
                 (let [width (min (- (util/get-width) 96)
                                  560)
@@ -888,7 +894,7 @@
             (when-let [id (cond
                             (<= (count url) 15) url
                             :else
-                            (last (re-find id-regex url)))]
+                            (last (util/safe-re-find id-regex url)))]
               (when-not (string/blank? id)
                 (let [width (min (- (util/get-width) 96)
                                  560)
@@ -1274,7 +1280,7 @@
                                2))
         elem (if heading-level
                (keyword (str "h" heading-level))
-               :div)]
+               :span.inline)]
     (->elem
      elem
      (merge
@@ -1358,7 +1364,11 @@
         properties (sort properties)]
     (cond
       (seq properties)
-      [:div.blocks-properties
+      [:div.block-properties
+       {:class (when pre-block? "page-properties")
+        :title (if pre-block?
+                 "Click to edit this page's properties"
+                 "Click to edit this block's properties")}
        (for [[k v] properties]
          (rum/with-key (property-cp config block k v)
            (str (:block/uuid block) "-" k)))]
@@ -1464,6 +1474,7 @@
 (rum/defc block-content < rum/reactive
   [config {:block/keys [uuid title body meta content marker dummy? page format repo children pre-block? properties idx container block-refs-count scheduled deadline repeated?] :as block} edit-input-id block-id slide?]
   (let [collapsed? (get properties :collapsed)
+        block-ref-with-title? (and (:block-ref? config) (seq title))
         dragging? (rum/react *dragging?)
         content (if (string? content) (string/trim content) "")
         mouse-down-key (if (util/ios?)
@@ -1475,12 +1486,17 @@
                                 (block-content-on-mouse-down e block block-id properties content format edit-input-id))
                :on-drag-over  (fn [event] (block-content-on-drag-over event uuid))
                :on-drag-leave (fn [_event] (block-content-on-drag-leave uuid))
-               :on-drop       (fn [event] (block-content-on-drop event block uuid))}]
-    [:div.flex.relative {:style {:width "100%"}}
-     [:div.flex-1.flex-col.relative.block-content
-      (cond-> {:id (str "block-content-" uuid)}
-        (not slide?)
-        (merge attrs))
+               :on-drop       (fn [event] (block-content-on-drop event block uuid))
+               :style {:width "100%"}}]
+    [:div.block-content.inline
+     (cond-> {:id (str "block-content-" uuid)}
+       (not slide?)
+       (merge attrs))
+
+     [:span
+     ;; .flex.relative {:style {:width "100%"}}
+     [:span
+      ;; .flex-1.flex-col.relative.block-content
 
       (cond
         (seq title)
@@ -1506,47 +1522,17 @@
                  (not (:slide? config)))
         (properties-cp config block))
 
-      (when (seq body)
-        (do
-          [:div.block-body {:style {:display (if (and collapsed? (seq title)) "none" "")}}
-          ;; TODO: consistent id instead of the idx (since it could be changed later)
-           (let [body (block/trim-break-lines! (:block/body block))]
-             (for [[idx child] (medley/indexed body)]
-               (when-let [block (markup-element-cp config child)]
-                 (rum/with-key (block-child block)
-                   (str uuid "-" idx)))))]))]
-     (when (and block-refs-count (> block-refs-count 0))
-       [:div
-        [:a.open-block-ref-link.bg-base-2
-         {:title "Open block references"
-          :style {:margin-top -1}
-          :on-click (fn []
-                      (state/sidebar-add-block!
-                       (state/get-current-repo)
-                       (:db/id block)
-                       :block-ref
-                       {:block block}))}
-         block-refs-count]])
-
-     (when (and (= marker "DONE")
-                (state/enable-timetracking?))
-       (let [start-time (or
-                         (get properties :now)
-                         (get properties :doing)
-                         (get properties :in-progress)
-                         (get properties :later)
-                         (get properties :todo))
-             finish-time (get properties :done)]
-         (when (and start-time finish-time (> finish-time start-time))
-           [:div.text-sm.absolute.time-spent {:style {:top 0
-                                                      :right 0
-                                                      :padding-left 2}
-                                              :title (str (date/int->local-time start-time) " ~ " (date/int->local-time finish-time))}
-            [:span.opacity-70
-             (utils/timeConversion (- finish-time start-time))]])))]))
+      (when (and (not block-ref-with-title?) (seq body))
+        [:div.block-body {:style {:display (if (and collapsed? (seq title)) "none" "")}}
+         ;; TODO: consistent id instead of the idx (since it could be changed later)
+         (let [body (block/trim-break-lines! (:block/body block))]
+           (for [[idx child] (medley/indexed body)]
+             (when-let [block (markup-element-cp config child)]
+               (rum/with-key (block-child block)
+                 (str uuid "-" idx)))))])]]]))
 
 (rum/defc block-content-or-editor < rum/reactive
-  [config {:block/keys [uuid title body meta content dummy? page format repo children pre-block? idx] :as block} edit-input-id block-id slide? heading-level]
+  [config {:block/keys [uuid title body meta content dummy? page format repo children marker properties block-refs-count pre-block? idx] :as block} edit-input-id block-id slide? heading-level]
   (let [editor-box (get config :editor-box)
         edit? (state/sub [:editor/editing? edit-input-id])]
     (if (and edit? editor-box)
@@ -1562,7 +1548,45 @@
                                  (editor-handler/select-block! uuid)))}
                    edit-input-id
                    config)]
-      (block-content config block edit-input-id block-id slide?))))
+      [:div.flex.flex-row.block-content-wrapper
+       [:div.flex.flex-1
+        (block-content config block edit-input-id block-id slide?)]
+       [:div.flex.flex-row
+        (when (and (:embed? config) (not (:page-embed? config)))
+          [:a.opacity-30.hover:opacity-100.svg-small.inline
+           {:on-mouse-down (fn [e]
+                             (util/stop e)
+                             (when-let [block (:block config)]
+                               (editor-handler/edit-block! block :max (:block/format block) (:block/uuid block))))}
+           svg/edit])
+
+        (when (and (= (:block/marker block) "DONE")
+                   (state/enable-timetracking?))
+          (let [start-time (or
+                            (get properties :now)
+                            (get properties :doing)
+                            (get properties :in-progress)
+                            (get properties :later)
+                            (get properties :todo))
+                finish-time (get properties :done)]
+            (when (and start-time finish-time (> finish-time start-time))
+              [:div.text-sm.time-spent.ml-1 {:title (str (date/int->local-time start-time) " ~ " (date/int->local-time finish-time))
+                                              :style {:padding-top 3}}
+               [:a.opacity-30.hover:opacity-100
+                (utils/timeConversion (- finish-time start-time))]])))
+
+        (when (and block-refs-count (> block-refs-count 0))
+          [:div
+           [:a.open-block-ref-link.bg-base-2.text-sm.ml-2
+            {:title "Open block references"
+             :style {:margin-top -1}
+             :on-click (fn []
+                         (state/sidebar-add-block!
+                          (state/get-current-repo)
+                          (:db/id block)
+                          :block-ref
+                          {:block block}))}
+            block-refs-count]])]])))
 
 (rum/defc dnd-separator-wrapper < rum/reactive
   [block slide? top?]
@@ -1604,7 +1628,10 @@
                           (let [parents (doall
                                          (for [{:block/keys [uuid title name]} parents]
                                            (when-not name ; not page
-                                             [:a {:href (rfe/href :page {:name uuid})}
+                                             [:a {:on-mouse-down (fn [e]
+                                                                   (util/stop e)
+                                                                   (route-handler/redirect! {:to :page
+                                                                                             :path-params {:name uuid}}))}
                                               (map-inline config title)])))
                                 parents (remove nil? parents)]
                             (reset! parents-atom parents)
@@ -2126,7 +2153,7 @@
 
         ["Paragraph" l]
        ;; TODO: speedup
-        (if (re-find #"\"Export_Snippet\" \"embed\"" (str l))
+        (if (util/safe-re-find #"\"Export_Snippet\" \"embed\"" (str l))
           (->elem :div (map-inline config l))
           (->elem :div.is-paragraph (map-inline config l)))
 

+ 16 - 6
src/main/frontend/components/block.css

@@ -1,11 +1,17 @@
 .blocks-container {
 }
 
+.block-content-wrapper {
+    width: 100%;
+}
+
 .block-content {
   min-height: 24px;
   max-width: 100%;
   overflow: initial;
   cursor: text;
+  white-space: pre-wrap;
+  overflow-wrap: break-word;
   word-break: break-word;
 
   img {
@@ -86,11 +92,10 @@
 }
 
 .open-block-ref-link {
-  @apply py-0 px-1 rounded opacity-50 hover:opacity-100;
-  font-size: 12px;
-  line-height: 1em;
-  position: relative;
-  right: -4px;
+  @apply opacity-30 hover:opacity-100;
+  background-color: var(--ls-page-properties-background-color);
+  padding: 1px 4px;
+  border-radius: 2px;
 }
 
 .block-body {
@@ -160,11 +165,16 @@
   }
 }
 
-.blocks-properties {
+.block-properties {
+  margin: 4px 0;
   padding: 4px 8px;
   background-color: var(--ls-block-properties-background-color, #f0f8ff);
 }
 
+.page-properties {
+    background-color: var(--ls-page-properties-background-color);
+}
+
 .marker-switch {
   padding: 2px 4px;
   opacity: 0.5;

+ 6 - 0
src/main/frontend/components/editor.cljs

@@ -286,6 +286,7 @@
                         (< delta-width (* max-width 0.5))))] ;; FIXME: for translateY layer
     [:div.absolute.rounded-md.shadow-lg.absolute-modal
      {:class (if x-overflow? "is-overflow-vw-x" "")
+      :on-mouse-down (fn [e] (.stopPropagation e))
       :style (merge
               {:top        (+ top offset-top)
                :max-height to-max-height
@@ -378,7 +379,12 @@
   {:init (fn [state]
            (assoc state ::heading-level (:heading-level (first (:rum/args state)))))
    :did-mount (fn [state]
+                ;; TODO:
+                ;; if we quickly click into a block when editing another block,
+                ;; this will happen before the `will-unmount` event, which will
+                ;; lost the content in the editing block.
                 (state/set-editor-args! (:rum/args state))
+                ;; (js/setTimeout #(state/set-editor-args! (:rum/args state)) 20)
                 state)}
   (mixins/event-mixin setup-key-listener!)
   (shortcut/mixin :shortcut.handler/block-editing-only)

+ 3 - 3
src/main/frontend/components/export.cljs

@@ -14,6 +14,9 @@
        [:h1.title "Export"]
 
        [:ul.mr-1
+        [:li.mb-4
+         [:a.font-medium {:on-click #(export/convert-repo-markdown-v2! current-repo)}
+          (t :convert-markdown)]]
         (when (util/electron?)
           [:li.mb-4
            [:a.font-medium {:on-click #(export/export-repo-as-html! current-repo)}
@@ -21,9 +24,6 @@
         [:li.mb-4
          [:a.font-medium {:on-click #(export/export-repo-as-markdown! current-repo)}
           (t :export-markdown)]]
-        [:li.mb-4
-         [:a.font-medium {:on-click #(export/convert-repo-markdown-v2! current-repo)}
-          (t :convert-markdown)]]
         [:li.mb-4
          [:a.font-medium {:on-click #(export/export-repo-as-edn! current-repo)}
           (t :export-edn)]]]

+ 167 - 171
src/main/frontend/components/page.cljs

@@ -64,6 +64,7 @@
     (when (:block/dummy? block)
       (editor-handler/edit-block! block :max (:block/format block) (:block/uuid block))))
   state)
+
 (rum/defc page-blocks-inner <
   {:did-mount open-first-block!
    :did-update open-first-block!}
@@ -77,31 +78,46 @@
 
 (declare page)
 
+(defn- get-page-format
+  [page-name]
+  (let [block? (util/uuid-string? page-name)
+        block-id (and block? (uuid page-name))
+        page (if block-id
+               (:block/name (:block/page (db/entity [:block/uuid block-id])))
+               page-name)]
+    (db/get-page-format page)))
+
 (rum/defc page-blocks-cp < rum/reactive
   db-mixins/query
-  [repo page-e file-path page-name page-original-name encoded-page-name sidebar? journal? block? block-id format]
-  (let [raw-page-blocks (get-blocks repo page-name page-original-name block? block-id)
-        page-blocks (block-handler/with-dummy-block raw-page-blocks format
-                      (if (empty? raw-page-blocks)
-                        {:block/page {:db/id (:db/id page-e)}
-                         :block/file {:db/id (:db/id (:block/file page-e))}})
-                      {:journal? journal?
-                       :page-name page-name})
-        hiccup-config {:id (if block? (str block-id) page-name)
-                       :sidebar? sidebar?
-                       :block? block?
-                       :editor-box editor/box
-                       :page page}
-        hiccup-config (common-handler/config-with-document-mode hiccup-config)
-        hiccup (block/->hiccup page-blocks hiccup-config {})]
-    (page-blocks-inner page-name page-blocks hiccup sidebar?)))
+  [repo page-e sidebar?]
+  (when page-e
+    (let [page-name (or (:block/name page-e)
+                        (str (:block/uuid page-e)))
+          page-original-name (or (:block/original-name page-e) page-name)
+          format (get-page-format page-name)
+          journal? (db/journal-page? page-name)
+          block? (util/uuid-string? page-name)
+          block-id (and block? (uuid page-name))
+          raw-page-blocks (get-blocks repo page-name page-original-name block? block-id)
+          page-blocks (block-handler/with-dummy-block raw-page-blocks format
+                        (if (empty? raw-page-blocks)
+                          {:block/page {:db/id (:db/id page-e)}
+                           :block/file {:db/id (:db/id (:block/file page-e))}})
+                        {:journal? journal?
+                         :page-name page-name})
+          hiccup-config {:id (if block? (str block-id) page-name)
+                         :sidebar? sidebar?
+                         :block? block?
+                         :editor-box editor/box
+                         :page page}
+          hiccup-config (common-handler/config-with-document-mode hiccup-config)
+          hiccup (block/->hiccup page-blocks hiccup-config {})]
+      (page-blocks-inner page-name page-blocks hiccup sidebar?))))
 
 (defn contents-page
-  [{:block/keys [name original-name file] :as contents}]
+  [page]
   (when-let [repo (state/get-current-repo)]
-    (let [format (db/get-page-format name)
-          file-path (:file/path file)]
-      (page-blocks-cp repo contents file-path name original-name name true false false nil format))))
+    (page-blocks-cp repo page true)))
 
 (rum/defc today-queries < rum/reactive
   [repo today? sidebar?]
@@ -211,13 +227,6 @@
 
 ;; A page is just a logical block
 (rum/defcs page < rum/reactive
-  #_
-  {:did-mount (fn [state]
-                (ui-handler/scroll-and-highlight! state)
-                state)
-   :did-update (fn [state]
-                 (ui-handler/scroll-and-highlight! state)
-                 state)}
   [state {:keys [repo page-name preview?] :as option}]
   (when-let [path-page-name (or page-name
                                 (get-page-name state)
@@ -240,153 +249,140 @@
                           (db/entity repo))
                      (db/entity repo [:block/name page-name]))
              ;; TODO: replace page with frontend.format.block/page->map
-              page (if page page (do
-                                   (db/transact! repo [{:block/name page-name
-                                                        :block/original-name path-page-name
-                                                        :block/uuid (db/new-block-id)}])
-                                   (db/entity repo [:block/name page-name])))
-              {:keys [title] :as properties} (:block/properties page)
-              page-name (:block/name page)
-              page-original-name (:block/original-name page)
-              title (or title page-original-name page-name)
-              file (:block/file page)
-              file-path (and (:db/id file) (:file/path (db/entity repo (:db/id file))))
-              today? (and
-                      journal?
-                      (= page-name (string/lower-case (date/journal-name))))
-              developer-mode? (state/sub [:ui/developer-mode?])
-              public? (true? (:public properties))]
-          [:div.flex-1.page.relative (if (seq (:block/tags page))
-                                       (let [page-names (model/get-page-names-by-ids (map :db/id (:block/tags page)))]
-                                         {:data-page-tags (text/build-data-value page-names)})
-                                       {})
-           [:div.relative
-            (when (and (not sidebar?)
-                       (not block?))
-              [:div.flex.flex-row.space-between
-               [:div.flex-1.flex-row
-                [:a {:on-click (fn [e]
-                                 (.preventDefault e)
-                                 (when (gobj/get e "shiftKey")
-                                   (when-let [page (db/pull repo '[*] [:block/name page-name])]
-                                     (state/sidebar-add-block!
-                                      repo
-                                      (:db/id page)
-                                      :page
-                                      {:page page}))))}
-                 [:h1.title {:style {:margin-left -2}}
-                  (if page-original-name
-                    (if (and (string/includes? page-original-name "[[")
-                             (string/includes? page-original-name "]]"))
-                      (let [ast (mldoc/->edn page-original-name (mldoc/default-config format))]
-                        (block/markup-element-cp {} (ffirst ast)))
-                      page-original-name)
-                    (or
-                     page-name
-                     path-page-name))]]]
-               (when (not config/publishing?)
-                 (let [contents? (= (string/lower-case (str page-name)) "contents")
-                       links (fn [] (->>
-                                     [(when-not contents?
-                                        {:title   (t :page/add-to-contents)
-                                         :options {:on-click (fn [] (page-handler/handle-add-page-to-contents! page-original-name))}})
-
-                                      {:title "Go to presentation mode"
+             page (if page page (do
+                                  (db/transact! repo [{:block/name page-name
+                                                       :block/original-name path-page-name
+                                                       :block/uuid (db/new-block-id)}])
+                                  (db/entity repo [:block/name page-name])))
+             {:keys [title] :as properties} (:block/properties page)
+             page-name (:block/name page)
+             page-original-name (:block/original-name page)
+             title (or title page-original-name page-name)
+             today? (and
+                     journal?
+                     (= page-name (string/lower-case (date/journal-name))))
+             developer-mode? (state/sub [:ui/developer-mode?])
+             public? (true? (:public properties))]
+         [:div.flex-1.page.relative (if (seq (:block/tags page))
+                                      (let [page-names (model/get-page-names-by-ids (map :db/id (:block/tags page)))]
+                                        {:data-page-tags (text/build-data-value page-names)})
+                                      {})
+          [:div.relative
+           (when (and (not sidebar?)
+                      (not block?))
+             [:div.flex.flex-row.space-between
+              [:div.flex-1.flex-row
+               [:a {:on-click (fn [e]
+                                (.preventDefault e)
+                                (when (gobj/get e "shiftKey")
+                                  (when-let [page (db/pull repo '[*] [:block/name page-name])]
+                                    (state/sidebar-add-block!
+                                     repo
+                                     (:db/id page)
+                                     :page
+                                     {:page page}))))}
+                [:h1.title {:style {:margin-left -2}}
+                 (if page-original-name
+                   (if (and (string/includes? page-original-name "[[")
+                            (string/includes? page-original-name "]]"))
+                     (let [ast (mldoc/->edn page-original-name (mldoc/default-config format))]
+                       (block/markup-element-cp {} (ffirst ast)))
+                     page-original-name)
+                   (or
+                    page-name
+                    path-page-name))]]]
+              (when (not config/publishing?)
+                (let [contents? (= (string/lower-case (str page-name)) "contents")
+                      links (fn [] (->>
+                                   [(when-not contents?
+                                      {:title   (t :page/add-to-contents)
+                                       :options {:on-click (fn [] (page-handler/handle-add-page-to-contents! page-original-name))}})
+
+                                    {:title "Go to presentation mode"
+                                     :options {:on-click (fn []
+                                                           (state/sidebar-add-block!
+                                                            repo
+                                                            (:db/id page)
+                                                            :page-presentation
+                                                            {:page page}))}}
+                                    (when-not contents?
+                                      {:title   (t :page/rename)
+                                       :options {:on-click #(state/set-modal! (rename-page-dialog title page-name))}})
+
+                                    (when-let [file-path (and (util/electron?) (page-handler/get-page-file-path))]
+                                      [{:title   (t :page/open-in-finder)
+                                        :options {:on-click #(js/window.apis.showItemInFolder file-path)}}
+                                       {:title   (t :page/open-with-default-app)
+                                        :options {:on-click #(js/window.apis.openPath file-path)}}])
+
+                                    (when-not contents?
+                                      {:title   (t :page/delete)
+                                       :options {:on-click #(state/set-modal! (delete-page-dialog page-name))}})
+
+                                    (when (state/get-current-page)
+                                      {:title   (t :export)
+                                       :options {:on-click #(state/set-modal! export/export-page)}})
+
+                                    (when (util/electron?)
+                                      {:title   (t (if public? :page/make-private :page/make-public))
+                                       :options {:on-click
+                                                 (fn []
+                                                   (page-handler/update-public-attribute!
+                                                    page-name
+                                                    (if public? false true))
+                                                   (state/close-modal!))}})
+
+                                    (when developer-mode?
+                                      {:title   "(Dev) Show page data"
                                        :options {:on-click (fn []
-                                                             (state/sidebar-add-block!
-                                                              repo
-                                                              (:db/id page)
-                                                              :page-presentation
-                                                              {:page page}))}}
-                                      (when-not contents?
-                                        {:title   (t :page/rename)
-                                         :options {:on-click #(state/set-modal! (rename-page-dialog title page-name))}})
-
-                                      (when-let [file-path (and (util/electron?) (page-handler/get-page-file-path))]
-                                        [{:title   (t :page/open-in-finder)
-                                          :options {:on-click #(js/window.apis.showItemInFolder file-path)}}
-                                         {:title   (t :page/open-with-default-app)
-                                          :options {:on-click #(js/window.apis.openPath file-path)}}])
-
-                                      (when-not contents?
-                                        {:title   (t :page/delete)
-                                         :options {:on-click #(state/set-modal! (delete-page-dialog page-name))}})
-
-                                      (when (state/get-current-page)
-                                        {:title   (t :export)
-                                         :options {:on-click #(state/set-modal! export/export-page)}})
-
-                                      (when (util/electron?)
-                                        {:title   (t (if public? :page/make-private :page/make-public))
-                                         :options {:on-click
-                                                   (fn []
-                                                     (page-handler/update-public-attribute!
-                                                      page-name
-                                                      (if public? false true))
-                                                     (state/close-modal!))}})
-
-                                      (when developer-mode?
-                                        {:title   "(Dev) Show page data"
-                                         :options {:on-click (fn []
-                                                               (let [page-data (with-out-str (pprint/pprint (db/pull (:db/id page))))]
-                                                                 (println page-data)
-                                                                 (notification/show!
-                                                                  [:div
-                                                                   [:pre.code page-data]
-                                                                   [:br]
-                                                                   (ui/button "Copy to clipboard"
-                                                                              :on-click #(.writeText js/navigator.clipboard page-data))]
-                                                                  :success
-                                                                  false)))}})]
-                                     (flatten)
-                                     (remove nil?)))]
-                   [:div.flex.flex-row
-                    [:a.opacity-30.hover:opacity-100.page-op.mr-1
-                     {:title "Search in current page"
-                      :on-click #(route-handler/go-to-search! :page)}
-                     svg/search]
-                    (ui/dropdown-with-links
-                     (fn [{:keys [toggle-fn]}]
-                       [:a.cp__vertial-menu-button
-                        {:title    "More options"
-                         :on-click toggle-fn}
-                        (svg/vertical-dots nil)])
-                     links
-                     {:modal-class (util/hiccup->class
-                                    "origin-top-right.absolute.right-0.top-10.mt-2.rounded-md.shadow-lg.whitespace-no-wrap.dropdown-overflow-auto.page-drop-options")
-                      :z-index     1})]))])
-            [:div
-            ;; [:div.content
-            ;;  (when (and file-path
-            ;;             (not sidebar?)
-            ;;             (not block?)
-            ;;             (not (state/hide-file?))
-            ;;             (not config/publishing?))
-            ;;    [:div.text-sm.ml-1.mb-4.flex-1.inline-flex
-            ;;     {:key "page-file"}
-            ;;     [:span.opacity-50 {:style {:margin-top 2}} (t :file/file)]
-            ;;     [:a.bg-base-2.px-1.ml-1.mr-3 {:style {:border-radius 4
-            ;;                                           :word-break    "break-word"}
-            ;;                                   :href  (rfe/href :file {:path file-path})}
-            ;;      file-path]])]
-
-             (when (and repo (not block?))
-               (let [alias (db/get-page-alias-names repo page-name)]
-                 (when (seq alias)
-                   [:div.text-sm.ml-1.mb-4 {:key "page-file"}
-                    [:span.opacity-50 "Alias: "]
-                    (for [item alias]
-                      [:a.ml-1.mr-1 {:href (rfe/href :page {:name item})}
-                       item])])))
-
-             (when (and block? (not sidebar?))
-               (let [config {:id "block-parent"
-                             :block? true}]
-                 [:div.mb-4
-                  (block/block-parents config repo block-id format)]))
+                                                             (let [page-data (with-out-str (pprint/pprint (db/pull (:db/id page))))]
+                                                               (println page-data)
+                                                               (notification/show!
+                                                                [:div
+                                                                 [:pre.code page-data]
+                                                                 [:br]
+                                                                 (ui/button "Copy to clipboard"
+                                                                   :on-click #(.writeText js/navigator.clipboard page-data))]
+                                                                :success
+                                                                false)))}})]
+                                   (flatten)
+                                   (remove nil?)))]
+                  [:div.flex.flex-row
+                   [:a.opacity-30.hover:opacity-100.page-op.mr-1
+                    {:title "Search in current page"
+                     :on-click #(route-handler/go-to-search! :page)}
+                    svg/search]
+                   (ui/dropdown-with-links
+                    (fn [{:keys [toggle-fn]}]
+                      [:a.cp__vertial-menu-button
+                       {:title    "More options"
+                        :on-click toggle-fn}
+                       (svg/vertical-dots nil)])
+                    links
+                    {:modal-class (util/hiccup->class
+                                   "origin-top-right.absolute.right-0.top-10.mt-2.rounded-md.shadow-lg.whitespace-no-wrap.dropdown-overflow-auto.page-drop-options")
+                     :z-index     1})]))])
+           [:div
+            (when (and repo (not block?))
+              (let [alias (db/get-page-alias-names repo page-name)]
+                (when (seq alias)
+                  [:div.text-sm.ml-1.mb-4 {:key "page-file"}
+                   [:span.opacity-50 "Alias: "]
+                   (for [item alias]
+                     [:a.ml-1.mr-1 {:href (rfe/href :page {:name item})}
+                      item])])))
+
+            (when (and block? (not sidebar?))
+              (let [config {:id "block-parent"
+                            :block? true}]
+                [:div.mb-4
+                 (block/block-parents config repo block-id format)]))
 
             ;; blocks
-             (page-blocks-cp repo page file-path page-name page-original-name page-name sidebar? journal? block? block-id format)]]
+            (let [page (if block?
+                         (db/entity repo [:block/uuid block-id])
+                         page)]
+              (page-blocks-cp repo page sidebar?))]]
 
            (when-not block?
              (today-queries repo today? sidebar?))

+ 497 - 0
src/main/frontend/components/page.cljs.~e600c29046589a778b9a28a2122606358d7ab458~

@@ -0,0 +1,497 @@
+(ns frontend.components.page
+  (:require [rum.core :as rum]
+            [frontend.util :as util :refer-macros [profile]]
+            [frontend.util.marker :as marker]
+            [frontend.tools.html-export :as html-export]
+            [frontend.handler.file :as file]
+            [frontend.handler.page :as page-handler]
+            [frontend.handler.ui :as ui-handler]
+            [frontend.handler.common :as common-handler]
+            [frontend.handler.route :as route-handler]
+            [frontend.handler.graph :as graph-handler]
+            [frontend.handler.notification :as notification]
+            [frontend.handler.editor :as editor-handler]
+            [frontend.state :as state]
+            [clojure.string :as string]
+            [frontend.components.block :as block]
+            [frontend.components.editor :as editor]
+            [frontend.components.reference :as reference]
+            [frontend.components.svg :as svg]
+            [frontend.components.export :as export]
+            [frontend.extensions.graph-2d :as graph-2d]
+            [frontend.ui :as ui]
+            [frontend.components.content :as content]
+            [frontend.config :as config]
+            [frontend.db :as db]
+            [frontend.db.model :as model]
+            [frontend.db.utils :as db-utils]
+            [frontend.mixins :as mixins]
+            [frontend.db-mixins :as db-mixins]
+            [goog.dom :as gdom]
+            [goog.object :as gobj]
+            [frontend.utf8 :as utf8]
+            [frontend.date :as date]
+            [frontend.graph :as graph]
+            [frontend.format.mldoc :as mldoc]
+            [cljs-time.coerce :as tc]
+            [cljs-time.core :as t]
+            [cljs.pprint :as pprint]
+            [frontend.context.i18n :as i18n]
+            [reitit.frontend.easy :as rfe]
+            [frontend.text :as text]
+            [frontend.modules.shortcut.core :as shortcut]
+            [frontend.handler.block :as block-handler]))
+
+(defn- get-page-name
+  [state]
+  (let [route-match (first (:rum/args state))]
+    (get-in route-match [:parameters :path :name])))
+
+(defn- get-blocks
+  [repo page-name page-original-name block? block-id]
+  (when page-name
+    (if block?
+      (db/get-block-and-children repo block-id)
+      (do
+        (page-handler/add-page-to-recent! repo page-original-name)
+        (db/get-page-blocks repo page-name)))))
+
+(defn- open-first-block!
+  [state]
+  (let [blocks (nth (:rum/args state) 1)
+        block (first blocks)]
+    (when (:block/dummy? block)
+      (editor-handler/edit-block! block :max (:block/format block) (:block/uuid block))))
+  state)
+(rum/defc page-blocks-inner <
+  {:did-mount open-first-block!
+   :did-update open-first-block!}
+  [page-name page-blocks hiccup sidebar?]
+  [:div.page-blocks-inner
+   (rum/with-key
+     (content/content page-name
+                      {:hiccup   hiccup
+                       :sidebar? sidebar?})
+     (str page-name "-hiccup"))])
+
+(declare page)
+
+(defn- get-page-format
+  [page-name]
+  (let [block? (util/uuid-string? page-name)
+        block-id (and block? (uuid page-name))
+        page (if block-id
+               (:block/name (:block/page (db/entity [:block/uuid block-id])))
+               page-name)]
+    (db/get-page-format page)))
+
+(rum/defc page-blocks-cp < rum/reactive
+  db-mixins/query
+  [repo page-e sidebar?]
+  (let [page-name (or (:block/name page-e)
+                      (str (:block/uuid page-e)))
+        page-original-name (or (:block/original-name page-e) page-name)
+        format (get-page-format page-name)
+        journal? (db/journal-page? page-name)
+        block? (util/uuid-string? page-name)
+        block-id (and block? (uuid page-name))
+        raw-page-blocks (get-blocks repo page-name page-original-name block? block-id)
+        page-blocks (block-handler/with-dummy-block raw-page-blocks format
+                      (if (empty? raw-page-blocks)
+                        {:block/page {:db/id (:db/id page-e)}
+                         :block/file {:db/id (:db/id (:block/file page-e))}})
+                      {:journal? journal?
+                       :page-name page-name})
+        hiccup-config {:id (if block? (str block-id) page-name)
+                       :sidebar? sidebar?
+                       :block? block?
+                       :editor-box editor/box
+                       :page page}
+        hiccup-config (common-handler/config-with-document-mode hiccup-config)
+        hiccup (block/->hiccup page-blocks hiccup-config {})]
+    (page-blocks-inner page-name page-blocks hiccup sidebar?)))
+
+(defn contents-page
+  [page]
+  (when-let [repo (state/get-current-repo)]
+    (page-blocks-cp repo page true)))
+
+(rum/defc today-queries < rum/reactive
+  [repo today? sidebar?]
+  (when (and today? (not sidebar?))
+    (let [queries (state/sub [:config repo :default-queries :journals])]
+      (when (seq queries)
+        [:div#today-queries.mt-10
+         (for [{:keys [title] :as query} queries]
+           (rum/with-key
+             (block/custom-query {:attr {:class "mt-10"}
+                                  :editor-box editor/box
+                                  :page page} query)
+             (str repo "-custom-query-" (:query query))))]))))
+
+(defn- delete-page!
+  [page-name]
+  (page-handler/delete! page-name
+                        (fn []
+                          (notification/show! (str "Page " page-name " was deleted successfully!")
+                                              :success)))
+  (state/close-modal!)
+  (route-handler/redirect-to-home!))
+
+(defn delete-page-dialog
+  [page-name]
+  (fn [close-fn]
+    (rum/with-context [[t] i18n/*tongue-context*]
+      [:div
+       [:div.sm:flex.sm:items-start
+        [:div.mx-auto.flex-shrink-0.flex.items-center.justify-center.h-12.w-12.rounded-full.bg-red-100.sm:mx-0.sm:h-10.sm:w-10
+         [:svg.h-6.w-6.text-red-600
+          {:stroke "currentColor", :view-box "0 0 24 24", :fill "none"}
+          [:path
+           {:d
+            "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
+            :stroke-width "2"
+            :stroke-linejoin "round"
+            :stroke-linecap "round"}]]]
+        [:div.mt-3.text-center.sm:mt-0.sm:ml-4.sm:text-left
+         [:h3#modal-headline.text-lg.leading-6.font-medium
+          (t :page/delete-confirmation)]]]
+
+       [: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"
+           :on-click (fn []
+                       (delete-page! page-name))}
+          (t :yes)]]
+        [: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}
+          (t :cancel)]]]])))
+
+(rum/defcs rename-page-dialog-inner <
+  (shortcut/disable-all-shortcuts)
+  (rum/local "" ::input)
+  [state title page-name close-fn]
+  (let [input (get state ::input)]
+    (rum/with-context [[t] i18n/*tongue-context*]
+      [: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
+          (t :page/rename-to title)]]]
+
+       [:input.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2
+        {:auto-focus true
+         :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"
+           :on-click (fn []
+                       (let [value (string/trim @input)]
+                         (when-not (string/blank? value)
+                           (page-handler/rename! page-name value)
+                           (state/close-modal!))))}
+          (t :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}
+          (t :cancel)]]]])))
+
+(defn rename-page-dialog
+  [title page-name]
+  (fn [close-fn]
+    (rename-page-dialog-inner title page-name close-fn)))
+
+(defn tagged-pages
+  [repo tag]
+  (let [pages (db/get-tag-pages repo tag)]
+    (when (seq pages)
+      [:div.references.mt-6.flex-1.flex-row
+       [:div.content
+        (ui/foldable
+         [:h2.font-bold.opacity-50 (util/format "Pages tagged with \"%s\"" tag)]
+         [:ul.mt-2
+          (for [[original-name name] pages]
+            [:li {:key (str "tagged-page-" name)}
+             [:a {:href (rfe/href :page {:name name})}
+              original-name]])] false)]])))
+
+;; A page is just a logical block
+(rum/defcs page < rum/reactive
+  [state {:keys [repo page-name preview?] :as option}]
+  (when-let [path-page-name (or page-name
+                                (get-page-name state)
+                                (state/get-current-page))]
+    (let [current-repo (state/sub :git/current-repo)
+         repo (or repo current-repo)
+         page-name (string/lower-case path-page-name)
+         block? (util/uuid-string? page-name)
+         block-id (and block? (uuid page-name))
+         format (let [page (if block-id
+                             (:block/name (:block/page (db/entity [:block/uuid block-id])))
+                             page-name)]
+                  (db/get-page-format page))
+         journal? (db/journal-page? page-name)
+         sidebar? (:sidebar? option)]
+     (rum/with-context [[t] i18n/*tongue-context*]
+       (let [route-page-name path-page-name
+             page (if block?
+                    (->> (:db/id (:block/page (db/entity repo [:block/uuid block-id])))
+                         (db/entity repo))
+                    (db/entity repo [:block/name page-name]))
+             ;; TODO: replace page with frontend.format.block/page->map
+             page (if page page (do
+                                  (db/transact! repo [{:block/name page-name
+                                                       :block/original-name path-page-name
+                                                       :block/uuid (db/new-block-id)}])
+                                  (db/entity repo [:block/name page-name])))
+             {:keys [title] :as properties} (:block/properties page)
+             page-name (:block/name page)
+             page-original-name (:block/original-name page)
+             title (or title page-original-name page-name)
+             today? (and
+                     journal?
+                     (= page-name (string/lower-case (date/journal-name))))
+             developer-mode? (state/sub [:ui/developer-mode?])
+             public? (true? (:public properties))]
+         [:div.flex-1.page.relative (if (seq (:block/tags page))
+                                      (let [page-names (model/get-page-names-by-ids (map :db/id (:block/tags page)))]
+                                        {:data-page-tags (text/build-data-value page-names)})
+                                      {})
+          [:div.relative
+           (when (and (not sidebar?)
+                      (not block?))
+             [:div.flex.flex-row.space-between
+              [:div.flex-1.flex-row
+               [:a {:on-click (fn [e]
+                                (.preventDefault e)
+                                (when (gobj/get e "shiftKey")
+                                  (when-let [page (db/pull repo '[*] [:block/name page-name])]
+                                    (state/sidebar-add-block!
+                                     repo
+                                     (:db/id page)
+                                     :page
+                                     {:page page}))))}
+                [:h1.title {:style {:margin-left -2}}
+                 (if page-original-name
+                   (if (and (string/includes? page-original-name "[[")
+                            (string/includes? page-original-name "]]"))
+                     (let [ast (mldoc/->edn page-original-name (mldoc/default-config format))]
+                       (block/markup-element-cp {} (ffirst ast)))
+                     page-original-name)
+                   (or
+                    page-name
+                    path-page-name))]]]
+              (when (not config/publishing?)
+                (let [contents? (= (string/lower-case (str page-name)) "contents")
+                      links (fn [] (->>
+                                   [(when-not contents?
+                                      {:title   (t :page/add-to-contents)
+                                       :options {:on-click (fn [] (page-handler/handle-add-page-to-contents! page-original-name))}})
+
+                                    {:title "Go to presentation mode"
+                                     :options {:on-click (fn []
+                                                           (state/sidebar-add-block!
+                                                            repo
+                                                            (:db/id page)
+                                                            :page-presentation
+                                                            {:page page}))}}
+                                    (when-not contents?
+                                      {:title   (t :page/rename)
+                                       :options {:on-click #(state/set-modal! (rename-page-dialog title page-name))}})
+
+                                    (when-let [file-path (and (util/electron?) (page-handler/get-page-file-path))]
+                                      [{:title   (t :page/open-in-finder)
+                                        :options {:on-click #(js/window.apis.showItemInFolder file-path)}}
+                                       {:title   (t :page/open-with-default-app)
+                                        :options {:on-click #(js/window.apis.openPath file-path)}}])
+
+                                    (when-not contents?
+                                      {:title   (t :page/delete)
+                                       :options {:on-click #(state/set-modal! (delete-page-dialog page-name))}})
+
+                                    (when (state/get-current-page)
+                                      {:title   (t :export)
+                                       :options {:on-click #(state/set-modal! export/export-page)}})
+
+                                    (when (util/electron?)
+                                      {:title   (t (if public? :page/make-private :page/make-public))
+                                       :options {:on-click
+                                                 (fn []
+                                                   (page-handler/update-public-attribute!
+                                                    page-name
+                                                    (if public? false true))
+                                                   (state/close-modal!))}})
+
+                                    (when developer-mode?
+                                      {:title   "(Dev) Show page data"
+                                       :options {:on-click (fn []
+                                                             (let [page-data (with-out-str (pprint/pprint (db/pull (:db/id page))))]
+                                                               (println page-data)
+                                                               (notification/show!
+                                                                [:div
+                                                                 [:pre.code page-data]
+                                                                 [:br]
+                                                                 (ui/button "Copy to clipboard"
+                                                                   :on-click #(.writeText js/navigator.clipboard page-data))]
+                                                                :success
+                                                                false)))}})]
+                                   (flatten)
+                                   (remove nil?)))]
+                  [:div.flex.flex-row
+                   [:a.opacity-30.hover:opacity-100.page-op.mr-1
+                    {:title "Search in current page"
+                     :on-click #(route-handler/go-to-search! :page)}
+                    svg/search]
+                   (ui/dropdown-with-links
+                    (fn [{:keys [toggle-fn]}]
+                      [:a.cp__vertial-menu-button
+                       {:title    "More options"
+                        :on-click toggle-fn}
+                       (svg/vertical-dots nil)])
+                    links
+                    {:modal-class (util/hiccup->class
+                                   "origin-top-right.absolute.right-0.top-10.mt-2.rounded-md.shadow-lg.whitespace-no-wrap.dropdown-overflow-auto.page-drop-options")
+                     :z-index     1})]))])
+           [:div
+            (when (and repo (not block?))
+              (let [alias (db/get-page-alias-names repo page-name)]
+                (when (seq alias)
+                  [:div.text-sm.ml-1.mb-4 {:key "page-file"}
+                   [:span.opacity-50 "Alias: "]
+                   (for [item alias]
+                     [:a.ml-1.mr-1 {:href (rfe/href :page {:name item})}
+                      item])])))
+
+            (when (and block? (not sidebar?))
+              (let [config {:id "block-parent"
+                            :block? true}]
+                [:div.mb-4
+                 (block/block-parents config repo block-id format)]))
+
+            ;; blocks
+            (let [page (if block?
+                         (db/entity repo [:block/uuid block-id])
+                         page)]
+              (page-blocks-cp repo page sidebar?))]]
+
+          (when-not block?
+            (today-queries repo today? sidebar?))
+
+          (tagged-pages repo page-name)
+
+          ;; referenced blocks
+          [:div {:key "page-references"}
+           (rum/with-key
+             (reference/references route-page-name false)
+             (str route-page-name "-refs"))]
+
+          ;; TODO: or we can lazy load them
+          (when-not sidebar?
+            [:div {:key "page-unlinked-references"}
+             (reference/unlinked-references route-page-name)])])))))
+
+(defonce layout (atom [js/window.outerWidth js/window.outerHeight]))
+
+(defonce graph-ref (atom nil))
+(defonce show-journal? (atom false))
+
+(rum/defcs global-graph < rum/reactive
+  (mixins/event-mixin
+   (fn [state]
+     (mixins/listen state js/window "resize"
+                    (fn [e]
+                      (reset! layout [js/window.outerWidth js/window.outerHeight])))))
+  [state]
+  (let [theme (state/sub :ui/theme)
+        sidebar-open? (state/sub :ui/sidebar-open?)
+        [width height] (rum/react layout)
+        dark? (= theme "dark")
+        graph (graph-handler/build-global-graph theme (rum/react show-journal?))]
+    (rum/with-context [[t] i18n/*tongue-context*]
+      [:div.relative#global-graph
+       (if (seq (:nodes graph))
+         (graph-2d/graph
+          (graph/build-graph-opts
+           graph
+           dark?
+           {:width (if (and (> width 1280) sidebar-open?)
+                     (- width 24 600)
+                     (- width 24))
+            :height height
+            :ref (fn [v] (reset! graph-ref v))
+            :ref-atom graph-ref}))
+         [:div.ls-center.mt-20
+          [:p.opacity-70.font-medium "Empty"]])
+       [:div.absolute.top-10.left-5
+        [:div.flex.flex-col
+         [:a.text-sm.font-medium
+          {:on-click (fn [_e]
+                       (swap! show-journal? not))}
+          (str (t :page/show-journals)
+               (if @show-journal? " (ON)"))]]]])))
+
+(rum/defc all-pages < rum/reactive
+  ;; {:did-mount (fn [state]
+  ;;               (let [current-repo (state/sub :git/current-repo)]
+  ;;                 (js/setTimeout #(db/remove-orphaned-pages! current-repo) 0))
+  ;;               state)}
+  []
+  (let [current-repo (state/sub :git/current-repo)]
+    (rum/with-context [[t] i18n/*tongue-context*]
+      [:div.flex-1
+       [:h1.title (t :all-pages)]
+       (when current-repo
+         (let [pages (page-handler/get-pages-with-modified-at current-repo)]
+           [:table.table-auto
+            [:thead
+             [:tr
+              [:th (t :block/name)]
+              [:th (t :file/last-modified-at)]]]
+            [:tbody
+             (for [page pages]
+               [:tr {:key page}
+                [:td [:a {:on-click (fn [e]
+                                      (let [repo (state/get-current-repo)
+                                            page (db/pull repo '[*] [:block/name (string/lower-case page)])]
+                                        (when (gobj/get e "shiftKey")
+                                          (state/sidebar-add-block!
+                                           repo
+                                           (:db/id page)
+                                           :page
+                                           {:page page}))))
+                          :href (rfe/href :page {:name page})}
+                      page]]
+                [:td [:span.text-gray-500.text-sm
+                      (t :file/no-data)]]])]]))])))
+
+(rum/defcs new < rum/reactive
+  (rum/local "" ::title)
+  (mixins/event-mixin
+   (fn [state]
+     (mixins/on-enter state
+                      :node (gdom/getElement "page-title")
+                      :on-enter (fn []
+                                  (let [title @(get state ::title)]
+                                    (when-not (string/blank? title)
+                                      (page-handler/create! title)))))))
+  [state]
+  (rum/with-context [[t] i18n/*tongue-context*]
+    (let [title (get state ::title)]
+      [:div#page-new.flex-1.flex-col {:style {:flex-wrap "wrap"}}
+       [:div.mt-10.mb-2 {:style {:font-size "1.5rem"}}
+        (t :page/new-title)]
+       [:input#page-title.focus:outline-none.ml-1
+        {:style {:border "none"
+                 :font-size "1.8rem"
+                 :max-width 300}
+         :auto-focus true
+         :auto-complete "off"
+         :on-change (fn [e]
+                      (reset! title (util/evalue e)))}]])))

+ 1 - 1
src/main/frontend/components/search.cljs

@@ -33,7 +33,7 @@
           lc-content (string/lower-case content)
           lc-q (string/lower-case q)]
       (if (or (string/includes? lc-content lc-q)
-              (not (re-find #" " q)))
+              (not (util/safe-re-find #" " q)))
         (let [i (string/index-of lc-content lc-q)
               [before after] [(subs content 0 i) (subs content (+ i (count q)))]]
           [:p

+ 8 - 3
src/main/frontend/components/settings.cljs

@@ -8,6 +8,7 @@
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.config :as config-handler]
             [frontend.handler.page :as page-handler]
+            [frontend.handler.route :as route-handler]
             [frontend.handler :as handler]
             [frontend.state :as state]
             [frontend.version :refer [version]]
@@ -253,7 +254,12 @@
             {:on-change (fn [e]
                           (let [format (util/evalue e)]
                             (when-not (string/blank? format)
-                              (config-handler/set-config! :date-formatter format))))}
+                              (config-handler/set-config! :date-formatter format)
+                              (notification/show!
+                               [: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))]
               [:option (cond->
                         {:key format}
@@ -366,8 +372,7 @@
          [:div.mt-1.sm:mt-0.sm:col-span-2
           [:div.max-w-lg.rounded-md.sm:max-w-xs
            (ui/button (t :settings-page/clear)
-             :on-click (fn []
-                         (handler/clear-cache!)))]]]]
+             :on-click handler/clear-cache!)]]]]
 
        [:div.panel-wrap
         [:div.it.app-updater.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start

+ 10 - 1
src/main/frontend/components/svg.cljs

@@ -291,7 +291,6 @@
      {:d
       "M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"}]]))
 
-
 (rum/defc pinned
   []
   [:svg.h-8.w-8.pinned
@@ -529,3 +528,13 @@
      "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
      :clip-rule "evenodd"
      :fill-rule "evenodd"}]])
+
+(def edit
+  [:svg.h-6.w-6
+   {:stroke "currentColor", :viewbox "0 0 24 24", :fill "none"}
+   [:path
+    {:d
+     "M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z",
+     :stroke-width "2",
+     :stroke-linejoin "round",
+     :stroke-linecap "round"}]])

+ 0 - 113
src/main/frontend/components/theme.css

@@ -92,119 +92,6 @@ html[data-theme='light'] {
   }
 }
 
-html.is-electron {
-  --frame-top-height: 24px;
-
-  .theme-inner {
-  }
-
-  .cp__header {
-    height: 2.6rem;
-    background-color: var(--ls-primary-background-color);
-    top: 0;
-  }
-
-  &.is-mac {
-    .cp__header {
-      height: calc(2.2rem + var(--frame-top-height));
-      padding-top: var(--frame-top-height);
-
-      &-logo {
-        height: var(--frame-top-height);
-      }
-
-      &:before {
-        content: ' ';
-        position: fixed;
-        top: 0;
-        left: 0;
-        z-index: 8;
-        -webkit-app-region: drag;
-        width: 100%;
-        height: var(--frame-top-height);
-      }
-    }
-
-    .cp__right-sidebar {
-      top: 4rem;
-    }
-
-    &.is-fullscreen {
-      .cp__header {
-        padding-top: 0;
-        height: 2.6rem;
-
-        &:before {
-          display: none;
-        }
-      }
-    }
-  }
-
-  #search {
-    -webkit-app-region: drag;
-
-    #search-wrapper {
-      -webkit-app-region: no-drag;
-    }
-  }
-
-  .ls-window-frame-title-bar {
-    background-color: var(--ls-primary-background-color);
-    position: fixed;
-    left: 0;
-    right: 0;
-    z-index: 9;
-    height: var(--frame-top-height);
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-    user-select: none;
-    -webkit-app-region: drag;
-
-    & > .l {
-      display: flex;
-    }
-
-    & > .r {
-      & > .inner {
-        display: flex;
-      }
-    }
-
-    & > .c {
-      font-size: 0.7rem;
-    }
-
-    a.it {
-      padding: 0 2px;
-      cursor: pointer;
-      -webkit-app-region: no-drag;
-
-      &:hover {
-        background-color: var(--ls-secondary-background-color);
-      }
-
-      &:active {
-        background-color: var(--ls-primary-background-color);
-      }
-
-      svg {
-        transform: scale(0.6);
-        color: var(--ls-primary-text-color);
-        cursor: pointer;
-      }
-
-      &.maximize {
-        svg {
-          transform: scale(0.5) translateY(2px) translateX(1px);
-          opacity: 0.7;
-        }
-      }
-    }
-  }
-}
-
 html.locked-scroll {
   overflow: hidden !important;
 }

+ 2 - 2
src/main/frontend/config.cljs

@@ -101,7 +101,7 @@
 
 (def mobile?
   (when-not util/node-test?
-    (re-find #"Mobi" js/navigator.userAgent)))
+    (util/safe-re-find #"Mobi" js/navigator.userAgent)))
 
 ;; TODO: protocol design for future formats support
 
@@ -309,7 +309,7 @@
 
 (defn local-asset?
   [s]
-  (re-find (re-pattern (str "^[./]*" local-assets-dir)) s))
+  (util/safe-re-find (re-pattern (str "^[./]*" local-assets-dir)) s))
 
 (defn get-local-asset-absolute-path
   [s]

+ 3 - 3
src/main/frontend/db.cljs

@@ -106,9 +106,9 @@
   (d/listen! conn :persistence
              (fn [tx-report]
                (when-not (util/electron?)
-                (let [tx-id (get-tx-id tx-report)]
-                  (state/set-last-transact-time! repo (util/time-ms))
-                  (persist-if-idle! repo)))
+                 (let [tx-id (get-tx-id tx-report)]
+                   (state/set-last-transact-time! repo (util/time-ms))
+                   (persist-if-idle! repo)))
 
                ;; rebuild search indices
                (let [data (:tx-data tx-report)

+ 14 - 14
src/main/frontend/db/model.cljs

@@ -72,6 +72,7 @@
     :block/updated-at
     :block/file
     :block/parent
+    :block/unordered
     {:block/page [:db/id :block/name :block/original-name :block/journal-day]}
     {:block/_parent ...}])
 
@@ -316,9 +317,11 @@
 
 (defn get-page-format
   [page-name]
-  (when-let [file (:block/file (db-utils/entity [:block/name page-name]))]
-    (when-let [path (:file/path (db-utils/entity (:db/id file)))]
-      (format/get-format path))))
+  (or
+   (when-let [file (:block/file (db-utils/entity [:block/name page-name]))]
+     (when-let [path (:file/path (db-utils/entity (:db/id file)))]
+       (format/get-format path)))
+   :markdown))
 
 (defn page-alias-set
   [repo-url page]
@@ -967,17 +970,14 @@
     (when-let [conn (conn/get-conn repo)]
       (let [page-id (:db/id (db-utils/entity [:block/name page]))
             pattern (re-pattern (str "(?i)(?<!#)(?<!\\[\\[)" page "(?!\\]\\])"))]
-        (->> (react/q repo [:block/unlinked-refs page-id] {}
-               '[:find [(pull ?block ?block-attrs) ...]
-                 :in $ ?pattern ?block-attrs ?page-id
-                 :where
-                 [?block :block/content ?content]
-                 [?block :block/page ?page]
-                 [(not= ?page ?page-id)]
-                 [(re-find ?pattern ?content)]]
-               pattern
-               block-attrs
-               page-id)
+        (->> (react/q repo [:block/unlinked-refs page-id]
+               {:query-fn (fn [db]
+                            (let [ids (->> (d/datoms db :aevt :block/content)
+                                           (filter #(re-find pattern (:v %)))
+                                           (map :e))
+                                  result (d/pull-many db block-attrs ids)]
+                              (remove (fn [block] (= page-id (:db/id (:block/page block)))) result)))}
+               nil)
              react
              (sort-by-left-recursive)
              db-utils/group-by-page)))))

+ 6 - 3
src/main/frontend/db/query_dsl.cljs

@@ -218,8 +218,11 @@
 
        (and (= 'property fe)
             (= 3 (count e)))
-       (let [v (some-> (name (nth e 2))
-                       (text/page-ref-un-brackets!))
+       (let [v (nth e 2)
+             v (if (or (string? v) (symbol? v))
+                 (some-> (name v)
+                         (text/page-ref-un-brackets!))
+                 v)
              sym (if (= current-filter 'or)
                    '?v
                      (uniq-symbol counter "?v"))]
@@ -321,7 +324,7 @@
                                                  (remove string/blank?)
                                                  (map (fn [x]
                                                         (if (or (contains? #{"+" "-"} (first x))
-                                                                (and (re-find #"\d" (first x))
+                                                                (and (util/safe-re-find #"\d" (first x))
                                                                      (some #(string/ends-with? x %) ["y" "m" "d" "h" "min"])))
                                                           (keyword (name x))
                                                           x)))

+ 2 - 2
src/main/frontend/db/query_react.cljs

@@ -26,12 +26,12 @@
     (= :current-page input)
     (string/lower-case (state/get-current-page))
     (and (keyword? input)
-         (re-find #"^\d+d(-before)?$" (name input)))
+         (util/safe-re-find #"^\d+d(-before)?$" (name input)))
     (let [input (name input)
           days (util/parse-int (subs input 0 (dec (count input))))]
       (date->int (t/minus (t/today) (t/days days))))
     (and (keyword? input)
-         (re-find #"^\d+d(-after)?$" (name input)))
+         (util/safe-re-find #"^\d+d(-after)?$" (name input)))
     (let [input (name input)
           days (util/parse-int (subs input 0 (dec (count input))))]
       (date->int (t/plus (t/today) (t/days days))))

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

@@ -243,7 +243,7 @@
         :publishing "Publishing"
         :export "Export"
         :export-json "Export as JSON"
-        :export-markdown "Export as Markdown"
+        :export-markdown "Export as standard Markdown (no block properties)"
         :export-public-pages "Export public pages"
         :export-edn "Export as EDN"
         :convert-markdown "Convert Markdown headings to unordered lists (# -> -)"

+ 1 - 1
src/main/frontend/extensions/html_parser.cljs

@@ -77,7 +77,7 @@
                              :a (let [href (:href attrs)
                                       title (:title attrs)
                                       label (map-join children)
-                                      has-img-tag? (re-find #"\[:img" (str x))]
+                                      has-img-tag? (util/safe-re-find #"\[:img" (str x))]
                                   (if has-img-tag?
                                     (export-hiccup x)
                                     (case format

+ 38 - 32
src/main/frontend/format/block.cljs

@@ -159,7 +159,7 @@
   (atom #{"background-color" "background_color"}))
 
 (defn extract-properties
-  [[_ properties] _start-pos _end-pos]
+  [properties]
   (let [properties (into {} properties)
         page-refs (->>
                    (map (fn [v]
@@ -170,40 +170,46 @@
                                             (filter (fn [s] (= \# (first s))))
                                             (map (fn [s] (subs s 1))))]
                               (concat page-refs tags))))
-                        (vals properties))
+                     (vals properties))
                    (apply concat)
                    (remove string/blank?))
         properties (->> properties
                         (medley/map-kv (fn [k v]
-                                         (if (coll? v)
-                                           [(keyword k) v]
-                                           (let [k (name k)
-                                                v (string/trim v)
-                                                k (string/replace k " " "-")
-                                                 k (string/lower-case k)
-                                                v (cond
-                                                    (= v "true")
-                                                    true
-                                                    (= v "false")
-                                                    false
-
-                                                    (re-find #"^\d+$" v)
-                                                    (util/safe-parse-int v)
-
-                                                    (and (= "\"" (first v) (last v))) ; wrapped in ""
-                                                    (string/trim (subs v 1 (dec (count v))))
-
-                                                    (contains? @non-parsing-properties (string/lower-case k))
-                                                    v
-
-                                                    :else
-                                                    (let [v' v]
-                                                      (if (and k v'
-                                                               (contains? config/markers k)
-                                                               (util/safe-parse-int v'))
-                                                        (util/safe-parse-int v')
-                                                        (text/split-page-refs-without-brackets v' true))))]
-                                            [(keyword k) v])))))]
+                                         (let [v (if (coll? v)
+                                                   v
+                                                   (let [k (name k)
+                                                         v (string/trim v)
+                                                         k (string/replace k " " "-")
+                                                         k (string/lower-case k)
+                                                         v (cond
+                                                             (= v "true")
+                                                             true
+                                                             (= v "false")
+                                                             false
+
+                                                             (util/safe-re-find #"^\d+$" v)
+                                                             (util/safe-parse-int v)
+
+                                                             (and (= "\"" (first v) (last v))) ; wrapped in ""
+                                                             (string/trim (subs v 1 (dec (count v))))
+
+                                                             (contains? @non-parsing-properties (string/lower-case k))
+                                                             v
+
+                                                             :else
+                                                             (if (and k v
+                                                                      (contains? config/markers k)
+                                                                      (util/safe-parse-int v))
+                                                               (util/safe-parse-int v)
+                                                               (text/split-page-refs-without-brackets v true)))]
+                                                     v))
+                                               k (keyword k)
+                                               v (if (and
+                                                      (string? v)
+                                                      (contains? #{:alias :aliases :tags} k))
+                                                   (set [v])
+                                                   v)]
+                                           [k v]))))]
     {:properties properties
      :page-refs page-refs}))
 
@@ -445,7 +451,7 @@
                   (recur headings (conj block-body ["Paragraph" other-body]) (rest blocks) timestamps' properties last-pos last-level children))
 
                 (property/properties-ast? block)
-                (let [properties (extract-properties block start_pos end_pos)]
+                (let [properties (extract-properties (second block))]
                   (recur headings block-body (rest blocks) timestamps properties last-pos last-level children))
 
                 (heading-block? block)

+ 21 - 9
src/main/frontend/format/mldoc.cljs

@@ -118,15 +118,21 @@
           directive?
           (fn [[item _]] (= "directive" (string/lower-case (first item))))
           grouped-ast (group-by directive? original-ast)
-          [directive-ast other-ast]
-          [(get grouped-ast true) (get grouped-ast false)]
-          properties (->> (map first directive-ast)
-                          (map (fn [[_ k v]]
-                                 (let [k (keyword (string/lower-case k))
-                                       v (if (contains? #{:title :description :roam_tags} k)
-                                           v
-                                           (text/split-page-refs-without-brackets v true))]
-                                   [k v])))
+          directive-ast (get grouped-ast true)
+          [properties-ast other-ast] (if (= "Property_Drawer" (ffirst ast))
+                                       [(last (first ast))
+                                        (rest original-ast)]
+                                       [(->> (map first directive-ast)
+                                             (map rest))
+                                        (get grouped-ast false)])
+          properties (->>
+                      properties-ast
+                      (map (fn [[k v]]
+                             (let [k (keyword (string/lower-case k))
+                                   v (if (contains? #{:title :description :roam_tags} k)
+                                       v
+                                       (text/split-page-refs-without-brackets v true))]
+                               [k v])))
                           (reverse)
                           (into {}))
           macro-properties (filter (fn [x] (= :macro (first x))) properties)
@@ -190,6 +196,12 @@
               [block pos-meta])
             [block pos-meta])) ast)))
 
+(defn block-with-title?
+  [type]
+  (contains? #{"Paragraph"
+               "Raw_Html"
+               "Hiccup"} type))
+
 (defn ->edn
   [content config]
   (try

+ 5 - 2
src/main/frontend/fs/watcher_handler.cljs

@@ -23,7 +23,9 @@
           (when-not (db/file-exists? repo path)
             (let [_ (file-handler/alter-file repo path content {:re-render-root? true
                                                                 :from-disk? true})]
-              (db/set-file-last-modified-at! repo path mtime)))
+              (db/set-file-last-modified-at! repo path mtime)
+              ;; return nil, otherwise the entire db will be transfered by ipc
+              nil))
 
           (and (= "change" type)
                (not (db/file-exists? repo path)))
@@ -34,7 +36,8 @@
                  (> 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))
+            (db/set-file-last-modified-at! repo path mtime)
+            nil)
 
           (contains? #{"add" "change" "unlink"} type)
           nil

+ 9 - 1
src/main/frontend/handler.cljs

@@ -28,7 +28,9 @@
             [frontend.handler.common :as common-handler]
             [electron.listener :as el]
             [electron.ipc :as ipc]
-            [frontend.version :as version]))
+            [frontend.version :as version]
+            [frontend.components.page :as page]
+            [frontend.components.editor :as editor]))
 
 (defn- watch-for-date!
   []
@@ -157,10 +159,16 @@
               (ipc/ipc "clearCache"))]
     (js/window.location.reload)))
 
+(defn- register-components-fns!
+  []
+  (state/set-page-blocks-cp! page/page-blocks-cp)
+  (state/set-editor-cp! editor/box))
+
 (defn start!
   [render]
   (let [{:keys [me logged? repos]} (get-me-and-repos)]
     (when me (state/set-state! :me me))
+    (register-components-fns!)
     (state/set-db-restoring! true)
     (render)
     (on-load-events)

+ 1 - 1
src/main/frontend/handler/block.cljs

@@ -39,7 +39,7 @@
      (if (seq blocks)
        blocks
        (let [page-block (when page-name (db/pull [:block/name (string/lower-case page-name)]))
-             create-title-property? (util/include-windows-reserved-chars? page-name)
+             create-title-property? (and page-name (util/include-windows-reserved-chars? page-name))
              content (if create-title-property?
                        (let [title (or (:block/original-name page-block)
                                        (:block/name page-block))

+ 22 - 16
src/main/frontend/handler/editor.cljs

@@ -301,7 +301,7 @@
         top-level? (= parent page)
         markdown-heading? (and (= format :markdown)
                                (= "Heading" first-elem-type))
-        heading? (= "Paragraph" first-elem-type)
+        block-with-title? (mldoc/block-with-title? first-elem-type)
         content (string/triml content)
         content (string/replace content (util/format "((%s))" (str uuid)) "")
         [content content'] (cond
@@ -316,7 +316,7 @@
                                [content (str (config/get-block-pattern format) " " content)])
 
                              :else
-                             (let [content' (str (config/get-block-pattern format) (if heading? " " "\n") content)]
+                             (let [content' (str (config/get-block-pattern format) (if block-with-title? " " "\n") content)]
                                [content content']))
         block (assoc block :block/content content')
         block (apply dissoc block (remove #{:block/pre-block?} db-schema/retract-attributes))
@@ -563,7 +563,7 @@
 
 (defn- with-timetracking-properties
   [block value]
-  (let [new-marker (first (re-find marker/bare-marker-pattern (or value "")))
+  (let [new-marker (first (util/safe-re-find marker/bare-marker-pattern (or value "")))
         new-marker (if new-marker (string/lower-case (string/trim new-marker)))
         new-marker? (and
                      new-marker
@@ -695,7 +695,7 @@
           format (or (db/get-page-format (state/get-current-page))
                      (state/get-preferred-format))
           cond-fn (fn [marker] (or (and (= :markdown format)
-                                        (re-find (re-pattern (str "#*\\s*" marker)) content))
+                                        (util/safe-re-find (re-pattern (str "#*\\s*" marker)) content))
                                      (util/starts-with? content "TODO")))
           [new-content marker] (cond
                                  (cond-fn "TODO")
@@ -980,16 +980,22 @@
                                   zip/up
                                   (zip/append-child [block])))]
                         loc**)) (zip/vector-zip []) blocks)]
-    (zip/root loc)))
+
+    (clojure.walk/postwalk (fn [e] (if (map? e) (dissoc e :level) e)) (zip/root loc))))
 
 (defn- compose-copied-blocks-contents-&-block-tree
   [repo block-ids]
   (let [blocks (db-utils/pull-many repo '[*] (mapv (fn [id] [:block/uuid id]) block-ids))
-        unordered? (:block/unordered (first blocks))
-        format (:block/format (first blocks))
-        level-blocks-map (blocks-with-level blocks)
+        blocks* (flatten
+                 (mapv (fn [b] (if (:collapsed (:block/properties b))
+                                 (vec (tree/sort-blocks (db/get-block-children repo (:block/uuid b)) b))
+                                 [b])) blocks))
+        block-ids* (mapv :block/uuid blocks*)
+        unordered? (:block/unordered (first blocks*))
+        format (:block/format (first blocks*))
+        level-blocks-map (blocks-with-level blocks*)
         level-blocks-uuid-map (into {} (mapv (fn [b] [(:block/uuid b) b]) (vals level-blocks-map)))
-        level-blocks (mapv (fn [uuid] (get level-blocks-uuid-map uuid)) block-ids)
+        level-blocks (mapv (fn [uuid] (get level-blocks-uuid-map uuid)) block-ids*)
         tree (blocks-vec->tree level-blocks)
         contents
         (mapv (fn [block]
@@ -1565,7 +1571,7 @@
 (defn get-matched-commands
   [input]
   (try
-    (let [edit-content (gobj/get input "value")
+    (let [edit-content (or (gobj/get input "value") "")
           pos (util/get-input-pos input)
           last-slash-caret-pos (:pos @*slash-caret-pos)
           last-command (and last-slash-caret-pos (subs edit-content last-slash-caret-pos pos))]
@@ -2525,7 +2531,7 @@
   (let [tree (->>
               (block/extract-blocks
                (mldoc/->edn text (mldoc/default-config format)) text true format))
-        min-level (apply min (mapv #(:block/level %) tree))
+        min-level (apply min (mapv :block/level tree))
         prefix-level (if (> min-level 1) (- min-level 1) 0)
         tree* (->> tree
                    (mapv #(assoc % :level (- (:block/level %) prefix-level)))
@@ -2539,7 +2545,7 @@
         (string/join "\n"
                      (mapv (fn [p] (->> (string/trim p)
                                         ((fn [p]
-                                           (if (re-find (if (= format :org)
+                                           (if (util/safe-re-find (if (= format :org)
                                                           #"\s*\*+\s+"
                                                           #"\s*-\s+") p)
                                              p
@@ -2569,9 +2575,9 @@
         ;; from external
         (let [format (or (db/get-page-format (state/get-current-page)) :markdown)]
           (match [format
-                  (nil? (re-find #"(?m)^\s*(?:[-+*]|#+)\s+" text))
-                  (nil? (re-find #"(?m)^\s*\*+\s+" text))
-                  (nil? (re-find #"(?:\r?\n){2,}" text))]
+                  (nil? (util/safe-re-find #"(?m)^\s*(?:[-+*]|#+)\s+" text))
+                  (nil? (util/safe-re-find #"(?m)^\s*\*+\s+" text))
+                  (nil? (util/safe-re-find #"(?:\r?\n){2,}" text))]
             [:markdown false _ _]
             (do
               (paste-text-parseable format text)
@@ -2609,7 +2615,7 @@
                                                       (string? existed-file-path)
                                                       (not util/mac?)
                                                       (not util/win32?)) ; FIXME: linux
-                                                   (when (re-find #"^(/[^/ ]*)+/?$" existed-file-path)
+                                                   (when (util/safe-re-find #"^(/[^/ ]*)+/?$" existed-file-path)
                                                      existed-file-path)
                                                    existed-file-path)
                                has-file-path? (not (string/blank? existed-file-path))

+ 1 - 5
src/main/frontend/handler/editor/keyboards.cljs

@@ -21,11 +21,7 @@
            (let [{:keys [on-hide format value block id repo dummy?]} (editor-handler/get-state)]
              (when on-hide
                (on-hide value event))
-             (when
-              (or (= event :esc)
-                  (= event :visibilitychange)
-                  (and (= event :click)
-                       (not (editor-handler/auto-complete?))))
+             (when (contains? #{:esc :visibilitychange :click} event)
                (state/clear-edit!))))))
      :node (gdom/getElement id)
     ;; :visibilitychange? true

+ 0 - 1
src/main/frontend/handler/editor/lifecycle.cljs

@@ -10,7 +10,6 @@
             [frontend.handler.notification :as notification]
             [frontend.db :as db]
             [frontend.date :as date]
-            [frontend.handler.file :as file]
             [promesa.core :as p]))
 
 (defn did-mount!

+ 2 - 1
src/main/frontend/handler/export.cljs

@@ -163,7 +163,8 @@
               (let [path (string/lower-case path)]
                 (or (string/ends-with? path ".md")
                     (string/ends-with? path ".markdown"))))
-            (get-file-contents repo {:init-level 1 :heading-to-list? true}))))
+            (get-file-contents repo {:init-level 1
+                                     :heading-to-list? true}))))
 
 (defn- get-embed-and-refs-blocks-pages-aux []
   (let [mem (atom {})]

+ 3 - 3
src/main/frontend/handler/extract.cljs

@@ -28,8 +28,6 @@
          (map string/lower-case)
          (distinct))))
 
-
-
 (defn get-page-name
   [file ast]
   ;; headline
@@ -191,7 +189,9 @@
                        (fn [{:file/keys [path content]} contents]
                          (println "Parsing : " path)
                          (when content
-                           (let [utf8-content (utf8/encode content)]
+                           ;; TODO: remove `text/scheduled-deadline-dash->star` once migration is done
+                           (let [content (text/scheduled-deadline-dash->star content)
+                                 utf8-content (utf8/encode content)]
                              (extract-blocks-pages repo-url path content utf8-content)))))
                       (remove empty?))]
       (when (seq result)

+ 0 - 1
src/main/frontend/handler/history.cljs

@@ -1,7 +1,6 @@
 (ns frontend.handler.history
   (:require [frontend.state :as state]
             [frontend.db :as db]
-            [frontend.handler.file :as file]
             [frontend.handler.editor :as editor]
             [frontend.handler.ui :as ui-handler]
             [promesa.core :as p]

+ 12 - 7
src/main/frontend/handler/page.cljs

@@ -7,7 +7,6 @@
             [frontend.config :as config]
             [frontend.handler.common :as common-handler]
             [frontend.handler.route :as route-handler]
-            [frontend.handler.file :as file-handler]
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.web.nfs :as web-nfs]
@@ -73,7 +72,7 @@
          tx (block/page-name->map title true)
          format (state/get-preferred-format)
          page-entity [:block/uuid (:block/uuid tx)]
-         create-title-property? (util/include-windows-reserved-chars? title)
+         create-title-property? (and title (util/include-windows-reserved-chars? title))
          default-properties (default-properties-block title format page-entity)
          empty-block {:block/uuid (db/new-block-id)
                       :block/left [:block/uuid (:block/uuid default-properties)]
@@ -440,15 +439,21 @@
       (fn [chosen _click?]
         (state/set-editor-show-page-search! false)
         (let [wrapped? (= "[[" (util/safe-subs edit-content (- pos 2) pos))
-              chosen (if (and (re-find #"\s+" chosen) (not wrapped?))
+              chosen (if (and (util/safe-re-find #"\s+" chosen) (not wrapped?))
                        (util/format "[[%s]]" chosen)
-                       chosen)]
+                       chosen)
+              q (if @editor-handler/*selected-text "" q)
+              [last-pattern forward-pos] (if wrapped?
+                                           [q 3]
+                                           (if (= \# (first q))
+                                             [(subs q 1) 1]
+                                             [q 2]))
+              last-pattern (str "#" (when wrapped? "[[") last-pattern)]
           (editor-handler/insert-command! id
                                           (str "#" (when wrapped? "[[") chosen)
                                           format
-                                          {:last-pattern (let [q (if @editor-handler/*selected-text "" q)]
-                                                           (str "#" (when wrapped? "[[") q))
-                                           :forward-pos (if wrapped? 3 2)})))
+                                          {:last-pattern last-pattern
+                                           :forward-pos forward-pos})))
       (fn [chosen _click?]
         (state/set-editor-show-page-search! false)
         (let [page-ref-text (get-page-ref-text chosen)]

+ 23 - 21
src/main/frontend/handler/web/nfs.cljs

@@ -42,27 +42,29 @@
 
 (defn- ->db-files
   [electron? dir-name result]
-  (if electron?
-    (map (fn [{:keys [path stat content]}]
-           (let [{:keys [mtime size]} stat]
-             {:file/path             path
-              :file/last-modified-at mtime
-              :file/size             size
-              :file/content content}))
-         result)
-    (let [result (flatten (bean/->clj result))]
-      (map (fn [file]
-             (let [handle (gobj/get file "handle")
-                   get-attr #(gobj/get file %)
-                   path (-> (get-attr "webkitRelativePath")
-                            (string/replace-first (str dir-name "/") ""))]
-               {:file/name             (get-attr "name")
-                :file/path             path
-                :file/last-modified-at (get-attr "lastModified")
-                :file/size             (get-attr "size")
-                :file/type             (get-attr "type")
-                :file/file             file
-                :file/handle           handle})) result))))
+  (->>
+   (if electron?
+     (map (fn [{:keys [path stat content]}]
+            (let [{:keys [mtime size]} stat]
+              {:file/path             path
+               :file/last-modified-at mtime
+               :file/size             size
+               :file/content content}))
+       result)
+     (let [result (flatten (bean/->clj result))]
+       (map (fn [file]
+              (let [handle (gobj/get file "handle")
+                    get-attr #(gobj/get file %)
+                    path (-> (get-attr "webkitRelativePath")
+                             (string/replace-first (str dir-name "/") ""))]
+                {:file/name             (get-attr "name")
+                 :file/path             path
+                 :file/last-modified-at (get-attr "lastModified")
+                 :file/size             (get-attr "size")
+                 :file/type             (get-attr "type")
+                 :file/file             file
+                 :file/handle           handle})) result)))
+   (sort-by :file/path)))
 
 (defn- filter-markup-and-built-in-files
   [files]

+ 46 - 44
src/main/frontend/modules/file/core.cljs

@@ -21,68 +21,70 @@
     (string/join (str "\n" spaces-tabs) lines)))
 
 (defn transform-content
-  [{:block/keys [format pre-block? title content unordered body heading-level left page]} level heading-to-list?]
+  [{:block/keys [format pre-block? title content unordered body heading-level left page scheduled deadline]} level {:keys [heading-to-list?]}]
   (let [content (or content "")
         heading-with-title? (seq title)
         first-block? (= left page)
         pre-block? (and first-block? pre-block?)
-        markdown-heading? (and (= format :markdown) (not unordered) (not heading-to-list?))]
-    (cond
-      (and first-block? pre-block?)
-      (let [content (-> (string/trim content)
-                        ;; FIXME: should only works with :filters
-                        (string/replace "\"" "\\\""))]
-        (str content "\n"))
+        markdown-heading? (and (= format :markdown) (not unordered) (not heading-to-list?))
+        content (cond
+                  (and first-block? pre-block?)
+                  (let [content (-> (string/trim content)
+                                    ;; FIXME: should only works with :filters
+                                    (string/replace "\"" "\\\""))]
+                    (str content "\n"))
 
-      :else
-      (let [[prefix spaces-tabs]
-            (cond
-              (= format :org)
-              [(->>
-                (repeat level "*")
-                (apply str)) ""]
+                  :else
+                  (let [[prefix spaces-tabs]
+                        (cond
+                          (= format :org)
+                          [(->>
+                            (repeat level "*")
+                            (apply str)) ""]
 
-              markdown-heading?
-              ["" ""]
+                          markdown-heading?
+                          ["" ""]
 
-              :else
-              (let [level (if (and heading-to-list? heading-level)
-                            (if (> heading-level 1)
-                              (dec heading-level)
-                              heading-level)
-                            level)
-                    spaces-tabs (->>
-                                 (repeat (dec level) (state/get-export-bullet-indentation))
-                                 (apply str))]
-                [(str spaces-tabs "-") (str spaces-tabs "  ")]))
-            content (if heading-to-list?
-                      (-> (string/replace content #"^\s?#+\s+" "")
-                          (string/replace #"^\s?#+\s?$" ""))
-                      content)
-            new-content (indented-block-content (string/trim content) spaces-tabs)
-            sep (cond
-                  markdown-heading?
-                  ""
+                          :else
+                          (let [level (if (and heading-to-list? heading-level)
+                                        (if (> heading-level 1)
+                                          (dec heading-level)
+                                          heading-level)
+                                        level)
+                                spaces-tabs (->>
+                                             (repeat (dec level) (state/get-export-bullet-indentation))
+                                             (apply str))]
+                            [(str spaces-tabs "-") (str spaces-tabs "  ")]))
+                        content (if heading-to-list?
+                                  (-> (string/replace content #"^\s?#+\s+" "")
+                                      (string/replace #"^\s?#+\s?$" ""))
+                                  content)
+                        new-content (indented-block-content (string/trim content) spaces-tabs)
+                        sep (cond
+                              markdown-heading?
+                              ""
 
-                  heading-with-title?
-                  " "
+                              heading-with-title?
+                              " "
 
-                  (string/blank? new-content)
-                  ""
+                              (string/blank? new-content)
+                              ""
 
-                  :else
-                  (str "\n" spaces-tabs))]
-        (str prefix sep new-content)))))
+                              :else
+                              (str "\n" spaces-tabs))]
+                    (str prefix sep new-content)))]
+    content))
 
 (defn tree->file-content
   [tree {:keys [init-level heading-to-list?]
-         :or {heading-to-list? false}}]
+         :or {heading-to-list? false}
+         :as opts}]
   (loop [block-contents []
          [f & r] tree
          level init-level]
     (if (nil? f)
       (string/join "\n" block-contents)
-      (let [content (transform-content f level heading-to-list?)
+      (let [content (transform-content f level opts)
             new-content
             (if-let [children (seq (:block/children f))]
               [content (tree->file-content children {:init-level (inc level)})]

+ 2 - 2
src/main/frontend/modules/outliner/tree.cljs

@@ -77,7 +77,7 @@
             root-block (with-children-and-refs root-block result)]
         [root-block]))))
 
-(defn sort-blocks-aux
+(defn- sort-blocks-aux
   [parents parent-groups]
   (mapv (fn [parent]
           (let [parent-id {:db/id (:db/id parent)}
@@ -90,5 +90,5 @@
 (defn sort-blocks
   "sort blocks by parent & left"
   [blocks-exclude-root root]
-  (let [parent-groups (atom (group-by #(:block/parent %) blocks-exclude-root))]
+  (let [parent-groups (atom (group-by :block/parent blocks-exclude-root))]
     (flatten (concat (sort-blocks-aux [root] parent-groups) (vals @parent-groups)))))

+ 5 - 5
src/main/frontend/modules/shortcut/dict.cljs

@@ -32,7 +32,7 @@
      :shortcut.ui/toggle-theme                "在暗色/亮色主题之间切换"
      :shortcut.ui/toggle-right-sidebar        "启用/关闭右侧栏"
      :shortcut.ui/toggle-settings             "显示/关闭设置"
-     :shortcut.ui/toggle-new-block            "切换 Enter/Alt+Enter 以插入新块"
+     :shortcut.ui/toggle-new-block            "切换 Enter/Shift+Enter 以插入新块"
      :shortcut.go/journals                    "跳转到日记"
      ;; TODO translate those in fr/de/etc..
      :shortcut.category/basics                "基础操作"
@@ -96,7 +96,7 @@
      :shortcut.ui/toggle-document-mode        "切換文檔模式"
      :shortcut.ui/toggle-theme                "“在暗色/亮色主題之間切換”"
      :shortcut.ui/toggle-right-sidebar        "啟用/關閉右側欄"
-     :shortcut.ui/toggle-new-block            "切換 Enter/Alt+Enter 以插入新塊"
+     :shortcut.ui/toggle-new-block            "切換 Enter/Shift+Enter 以插入新塊"
      :shortcut.go/journals                    "跳轉到日記"
      :shortcut.category/formatting            "格式化"}
     :de
@@ -118,7 +118,7 @@
      :shortcut.ui/toggle-document-mode        "Dokumentenmodus umschalten"
      :shortcut.ui/toggle-theme                "Umschalten zwischen dunklem/hellem Thema"
      :shortcut.ui/toggle-right-sidebar        "Rechte Seitenleiste umschalten"
-     :shortcut.ui/toggle-new-block            "Umschalten von Enter/Alt+Enter zum Einfügen eines neuen Blocks"
+     :shortcut.ui/toggle-new-block            "Umschalten von Enter/Shift+Enter zum Einfügen eines neuen Blocks"
      :shortcut.go/journals                    "Zu Journalen springen"
      :shortcut.git/commit                     "Git Commit-Nachricht"
      :shortcut.editor/select-block-down       "Block unterhalb auswählen"
@@ -146,7 +146,7 @@
      :shortcut.ui/toggle-document-mode        "Intervertir le mode document"
      :shortcut.ui/toggle-theme                "Intervertir le thème foncé/clair"
      :shortcut.ui/toggle-right-sidebar        "Afficher/cacher la barre latérale"
-     :shortcut.ui/toggle-new-block            "Activer Entreée ou Alt+Enter pour insérer un bloc"
+     :shortcut.ui/toggle-new-block            "Activer Entreée ou Shift+Enter pour insérer un bloc"
      :shortcut.go/journals                    "Aller au Journal"
      :shortcut.category/formatting            "Formats"}
     :af
@@ -173,4 +173,4 @@
      :shortcut.category/formatting            "Formatering"
      :shortcut.ui/toggle-theme                "Wissel tussen donker/lig temas"
      :shortcut.ui/toggle-right-sidebar        "Wissel regter sybalk"
-     :shortcut.ui/toggle-new-block            "Wissel Enter/Alt+enter vir die byvoeging van nuwe blokke"}}))
+     :shortcut.ui/toggle-new-block            "Wissel Enter/Shift+enter vir die byvoeging van nuwe blokke"}}))

+ 3 - 2
src/main/frontend/security.cljs

@@ -1,5 +1,6 @@
 (ns frontend.security
-  (:require [clojure.walk :as walk]))
+  (:require [clojure.walk :as walk]
+            [frontend.util :as util]))
 
 ;; To prevent from cross-site scripting vulnerability, we should add security checks for both hiccup and raw html.
 ;; Hiccup: [:a {:href "javascript:alert('hei')"} "click me"]
@@ -11,7 +12,7 @@
    (= :a (first f))
    (:href (second f))
    (:href (second f))
-   (re-find #"(?i)javascript" (:href (second f)))))
+   (util/safe-re-find #"(?i)javascript" (:href (second f)))))
 
 (defn remove-javascript-links-in-href
   [hiccup]

+ 21 - 3
src/main/frontend/state.cljs

@@ -129,7 +129,9 @@
     ;; copied blocks
     :copy/blocks {:copy/content nil :copy/block-tree nil}
 
-    :date-picker/date nil}))
+    :date-picker/date nil
+
+    :view/components {}}))
 
 (defn get-route-match
   []
@@ -313,7 +315,7 @@
    (or
     (when-let [workflow (:preferred-workflow (get-config))]
       (let [workflow (name workflow)]
-        (if (re-find #"now|NOW" workflow)
+        (if (util/safe-re-find #"now|NOW" workflow)
           :now
           :todo)))
     (get-in @state [:me :preferred_workflow] :now))))
@@ -1257,7 +1259,7 @@
 
 (defn get-export-bullet-indentation
   []
-  (case (get (get-config) :export/bullet-indentation :two-spaces)
+  (case (get (get-config) :export/bullet-indentation :tab)
     :eight-spaces
     "        "
     :four-spaces
@@ -1266,3 +1268,19 @@
     "  "
     :tab
     "\t"))
+
+(defn set-page-blocks-cp!
+  [value]
+  (set-state! [:view/components :page-blocks] value))
+
+(defn get-page-blocks-cp
+  []
+  (get-in @state [:view/components :page-blocks]))
+
+(defn set-editor-cp!
+  [value]
+  (set-state! [:view/components :editor] value))
+
+(defn get-editor-cp
+  []
+  (get-in @state [:view/components :editor]))

+ 12 - 16
src/main/frontend/text.cljs

@@ -78,8 +78,8 @@
    (cond
      (and (string? s)
             ;; Either a page ref, a tag or a comma separated collection
-            (or (re-find page-ref-re s)
-                (re-find (if comma? #"[\,|,|#]+" #"#") s)))
+            (or (util/safe-re-find page-ref-re s)
+                (util/safe-re-find (if comma? #"[\,|,|#]+" #"#") s)))
      (let [result (->> (string/split s page-ref-re-2)
                        (map (fn [s] (if (string/ends-with? (string/trimr s) "]],")
                                      (let [s (string/trimr s)]
@@ -109,7 +109,7 @@
     (let [pattern (util/format
                    "^[%s]+\\s?"
                    (config/get-block-pattern format))]
-      (re-find (re-pattern pattern) text))
+      (util/safe-re-find (re-pattern pattern) text))
     ""))
 
 (defn- remove-level-space-aux!
@@ -136,18 +136,6 @@
      :else
      (remove-level-space-aux! text (config/get-block-pattern format) space?))))
 
-(defn append-newline-after-level-spaces
-  [text format]
-  (if-not (string/blank? text)
-    (let [pattern (util/format
-                   "^[%s]+\\s?\n?"
-                   (config/get-block-pattern format))
-          matched-text (re-find (re-pattern pattern) text)]
-      (if matched-text
-        (string/replace-first text matched-text (str (string/trimr matched-text) "\n"))
-        text))))
-
-
 (defn build-data-value
   [col]
   (let [items (map (fn [item] (str "\"" item "\"")) col)]
@@ -156,4 +144,12 @@
 
 (defn image-link?
   [img-formats s]
-  (some (fn [fmt] (re-find (re-pattern (str "(?i)\\." fmt "(?:\\?([^#]*))?(?:#(.*))?$")) s)) img-formats))
+  (some (fn [fmt] (util/safe-re-find (re-pattern (str "(?i)\\." fmt "(?:\\?([^#]*))?(?:#(.*))?$")) s)) img-formats))
+
+(defn scheduled-deadline-dash->star
+  [content]
+  (-> content
+      (string/replace "- TODO -> DONE [" "* TODO -> DONE [")
+      (string/replace "- DOING -> DONE [" "* DOING -> DONE [")
+      (string/replace "- LATER -> DONE [" "* LATER -> DONE [")
+      (string/replace "- NOW -> DONE [" "* NOW -> DONE [")))

+ 6 - 6
src/main/frontend/ui.cljs

@@ -340,12 +340,12 @@
              {:id       (str "ac-" idx)
               :class    (when (= @current-idx idx)
                           "chosen")
-               ;; :tab-index -1
-              :on-click (fn [e]
-                          (.preventDefault e)
-                          (if (and (gobj/get e "shiftKey") on-shift-chosen)
-                            (on-shift-chosen item)
-                            (on-chosen item)))}
+              ;; :tab-index -1
+              :on-mouse-down (fn [e]
+                               (util/stop e)
+                               (if (and (gobj/get e "shiftKey") on-shift-chosen)
+                                 (on-shift-chosen item)
+                                 (on-chosen item)))}
              (if item-render (item-render item) item))
             idx))]
        (when empty-div

+ 43 - 34
src/main/frontend/util.cljc

@@ -49,11 +49,20 @@
        (and (string/includes? ua "webkit")
             (not (string/includes? ua "chrome"))))))
 
+(defn safe-re-find
+  [pattern s]
+  #?(:cljs
+     (when-not (string? s)
+       ;; TODO: sentry
+       (js/console.trace)))
+  (when (string? s)
+    (re-find pattern s)))
+
 #?(:cljs
    (defn mobile?
      []
      (when-not node-test?
-       (re-find #"Mobi" js/navigator.userAgent))))
+       (safe-re-find #"Mobi" js/navigator.userAgent))))
 
 #?(:cljs
    (defn electron?
@@ -292,29 +301,29 @@
 ;; Caret
 #?(:cljs
    (defn caret-range [node]
-     (let [doc (or (gobj/get node "ownerDocument")
-                   (gobj/get node "document"))
-           win (or (gobj/get doc "defaultView")
-                   (gobj/get doc "parentWindow"))
-           selection (.getSelection win)]
-       (if selection
-         (let [range-count (gobj/get selection "rangeCount")]
-           (when (> range-count 0)
-             (let [range (-> (.getSelection win)
-                             (.getRangeAt 0))
-                   pre-caret-range (.cloneRange range)]
-               (.selectNodeContents pre-caret-range node)
-               (.setEnd pre-caret-range
-                        (gobj/get range "endContainer")
-                        (gobj/get range "endOffset"))
-               (.toString pre-caret-range))))
-         (when-let [selection (gobj/get doc "selection")]
-           (when (not= "Control" (gobj/get selection "type"))
-             (let [text-range (.createRange selection)
-                   pre-caret-text-range (.createTextRange (gobj/get doc "body"))]
-               (.moveToElementText pre-caret-text-range node)
-               (.setEndPoint pre-caret-text-range "EndToEnd" text-range)
-               (gobj/get pre-caret-text-range "text"))))))))
+     (when-let [doc (or (gobj/get node "ownerDocument")
+                        (gobj/get node "document"))]
+       (let [win (or (gobj/get doc "defaultView")
+                     (gobj/get doc "parentWindow"))
+             selection (.getSelection win)]
+         (if selection
+           (let [range-count (gobj/get selection "rangeCount")]
+             (when (> range-count 0)
+               (let [range (-> (.getSelection win)
+                               (.getRangeAt 0))
+                     pre-caret-range (.cloneRange range)]
+                 (.selectNodeContents pre-caret-range node)
+                 (.setEnd pre-caret-range
+                          (gobj/get range "endContainer")
+                          (gobj/get range "endOffset"))
+                 (.toString pre-caret-range))))
+           (when-let [selection (gobj/get doc "selection")]
+             (when (not= "Control" (gobj/get selection "type"))
+               (let [text-range (.createRange selection)
+                     pre-caret-text-range (.createTextRange (gobj/get doc "body"))]
+                 (.moveToElementText pre-caret-text-range node)
+                 (.setEndPoint pre-caret-text-range "EndToEnd" text-range)
+                 (gobj/get pre-caret-text-range "text")))))))))
 
 #?(:cljs
    (defn set-caret-pos!
@@ -391,7 +400,7 @@
 #?(:cljs
    (defn scroll-to-element
      [elem-id]
-     (when-not (re-find #"^/\d+$" elem-id)
+     (when-not (safe-re-find #"^/\d+$" elem-id)
        (when elem-id
          (when-let [elem (gdom/getElement elem-id)]
            (.scroll (app-scroll-container-node)
@@ -860,11 +869,11 @@
 (defonce exactly-uuid-pattern (re-pattern (str "^" uuid-pattern "$")))
 (defn uuid-string?
   [s]
-  (re-find exactly-uuid-pattern s))
+  (safe-re-find exactly-uuid-pattern s))
 
 (defn extract-uuid
   [s]
-  (re-find (re-pattern uuid-pattern) s))
+  (safe-re-find (re-pattern uuid-pattern) s))
 
 (defn drop-nth [n coll]
   (keep-indexed #(if (not= %1 n) %2) coll))
@@ -876,7 +885,7 @@
 
 (defn file-page?
   [page-name]
-  (when page-name (re-find #"\." page-name)))
+  (when page-name (safe-re-find #"\." page-name)))
 
 #?(:cljs
    (defn react
@@ -925,7 +934,7 @@
    (defn get-prev-block-with-same-level
      [block]
      (let [id (gobj/get block "id")
-           prefix (re-find #"ls-block-[\d]+" id)]
+           prefix (safe-re-find #"ls-block-[\d]+" id)]
        (when-let [blocks (d/by-class "ls-block")]
          (when-let [index (.indexOf blocks block)]
            (let [level (d/attr block "level")]
@@ -996,8 +1005,8 @@
   [tag-name]
   (when tag-name
     (and
-     (not (re-find #"#" tag-name))
-     (re-find regex/valid-tag-pattern tag-name))))
+     (not (safe-re-find #"#" tag-name))
+     (safe-re-find regex/valid-tag-pattern tag-name))))
 
 (defn encode-str
   [s]
@@ -1032,8 +1041,8 @@
      []
      (let [user-agent js/navigator.userAgent
            vendor js/navigator.vendor]
-       (and (re-find #"Chrome" user-agent)
-            (re-find #"Google Inc" user-agent)))))
+       (and (safe-re-find #"Chrome" user-agent)
+            (safe-re-find #"Google Inc" user-agent)))))
 
 #?(:cljs
    (defn indexeddb-check?
@@ -1089,7 +1098,7 @@
 
 (defn include-windows-reserved-chars?
   [s]
-  (re-find windows-reserved-chars s))
+  (safe-re-find windows-reserved-chars s))
 
 (defn page-name-sanity
   [page-name]

+ 1 - 1
src/main/frontend/util/marker.cljs

@@ -26,7 +26,7 @@
         (if-let [matches (seq (util/re-pos new-line-re-pattern content))]
           (let [[start-pos content] (last matches)]
             (+ start-pos (count content)))
-          (count (re-find re-pattern content)))
+          (count (util/safe-re-find re-pattern content)))
         new-content
         (str (subs content 0 pos)
              (string/replace-first (subs content pos)

+ 1 - 1
src/main/frontend/util/priority.cljs

@@ -14,7 +14,7 @@
         (if-let [matches (seq (util/re-pos new-line-re-pattern content))]
           (let [[start-pos content] (last matches)]
             (+ start-pos (count content)))
-          (count (re-find re-pattern content)))
+          (count (util/safe-re-find re-pattern content)))
         skip-marker-pos
         (if-let [matches (seq (util/re-pos marker/bare-marker-pattern (subs content skip-hash-pos)))]
           (let [[start-pos content] (last matches)]

+ 30 - 16
src/main/frontend/util/property.cljs

@@ -3,7 +3,8 @@
             [frontend.util :as util]
             [clojure.set :as set]
             [frontend.config :as config]
-            [medley.core :as medley]))
+            [medley.core :as medley]
+            [frontend.format.mldoc :as mldoc]))
 
 (defonce properties-start ":PROPERTIES:")
 (defonce properties-end ":END:")
@@ -24,34 +25,34 @@
 (defn contains-properties?
   [content]
   (and (string/includes? content properties-start)
-       (re-find properties-end-pattern content)))
+       (util/safe-re-find properties-end-pattern content)))
 
 (defn simplified-property?
   [line]
   (boolean
    (and (string? line)
-        (re-find #"^\s?[^ ]+:: " line))))
+        (util/safe-re-find #"^\s?[^ ]+:: " line))))
 
 (defn front-matter-property?
   [line]
   (boolean
    (and (string? line)
-        (re-find #"^\s*[^ ]+: " line))))
+        (util/safe-re-find #"^\s*[^ ]+: " line))))
 
 (defn get-property-key
   [line format]
   (and (string? line)
        (when-let [key (last
                        (if (= format :org)
-                         (re-find #"^\s*:([^: ]+): " line)
-                         (re-find #"^\s*([^ ]+):: " line)))]
+                         (util/safe-re-find #"^\s*:([^: ]+): " line)
+                         (util/safe-re-find #"^\s*([^ ]+):: " line)))]
          (keyword key))))
 
 (defn org-property?
   [line]
   (boolean
    (and (string? line)
-        (re-find #"^\s*:[^: ]+: " line)
+        (util/safe-re-find #"^\s*:[^: ]+: " line)
         (when-let [key (get-property-key line :org)]
           (not (contains? #{:PROPERTIES :END} key))))))
 
@@ -92,7 +93,7 @@
     (let [org? (= format :org)
           kv-format (if org? ":%s: %s" "%s:: %s")
           full-format (if org? ":PROPERTIES:\n%s\n:END:\n" "%s\n")
-          properties-content (->> (map (fn [[k v]] (util/format kv-format k v)) properties)
+          properties-content (->> (map (fn [[k v]] (util/format kv-format (name k) v)) properties)
                                   (string/join "\n"))]
       (util/format full-format properties-content))))
 
@@ -141,19 +142,26 @@
   ([format content key value]
    (insert-property format content key value false))
   ([format content key value front-matter?]
-   (when (and (not (string/blank? (name key)))
+   (when (and (string? content)
+              (not (string/blank? (name key)))
               (not (string/blank? (str value))))
-     (let [org? (= :org format)
+     (let [ast (mldoc/->edn content (mldoc/default-config format))
+           title? (mldoc/block-with-title? (ffirst (map first ast)))
+           lines (string/split-lines content)
+           [title body] (if title?
+                          [(first lines) (string/join "\n" (rest lines))]
+                          [nil (string/join "\n" lines)])
+           org? (= :org format)
            key (string/lower-case (name key))
            value (string/trim (str value))
-           lines (string/split-lines content)
            start-idx (.indexOf lines properties-start)
            end-idx (.indexOf lines properties-end)]
        (cond
          (and org? (not (contains-properties? content)))
-         (let [properties (build-properties-str format {key value})
-               [title body] (util/safe-split-first "\n" content)]
-           (str title "\n" properties body))
+         (let [properties (build-properties-str format {key value})]
+           (if title
+             (str title "\n" properties body)
+             (str properties content)))
 
          (and (>= start-idx 0) (> end-idx 0) (> end-idx start-idx))
          (let [exists? (atom false)
@@ -198,9 +206,15 @@
                                  lines))
                              groups)
                lines (if no-properties?
-                       (if (string/blank? content)
+                       (cond
+                         (string/blank? content)
                          [new-property-s]
-                         (cons (first lines) (cons new-property-s (rest lines))))
+
+                         title?
+                         (cons (first lines) (cons new-property-s (rest lines)))
+
+                         :else
+                         (cons new-property-s lines))
                        lines)]
            (string/join "\n" lines))
 

+ 36 - 0
src/test/frontend/format/block_test.cljs

@@ -0,0 +1,36 @@
+(ns frontend.format.block-test
+  (:require [frontend.format.block :as block]
+            [cljs.test :refer [deftest is are testing use-fixtures run-tests]]))
+
+(deftest test-extract-properties
+  (are [x y] (= (:properties (block/extract-properties x)) y)
+    [["year" "1000"]] {:year 1000}
+    [["year" "\"1000\""]] {:year "1000"}
+    [["background-color" "#000000"]] {:background-color "#000000"}
+    [["alias" "name/with space"]] {:alias #{"name/with space"}}
+    [["year" "1000"] ["alias" "name/with space"]] {:year 1000, :alias #{"name/with space"}}
+    [["year" "1000"] ["tags" "name/with space"]] {:year 1000, :tags #{"name/with space"}}
+    [["year" "1000"] ["tags" "name/with space, another"]] {:year 1000, :tags #{"name/with space" "another"}}
+    [["year" "1000"] ["alias" "name/with space, another"]] {:year 1000, :alias #{"name/with space" "another"}}
+    [["year" "1000"] ["alias" "name/with space, [[another [[nested]]]]"]] {:year 1000, :alias #{"name/with space" "another [[nested]]"}}
+    ;; FIXME:
+    ;; [["year" "1000"] ["alias" "name/with space, [[[[nested]] another]]"]] {:year 1000, :alias #{"name/with space" "[[nested]] another"}}
+    [["foo" "bar"]] {:foo "bar"}
+    [["foo" "bar, baz"]] {:foo #{"bar" "baz"}}
+    [["foo" "bar, [[baz]]"]] {:foo #{"bar" "baz"}}
+    [["foo" "[[bar]], [[baz]]"]] {:foo #{"bar" "baz"}}
+    [["foo" "[[bar]], [[nested [[baz]]]]"]] {:foo #{"bar" "nested [[baz]]"}}
+    [["foo" "[[bar]], [[nested [[baz]]]]"]] {:foo #{"bar" "nested [[baz]]"}}
+    [["foo" "bar, [[baz, test]]"]] {:foo #{"bar" "baz, test"}}
+    [["foo" "bar, [[baz, test, [[nested]]]]"]] {:foo #{"bar" "baz, test, [[nested]]"}})
+
+  (are [x y] (= (vec (:page-refs (block/extract-properties x))) y)
+    [["year" "1000"]] []
+    [["year" "\"1000\""]] []
+    [["foo" "[[bar]] test"]] ["bar"]
+    [["foo" "[[bar]] test [[baz]]"]] ["bar" "baz"]
+    ;; FIXME:
+    ;; [["foo" "[[bar]] test [[baz]] [[nested [[baz]]]]"]] ["bar" "baz" "nested [[baz]]"]
+    ))
+
+#_(run-tests)

+ 0 - 12
src/test/frontend/text_test.cljs

@@ -83,16 +83,4 @@
       "**foobar" "foobar"
       "*********************foobar" "foobar")))
 
-(deftest append-newline-after-level-spaces
-  []
-  (are [x y] (= (text/append-newline-after-level-spaces x :markdown) y)
-    "# foobar" "#\nfoobar"
-    "# foobar\nfoo" "#\nfoobar\nfoo"
-    "## foobar\nfoo" "##\nfoobar\nfoo")
-
-  (are [x y] (= (text/append-newline-after-level-spaces x :org) y)
-    "* foobar" "*\nfoobar"
-    "* foobar\nfoo" "*\nfoobar\nfoo"
-    "** foobar\nfoo" "**\nfoobar\nfoo"))
-
 #_(cljs.test/test-ns 'frontend.text-test)

+ 33 - 8
src/test/frontend/util/property_test.cljs

@@ -1,4 +1,4 @@
-(ns frontend.util.property_test
+(ns frontend.util.property-test
   (:require [cljs.test :refer [deftest is are testing]]
             [frontend.util.property :as property]))
 
@@ -19,10 +19,7 @@
       "hello\n\nworld"
 
       "hello\naa:: bb\nid:: f9873a81-07b9-4246-b910-53a6f5ec7e04\n\nworld"
-      "hello\naa:: bb\n\nworld"
-      )
-    )
-  )
+      "hello\naa:: bb\n\nworld")))
 
 (deftest test-remove-properties
   (testing "properties with non-blank lines"
@@ -61,11 +58,24 @@
     (property/insert-property :org "hello\n:PROPERTIES:\n:a: b\n:END:\n" "c" "d")
     "hello\n:PROPERTIES:\n:a: b\n:c: d\n:END:"
 
-    (property/insert-property :org "hello\n:PROPERTIES:\n:a: b\n:END: world\n" "c" "d")
-    "hello\n:PROPERTIES:\n:c: d\n:END:\n:PROPERTIES:\n:a: b\n:END: world\n"
+    (property/insert-property :org "hello\n:PROPERTIES:\n:a: b\n:END:\nworld\n" "c" "d")
+    "hello\n:PROPERTIES:\n:a: b\n:c: d\n:END:\nworld"
+
+    (property/insert-property :org "#+BEGIN_QUOTE
+ hello world
+  #+END_QUOTE" "c" "d")
+    ":PROPERTIES:\n:c: d\n:END:\n#+BEGIN_QUOTE\n hello world\n  #+END_QUOTE"
 
     (property/insert-property :markdown "hello\na:: b\nworld\n" "c" "d")
-    "hello\na:: b\nc:: d\nworld"))
+    "hello\na:: b\nc:: d\nworld"
+
+    (property/insert-property :markdown "> quote" "c" "d")
+    "c:: d\n> quote"
+
+    (property/insert-property :markdown "#+BEGIN_QUOTE
+ hello world
+  #+END_QUOTE" "c" "d")
+    "c:: d\n#+BEGIN_QUOTE\n hello world\n  #+END_QUOTE"))
 
 (deftest test->new-properties
   (are [x y] (= (property/->new-properties x) y)
@@ -90,5 +100,20 @@
     "hello\n:PROPERTIES:\n:foo: bar\n:nice\n:END:\nnice"
     "hello\nfoo:: bar\n:nice\nnice"))
 
+(deftest test-build-properties-str
+  (are [x y] (= (property/build-properties-str :mardown x) y)
+    {:title "a"}
+    "title:: a\n"
+    {:title "a/b/c"}
+    "title:: a/b/c\n"
+    {:title "a/b/c" :tags "d,e"}
+    "title:: a/b/c\ntags:: d,e\n")
+  (are [x y] (= (property/build-properties-str :org x) y)
+    {:title "a"}
+    ":PROPERTIES:\n:title: a\n:END:\n"
+    {:title "a/b/c"}
+    ":PROPERTIES:\n:title: a/b/c\n:END:\n"
+    {:title "a/b/c" :tags "d,e"}
+    ":PROPERTIES:\n:title: a/b/c\n:tags: d,e\n:END:\n"))
 
 #_(cljs.test/run-tests)

+ 4 - 4
yarn.lock

@@ -3860,10 +3860,10 @@ mkdirp@^0.5.4, mkdirp@~0.5.1:
   dependencies:
     minimist "^1.2.5"
 
[email protected]6:
-  version "0.6.16"
-  resolved "https://registry.yarnpkg.com/mldoc/-/mldoc-0.6.16.tgz#6cad0c9ce0015c92a630d7dc4c4fb2835f34eb98"
-  integrity sha512-9f9hCPaxIjd5bFugR1pgcH6m5Fwjnme9eGVty/uUch+k81wdey+fCzbp8szazP2J3Eneu5Z/181Tre501mpwwQ==
[email protected]8:
+  version "0.6.18"
+  resolved "https://registry.yarnpkg.com/mldoc/-/mldoc-0.6.18.tgz#a1c0b76d193ef9cd98c2763de3e8414cd83b9368"
+  integrity sha512-Bg460Jdp4kBgmAYUdVtWG7AZSQ0EWVNsiWQ/7mljVLQp5yyERcySzw1nM4Hs5eZ5IKStzl6qz4m6XN/q11AUJw==
   dependencies:
     yargs "^12.0.2"
 

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov