Просмотр исходного кода

Merge remote-tracking branch 'upstream/master' into whiteboards

Peng Xiao 3 лет назад
Родитель
Сommit
37d0ad2524
44 измененных файлов с 615 добавлено и 99 удалено
  1. 2 1
      .carve/ignore
  2. 2 2
      android/app/build.gradle
  3. 4 0
      deps/graph-parser/.carve/ignore
  4. 7 1
      deps/graph-parser/src/logseq/graph_parser/block.cljs
  5. 2 1
      deps/graph-parser/src/logseq/graph_parser/cli.cljs
  6. 3 3
      deps/graph-parser/src/logseq/graph_parser/config.cljs
  7. 2 1
      deps/graph-parser/src/logseq/graph_parser/mldoc.cljc
  8. 14 1
      deps/graph-parser/src/logseq/graph_parser/property.cljs
  9. 39 11
      deps/graph-parser/src/logseq/graph_parser/text.cljs
  10. 1 7
      deps/graph-parser/src/logseq/graph_parser/util.cljs
  11. 5 0
      deps/graph-parser/src/logseq/graph_parser/util/page_ref.cljs
  12. 14 1
      deps/graph-parser/test/logseq/graph_parser/property_test.cljs
  13. 6 1
      deps/graph-parser/test/logseq/graph_parser/text_test.cljs
  14. 138 1
      deps/graph-parser/test/logseq/graph_parser_test.cljs
  15. 1 1
      resources/js/preload.js
  16. 1 1
      resources/package.json
  17. 1 1
      shadow-cljs.edn
  18. 18 0
      src/electron/electron/find_in_page.cljs
  19. 8 1
      src/electron/electron/handler.cljs
  20. 2 2
      src/electron/electron/window.cljs
  21. 11 1
      src/main/electron/listener.cljs
  22. 10 3
      src/main/frontend/components/block.cljs
  23. 87 0
      src/main/frontend/components/find_in_page.cljs
  24. 21 0
      src/main/frontend/components/find_in_page.css
  25. 1 1
      src/main/frontend/components/search.cljs
  26. 4 0
      src/main/frontend/components/sidebar.cljs
  27. 1 1
      src/main/frontend/db.cljs
  28. 0 12
      src/main/frontend/db/model.cljs
  29. 10 3
      src/main/frontend/db/query_dsl.cljs
  30. 3 2
      src/main/frontend/handler/editor.cljs
  31. 8 5
      src/main/frontend/handler/page.cljs
  32. 54 1
      src/main/frontend/handler/search.cljs
  33. 21 1
      src/main/frontend/modules/shortcut/config.cljs
  34. 3 0
      src/main/frontend/modules/shortcut/dicts.cljc
  35. 1 0
      src/main/frontend/state.cljs
  36. 7 1
      src/main/frontend/ui.cljs
  37. 5 3
      src/main/frontend/util.cljc
  38. 8 6
      src/main/frontend/utils.js
  39. 1 1
      src/main/frontend/version.cljs
  40. 42 9
      src/test/frontend/db/query_dsl_test.cljs
  41. 19 0
      src/test/frontend/test/frontend_node_test_runner.cljs
  42. 8 0
      src/test/frontend/test/helper.clj
  43. 16 12
      src/test/frontend/test/node_test_runner.cljs
  44. 4 0
      templates/config.edn

+ 2 - 1
.carve/ignore

@@ -75,7 +75,8 @@ frontend.util/trace!
 frontend.util.pool/terminate-pool!
 ;; Repl fn
 frontend.util.property/add-page-properties
-;; Test runner used by shadow
+;; Test runners used by shadow
 frontend.test.node-test-runner/main
+frontend.test.frontend-node-test-runner/main
 ;; Test runner for nbb
 logseq.graph-parser.nbb-test-runner/run-tests

+ 2 - 2
android/app/build.gradle

@@ -6,8 +6,8 @@ android {
         applicationId "com.logseq.app"
         minSdkVersion rootProject.ext.minSdkVersion
         targetSdkVersion rootProject.ext.targetSdkVersion
-        versionCode 35
-        versionName "0.8.1"
+        versionCode 36
+        versionName "0.8.2"
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
         aaptOptions {
              // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

+ 4 - 0
deps/graph-parser/.carve/ignore

@@ -18,3 +18,7 @@ logseq.graph-parser.util.page-ref/left-and-right-brackets
 logseq.graph-parser.util.page-ref/->page-ref
 ;; API
 logseq.graph-parser.util.page-ref/get-page-name!
+;; API
+logseq.graph-parser.property/->block-content
+;; API
+logseq.graph-parser.property/property-value-from-content

+ 7 - 1
deps/graph-parser/src/logseq/graph_parser/block.cljs

@@ -157,6 +157,8 @@
              distinct)
     []))
 
+;; TODO: Use text/parse-property to determine refs rather than maintain this similar
+;; implementation to parse-property
 (defn- get-page-ref-names-from-properties
   [format properties user-config]
   (let [page-refs (->>
@@ -174,7 +176,11 @@
                             (and (string? v)
                                  (not (gp-mldoc/link? format v)))
                             (let [v (string/trim v)
-                                  result (text/split-page-refs-without-brackets v {:un-brackets? false})]
+                                  result (if (:rich-property-values? user-config)
+                                           (if (gp-util/wrapped-by-quotes? v)
+                                             []
+                                             (text/extract-page-refs-and-tags v))
+                                           (text/split-page-refs-without-brackets v {:un-brackets? false}))]
                               (if (coll? result)
                                 (map text/page-ref-un-brackets! result)
                                 []))

+ 2 - 1
deps/graph-parser/src/logseq/graph_parser/cli.cljs

@@ -44,7 +44,8 @@ TODO: Fail fast when process exits 1"
 
 (defn- parse-files
   [conn files {:keys [config] :as options}]
-  (let [extract-options (merge {:date-formatter (gp-config/get-date-formatter config)}
+  (let [extract-options (merge {:date-formatter (gp-config/get-date-formatter config)
+                                :user-config config}
                                (select-keys options [:verbose]))]
     (mapv
      (fn [{:file/keys [path content]}]

+ 3 - 3
deps/graph-parser/src/logseq/graph_parser/config.cljs

@@ -1,7 +1,6 @@
 (ns logseq.graph-parser.config
   "Config that is shared between graph-parser and rest of app"
-  (:require [logseq.graph-parser.util :as gp-util]
-            [clojure.set :as set]
+  (:require [clojure.set :as set]
             [clojure.string :as string]))
 
 (def app-name
@@ -12,7 +11,8 @@
 
 (defn local-asset?
   [s]
-  (gp-util/safe-re-find (re-pattern (str "^[./]*" local-assets-dir)) s))
+  (and (string? s)
+       (re-find (re-pattern (str "^[./]*" local-assets-dir)) s)))
 
 (defonce default-draw-directory "draws")
 ;; TODO read configurable value?

+ 2 - 1
deps/graph-parser/src/logseq/graph_parser/mldoc.cljc

@@ -154,7 +154,8 @@
                           (remove string/blank?)))
           tags (:tags properties)
           tags (->> (->vec-concat tags filetags)
-                    (remove string/blank?))
+                    (remove string/blank?)
+                    vec)
           properties (assoc properties :tags tags :alias alias)
           properties (-> properties
                          (update :filetags (constantly filetags)))

+ 14 - 1
deps/graph-parser/src/logseq/graph_parser/property.cljs

@@ -8,6 +8,19 @@
 
 (def colons "Property delimiter for markdown mode" "::")
 
+(defn ->block-content
+  "Creates a block content string from properties map"
+  [properties]
+  (->> properties
+       (map #(str (name (key %)) (str colons " ") (val %)))
+       (string/join "\n")))
+
+(defn property-value-from-content
+  "Extracts full property value from block content"
+  [property content]
+  (second (re-find (re-pattern (str property colons "\\s+(.*)"))
+                   content)))
+
 (defn properties-ast?
   [block]
   (and
@@ -60,7 +73,7 @@
   [content]
   (when content
     (and (string/includes? content properties-start)
-         (gp-util/safe-re-find properties-end-pattern content))))
+         (re-find properties-end-pattern content))))
 
 (defn ->new-properties
   "New syntax: key:: value"

+ 39 - 11
deps/graph-parser/src/logseq/graph_parser/text.cljs

@@ -5,6 +5,7 @@
             [clojure.set :as set]
             [logseq.graph-parser.mldoc :as gp-mldoc]
             [logseq.graph-parser.util :as gp-util]
+            [logseq.graph-parser.property :as gp-property]
             [logseq.graph-parser.util.page-ref :as page-ref :refer [right-brackets]]))
 
 (defn get-file-basename
@@ -109,8 +110,8 @@
 
      (and (string? s)
             ;; Either a page ref, a tag or a comma separated collection
-            (or (gp-util/safe-re-find page-ref/page-ref-re s)
-                (gp-util/safe-re-find #"[\,|,|#|\"]+" s)))
+            (or (re-find page-ref/page-ref-re s)
+                (re-find #"[\,|,|#|\"]+" s)))
      (let [result (->> (sep-by-quotes s)
                        (mapcat
                         (fn [s]
@@ -199,7 +200,35 @@
 (defonce non-parsing-properties
   (atom #{"background-color" "background_color"}))
 
+(defn parse-non-string-property-value
+  "Return parsed non-string property value or nil if none is found"
+  [v]
+  (cond
+    (= v "true")
+    true
+
+    (= v "false")
+    false
+
+    (re-find #"^\d+$" v)
+    (parse-long v)))
+
+(def ^:private page-ref-or-tag-re
+  (re-pattern (str "#?" (page-ref/->page-ref-re-str "(.*?)") "|"
+                   ;; Don't capture punctuation at end of a tag
+                   "#([\\S]+[^\\s.!,])")))
+
+(defn extract-page-refs-and-tags
+  "Returns set of page-refs and tags in given string or returns string if none
+  are found"
+  [string]
+  (let [refs (map #(or (second %) (get % 2))
+                  (re-seq page-ref-or-tag-re string))]
+    (if (seq refs) (set refs) string)))
+
 (defn parse-property
+  "Property value parsing that takes into account built-in properties, format
+  and user config"
   ([k v config-state]
    (parse-property :markdown k v config-state))
   ([format k v config-state]
@@ -212,14 +241,6 @@
                    (get config-state :ignored-page-references-keywords)) k)
        v
 
-       (= v "true")
-       true
-       (= v "false")
-       false
-
-       (and (not= k "alias") (gp-util/safe-re-find #"^\d+$" v))
-       (parse-long v)
-
        (gp-util/wrapped-by-quotes? v) ; wrapped in ""
        v
 
@@ -229,5 +250,12 @@
        (gp-mldoc/link? format v)
        v
 
+       (contains? gp-property/editable-linkable-built-in-properties (keyword k))
+       (split-page-refs-without-brackets v)
+
        :else
-       (split-page-refs-without-brackets v)))))
+       (if-some [res (parse-non-string-property-value v)]
+         res
+         (if (:rich-property-values? config-state)
+           (extract-page-refs-and-tags v)
+           (split-page-refs-without-brackets v)))))))

+ 1 - 7
deps/graph-parser/src/logseq/graph_parser/util.cljs

@@ -6,12 +6,6 @@
             [logseq.graph-parser.log :as log]
             [cljs.reader :as reader]))
 
-(defn safe-re-find
-  "Copy of frontend.util/safe-re-find. Too basic to couple to main app"
-  [pattern s]
-  (when (string? s)
-    (re-find pattern s)))
-
 (defn path-normalize
   "Normalize file path (for reading paths from FS, not required by writting)"
   [s]
@@ -40,7 +34,7 @@
 (defn tag-valid?
   [tag-name]
   (when (string? tag-name)
-    (not (safe-re-find #"[# \t\r\n]+" tag-name))))
+    (not (re-find #"[# \t\r\n]+" tag-name))))
 
 (defn safe-subs
   ([s start]

+ 5 - 0
deps/graph-parser/src/logseq/graph_parser/util/page_ref.cljs

@@ -27,6 +27,11 @@ a logseq page-ref e.g. [[page name]]"
   [page-name]
   (str left-brackets page-name right-brackets))
 
+(defn ->page-ref-re-str
+  "Create a page ref regex escaped string given a page name"
+  [page-name]
+  (string/replace (->page-ref page-name) #"([\[\]])" "\\$1"))
+
 (defn get-page-name
   "Extracts page-name from page-ref string"
   [s]

+ 14 - 1
deps/graph-parser/test/logseq/graph_parser/property_test.cljs

@@ -1,5 +1,5 @@
 (ns logseq.graph-parser.property-test
-  (:require [cljs.test :refer [are deftest]]
+  (:require [cljs.test :refer [are deftest is]]
             [logseq.graph-parser.property :as gp-property]))
 
 (deftest test->new-properties
@@ -24,3 +24,16 @@
 
     "hello\n:PROPERTIES:\n:foo: bar\n:nice\n:END:\nnice"
     "hello\nfoo:: bar\n:nice\nnice"))
+
+(deftest property-value-from-content
+  (is (= "62b38254-4be7-4627-a2b7-6d9ee20999e5"
+         (gp-property/property-value-from-content
+          "id"
+          "type:: blog-posting\ndesc:: nice walkthrough on creating a blog with #nbb\nid:: 62b38254-4be7-4627-a2b7-6d9ee20999e5"))
+      "Pulls value from end of block content")
+
+  (is (= "nice walkthrough on creating a blog with #nbb"
+         (gp-property/property-value-from-content
+          "desc"
+          "type:: blog-posting\ndesc:: nice walkthrough on creating a blog with #nbb\nid:: 62b38254-4be7-4627-a2b7-6d9ee20999e5"))
+      "Pulls value from middle of block content"))

+ 6 - 1
deps/graph-parser/test/logseq/graph_parser/text_test.cljs

@@ -1,5 +1,5 @@
 (ns logseq.graph-parser.text-test
-  (:require [cljs.test :refer [are deftest testing]]
+  (:require [cljs.test :refer [are deftest testing is]]
             [logseq.graph-parser.text :as text]))
 
 (deftest test-get-page-name
@@ -109,4 +109,9 @@
       :tags "\"[[foo]], [[bar]]\"" "\"[[foo]], [[bar]]\""
       :tags "baz, \"[[foo]], [[bar]]\"" #{"baz"})))
 
+(deftest extract-page-refs-and-tags
+  (is (= #{"cljs" "nbb" "js" "amazing"}
+       (text/extract-page-refs-and-tags "This project is written with #cljs, #nbb and #js. #amazing!"))
+      "Don't extract punctation at end of a tag"))
+
 #_(cljs.test/test-ns 'logseq.graph-parser.text-test)

+ 138 - 1
deps/graph-parser/test/logseq/graph_parser_test.cljs

@@ -1,9 +1,10 @@
 (ns logseq.graph-parser-test
-  (:require [cljs.test :refer [deftest testing is]]
+  (:require [cljs.test :refer [deftest testing is are]]
             [clojure.string :as string]
             [logseq.graph-parser :as graph-parser]
             [logseq.db :as ldb]
             [logseq.graph-parser.block :as gp-block]
+            [logseq.graph-parser.property :as gp-property]
             [datascript.core :as d]))
 
 (def foo-edn
@@ -97,3 +98,139 @@
     (test-property-order 4))
   (testing "Sort order and persistence of 10 properties"
     (test-property-order 10)))
+
+(defn- quoted-property-values-test
+  [user-config]
+  (let [conn (ldb/start-conn)
+        _ (graph-parser/parse-file conn
+                                   "foo.md"
+                                   "- desc:: \"#foo is not a ref\""
+                                   {:extract-options {:user-config user-config}})
+        block (->> (d/q '[:find (pull ?b [* {:block/refs [*]}])
+                       :in $
+                       :where [?b :block/properties]]
+                     @conn)
+                (map first)
+                first)]
+    (is (= {:desc "\"#foo is not a ref\""}
+           (:block/properties block))
+        "Quoted value is unparsed")
+    (is (= ["desc"]
+           (map :block/original-name (:block/refs block)))
+        "No refs from property value")))
+
+(deftest quoted-property-values
+  (testing "With default config"
+    (quoted-property-values-test {}))
+  (testing "With :rich-property-values config"
+    (quoted-property-values-test {:rich-property-values? true})))
+
+(deftest page-properties-persistence
+  (testing "Non-string property values"
+    (let [conn (ldb/start-conn)]
+      (graph-parser/parse-file conn
+                               "lythe-of-heaven.md"
+                               "rating:: 8\nrecommend:: true\narchive:: false"
+                               {})
+      (is (= {:rating 8 :recommend true :archive false}
+             (->> (d/q '[:find (pull ?b [*])
+                         :in $
+                         :where [?b :block/properties]]
+                       @conn)
+                  (map (comp :block/properties first))
+                  first)))))
+
+  (testing "Linkable built-in properties"
+    (let [conn (ldb/start-conn)
+          _ (graph-parser/parse-file conn
+                                     "lol.md"
+                                     "alias:: 233\ntags:: fun, facts"
+                                     {})
+          block (->> (d/q '[:find (pull ?b [:block/properties {:block/alias [:block/name]} {:block/tags [:block/name]}])
+                            :in $
+                            :where [?b :block/name "lol"]]
+                          @conn)
+                     (map first)
+                     first)]
+
+      (is (= {:block/alias [{:block/name "233"}]
+              :block/tags [{:block/name "fun"} {:block/name "facts"}]
+              :block/properties {:alias ["233"] :tags ["fun" "facts"]}}
+             block))
+
+      (is (every? vector? (vals (:block/properties block)))
+          "Linked built-in property values as vectors provides for easier transforms"))))
+
+(defn- property-relationships-test
+  "Runs tests on page properties and block properties. file-properties is what is
+  visible in a file and db-properties is what is pulled out from the db"
+  [file-properties db-properties user-config]
+  (let [conn (ldb/start-conn)
+        page-content (gp-property/->block-content file-properties)
+        ;; Create Block properties from given page ones
+        block-property-transform (fn [m] (update-keys m #(keyword (str "block-" (name %)))))
+        block-content (gp-property/->block-content (block-property-transform file-properties))
+        _ (graph-parser/parse-file conn
+                                   "property-relationships.md"
+                                   (str page-content "\n- " block-content)
+                                   {:extract-options {:user-config user-config}})
+        pages (->> (d/q '[:find (pull ?b [* :block/properties])
+                          :in $
+                          :where [?b :block/name] [?b :block/properties]]
+                        @conn)
+                   (map first))
+        _ (assert (= 1 (count pages)))
+        blocks (->> (d/q '[:find (pull ?b [:block/pre-block? :block/properties
+                                           {:block/refs [:block/original-name]}])
+                           :in $
+                           :where [?b :block/properties] [(missing? $ ?b :block/name)]]
+                         @conn)
+                    (map first)
+                    (map (fn [m] (update m :block/refs #(map :block/original-name %)))))
+        block-db-properties (block-property-transform db-properties)]
+
+    (is (= db-properties (:block/properties (first pages)))
+        "page has expected properties")
+
+    (is (= [true nil] (map :block/pre-block? blocks))
+        "page has 2 blocks, one of which is a pre-block")
+
+    (is (= [db-properties block-db-properties]
+           (map :block/properties blocks))
+        "pre-block/page and block have expected properties")
+
+    ;; has expected refs
+    (are [db-props refs]
+         (= (->> (vals db-props)
+                 ;; ignore string values
+                 (mapcat #(if (coll? %) % []))
+                 (concat (map name (keys db-props)))
+                 set)
+            (set refs))
+         ; pre-block/page has expected refs
+         db-properties (first (map :block/refs blocks))
+         ;; block has expected refs
+         block-db-properties (second (map :block/refs blocks)))))
+
+(deftest property-relationships
+  (let [properties {:single-link "[[bar]]"
+                    :multi-link "[[Logseq]] is the fastest #triples #[[text editor]]"
+                    :desc "This is a multiple sentence description. It has one [[link]]"
+                    :comma-prop "one, two,three"}]
+    (testing "With default config"
+      (property-relationships-test
+       properties
+       {:single-link #{"bar"}
+        :multi-link #{"Logseq" "is the fastest" "triples" "text editor"}
+        :desc #{"This is a multiple sentence description. It has one" "link"}
+        :comma-prop #{"one" "two" "three"}}
+       {}))
+
+    (testing "With :rich-property-values config"
+      (property-relationships-test
+       properties
+       {:single-link #{"bar"}
+        :multi-link #{"Logseq" "triples" "text editor"}
+        :desc #{"link"}
+        :comma-prop "one, two,three"}
+       {:rich-property-values? true}))))

+ 1 - 1
resources/js/preload.js

@@ -72,7 +72,7 @@ contextBridge.exposeInMainWorld('apis', {
 
   showItemInFolder (fullpath) {
     if (IS_WIN32) {
-      shell.openPath(path.dirname(fullpath))
+      shell.openPath(path.dirname(fullpath).replaceAll("/", "\\"))
     } else {
       shell.showItemInFolder(fullpath)
     }

+ 1 - 1
resources/package.json

@@ -1,6 +1,6 @@
 {
   "name": "Logseq",
-  "version": "0.8.1",
+  "version": "0.8.2",
   "main": "electron.js",
   "author": "Logseq",
   "license": "AGPL-3.0",

+ 1 - 1
shadow-cljs.edn

@@ -72,7 +72,7 @@
          :devtools        {:enabled false}
          ;; disable :static-fns to allow for with-redefs and repl development
          :compiler-options {:static-fns false}
-         :main            frontend.test.node-test-runner/main}
+         :main            frontend.test.frontend-node-test-runner/main}
 
   :publishing {:target        :browser
                :module-loader true

+ 18 - 0
src/electron/electron/find_in_page.cljs

@@ -0,0 +1,18 @@
+(ns electron.find-in-page
+  (:require [electron.utils :as utils]
+            [cljs-bean.core :as bean]))
+
+(defn find!
+  [^js window search option]
+  (when window
+    (let [contents ^js (.-webContents window)]
+      (.findInPage contents search option)
+      (.on contents "found-in-page"
+           (fn [_event result]
+             (utils/send-to-renderer window "foundInPage" (bean/->clj result))))
+      true)))
+
+(defn clear!
+  [^js window]
+  (when window
+    (.stopFindInPage ^js (.-webContents window) "clearSelection")))

+ 8 - 1
src/electron/electron/handler.cljs

@@ -21,7 +21,8 @@
             [electron.plugin :as plugin]
             [electron.window :as win]
             [electron.file-sync-rsapi :as rsapi]
-            [electron.backup-file :as backup-file]))
+            [electron.backup-file :as backup-file]
+            [electron.find-in-page :as find]))
 
 (defmulti handle (fn [_window args] (keyword (first args))))
 
@@ -536,6 +537,12 @@
     (f)
     (state/set-state! :window/once-persist-done nil)))
 
+(defmethod handle :find-in-page [^js win [_ search option]]
+  (find/find! win search (bean/->js option)))
+
+(defmethod handle :clear-find-in-page [^js win [_]]
+  (find/clear! win))
+
 (defn set-ipc-handler! [window]
   (let [main-channel "main"]
     (.handle ipcMain main-channel

+ 2 - 2
src/electron/electron/window.cljs

@@ -118,7 +118,7 @@
 (defn setup-window-listeners!
   [^js win]
   (when win
-    (let [web-contents (. win -webContents)          
+    (let [web-contents (. win -webContents)
           new-win-handler
           (fn [e url]
             (let [url (if (string/starts-with? url "file:")
@@ -140,7 +140,7 @@
 
           context-menu-handler
           (context-menu/setup-context-menu! win)]
-      
+
       (doto web-contents
         (.on "new-window" new-win-handler)
         (.on "will-navigate" will-navigate-handler))

+ 11 - 1
src/main/electron/listener.cljs

@@ -17,7 +17,8 @@
             [frontend.ui :as ui]
             [frontend.handler.notification :as notification]
             [frontend.handler.repo :as repo-handler]
-            [frontend.handler.user :as user]))
+            [frontend.handler.user :as user]
+            [dommy.core :as dom]))
 
 (defn persist-dbs!
   []
@@ -124,6 +125,15 @@
                                        :on-error   error-f}]
                          (repo-handler/persist-db! repo handlers))))
 
+  (js/window.apis.on "foundInPage"
+                     (fn [data]
+                       (let [data' (bean/->clj data)]
+                         (state/set-state! [:ui/find-in-page :matches] data')
+                         (dom/remove-style! (dom/by-id "search-in-page-input") :visibility)
+                         (dom/set-text! (dom/by-id "search-in-page-placeholder") "")
+                         (ui/focus-element "search-in-page-input")
+                         true)))
+
   (js/window.apis.on "loginCallback"
                      (fn [code]
                        (user/login-callback code)))

+ 10 - 3
src/main/frontend/components/block.cljs

@@ -63,6 +63,7 @@
             [logseq.graph-parser.config :as gp-config]
             [logseq.graph-parser.mldoc :as gp-mldoc]
             [logseq.graph-parser.text :as text]
+            [logseq.graph-parser.property :as gp-property]
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.util.block-ref :as block-ref]
             [logseq.graph-parser.util.page-ref :as page-ref]
@@ -1821,9 +1822,15 @@
   [:span ", "])
 
 (rum/defc property-cp
-  [config block k v]
-  (let [date (and (= k :date) (date/get-locale-string (str v)))
-        property-pages-enabled? (contains? #{true nil} (:property-pages/enabled? (state/get-config)))]
+  [config block k value]
+  (let [date (and (= k :date) (date/get-locale-string (str value)))
+        user-config (state/get-config)
+        ;; In this mode and when value is a set of refs, display full property text
+        ;; because :block/properties value only contains refs but user wants to see text
+        v (if (and (:rich-property-values? user-config) (coll? value))
+            (gp-property/property-value-from-content (name k) (:block/content block))
+            value)
+        property-pages-enabled? (contains? #{true nil} (:property-pages/enabled? user-config))]
     [:div
      (if property-pages-enabled?
        (page-cp (assoc config :property? true) {:block/name (subs (str k) 1)})

+ 87 - 0
src/main/frontend/components/find_in_page.cljs

@@ -0,0 +1,87 @@
+(ns frontend.components.find-in-page
+  (:require [rum.core :as rum]
+            [frontend.ui :as ui]
+            [frontend.state :as state]
+            [frontend.util :as util]
+            [frontend.handler.search :as search-handler :refer [debounced-search]]
+            [goog.dom :as gdom]
+            [frontend.mixins :as mixins]
+            [clojure.string :as string]))
+
+(rum/defc search-input
+  [q matches]
+  [:div.flex.w-48.relative
+   [:input#search-in-page-input.form-input.block.sm:text-sm.sm:leading-5.my-2.border-none.mr-4.outline-none
+    {:auto-focus true
+     :placeholder "Find in page"
+     :aria-label "Find in page"
+     :value q
+     :on-change (fn [e]
+                  (let [value (util/evalue e)]
+                    (state/set-state! [:ui/find-in-page :q] value)
+                    (debounced-search)))}]
+   (when-not (string/blank? q)
+     (when-let [total (:matches matches)]
+      [:div.text-sm.absolute.top-2.right-0.py-2.px-4
+       (:activeMatchOrdinal matches 0)
+       "/"
+       total]))
+   [:div#search-in-page-placeholder.absolute.top-2.left-0.p-2.sm:text-sm]])
+
+(rum/defc search-inner < rum/static
+  (mixins/event-mixin
+   (fn [state]
+     (mixins/hide-when-esc-or-outside
+      state
+      :node (gdom/getElement "search-in-page")
+      :on-hide (fn []
+                 (search-handler/electron-exit-find-in-page!)))))
+  [{:keys [matches match-case? q]}]
+  [:div#search-in-page.flex.flex-row.absolute.top-2.right-4.shadow-lg.px-2.py-1.faster-fade-in.items-center
+
+   (search-input q matches)
+
+   (ui/button
+    (ui/icon "letter-case")
+    :on-click (fn []
+                (state/update-state! [:ui/find-in-page :match-case?] not)
+                (debounced-search))
+    :intent "link"
+    :small? true
+    :title "Match case"
+    :class (str (when match-case? "active ") "text-lg"))
+
+   (ui/button
+    (ui/icon "caret-up")
+    :on-click (fn []
+                (state/set-state! [:ui/find-in-page :backward?] true)
+                (debounced-search))
+    :intent "link"
+    :small? true
+    :class "text-lg"
+    :title "Previous result")
+
+   (ui/button
+    (ui/icon "caret-down")
+    :on-click (fn []
+                (state/set-state! [:ui/find-in-page :backward?] false)
+                (debounced-search))
+    :intent "link"
+    :small? true
+    :class "text-lg"
+    :title "Next result")
+
+   (ui/button
+    (ui/icon "x")
+    :on-click (fn []
+                (search-handler/electron-exit-find-in-page!))
+    :intent "link"
+    :small? true
+    :class "text-lg"
+    :title "Close")])
+
+(rum/defc search < rum/reactive
+  []
+  (let [{:keys [active?] :as opt} (state/sub :ui/find-in-page)]
+    (when active?
+      (search-inner opt))))

+ 21 - 0
src/main/frontend/components/find_in_page.css

@@ -0,0 +1,21 @@
+#search-in-page {
+  z-index: 999;
+  background-color: var(--ls-primary-background-color);
+
+  .form-input:focus {
+      box-shadow: none;
+  }
+
+  .ui__button[intent='link'],
+  .ui__button[intent='link']:focus {
+      border-color: none;
+  }
+
+  .ui__button {
+      margin-top: 0;
+
+      &.active {
+        color: var(--ls-link-text-color);
+      }
+  }
+}

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

@@ -297,7 +297,7 @@
     [:div "Recent search:"]
     (ui/with-shortcut :go/search-in-page "bottom"
       [:div.flex-row.flex.align-items
-       [:div.mr-2 "Search in page:"]
+       [:div.mr-2 "Search blocks in page:"]
        [:div {:style {:margin-top 3}}
         (ui/toggle in-page-search?
                    (fn [_value]

+ 4 - 0
src/main/frontend/components/sidebar.cljs

@@ -12,6 +12,7 @@
             [frontend.components.svg :as svg]
             [frontend.components.theme :as theme]
             [frontend.components.widgets :as widgets]
+            [frontend.components.find-in-page :as find-in-page]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.db :as db]
@@ -347,6 +348,9 @@
      [:div#main-content-container.scrollbar-spacing.w-full.flex.justify-center.flex-row
       {:data-is-margin-less-pages margin-less-pages?}
 
+      (when (util/electron?)
+        (find-in-page/search))
+
       (when show-action-bar?
         (action-bar/action-bar))
 

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

@@ -48,7 +48,7 @@
   get-files get-files-blocks get-files-full get-journals-length
   get-latest-journals get-page get-page-alias get-page-alias-names get-paginated-blocks
   get-page-blocks-count get-page-blocks-no-cache get-page-file get-page-format get-page-properties
-  get-page-referenced-blocks get-page-referenced-blocks-full get-page-referenced-pages get-page-unlinked-references get-page-referenced-blocks-no-cache
+  get-page-referenced-blocks get-page-referenced-blocks-full get-page-referenced-pages get-page-unlinked-references
   get-all-pages get-pages get-pages-relation get-pages-that-mentioned-page get-public-pages get-tag-pages
   journal-page? page-alias-set pull-block
   set-file-last-modified-at! page-empty? page-exists? page-empty-or-dummy? get-alias-source-page

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

@@ -1152,18 +1152,6 @@
                                db-utils/seq-flatten)]
       (mapv (fn [page] [page (get-page-alias repo page)]) mentioned-pages))))
 
-(defn get-page-referenced-blocks-no-cache
-  [page-id]
-  (when-let [repo (state/get-current-repo)]
-    (->>
-     (d/q '[:find (pull ?b [*])
-            :in $ ?page-id
-            :where
-            [?b :block/refs ?page-id]]
-          (conn/get-db repo)
-          page-id)
-     (flatten))))
-
 (defn get-page-referenced-blocks-full
   ([page]
    (get-page-referenced-blocks-full (state/get-current-repo) page nil))

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

@@ -6,7 +6,6 @@
             [clojure.set :as set]
             [clojure.string :as string]
             [clojure.walk :as walk]
-            [frontend.state :as state]
             [frontend.date :as date]
             [frontend.db.model :as model]
             [frontend.db.query-react :as query-react]
@@ -235,12 +234,20 @@
     (= 4 (count e))
     (build-between-three-arg e)))
 
+
+(defn parse-property-value
+  "Parses non-string property values or any page-ref like values"
+  [v]
+  (if-some [res (text/parse-non-string-property-value v)]
+    res
+    (text/split-page-refs-without-brackets v)))
+
 (defn- build-property-two-arg
   [e]
   (let [k (string/replace (name (nth e 1)) "_" "-")
         v (nth e 2)
         v (if-not (nil? v)
-            (text/parse-property k v (state/get-config))
+            (parse-property-value (str v))
             v)
         v (if (coll? v) (first v) v)]
     {:query (list 'property '?b (keyword k) v)
@@ -285,7 +292,7 @@
   (let [[k v] (rest e)
         k (string/replace (name k) "_" "-")]
     (if (some? v)
-      (let [v' (text/parse-property k v (state/get-config))
+      (let [v' (parse-property-value (str v))
             val (if (coll? v') (first v') v')]
         {:query (list 'page-property '?p (keyword k) val)
          :rules [:page-property]})

+ 3 - 2
src/main/frontend/handler/editor.cljs

@@ -2922,8 +2922,9 @@
 
 (defn- cut-blocks-and-clear-selections!
   [copy?]
-  (cut-selection-blocks copy?)
-  (clear-selection!))
+  (when-not (get-in @state/state [:ui/find-in-page :active?])
+    (cut-selection-blocks copy?)
+    (clear-selection!)))
 
 (defn shortcut-copy-selection
   [_e]

+ 8 - 5
src/main/frontend/handler/page.cljs

@@ -373,9 +373,9 @@
   ;; update all pages which have references to this page
   (let [repo (state/get-current-repo)
         to-page (db/entity [:block/name (util/page-name-sanity-lc new-name)])
-        blocks   (db/get-page-referenced-blocks-no-cache (:db/id page))
-        page-ids (->> (map :block/page blocks)
-                      (remove nil?)
+        blocks (:block/_refs (db/entity (:db/id page)))
+        page-ids (->> (map (fn [b]
+                             {:db/id (:db/id (:block/page b))}) blocks)
                       (set))
         tx       (->> (map (fn [{:block/keys [uuid content properties] :as block}]
                              (let [content    (let [content' (replace-old-page! content old-original-name new-name)]
@@ -389,8 +389,11 @@
                                   {:block/uuid       uuid
                                    :block/content    content
                                    :block/properties properties
-                                   :block/properties-order (map first properties)
-                                   :block/refs (rename-update-block-refs! (:block/refs block) (:db/id page) (:db/id to-page))})))) blocks)
+                                   :block/properties-order (when (seq properties)
+                                                             (map first properties))
+                                   :block/refs (->> (rename-update-block-refs! (:block/refs block) (:db/id page) (:db/id to-page))
+                                                    (map :db/id)
+                                                    (set))})))) blocks)
                       (remove nil?))]
     (db/transact! repo tx)
     (doseq [page-id page-ids]

+ 54 - 1
src/main/frontend/handler/search.cljs

@@ -9,7 +9,10 @@
             [promesa.core :as p]
             [logseq.graph-parser.text :as text]
             [frontend.util.drawer :as drawer]
-            [frontend.util.property :as property]))
+            [frontend.util.property :as property]
+            [electron.ipc :as ipc]
+            [goog.functions :refer [debounce]]
+            [dommy.core :as dom]))
 
 (defn add-search-to-recent!
   [repo q]
@@ -51,6 +54,56 @@
            (swap! state/state assoc search-key result)
            result))))))
 
+(defn open-find-in-page!
+  []
+  (when (util/electron?)
+    (let [{:keys [active?]} (:ui/find-in-page @state/state)]
+      (when-not active? (state/set-state! [:ui/find-in-page :active?] true)))))
+
+(defn electron-find-in-page!
+  []
+  (when (util/electron?)
+    (let [{:keys [active? backward? match-case? q]} (:ui/find-in-page @state/state)
+          option (cond->
+                  {}
+
+                   (not active?)
+                   (assoc :findNext true)
+
+                   backward?
+                   (assoc :forward false)
+
+                   match-case?
+                   (assoc :matchCase true))]
+      (open-find-in-page!)
+      (when-not (string/blank? q)
+        (dom/set-style! (dom/by-id "search-in-page-input")
+                        :visibility "hidden")
+        (when (> (count q) 1)
+          (dom/set-html! (dom/by-id "search-in-page-placeholder")
+                         (util/format
+                          "<span><span>%s</span><span style=\"margin-left: -4px;\">%s</span></span>"
+                          (first q)
+                          (str " " (subs q 1)))))
+        (ipc/ipc "find-in-page" q option)))))
+
+(defonce debounced-search (debounce electron-find-in-page! 500))
+
+(defn loop-find-in-page!
+  [backward?]
+  (when (and (get-in @state/state [:ui/find-in-page :active?])
+             (not (state/editing?)))
+    (state/set-state! [:ui/find-in-page :backward?] backward?)
+    (debounced-search)))
+
+(defn electron-exit-find-in-page!
+  [& {:keys [clear-state?]
+      :or {clear-state? true}}]
+  (when (util/electron?)
+    (ipc/ipc "clear-find-in-page")
+    (when clear-state?
+      (state/set-state! :ui/find-in-page nil))))
+
 (defn clear-search!
   ([]
    (clear-search! true))

+ 21 - 1
src/main/frontend/modules/shortcut/config.cljs

@@ -249,6 +249,20 @@
                                                 (editor-handler/escape-editing)
                                                 (route-handler/go-to-search! :global))}
 
+   :go/electron-find-in-page       {:binding "mod+f"
+                                    :fn      #(when (util/electron?)
+                                                (search-handler/open-find-in-page!))}
+
+   :go/electron-jump-to-the-next {:binding ["enter" "mod+g"]
+                                    :fn      (fn [_state _e]
+                                               (when (util/electron?)
+                                                 (search-handler/loop-find-in-page! false)))}
+
+   :go/electron-jump-to-the-previous {:binding ["shift+enter" "mod+shift+g"]
+                                             :fn      (fn [_state _e]
+                                                        (when (util/electron?)
+                                                          (search-handler/loop-find-in-page! true)))}
+
    :go/journals                    {:binding "g j"
                                     :fn      route-handler/go-to-journals!}
 
@@ -280,7 +294,7 @@
    :graph/open                     {:fn      #(do
                                                 (editor-handler/escape-editing)
                                                 (state/set-state! :ui/open-select :graph-open))
-                                    :binding "mod+shift+g"}
+                                    :binding "alt+shift+g"}
 
    :graph/remove                   {:fn      #(do
                                                 (editor-handler/escape-editing)
@@ -501,6 +515,9 @@
                           :ui/toggle-brackets
                           :go/search-in-page
                           :go/search
+                          :go/electron-find-in-page
+                          :go/electron-jump-to-the-next
+                          :go/electron-jump-to-the-previous
                           :go/backward
                           :go/forward
                           :search/re-index
@@ -553,6 +570,9 @@
     :editor/select-all-blocks
     :go/search
     :go/search-in-page
+    :go/electron-find-in-page
+    :go/electron-jump-to-the-next
+    :go/electron-jump-to-the-previous
     :editor/undo
     :editor/redo
     :editor/copy

+ 3 - 0
src/main/frontend/modules/shortcut/dicts.cljc

@@ -77,6 +77,9 @@
    :editor/zoom-out                "Zoom out editing block / Backwards otherwise"
    :ui/toggle-brackets             "Toggle whether to display brackets"
    :go/search-in-page              "Search in the current page"
+   :go/electron-find-in-page       "Find in page"
+   :go/electron-jump-to-the-next   "Jump to the next match to your Find bar search"
+   :go/electron-jump-to-the-previous "Jump to the previous match to your Find bar search"
    :go/search                      "Full text search"
    :go/journals                    "Go to journals"
    :go/backward                    "Backwards"

+ 1 - 0
src/main/frontend/state.cljs

@@ -232,6 +232,7 @@
 
      :encryption/graph-parsing?             false
 
+     :ui/find-in-page                     nil
      })))
 
 ;; block uuid -> {content(String) -> ast}

+ 7 - 1
src/main/frontend/ui.cljs

@@ -166,7 +166,7 @@
    opts))
 
 (defn button
-  [text & {:keys [background href class intent on-click small? large?]
+  [text & {:keys [background href class intent on-click small? large? title]
            :or   {small? false large? false}
            :as   option}]
   (let [klass (when-not intent ".bg-indigo-600.hover:bg-indigo-700.focus:border-indigo-700.active:bg-indigo-700.text-center")
@@ -176,6 +176,7 @@
     [:button.ui__button
      (merge
       {:type  "button"
+       :title title
        :class (str (util/hiccup->class klass) " " class)}
       (dissoc option :background :class :small? :large?)
       (when href
@@ -274,6 +275,11 @@
   []
   (gdom/getElement "main-content-container"))
 
+(defn focus-element
+  [element]
+  (when-let [element ^js (gdom/getElement element)]
+    (.focus element)))
+
 (defn get-scroll-top []
   (.-scrollTop (main-node)))
 

+ 5 - 3
src/main/frontend/util.cljc

@@ -79,8 +79,7 @@
    (defn electron?
      []
      (when (and js/window (gobj/get js/window "navigator"))
-       (let [ua (string/lower-case js/navigator.userAgent)]
-         (string/includes? ua " electron")))))
+       (gstring/caseInsensitiveContains js/navigator.userAgent " electron"))))
 
 #?(:cljs
    (defn mocked-open-dir-path
@@ -486,7 +485,10 @@
 
 #?(:cljs
    (defn safe-path-join [prefix & paths]
-     (apply node-path.join (cons prefix paths))))
+     (let [path (apply node-path.join (cons prefix paths))]
+       (if (and (electron?) (gstring/caseInsensitiveStartsWith path "file://"))
+         (js/decodeURIComponent (subs path 7))
+         path))))
 
 (defn trim-safe
   [s]

+ 8 - 6
src/main/frontend/utils.js

@@ -315,12 +315,14 @@ export const nodePath = Object.assign({}, path, {
   join (input, ...paths) {
     let orURI = null
 
-    try {
-      orURI = new URL(input)
-      input = input.replace(orURI.protocol + '//', '')
-        .replace(orURI.protocol, '')
-        .replace(/^\/+/, '/')
-    } catch (_e) {}
+    if (input.startsWith("file://")) {
+      try {
+        orURI = new URL(input)
+        input = input.replace(orURI.protocol + '//', '')
+          .replace(orURI.protocol, '')
+          .replace(/^\/+/, '/')
+      } catch (_e) {}
+    }
 
     input = path.join(input, ...paths)
 

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

@@ -1,3 +1,3 @@
 (ns frontend.version)
 
-(defonce version "0.8.1")
+(defonce version "0.8.2")

+ 42 - 9
src/test/frontend/db/query_dsl_test.cljs

@@ -3,7 +3,7 @@
             [clojure.string :as str]
             [frontend.db :as db]
             [frontend.db.query-dsl :as query-dsl]
-            [frontend.test.helper :as test-helper :refer [load-test-files]]))
+            [frontend.test.helper :as test-helper :include-macros true :refer [load-test-files]]))
 
 ;; TODO: quickcheck
 ;; 1. generate query filters
@@ -45,7 +45,8 @@
 ;; Tests
 ;; =====
 
-(deftest block-property-queries
+(defn- block-property-queries-test
+  []
   (load-test-files [{:file/path "journals/2022_02_28.md"
                      :file/content "a:: b
 - b1
@@ -64,7 +65,7 @@ prop-d:: nada"}])
   (testing "Blocks have given property value"
     (is (= #{"b1" "b2"}
            (set (map (comp first str/split-lines :block/content)
-                 (dsl-query "(property prop-a val-a)")))))
+                     (dsl-query "(property prop-a val-a)")))))
 
     (is (= ["b2"]
            (map (comp first str/split-lines :block/content)
@@ -112,15 +113,27 @@ prop-d:: nada"}])
               (dsl-query "(property prop-d)")))
       "Blocks that have a property"))
 
-(deftest page-property-queries
+(deftest block-property-queries
+  (testing "block property tests with default config"
+    (test-helper/with-config {}
+      (block-property-queries-test)))
+
+  (test-helper/start-test-db!) ;; reset db
+
+  (testing "block property tests with rich-property-values? config"
+    (test-helper/with-config {:rich-property-values? true}
+      (block-property-queries-test))))
+
+(defn- page-property-queries-test
+  []
   (load-test-files [{:file/path "pages/page1.md"
-                     :file/content "parent:: [[child page 1]], [[child-no-space]]"}
+                     :file/content "parent:: [[child page 1]], [[child-no-space]]\ninteresting:: true"}
                     {:file/path "pages/page2.md"
-                     :file/content "foo:: bar"}
+                     :file/content "foo:: #bar\ninteresting:: false"}
                     {:file/path "pages/page3.md"
-                     :file/content "parent:: [[child page 1]], child page 2\nfoo:: bar"}
+                     :file/content "parent:: [[child page 1]], [[child page 2]]\nfoo:: bar\ninteresting:: false"}
                     {:file/path "pages/page4.md"
-                     :file/content "parent:: child page 2\nfoo:: baz"}])
+                     :file/content "parent:: [[child page 2]]\nfoo:: baz"}])
 
   (is (= ["page1" "page3" "page4"]
          (map :block/name (dsl-query "(page-property parent)")))
@@ -160,7 +173,27 @@ prop-d:: nada"}])
          (map
           :block/name
           (dsl-query "(and (not (page-property foo bar)) (page-property parent [[child page 2]]))")))
-      "Page property queries nested NOT in first clause"))
+      "Page property queries nested NOT in first clause")
+
+  (testing "boolean values"
+    (is (= ["page1"]
+           (map :block/name (dsl-query "(page-property interesting true)")))
+        "Boolean true")
+
+    (is (= ["page2" "page3"]
+           (map :block/name (dsl-query "(page-property interesting false)")))
+        "Boolean false")))
+
+(deftest page-property-queries
+  (testing "page property tests with default config"
+    (test-helper/with-config {}
+      (page-property-queries-test)))
+
+  (test-helper/start-test-db!) ;; reset db
+
+  (testing "page property tests with rich-property-values? config"
+    (test-helper/with-config {:rich-property-values? true}
+      (page-property-queries-test))))
 
 (deftest task-queries
   (load-test-files [{:file/path "pages/page1.md"

+ 19 - 0
src/test/frontend/test/frontend_node_test_runner.cljs

@@ -0,0 +1,19 @@
+(ns frontend.test.frontend-node-test-runner
+  "This is a custom version of the node-test-runner for the frontend build"
+  {:dev/always true} ;; necessary for test-data freshness
+  (:require [frontend.test.node-test-runner :as node-test-runner]
+            [shadow.test.env :as env]
+            [lambdaisland.glogi.console :as glogi-console]
+            ;; activate humane test output for all tests
+            [pjstadig.humane-test-output]))
+
+;; Needed for new test runners
+(defn ^:dev/after-load reset-test-data! []
+  (-> (env/get-test-data)
+      (env/reset-test-data!)))
+
+(defn main [& args]
+  []
+  (glogi-console/install!) ;; see log messages
+  (reset-test-data!)
+  (node-test-runner/parse-and-run-tests args))

+ 8 - 0
src/test/frontend/test/helper.clj

@@ -0,0 +1,8 @@
+(ns frontend.test.helper)
+
+(defmacro with-config
+  [config & body]
+  `(let [repo# (frontend.state/get-current-repo)]
+     (frontend.state/set-config! repo# ~config)
+     ~@body
+     (frontend.state/set-config! repo# nil)))

+ 16 - 12
src/test/frontend/test/node_test_runner.cljs

@@ -1,6 +1,6 @@
 (ns frontend.test.node-test-runner
-  "shadow-cljs test runner for :node-test that provides the same test selection
-  options as
+  "Application agnostic shadow-cljs test runner for :node-test that provides the
+  same test selection options as
   https://github.com/cognitect-labs/test-runner#invoke-with-clojure--m-clojuremain.
   This gives the user a fair amount of control over which tests and namespaces
   to call from the commandline. Once this test runner is stable enough we should
@@ -12,9 +12,7 @@
             [clojure.set :as set]
             [shadow.test :as st]
             [cljs.test :as ct]
-            ["util" :as util]
-            ;; activate humane test output for all tests
-            [pjstadig.humane-test-output]))
+            [goog.string :as gstring]))
 
 ;; Cljs.test customization
 ;; Inherit behavior from default reporter
@@ -47,10 +45,10 @@
 (defn- print-summary
   "Print help summary given args and opts strings"
   [options-summary additional-msg]
-  (println (util/format "Usage: %s [OPTIONS]\nOptions:\n%s%s"
-                        "$0"
-                        options-summary
-                        additional-msg)))
+  (println (gstring/format "Usage: %s [OPTIONS]\nOptions:\n%s%s"
+                           "$0"
+                           options-summary
+                           additional-msg)))
 
 (defn- parse-options
   "Processes a command's functionality given a cli options definition, arguments
@@ -172,9 +170,9 @@ returns selected tests and namespaces to run"
         (st/run-test-vars test-env test-vars))
       (st/run-all-tests test-env nil))))
 
-(defn main [& args]
-  (reset-test-data!)
-
+(defn parse-and-run-tests
+  "Main entry point for custom test runners"
+  [args]
   (let [{:keys [options summary]} (parse-options args cli-options)]
     (if (:help options)
       (do
@@ -182,3 +180,9 @@ returns selected tests and namespaces to run"
                        "\n\nMultiple options are ANDed. Defaults to running all tests")
         (js/process.exit 0))
       (run-tests (keys (env/get-tests)) (env/get-test-vars) options))))
+
+(defn main
+  "Main entry point if this ns is configured as a test runner"
+  [& args]
+  (reset-test-data!)
+  (parse-and-run-tests args))

+ 4 - 0
templates/config.edn

@@ -228,6 +228,10 @@
  ;; E.g.:property-pages/excludelist #{:duration :author}
  ;; :property-pages/excludelist
 
+ ;; Enables property values to contain a mix of tags, page-refs, special
+ ;; punctuation and free-form text
+ ;; :rich-property-values? true
+
  ;; logbook setup
  ;; :logbook/settings
  ;; {:with-second-support? false ;limit logbook to minutes, seconds will be eliminated