فهرست منبع

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

Peng Xiao 3 سال پیش
والد
کامیت
cca1d35510
48فایلهای تغییر یافته به همراه1176 افزوده شده و 315 حذف شده
  1. 1 1
      deps/db/src/logseq/db/rules.cljc
  2. 3 1
      deps/graph-parser/src/logseq/graph_parser.cljs
  3. 1 0
      deps/graph-parser/src/logseq/graph_parser/block.cljs
  4. 22 10
      deps/graph-parser/src/logseq/graph_parser/extract.cljc
  5. 9 9
      deps/graph-parser/src/logseq/graph_parser/test/docs_graph_helper.cljs
  6. 93 20
      deps/graph-parser/src/logseq/graph_parser/util.cljs
  7. 3 2
      deps/graph-parser/test/logseq/graph_parser/cli_test.cljs
  8. 1 1
      deps/graph-parser/test/logseq/graph_parser/mldoc_test.cljs
  9. 2 0
      deps/graph-parser/test/logseq/graph_parser/nbb_test_runner.cljs
  10. 43 0
      deps/graph-parser/test/logseq/graph_parser/util/file_name_test.cljs
  11. 0 5
      src/electron/electron/handler.cljs
  12. 7 37
      src/electron/electron/search.cljs
  13. 165 0
      src/main/frontend/components/conversion.cljs
  14. 39 34
      src/main/frontend/components/file.cljs
  15. 1 1
      src/main/frontend/components/repo.cljs
  16. 1 1
      src/main/frontend/components/search.cljs
  17. 15 4
      src/main/frontend/components/settings.cljs
  18. 7 0
      src/main/frontend/components/settings.css
  19. 1 1
      src/main/frontend/components/sidebar.cljs
  20. 1 1
      src/main/frontend/db.cljs
  21. 16 9
      src/main/frontend/db/model.cljs
  22. 36 3
      src/main/frontend/dicts.cljc
  23. 37 22
      src/main/frontend/extensions/pdf/assets.cljs
  24. 0 1
      src/main/frontend/extensions/pdf/highlights.cljs
  25. 19 15
      src/main/frontend/handler/common/file.cljs
  26. 5 4
      src/main/frontend/handler/config.cljs
  27. 103 0
      src/main/frontend/handler/conversion.cljs
  28. 26 10
      src/main/frontend/handler/events.cljs
  29. 42 34
      src/main/frontend/handler/page.cljs
  30. 5 2
      src/main/frontend/handler/repo.cljs
  31. 0 12
      src/main/frontend/handler/search.cljs
  32. 1 0
      src/main/frontend/handler/web/nfs.cljs
  33. 8 5
      src/main/frontend/mobile/intent.cljs
  34. 3 3
      src/main/frontend/modules/file/core.cljs
  35. 1 1
      src/main/frontend/modules/shortcut/config.cljs
  36. 0 5
      src/main/frontend/search.cljs
  37. 0 1
      src/main/frontend/search/browser.cljs
  38. 0 3
      src/main/frontend/search/node.cljs
  39. 0 1
      src/main/frontend/search/protocol.cljs
  40. 8 6
      src/main/frontend/state.cljs
  41. 7 37
      src/main/frontend/util.cljc
  42. 127 2
      src/main/frontend/util/fs.cljs
  43. 9 0
      src/main/frontend/utils.js
  44. 139 0
      src/test/frontend/db/name_sanity_test.cljs
  45. 5 4
      src/test/frontend/extensions/pdf/assets_test.cljs
  46. 146 0
      src/test/frontend/handler/repo_conversion_test.cljs
  47. 3 2
      src/test/frontend/handler/repo_test.cljs
  48. 15 5
      templates/config.edn

+ 1 - 1
deps/db/src/logseq/db/rules.cljc

@@ -2,7 +2,7 @@
   "Datalog rules for use with logseq.db.schema")
   "Datalog rules for use with logseq.db.schema")
 
 
 (def ^:large-vars/data-var rules
 (def ^:large-vars/data-var rules
-  ;; rule "parent" is optimized for child node -> parent node nesting queries
+  ;; rule "parent" is optimized for parent node -> child node nesting queries
   '[[(parent ?p ?c)
   '[[(parent ?p ?c)
      [?c :block/parent ?p]]
      [?c :block/parent ?p]]
     [(parent ?p ?c)
     [(parent ?p ?c)

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

@@ -21,7 +21,9 @@
         {:keys [tx ast]}
         {:keys [tx ast]}
         (let [extract-options' (merge {:block-pattern (gp-config/get-block-pattern format)
         (let [extract-options' (merge {:block-pattern (gp-config/get-block-pattern format)
                                        :date-formatter "MMM do, yyyy"
                                        :date-formatter "MMM do, yyyy"
-                                       :supported-formats (gp-config/supported-formats)}
+                                       :supported-formats (gp-config/supported-formats)
+                                       :uri-encoded? false
+                                       :filename-format :legacy}
                                       extract-options
                                       extract-options
                                       {:db @conn})
                                       {:db @conn})
               {:keys [pages blocks ast]
               {:keys [pages blocks ast]

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

@@ -57,6 +57,7 @@
                   (and
                   (and
                    (= typ "Search")
                    (= typ "Search")
                    (not (contains? #{\# \* \/ \[} (first value)))
                    (not (contains? #{\# \* \/ \[} (first value)))
+                   ;; FIXME: use `gp-util/get-format` instead
                    (let [ext (some-> (gp-util/get-file-ext value) keyword)]
                    (let [ext (some-> (gp-util/get-file-ext value) keyword)]
                      (when (and (not (string/starts-with? value "http:"))
                      (when (and (not (string/starts-with? value "http:"))
                                 (not (string/starts-with? value "https:"))
                                 (not (string/starts-with? value "https:"))

+ 22 - 10
deps/graph-parser/src/logseq/graph_parser/extract.cljc

@@ -28,9 +28,19 @@
         result))))
         result))))
 
 
 (defn- get-page-name
 (defn- get-page-name
-  [file ast page-name-order]
+  "Get page name with overridden order of
+     `title::` property
+     file name parsing
+     first block content
+   note: `page-name-order` is deprecated on Apr. 2021
+   uri-encoded? - since paths on mobile are uri-encoded, need to decode them first
+   filename-format - the format used to parse file name
+   "
+  [file ast uri-encoded? filename-format]
   ;; headline
   ;; headline
-  (let [ast (map first ast)]
+  (let [ast  (map first ast)
+        file (if uri-encoded? (js/decodeURI file) file)]
+    ;; check backward compatibility?
     (if (string/includes? file "pages/contents.")
     (if (string/includes? file "pages/contents.")
       "Contents"
       "Contents"
       (let [first-block (last (first (filter gp-block/heading-block? ast)))
       (let [first-block (last (first (filter gp-block/heading-block? ast)))
@@ -42,11 +52,13 @@
                                (and first-block
                                (and first-block
                                     (string? title)
                                     (string? title)
                                     title))
                                     title))
-            file-name (filepath->page-name file)]
+            file-name (when-let [result (gp-util/path->file-body file)]
+                        (if (gp-config/mldoc-support? (gp-util/get-file-ext file))
+                          (gp-util/title-parsing result filename-format)
+                          result))]
         (or property-name
         (or property-name
-            (if (= page-name-order "heading")
-              (or first-block-name file-name)
-              (or file-name first-block-name)))))))
+            file-name
+            first-block-name)))))
 
 
 (defn- extract-page-alias-and-tags
 (defn- extract-page-alias-and-tags
   [page-m page page-name properties]
   [page-m page page-name properties]
@@ -114,14 +126,14 @@
 
 
 ;; TODO: performance improvement
 ;; TODO: performance improvement
 (defn- extract-pages-and-blocks
 (defn- extract-pages-and-blocks
-  [format ast properties file content {:keys [date-formatter page-name-order db] :as options}]
+  "uri-encoded? - if is true, apply URL decode on the file path"
+  [format ast properties file content {:keys [date-formatter db uri-encoded? filename-format] :as options}]
   (try
   (try
-    (let [page (get-page-name file ast page-name-order)
+    (let [page (get-page-name file ast uri-encoded? filename-format)
           [page page-name _journal-day] (gp-block/convert-page-if-journal page date-formatter)
           [page page-name _journal-day] (gp-block/convert-page-if-journal page date-formatter)
           options' (-> options
           options' (-> options
                        (assoc :page-name page-name
                        (assoc :page-name page-name
-                              :original-page-name page)
-                       (dissoc :page-name-order))
+                              :original-page-name page))
           blocks (->> (gp-block/extract-blocks ast content false format options')
           blocks (->> (gp-block/extract-blocks ast content false format options')
                       (gp-block/with-parent-and-left {:block/name page-name})
                       (gp-block/with-parent-and-left {:block/name page-name})
                       (vec))
                       (vec))

+ 9 - 9
deps/graph-parser/src/logseq/graph_parser/test/docs_graph_helper.cljs

@@ -18,15 +18,15 @@
                            (clj->js (merge {:stdio "inherit"} opts))))
                            (clj->js (merge {:stdio "inherit"} opts))))
 
 
 (defn clone-docs-repo-if-not-exists
 (defn clone-docs-repo-if-not-exists
-  [dir]
+  [dir branch]
   (when-not (.existsSync fs dir)
   (when-not (.existsSync fs dir)
-    (sh ["git" "clone" "--depth" "1" "-b" "v0.6.7" "-c" "advice.detachedHead=false"
+    (sh ["git" "clone" "--depth" "1" "-b" branch "-c" "advice.detachedHead=false"
          "https://github.com/logseq/docs" dir] {})))
          "https://github.com/logseq/docs" dir] {})))
 
 
 
 
 ;; Fns for common test assertions
 ;; Fns for common test assertions
 ;; ==============================
 ;; ==============================
-(defn- get-top-block-properties
+(defn get-top-block-properties
   [db]
   [db]
   (->> (d/q '[:find (pull ?b [*])
   (->> (d/q '[:find (pull ?b [*])
               :where
               :where
@@ -39,7 +39,7 @@
        (filter #(>= (val %) 5))
        (filter #(>= (val %) 5))
        (into {})))
        (into {})))
 
 
-(defn- get-all-page-properties
+(defn get-all-page-properties
   [db]
   [db]
   (->> (d/q '[:find (pull ?b [*])
   (->> (d/q '[:find (pull ?b [*])
               :where
               :where
@@ -51,7 +51,7 @@
        (apply merge-with +)
        (apply merge-with +)
        (into {})))
        (into {})))
 
 
-(defn- get-block-format-counts
+(defn get-block-format-counts
   [db]
   [db]
   (->> (d/q '[:find (pull ?b [*]) :where [?b :block/format]] db)
   (->> (d/q '[:find (pull ?b [*]) :where [?b :block/format]] db)
        (map first)
        (map first)
@@ -84,7 +84,7 @@
         "Journal page count on disk equals count in db")
         "Journal page count on disk equals count in db")
 
 
     (is (= {"CANCELED" 2 "DONE" 6 "LATER" 4 "NOW" 5}
     (is (= {"CANCELED" 2 "DONE" 6 "LATER" 4 "NOW" 5}
-           (->> (d/q '[:find (pull ?b [*]) :where [?b :block/marker] ]
+           (->> (d/q '[:find (pull ?b [*]) :where [?b :block/marker]]
                      db)
                      db)
                 (map first)
                 (map first)
                 (group-by :block/marker)
                 (group-by :block/marker)
@@ -92,7 +92,7 @@
                 (into {})))
                 (into {})))
         "Task marker counts")
         "Task marker counts")
 
 
-    (is (= {:markdown 3143 :org 460}
+    (is (= {:markdown 3143 :org 460} ;; 2 pages for namespaces are not parsed
            (get-block-format-counts db))
            (get-block-format-counts db))
         "Block format counts")
         "Block format counts")
 
 
@@ -100,8 +100,7 @@
             :updated-at 47 :created-at 47
             :updated-at 47 :created-at 47
             :card-last-score 6 :card-repeats 6 :card-next-schedule 6
             :card-last-score 6 :card-repeats 6 :card-next-schedule 6
             :card-last-interval 6 :card-ease-factor 6 :card-last-reviewed 6
             :card-last-interval 6 :card-ease-factor 6 :card-last-reviewed 6
-            :alias 6 :logseq.macro-arguments 94 :logseq.macro-name 94
-            :heading 64}
+            :alias 6 :logseq.macro-arguments 94 :logseq.macro-name 94 :heading 64}
            (get-top-block-properties db))
            (get-top-block-properties db))
         "Counts for top block properties")
         "Counts for top block properties")
 
 
@@ -132,6 +131,7 @@
                 set))
                 set))
         "Has correct namespaces")))
         "Has correct namespaces")))
 
 
+;; TODO update me to the number of the latest version of doc when namespace is updated
 (defn docs-graph-assertions
 (defn docs-graph-assertions
   "These are common assertions that should pass in both graph-parser and main
   "These are common assertions that should pass in both graph-parser and main
   logseq app. It is important to run these in both contexts to ensure that the
   logseq app. It is important to run these in both contexts to ensure that the

+ 93 - 20
deps/graph-parser/src/logseq/graph_parser/util.cljs

@@ -7,8 +7,17 @@
             [clojure.walk :as walk]
             [clojure.walk :as walk]
             [logseq.graph-parser.log :as log]))
             [logseq.graph-parser.log :as log]))
 
 
+(defn safe-url-decode
+  [string]
+  (if (string/includes? string "%")
+    (try (some-> string str (js/decodeURIComponent))
+         (catch :default _
+           string))
+    string))
+
 (defn path-normalize
 (defn path-normalize
-  "Normalize file path (for reading paths from FS, not required by writting)"
+  "Normalize file path (for reading paths from FS, not required by writting)
+   Keep capitalization senstivity"
   [s]
   [s]
   (.normalize s "NFC"))
   (.normalize s "NFC"))
 
 
@@ -75,14 +84,6 @@
     (str "0" n)
     (str "0" n)
     (str n)))
     (str n)))
 
 
-(defn get-file-ext
-  "Copy of frontend.util/get-file-ext. Too basic to couple to main app"
-  [file]
-  (and
-   (string? file)
-   (string/includes? file ".")
-   (some-> (last (string/split file #"\.")) string/lower-case)))
-
 (defn remove-boundary-slashes
 (defn remove-boundary-slashes
   [s]
   [s]
   (when (string? s)
   (when (string? s)
@@ -104,17 +105,40 @@
                  (conj result (str prev "/" (first others)))))
                  (conj result (str prev "/" (first others)))))
         result))))
         result))))
 
 
+(defn decode-namespace-underlines
+  "Decode namespace underlines to slashed;
+   If continuous underlines, only decode at start;
+   Having empty namespace is invalid."
+  [string]
+  (string/replace string "___" "/"))
+
 (defn page-name-sanity
 (defn page-name-sanity
-  "Sanitize the page-name."
-  ([page-name]
-   (page-name-sanity page-name false))
-  ([page-name replace-slash?]
-   (let [page (some-> page-name
-                      (remove-boundary-slashes)
-                      (path-normalize))]
-     (if replace-slash?
-       (string/replace page #"/" "%2F")
-       page))))
+  "Sanitize the page-name. Unify different diacritics and other visual differences.
+   Two objectives:
+   1. To be the same as in the filesystem;
+   2. To be easier to search"
+  [page-name]
+  (some-> page-name
+          (remove-boundary-slashes)
+          (path-normalize)))
+
+(defn make-valid-namespaces
+  "Remove those empty namespaces from title to make it a valid page name."
+  [title]
+  (->> (string/split title "/")
+       (remove empty?)
+       (string/join "/")))
+
+(def url-encoded-pattern #"(?i)%[0-9a-f]{2}") ;; (?i) for case-insensitive mode
+
+(defn- tri-lb-title-parsing
+  "Parsing file name under the new file name format
+   Avoid calling directly"
+  [file-name]
+  (some-> file-name
+          (decode-namespace-underlines)
+          (string/replace url-encoded-pattern safe-url-decode)
+          (make-valid-namespaces)))
 
 
 (defn page-name-sanity-lc
 (defn page-name-sanity-lc
   "Sanitize the query string for a page name (mandate for :block/name)"
   "Sanitize the query string for a page name (mandate for :block/name)"
@@ -146,10 +170,38 @@
     ;; default
     ;; default
     (keyword format)))
     (keyword format)))
 
 
+(defn path->file-name
+  ;; Only for interal paths, as they are converted to POXIS already
+  ;; https://github.com/logseq/logseq/blob/48b8e54e0fdd8fbd2c5d25b7f1912efef8814714/deps/graph-parser/src/logseq/graph_parser/extract.cljc#L32
+  ;; Should be converted to POXIS first for external paths
+  [path]
+  (if (string/includes? path "/")
+    (last (split-last "/" path))
+    path))
+
+(defn path->file-body
+  [path]
+  (when-let [file-name (path->file-name path)]
+    (if (string/includes? file-name ".")
+      (first (split-last "." file-name))
+      file-name)))
+
+(defn path->file-ext
+  [path-or-file-name]
+  (last (split-last "." path-or-file-name)))
+
 (defn get-format
 (defn get-format
   [file]
   [file]
   (when file
   (when file
-    (normalize-format (keyword (string/lower-case (last (string/split file #"\.")))))))
+    (normalize-format (keyword (string/lower-case (path->file-ext file))))))
+
+(defn get-file-ext
+  "Copy of frontend.util/get-file-ext. Too basic to couple to main app"
+  [file]
+  (and
+   (string? file)
+   (string/includes? file ".")
+   (some-> (path->file-ext file) string/lower-case)))
 
 
 (defn valid-edn-keyword?
 (defn valid-edn-keyword?
   "Determine if string is a valid edn keyword"
   "Determine if string is a valid edn keyword"
@@ -160,6 +212,27 @@
     (catch :default _
     (catch :default _
       false)))
       false)))
 
 
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;     Keep for backward compatibility     ;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; Rule of dir-ver 0
+;; Source: https://github.com/logseq/logseq/blob/e7110eea6790eda5861fdedb6b02c2a78b504cd9/deps/graph-parser/src/logseq/graph_parser/extract.cljc#L35
+(defn legacy-title-parsing
+  [file-name-body]
+  (js/decodeURIComponent (string/replace file-name-body "." "/")))
+
+;; Register sanitization / parsing fns in:
+;; logseq.graph-parser.util (parsing only)
+;; frontend.util.fs         (sanitization only)
+;; frontend.handler.conversion (both)
+(defn title-parsing
+  "Convert file name in the given file name format to page title"
+  [file-name-body filename-format]
+  (case filename-format
+    :triple-lowbar (tri-lb-title-parsing file-name-body)
+    (legacy-title-parsing file-name-body)))
+
 (defn safe-read-string
 (defn safe-read-string
   [content]
   [content]
   (try
   (try

+ 3 - 2
deps/graph-parser/test/logseq/graph_parser/cli_test.cljs

@@ -7,8 +7,9 @@
 ;; Integration test that test parsing a large graph like docs
 ;; Integration test that test parsing a large graph like docs
 (deftest ^:integration parse-graph
 (deftest ^:integration parse-graph
   (let [graph-dir "test/docs"
   (let [graph-dir "test/docs"
-        _ (docs-graph-helper/clone-docs-repo-if-not-exists graph-dir)
-        {:keys [conn files asts]} (gp-cli/parse-graph graph-dir {:verbose false})]
+        ;; TODO update docs filename rules to the latest version when the namespace PR is released
+        _ (docs-graph-helper/clone-docs-repo-if-not-exists graph-dir "v0.6.7")
+        {:keys [conn files asts]} (gp-cli/parse-graph graph-dir {:verbose false})] ;; legacy parsing
 
 
     (docs-graph-helper/docs-graph-assertions @conn files)
     (docs-graph-helper/docs-graph-assertions @conn files)
 
 

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

@@ -122,7 +122,7 @@ body"
 
 
 (deftest ^:integration test->edn
 (deftest ^:integration test->edn
   (let [graph-dir "test/docs"
   (let [graph-dir "test/docs"
-        _ (docs-graph-helper/clone-docs-repo-if-not-exists graph-dir)
+        _ (docs-graph-helper/clone-docs-repo-if-not-exists graph-dir "v0.6.7")
         files (gp-cli/build-graph-files graph-dir)
         files (gp-cli/build-graph-files graph-dir)
         asts-by-file (->> files
         asts-by-file (->> files
                           (map (fn [{:file/keys [path content]}]
                           (map (fn [{:file/keys [path content]}]

+ 2 - 0
deps/graph-parser/test/logseq/graph_parser/nbb_test_runner.cljs

@@ -9,6 +9,7 @@
             [logseq.graph-parser.cli-test]
             [logseq.graph-parser.cli-test]
             [logseq.graph-parser.util.page-ref-test]
             [logseq.graph-parser.util.page-ref-test]
             [logseq.graph-parser.util-test]
             [logseq.graph-parser.util-test]
+            [logseq.graph-parser.util.file-name-test]
             [logseq.graph-parser-test]))
             [logseq.graph-parser-test]))
 
 
 (defmethod cljs.test/report [:cljs.test/default :end-run-tests] [m]
 (defmethod cljs.test/report [:cljs.test/default :end-run-tests] [m]
@@ -26,4 +27,5 @@
    'logseq.graph-parser.cli-test
    'logseq.graph-parser.cli-test
    'logseq.graph-parser.util.page-ref-test
    'logseq.graph-parser.util.page-ref-test
    'logseq.graph-parser-test
    'logseq.graph-parser-test
+   'logseq.graph-parser.util.file-name-test
    'logseq.graph-parser.util-test))
    'logseq.graph-parser.util-test))

+ 43 - 0
deps/graph-parser/test/logseq/graph_parser/util/file_name_test.cljs

@@ -0,0 +1,43 @@
+(ns logseq.graph-parser.util.file-name-test
+  (:require [logseq.graph-parser.util :as gp-util]
+            [cljs.test :refer [is deftest]]))
+
+;; This is a copy of frontend.util.fs/multiplatform-reserved-chars for reserved chars testing
+(def multiplatform-reserved-chars ":\\*\\?\"<>|\\#\\\\")
+
+;; Stuffs should be parsable (don't crash) when users dump some random files
+(deftest page-name-parsing-tests
+  (is (string? (#'gp-util/tri-lb-title-parsing  "___-_-_-_---___----")))
+  (is (string? (#'gp-util/tri-lb-title-parsing  "_____///____---___----")))
+  (is (string? (#'gp-util/tri-lb-title-parsing  "/_/////---/_----")))
+  (is (string? (#'gp-util/tri-lb-title-parsing  "/\\#*%lasdf\\//__--dsll_____----....-._0x2B")))
+  (is (string? (#'gp-util/tri-lb-title-parsing  "/\\#*%l;;&&;&\\//__--dsll_____----....-._0x2B")))
+  (is (string? (#'gp-util/tri-lb-title-parsing  multiplatform-reserved-chars)))
+  (is (string? (#'gp-util/tri-lb-title-parsing  "dsa&amp&semi;l dsalfjk jkl"))))
+
+(deftest uri-decoding-tests
+  (is (= (gp-util/safe-url-decode "%*-sd%%%saf%=lks") "%*-sd%%%saf%=lks")) ;; Contains %, but invalid
+  (is (= (gp-util/safe-url-decode "%2FDownloads%2FCNN%3AIs%5CAll%3AYou%20Need.pdf") "/Downloads/CNN:Is\\All:You Need.pdf"))
+  (is (= (gp-util/safe-url-decode "asldkflksdaf啦放假啦睡觉啦啊啥的都撒娇浪费;dla") "asldkflksdaf啦放假啦睡觉啦啊啥的都撒娇浪费;dla")))
+
+(deftest page-name-sanitization-backward-tests
+  (is (= "abc.def.ghi.jkl" (#'gp-util/tri-lb-title-parsing "abc.def.ghi.jkl")))
+  (is (= "abc/def/ghi/jkl" (#'gp-util/tri-lb-title-parsing "abc%2Fdef%2Fghi%2Fjkl")))
+  (is (= "abc%/def/ghi/jkl" (#'gp-util/tri-lb-title-parsing "abc%25%2Fdef%2Fghi%2Fjkl")))
+  (is (= "abc%2——ef/ghi/jkl" (#'gp-util/tri-lb-title-parsing "abc%2——ef%2Fghi%2Fjkl")))
+  (is (= "abc&amp;2Fghi/jkl" (#'gp-util/tri-lb-title-parsing "abc&amp;2Fghi%2Fjkl")))
+  (is (= "abc&lt;2Fghi/jkl" (#'gp-util/tri-lb-title-parsing "abc&lt;2Fghi%2Fjkl")))
+  (is (= "abc&percnt;2Fghi/jkl" (#'gp-util/tri-lb-title-parsing "abc&percnt;2Fghi%2Fjkl")))
+  (is (= "abc&semi;&;2Fghi/jkl" (#'gp-util/tri-lb-title-parsing "abc&semi;&;2Fghi%2Fjkl")))
+  ;; happens when importing some compatible files on *nix / macOS
+  (is (= multiplatform-reserved-chars (#'gp-util/tri-lb-title-parsing multiplatform-reserved-chars))))
+
+(deftest path-utils-tests
+  (is (= "asldk lakls " (gp-util/path->file-body "/data/app/asldk lakls .lsad")))
+  (is (= "asldk lakls " (gp-util/path->file-body "asldk lakls .lsad")))
+  (is (= "asldk lakls" (gp-util/path->file-body "asldk lakls")))
+  (is (= "asldk lakls" (gp-util/path->file-body "/data/app/asldk lakls")))
+  (is (= "asldk lakls" (gp-util/path->file-body "file://data/app/asldk lakls.as")))
+  (is (= "中文asldk lakls" (gp-util/path->file-body "file://中文data/app/中文asldk lakls.as")))
+  (is (= "lsad" (gp-util/path->file-ext "asldk lakls .lsad")))
+  (is (= "lsad" (gp-util/path->file-ext "中文asldk lakls .lsad"))))

+ 0 - 5
src/electron/electron/handler.cljs

@@ -292,7 +292,6 @@
   (search/truncate-blocks-table! repo)
   (search/truncate-blocks-table! repo)
   ;; unneeded serialization
   ;; unneeded serialization
   (search/upsert-blocks! repo (bean/->js data))
   (search/upsert-blocks! repo (bean/->js data))
-  (search/write-search-version! repo)
   [])
   [])
 
 
 (defmethod handle :transact-blocks [_window [_ repo data]]
 (defmethod handle :transact-blocks [_window [_ repo data]]
@@ -526,10 +525,6 @@
     (f window graph-name)
     (f window graph-name)
     (state/set-state! :window/once-graph-ready nil)))
     (state/set-state! :window/once-graph-ready nil)))
 
 
-(defmethod handle :searchVersionChanged?
-  [^js _win [_ graph]]
-  (search/version-changed? graph))
-
 (defmethod handle :reloadWindowPage [^js win]
 (defmethod handle :reloadWindowPage [^js win]
   (logger/warn ::reload-window-page)
   (logger/warn ::reload-window-page)
   (when-let [web-content (.-webContents win)]
   (when-let [web-content (.-webContents win)]

+ 7 - 37
src/electron/electron/search.cljs

@@ -6,12 +6,6 @@
             ["electron" :refer [app]]
             ["electron" :refer [app]]
             [electron.logger :as logger]))
             [electron.logger :as logger]))
 
 
-;; version of the search cache
-;; ver. 0.0.1: initial version
-;; ver. 0.0.2: bump version as page name breaking changes of LogSeq 0.5.7 ~ 0.5.9
-(defonce version "0.0.2")
-(defonce invalid-version "0.0.0")
-
 (defonce databases (atom nil))
 (defonce databases (atom nil))
 
 
 (defn close!
 (defn close!
@@ -79,15 +73,9 @@
   (let [path (.getPath ^object app "userData")]
   (let [path (.getPath ^object app "userData")]
     (path/join path "search")))
     (path/join path "search")))
 
 
-(defn get-search-ver-dir
-  []
-  (let [path (.getPath ^object app "userData")]
-    (path/join path "search.versions")))
-
 (defn ensure-search-dir!
 (defn ensure-search-dir!
   []
   []
-  (fs/ensureDirSync (get-search-dir))
-  (fs/ensureDirSync (get-search-ver-dir)))
+  (fs/ensureDirSync (get-search-dir)))
 
 
 (defn get-db-full-path
 (defn get-db-full-path
   [db-name]
   [db-name]
@@ -95,29 +83,12 @@
         search-dir (get-search-dir)]
         search-dir (get-search-dir)]
     [db-name (path/join search-dir db-name)]))
     [db-name (path/join search-dir db-name)]))
 
 
-(defn get-db-version-path
-  "File for storing search cache version"
+(defn get-db-path
+  "Search cache paths"
   [db-name]
   [db-name]
   (let [db-name (sanitize-db-name db-name)
   (let [db-name (sanitize-db-name db-name)
-        search-dir (get-search-dir)
-        search-ver-dir (get-search-ver-dir)]
-    [db-name (path/join search-dir db-name) (path/join search-ver-dir db-name)]))
-
-(defn get-search-version
-  [db-name]
-  (let [[_db-name db-full-path db-ver-path] (get-db-version-path db-name)]
-    (if (and (fs/existsSync db-ver-path) (fs/existsSync db-full-path)) ;; avoid case that only ver file exists
-      (.toString (fs/readFileSync db-ver-path))
-      invalid-version))) ;; no any cache exists
-
-(defn write-search-version!
-  [db-name]
-  (let [[_db-name _db-full-path db-ver-path] (get-db-version-path db-name)]
-    (fs/writeFileSync db-ver-path version)))
-
-(defn version-changed?
-  [db-name]
-  (not= version (get-search-version db-name)))
+        search-dir (get-search-dir)]
+    [db-name (path/join search-dir db-name)]))
 
 
 (defn open-db!
 (defn open-db!
   [db-name]
   [db-name]
@@ -224,10 +195,9 @@
   [repo]
   [repo]
   (when-let [database (get-db repo)]
   (when-let [database (get-db repo)]
     (.close database)
     (.close database)
-    (let [[db-name db-full-path db-ver-path] (get-db-version-path repo)]
-      (logger/info "Delete search indice" {:path db-full-path})
+    (let [[db-name db-full-path] (get-db-path repo)]
+      (logger/info "Delete search indice: " db-full-path)
       (fs/unlinkSync db-full-path)
       (fs/unlinkSync db-full-path)
-      (fs/unlinkSync db-ver-path)
       (swap! databases dissoc db-name))))
       (swap! databases dissoc db-name))))
 
 
 (defn query
 (defn query

+ 165 - 0
src/main/frontend/components/conversion.cljs

@@ -0,0 +1,165 @@
+(ns frontend.components.conversion
+  (:require [clojure.core.async :as async]
+            [cljs.core.async.interop :refer [p->c]]
+            [promesa.core :as p]
+            [electron.ipc :as ipc]
+            [logseq.graph-parser.util :as gp-util]
+            [frontend.util :as util]
+            [frontend.state :as state]
+            [frontend.ui :as ui]
+            [frontend.handler.page :as page-handler]
+            [frontend.handler.conversion :refer [supported-filename-formats write-filename-format! calc-rename-target]]
+            [frontend.db :as db]
+            [frontend.context.i18n :refer [t]]
+            [rum.core :as rum]))
+
+(defn- ask-for-re-index
+  "Multiple-windows? (optional) - if multiple exist on the current graph
+   Dont receive param `repo` as `graph/ask-for-re-index` event doesn't accept repo param"
+  ([]
+   (p/let [repo (state/get-current-repo)
+           multiple-windows? (ipc/ipc "graphHasMultipleWindows" repo)]
+     (ask-for-re-index multiple-windows?)))
+  ([multiple-windows?]
+   (state/pub-event! [:graph/ask-for-re-index (atom multiple-windows?)
+                      (ui/admonition :tip [:p (t :file-rn/re-index)])])))
+
+(defn- <close-modal-on-done
+  "Ask users to re-index when the modal is exited"
+  []
+  (async/go (state/close-settings!)
+            (async/<! (async/timeout 100)) ;; modal race condition requires investigation
+            (ask-for-re-index)))
+
+(rum/defc legacy-warning
+  [repo *target-format *dir-format *solid-format]
+  [:div ;; Normal UX stage 1: show the admonition & button for users using legacy format
+   (ui/admonition :warning [:p (t :file-rn/format-deprecated)])
+   [:p (t :file-rn/instruct-1)]
+   [:p (t :file-rn/instruct-2)
+    (ui/button (t :file-rn/confirm-proceed) ;; the button is for triple-lowbar only
+               :class "text-md p-2 mr-1"
+               :on-click #(do (reset! *target-format :triple-lowbar)
+                            (reset! *dir-format (state/get-filename-format repo)) ;; assure it's uptodate
+                            (write-filename-format! repo :triple-lowbar)
+                            (reset! *solid-format :triple-lowbar)))]
+   [:p (t :file-rn/instruct-3)]])
+
+(rum/defc filename-format-select
+  "A dropdown menu for selecting the target filename format"
+  [*target-format disabled?]
+  [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
+   [:label.block.text-sm.font-medium.leading-5
+    (t :file-rn/select-format)
+    [:select.form-select.is-small {:disabled disabled?
+                                   :value     (name @*target-format)
+                                   :on-change (fn [e]
+                                                (let [format-str (util/evalue e)]
+                                                  (reset! *target-format (keyword format-str))))}
+     (for [format supported-filename-formats]
+       (let [format-str (name format)]
+         [:option {:key format-str :value format-str} format-str]))]]])
+
+;; UI for files that have been breaking changed. Conversion required to revert the change.
+;; UI logic:
+;;   When dropdown box item switched, activate the `Proceed` button;
+;;   When `Proceed` button clicked, write the filename format to config and allow the renaming actions
+(rum/defcs files-breaking-changed < rum/reactive
+  (rum/local nil ::pages)          ;; pages require renaming, a map of {path -> [page-entity file-entity]}
+  (rum/local nil ::dir-format)     ;; format previously (on `proceed `button clicked)
+  (rum/local nil ::target-format)  ;; format to be converted to (on `proceed` button clicked)
+  (rum/local nil ::solid-format)   ;; format persisted to config
+  (rum/local false ::switch-disabled?) ;; disable the dropdown box when proceeded
+  [state]
+  (let [repo           (state/sub :git/current-repo)
+        *dir-format    (::dir-format state)
+        *target-format (::target-format state)
+        *solid-format  (::solid-format state)
+        *pages         (::pages state)
+        need-persist?  (not= @*solid-format @*target-format)
+        *switch-disabled? (::switch-disabled? state)]
+    (when (nil? @*pages) ;; would triggered on initialization
+      (let [pages-with-file (db/get-pages-with-file repo)
+            the-keys        (map (fn [[_page file]] (:file/path file)) pages-with-file)]
+        (reset! *pages (zipmap the-keys pages-with-file))))
+    (when (and (nil? @*dir-format) ;; would triggered on initialization
+               (nil? @*solid-format)
+               (nil? @*target-format))
+      (let [config-format (state/get-filename-format repo)]
+        (reset! *dir-format config-format)
+        (reset! *solid-format config-format)
+        (reset! *target-format :triple-lowbar)))
+    [:div
+     (when (state/developer-mode?)
+       [:div
+        (filename-format-select *target-format @*switch-disabled?)
+        (ui/button (t :file-rn/select-confirm-proceed) ;; the button is for persisting selected format
+                   :disabled (not need-persist?)
+                   :class "text-sm p-1 mr-1"
+                   :on-click #(do (reset! *dir-format (state/get-filename-format repo)) ;; assure it's uptodate
+                                  (write-filename-format! repo @*target-format)
+                                  (reset! *solid-format @*target-format)
+                                  (reset! *switch-disabled? true)))
+        [:hr]])
+     [:h1.title (t :settings-page/filename-format)]
+     [:div.rounded-md.opacity-70
+      [:p (t :file-rn/filename-desc-1)]
+      [:p (t :file-rn/filename-desc-2)]
+      [:p (t :file-rn/filename-desc-3)]
+      [:p (t :file-rn/filename-desc-4)]]
+     (when (= @*solid-format :legacy)
+       (legacy-warning repo *target-format *dir-format *solid-format))
+     [:div.cp__settings-files-breaking-changed {:disabled need-persist?} [:hr]
+      (let [rename-items  (->> (vals @*pages)
+                               (map (fn [[page file]]
+                                      (when-let [ret (calc-rename-target page (:file/path file) @*dir-format @*target-format)]
+                                        (merge ret {:page page :file file}))))
+                               (remove nil?))
+            <rename-all   #(async/go (doseq [{:keys [file target status]} rename-items]
+                                       (when (not= status :unreachable)
+                                         (async/<! (p->c (page-handler/rename-file! file target (constantly nil) true)))))
+                                     (<close-modal-on-done))]
+
+        (if (not-empty rename-items)
+          [:div ;; Normal UX stage 2: close stage 1 UI, show the action description as admolition
+           (if (and (= @*solid-format :triple-lowbar)
+                    (= @*dir-format :legacy))
+             (ui/admonition :tip [:p (t :file-rn/need-action)])
+             [:p (t :file-rn/need-action)])
+           [:p
+            (ui/button
+             (str (t :file-rn/all-action) " (" (count rename-items) ")")
+             :on-click <rename-all
+             :class "text-md p-2 mr-1")
+            (t :file-rn/or-select-actions)
+            [:a {:on-click <close-modal-on-done}
+             (t :file-rn/close-panel)]
+            (t :file-rn/or-select-actions-2)]
+           [:p (t :file-rn/legend)]
+           [:table.table-auto
+            [:tbody
+             (for [{:keys [page file status target old-title changed-title]} rename-items]
+               (let [path           (:file/path file)
+                     src-file-name  (gp-util/path->file-name path)
+                     tgt-file-name  (str target "." (gp-util/path->file-ext path))
+                     rm-item-fn     #(swap! *pages dissoc path)
+                     rename-fn      #(page-handler/rename-file! file target rm-item-fn)
+                     rename-but     [:a {:on-click rename-fn
+                                         :title (t :file-rn/apply-rename)}
+                                     [:span (t :file-rn/rename src-file-name tgt-file-name)]]
+                     rename-but-sm  (ui/button
+                                     (t :file-rn/rename-sm)
+                                     :on-click rename-fn
+                                     :class "text-sm p-1 mr-1"
+                                     :style {:word-break "normal"})]
+                 [:tr {:key (:block/name page)}
+                  [:td [:div [:p "📄 " old-title]]
+                   (case status
+                     :breaking ;; if properety title override the title, it't not breaking change
+                     [:div [:p "🟡 " (t :file-rn/suggest-rename) rename-but]
+                      [:p (t :file-rn/otherwise-breaking) " \"" changed-title \"]]
+                     :unreachable
+                     [:div [:p "🔴 " (t :file-rn/unreachable-title changed-title)]]
+                     [:div [:p "🟢 " (t :file-rn/optional-rename) rename-but]])]
+                  [:td rename-but-sm]]))]]]
+          [:div "🎉 " (t :file-rn/no-action)]))]]))

+ 39 - 34
src/main/frontend/components/file.cljs

@@ -22,44 +22,49 @@
   (let [route-match (first (:rum/args state))]
   (let [route-match (first (:rum/args state))]
     (get-in route-match [:parameters :path :path])))
     (get-in route-match [:parameters :path :path])))
 
 
-(rum/defc files < rum/reactive
+(rum/defc files-all < rum/reactive
+  []
+  (when-let [current-repo (state/sub :git/current-repo)]
+    (let [files (db/get-files current-repo)
+          mobile? (util/mobile?)]
+      [:table.table-auto
+       [:thead
+        [:tr
+         [:th (t :file/name)]
+         (when-not mobile?
+           [:th (t :file/last-modified-at)])
+         (when-not mobile?
+           [:th ""])]]
+       [:tbody
+        (for [[file modified-at] files]
+          (let [file-id file]
+            [:tr {:key file-id}
+             [:td
+              (let [href (if (gp-config/draw? file)
+                           (rfe/href :draw nil {:file (string/replace file (str gp-config/default-draw-directory "/") "")})
+                           (rfe/href :file {:path file-id}))]
+                [:a {:href href}
+                 file])]
+             (when-not mobile?
+               [:td [:span.text-gray-500.text-sm
+                     (if (zero? modified-at)
+                       (t :file/no-data)
+                       (date/get-date-time-string
+                        (t/to-default-time-zone (tc/to-date-time modified-at))))]])
+
+             (when-not mobile?
+               [:td [:a.text-sm
+                     {:on-click (fn [_e]
+                                  (export-handler/download-file! file))}
+                     [:span (t :download)]]])]))]])))
+
+(rum/defc files
   []
   []
   [:div.flex-1.overflow-hidden
   [:div.flex-1.overflow-hidden
    [:h1.title
    [:h1.title
     (t :all-files)]
     (t :all-files)]
-   (when-let [current-repo (state/sub :git/current-repo)]
-     (let [files (db/get-files current-repo)
-           mobile? (util/mobile?)]
-       [:table.table-auto
-        [:thead
-         [:tr
-          [:th (t :file/name)]
-          (when-not mobile?
-            [:th (t :file/last-modified-at)])
-          (when-not mobile?
-            [:th ""])]]
-        [:tbody
-         (for [[file modified-at] files]
-           (let [file-id file]
-             [:tr {:key file-id}
-              [:td
-               (let [href (if (gp-config/draw? file)
-                            (rfe/href :draw nil {:file (string/replace file (str gp-config/default-draw-directory "/") "")})
-                            (rfe/href :file {:path file-id}))]
-                 [:a {:href href}
-                  file])]
-              (when-not mobile?
-                [:td [:span.text-gray-500.text-sm
-                      (if (zero? modified-at)
-                        (t :file/no-data)
-                        (date/get-date-time-string
-                         (t/to-default-time-zone (tc/to-date-time modified-at))))]])
-
-              (when-not mobile?
-                [:td [:a.text-sm
-                      {:on-click (fn [_e]
-                                   (export-handler/download-file! file))}
-                      [:span (t :download)]]])]))]]))])
+   (files-all)
+   ])
 
 
 (rum/defcs file < rum/reactive
 (rum/defcs file < rum/reactive
   {:did-mount (fn [state]
   {:did-mount (fn [state]

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

@@ -186,7 +186,7 @@
                       :options (cond->
                       :options (cond->
                                 {:on-click
                                 {:on-click
                                  (fn []
                                  (fn []
-                                   (state/pub-event! [:graph/ask-for-re-index *multiple-windows?]))})}
+                                   (state/pub-event! [:graph/ask-for-re-index *multiple-windows? nil]))})}
         new-window-link (when (and (util/electron?)
         new-window-link (when (and (util/electron?)
                                    ;; New Window button in menu bar of macOS is available.
                                    ;; New Window button in menu bar of macOS is available.
                                    (not util/mac?))
                                    (not util/mac?))

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

@@ -203,7 +203,7 @@
 (defn- search-item-render
 (defn- search-item-render
   [search-q {:keys [type data alias]}]
   [search-q {:keys [type data alias]}]
   (let [search-mode (state/get-search-mode)
   (let [search-mode (state/get-search-mode)
-        data (if (string? data) (pdf-assets/fix-local-asset-filename data) data)]
+        data (if (string? data) (pdf-assets/fix-local-asset-pagename data) data)]
     [:div {:class "py-2"}
     [:div {:class "py-2"}
      (case type
      (case type
        :graph-add-filter
        :graph-add-filter

+ 15 - 4
src/main/frontend/components/settings.cljs

@@ -28,7 +28,8 @@
             [reitit.frontend.easy :as rfe]
             [reitit.frontend.easy :as rfe]
             [rum.core :as rum]
             [rum.core :as rum]
             [frontend.mobile.util :as mobile-util]
             [frontend.mobile.util :as mobile-util]
-            [frontend.db :as db]))
+            [frontend.db :as db]
+            [frontend.components.conversion :as conversion-component]))
 
 
 (defn toggle
 (defn toggle
   [label-for name state on-toggle & [detail-text]]
   [label-for name state on-toggle & [detail-text]]
@@ -416,7 +417,8 @@
              (config-handler/set-config! :feature/enable-encryption? value)
              (config-handler/set-config! :feature/enable-encryption? value)
              (when value
              (when value
                (state/close-modal!)
                (state/close-modal!)
-               (js/setTimeout (fn [] (state/pub-event! [:graph/ask-for-re-index (atom false)]))
+               ;; FIXME: Don't send the `(atom false)` ! Should check multi-window! or internal status error happens
+               (js/setTimeout (fn [] (state/pub-event! [:graph/ask-for-re-index (atom false) nil]))
                               100)))
                               100)))
           [:p.text-sm.opacity-50 "⚠️ This feature is experimental! "
           [:p.text-sm.opacity-50 "⚠️ This feature is experimental! "
            [:span "You can use "]
            [:span "You can use "]
@@ -537,6 +539,14 @@
    {:left-label (t :settings-page/network-proxy)
    {:left-label (t :settings-page/network-proxy)
     :action (user-proxy-settings agent-opts)}))
     :action (user-proxy-settings agent-opts)}))
 
 
+(defn filename-format-row []
+  (row-with-button-action
+   {:left-label (t :settings-page/filename-format)
+    :button-label (t :settings-page/edit-setting)
+    :on-click #(state/set-sub-modal!
+                (fn [_] (conversion-component/files-breaking-changed))
+                {:id :filename-format-panel :center? true})}))
+
 (rum/defcs settings-general < rum/reactive
 (rum/defcs settings-general < rum/reactive
   [_state current-repo]
   [_state current-repo]
   (let [preferred-language (state/sub [:preferred-language])
   (let [preferred-language (state/sub [:preferred-language])
@@ -604,7 +614,7 @@
      [:p (t :settings-page/git-confirm)])])
      [:p (t :settings-page/git-confirm)])])
 
 
 (rum/defc settings-advanced < rum/reactive
 (rum/defc settings-advanced < rum/reactive
-  []
+  [current-repo]
   (let [instrument-disabled? (state/sub :instrument/disabled?)
   (let [instrument-disabled? (state/sub :instrument/disabled?)
         developer-mode? (state/sub [:ui/developer-mode?])
         developer-mode? (state/sub [:ui/developer-mode?])
         https-agent-opts (state/sub [:electron/user-cfgs :settings/agent])]
         https-agent-opts (state/sub [:electron/user-cfgs :settings/agent])]
@@ -613,6 +623,7 @@
      (usage-diagnostics-row t instrument-disabled?)
      (usage-diagnostics-row t instrument-disabled?)
      (when-not (mobile-util/native-platform?) (developer-mode-row t developer-mode?))
      (when-not (mobile-util/native-platform?) (developer-mode-row t developer-mode?))
      (when (util/electron?) (https-user-agent-row https-agent-opts))
      (when (util/electron?) (https-user-agent-row https-agent-opts))
+     (when (and (util/electron?) (not (config/demo-graph? current-repo))) (filename-format-row))
      (clear-cache-row t)
      (clear-cache-row t)
 
 
      (ui/admonition
      (ui/admonition
@@ -769,7 +780,7 @@
          (settings-git)
          (settings-git)
 
 
          :advanced
          :advanced
-         (settings-advanced)
+         (settings-advanced current-repo)
 
 
          :features
          :features
          (settings-features)
          (settings-features)

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

@@ -286,6 +286,13 @@
       }
       }
     }
     }
   }
   }
+
+  &-files-breaking-changed {
+    &[disabled] {
+      opacity: 0.5;
+      pointer-events: none;
+    }
+  }
 }
 }
 
 
 html.is-native-android,
 html.is-native-android,

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

@@ -88,7 +88,7 @@
               (route-handler/redirect-to-whiteboard! name)
               (route-handler/redirect-to-whiteboard! name)
               (route-handler/redirect-to-page! name {:click-from-recent? recent?})))))}
               (route-handler/redirect-to-page! name {:click-from-recent? recent?})))))}
      [:span.page-icon (if whiteboard-page? (ui/icon "whiteboard" {:extension? true}) icon)]
      [:span.page-icon (if whiteboard-page? (ui/icon "whiteboard" {:extension? true}) icon)]
-     [:span.page-title (pdf-assets/fix-local-asset-filename original-name)]]))
+     [:span.page-title (pdf-assets/fix-local-asset-pagename original-name)]]))
 
 
 (defn get-page-icon [page-entity]
 (defn get-page-icon [page-entity]
   (let [default-icon (ui/icon "page" {:extension? true})
   (let [default-icon (ui/icon "page" {:extension? true})

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

@@ -46,7 +46,7 @@
   get-block-children-ids get-block-immediate-children get-block-page
   get-block-children-ids get-block-immediate-children get-block-page
   get-custom-css get-date-scheduled-or-deadlines
   get-custom-css get-date-scheduled-or-deadlines
   get-file-blocks get-file-last-modified-at get-file get-file-page get-file-page-id file-exists?
   get-file-blocks get-file-last-modified-at get-file get-file-page get-file-page-id file-exists?
-  get-files get-files-blocks get-files-full get-journals-length
+  get-files get-files-blocks get-files-full get-journals-length get-pages-with-file
   get-latest-journals get-page get-page-alias get-page-alias-names get-paginated-blocks
   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-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 get-page-referenced-blocks-full get-page-referenced-pages get-page-unlinked-references

+ 16 - 9
src/main/frontend/db/model.cljs

@@ -70,10 +70,7 @@
               nil)
               nil)
      react)))
      react)))
 
 
-(defn get-original-name
-  [page-entity]
-  (or (:block/original-name page-entity)
-      (:block/name page-entity)))
+(def get-original-name util/get-page-original-name)
 
 
 (defn get-tag-pages
 (defn get-tag-pages
   [repo tag-name]
   [repo tag-name]
@@ -124,6 +121,16 @@
      [?page :block/name]]
      [?page :block/name]]
    (conn/get-db repo)))
    (conn/get-db repo)))
 
 
+(defn get-pages-with-file
+  "Return full file entity for calling file renaming"
+  [repo]
+  (d/q
+   '[:find (pull ?page [:block/name :block/properties :block/journal?]) (pull ?file [*])
+     :where
+     [?page :block/name ?page-name]
+     [?page :block/file ?file]]
+   (conn/get-db repo)))
+
 (defn get-page-alias
 (defn get-page-alias
   [repo page-name]
   [repo page-name]
   (when-let [db (and repo (conn/get-db repo))]
   (when-let [db (and repo (conn/get-db repo))]
@@ -138,6 +145,7 @@
              distinct)))
              distinct)))
 
 
 (defn get-alias-source-page
 (defn get-alias-source-page
+  "return the source page (page-name) of an alias"
   [repo alias]
   [repo alias]
   (when-let [db (and repo (conn/get-db repo))]
   (when-let [db (and repo (conn/get-db repo))]
     (let [alias (util/page-name-sanity-lc alias)
     (let [alias (util/page-name-sanity-lc alias)
@@ -150,6 +158,8 @@
                       db
                       db
                       alias)
                       alias)
                  (db-utils/seq-flatten))]
                  (db-utils/seq-flatten))]
+      ;; may be a case that a user added same alias into multiple pages.
+      ;; only return the first result for idiot-proof
       (when (seq pages)
       (when (seq pages)
         (some (fn [page]
         (some (fn [page]
                 (let [aliases (->> (get-in page [:block/properties :alias])
                 (let [aliases (->> (get-in page [:block/properties :alias])
@@ -174,16 +184,13 @@
          ;; (sort-by last)
          ;; (sort-by last)
          (reverse))))
          (reverse))))
 
 
-(defn get-files-v2
+(defn get-files-entity
   [repo]
   [repo]
   (when-let [db (conn/get-db repo)]
   (when-let [db (conn/get-db repo)]
     (->> (d/q
     (->> (d/q
           '[:find ?file ?path
           '[:find ?file ?path
-            ;; ?modified-at
             :where
             :where
-            [?file :file/path ?path]
-            ;; [?file :file/last-modified-at ?modified-at]
-            ]
+            [?file :file/path ?path]]
           db)
           db)
          (seq)
          (seq)
          ;; (sort-by last)
          ;; (sort-by last)

+ 36 - 3
src/main/frontend/dicts.cljc

@@ -122,6 +122,33 @@
         :file/last-modified-at "Last modified at"
         :file/last-modified-at "Last modified at"
         :file/no-data "No data"
         :file/no-data "No data"
         :file/format-not-supported "Format .{1} is not supported."
         :file/format-not-supported "Format .{1} is not supported."
+        :file-rn/re-index "Re-index is strongly recommended after the files are renamed and on other devices after syncing."
+        :file-rn/need-action "File rename actions are suggested to match the new format. Re-index is required on all devices when the renamed files are synced."
+        :file-rn/or-select-actions " or individually rename files below, then "
+        :file-rn/or-select-actions-2 ". These actions are not available once you close this panel."
+        :file-rn/legend "🟢 Optional rename actions; 🟡 Rename action required to avoid title change; 🔴 Breaking change."
+        :file-rn/close-panel "Close the Panel"
+        :file-rn/all-action "Apply all Actions!"
+        :file-rn/select-format "(Developer Mode Option, Dangerous!) Select filename format"
+        :file-rn/rename "rename file \"{1}\" to \"{2}\""
+        :file-rn/rename-sm "Rename"
+        :file-rn/apply-rename "Apply the file rename operation"
+        :file-rn/affected-pages "Affected Pages after the format change"
+        :file-rn/suggest-rename "Action required: "
+        :file-rn/otherwise-breaking "Or title will becomes"
+        :file-rn/no-action "Well done! No further action required"
+        :file-rn/confirm-proceed "Update format!"
+        :file-rn/select-confirm-proceed "Dev: write format"
+        :file-rn/unreachable-title "Warning! The page name will become {1} under current filename format, unless setup `title::` property manually"
+        :file-rn/optional-rename "Suggestion: "
+        :file-rn/format-deprecated "You are currently using an outdated format. Updating to the latest format is highly recommended. Please backup your data and close Logseq clients on other devices before the operation."
+        :file-rn/filename-desc-1 "This setting configures how a page is stored to a file. Logseq stores a page to a file with the same name."
+        :file-rn/filename-desc-2 "Some characters like \"/\" or \"?\" are invalid for a filename."
+        :file-rn/filename-desc-3 "Logseq replaces invalid characters with their URL encoded equivalent to make them valid (e.g. \"?\" becomes \"%3F\")."
+        :file-rn/filename-desc-4 "The namespace separator \"/\" is also replaced by \"___\" (triple underscore) for aesthetic consideration."
+        :file-rn/instruct-1 "It's a 2-step process to update filename format:"
+        :file-rn/instruct-2 "1. Click "
+        :file-rn/instruct-3 "2. Follow the instructions below to rename the files to the new format:"
         :page/created-at "Created At"
         :page/created-at "Created At"
         :page/updated-at "Updated At"
         :page/updated-at "Updated At"
         :page/backlinks "Back Links"
         :page/backlinks "Back Links"
@@ -172,6 +199,7 @@
         :settings-page/edit-global-config-edn "Edit global config.edn"
         :settings-page/edit-global-config-edn "Edit global config.edn"
         :settings-page/edit-custom-css "Edit custom.css"
         :settings-page/edit-custom-css "Edit custom.css"
         :settings-page/edit-export-css "Edit export.css"
         :settings-page/edit-export-css "Edit export.css"
+        :settings-page/edit-setting "Edit"
         :settings-page/custom-configuration "Custom configuration"
         :settings-page/custom-configuration "Custom configuration"
         :settings-page/custom-global-configuration "Custom global configuration"
         :settings-page/custom-global-configuration "Custom global configuration"
         :settings-page/custom-theme "Custom theme"
         :settings-page/custom-theme "Custom theme"
@@ -211,6 +239,7 @@
         :settings-page/plugin-system "Plugins"
         :settings-page/plugin-system "Plugins"
         :settings-page/enable-flashcards "Flashcards"
         :settings-page/enable-flashcards "Flashcards"
         :settings-page/network-proxy "Network proxy"
         :settings-page/network-proxy "Network proxy"
+        :settings-page/filename-format "Filename format"
         :settings-page/alpha-features "Alpha features"
         :settings-page/alpha-features "Alpha features"
         :settings-page/login-prompt "To access new features before anyone else you must be a financial supporter or alpha tester of Logseq and therefore log in first."
         :settings-page/login-prompt "To access new features before anyone else you must be a financial supporter or alpha tester of Logseq and therefore log in first."
         :settings-page/sync "Sync"
         :settings-page/sync "Sync"
@@ -369,7 +398,10 @@
 
 
         :file-sync/other-user-graph "Current local graph is bound to other user's remote graph. So can't start syncing."
         :file-sync/other-user-graph "Current local graph is bound to other user's remote graph. So can't start syncing."
         :file-sync/graph-deleted "The current remote graph has been deleted"
         :file-sync/graph-deleted "The current remote graph has been deleted"
-        }
+
+        :conversion/non-desktop "Graph directory in old versions needs to be converted to the new format.
+          Please use the desktop app to do the conversion."
+        :conversion/write-filename-format "Apply format for incoming files"}
 
 
    :de {:help/about "Über Logseq"
    :de {:help/about "Über Logseq"
         :on-boarding/demo-graph "Dies ist ein Demo-Graph. Änderungen werden nicht gespeichert, solange Sie kein lokales Verzeichnis öffnen."
         :on-boarding/demo-graph "Dies ist ein Demo-Graph. Änderungen werden nicht gespeichert, solange Sie kein lokales Verzeichnis öffnen."
@@ -1326,6 +1358,7 @@
            :settings-page/tab-version-control "多版本控制"
            :settings-page/tab-version-control "多版本控制"
            :settings-page/plugin-system "插件系统"
            :settings-page/plugin-system "插件系统"
            :settings-page/network-proxy "网络代理"
            :settings-page/network-proxy "网络代理"
+           :settings-page/filename-format "文件名格式"
            :logseq "Logseq"
            :logseq "Logseq"
            :on "已打开"
            :on "已打开"
            :more-options "更多选项"
            :more-options "更多选项"
@@ -2749,7 +2782,7 @@
            :settings-page/edit-export-css "Editar export.css"
            :settings-page/edit-export-css "Editar export.css"
            :settings-page/enable-flashcards "Flashcards"
            :settings-page/enable-flashcards "Flashcards"
            :settings-page/export-theme "Exportar Tema"
            :settings-page/export-theme "Exportar Tema"
-           
+
            :discourse-title "Nosso fórum!"
            :discourse-title "Nosso fórum!"
            :importing "Importando"
            :importing "Importando"
            :asset/copy "Copiar imagem"
            :asset/copy "Copiar imagem"
@@ -3092,7 +3125,7 @@
         :settings-page/export-theme "Exportar tema"
         :settings-page/export-theme "Exportar tema"
         :settings-page/network-proxy "Proxy de rede"
         :settings-page/network-proxy "Proxy de rede"
         :settings-page/plugin-system "Sistema de plugins"
         :settings-page/plugin-system "Sistema de plugins"
-        
+
         :discourse-title "Nosso fórum!"
         :discourse-title "Nosso fórum!"
         :importing "Importando"
         :importing "Importando"
         :asset/copy "Copiar imagem"
         :asset/copy "Copiar imagem"

+ 37 - 22
src/main/frontend/extensions/pdf/assets.cljs

@@ -10,6 +10,7 @@
             [frontend.util.page-property :as page-property]
             [frontend.util.page-property :as page-property]
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.util :as util]
             [frontend.util :as util]
+            [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.config :as gp-config]
             [logseq.graph-parser.config :as gp-config]
             [logseq.graph-parser.util.block-ref :as block-ref]
             [logseq.graph-parser.util.block-ref :as block-ref]
             [medley.core :as medley]
             [medley.core :as medley]
@@ -17,9 +18,21 @@
             [reitit.frontend.easy :as rfe]
             [reitit.frontend.easy :as rfe]
             [rum.core :as rum]))
             [rum.core :as rum]))
 
 
-(defn hls-file?
-  [filename]
-  (and filename (string? filename) (string/starts-with? filename "hls__")))
+(def HLS-PREFIX "hls__")
+
+(def HLS-PREFIX-DISPLAY "📒")
+
+(def HLS-PREFIX-LEN (count HLS-PREFIX))
+
+(def HLS-PREFIX-PATTERN (re-pattern (str "^" HLS-PREFIX)))
+
+(defn make-hls
+  [name]
+  (str HLS-PREFIX name))
+
+(defn hls-page?
+  [title]
+  (and title (string? title) (string/starts-with? title HLS-PREFIX)))
 
 
 (defn inflate-asset
 (defn inflate-asset
   [full-path]
   [full-path]
@@ -154,7 +167,7 @@
   [pdf-current]
   [pdf-current]
   (let [page-name (:key pdf-current)
   (let [page-name (:key pdf-current)
         page-name (string/trim page-name)
         page-name (string/trim page-name)
-        page-name (str "hls__" page-name)
+        page-name (make-hls page-name)
         page (db-model/get-page page-name)
         page (db-model/get-page page-name)
         url (:url pdf-current)
         url (:url pdf-current)
         format (state/get-preferred-format)
         format (state/get-preferred-format)
@@ -222,7 +235,7 @@
         page (db-utils/pull (:db/id (:block/page block)))
         page (db-utils/pull (:db/id (:block/page block)))
         page-name (:block/original-name page)
         page-name (:block/original-name page)
         file-path (:file-path (:block/properties page))]
         file-path (:file-path (:block/properties page))]
-    (when-let [target-key (and page-name (subs page-name 5))]
+    (when-let [target-key (and page-name (subs page-name HLS-PREFIX-LEN))]
       (p/let [hls (resolve-hls-data-by-key$ target-key)
       (p/let [hls (resolve-hls-data-by-key$ target-key)
               hls (and hls (:highlights hls))]
               hls (and hls (:highlights hls))]
         (let [file-path (if file-path file-path (str target-key ".pdf"))]
         (let [file-path (if file-path file-path (str target-key ".pdf"))]
@@ -242,37 +255,39 @@
   ([current] (goto-annotations-page! current nil))
   ([current] (goto-annotations-page! current nil))
   ([current id]
   ([current id]
    (when-let [name (:key current)]
    (when-let [name (:key current)]
-     (rfe/push-state :page {:name (str "hls__" name)} (if id {:anchor (str "block-content-" + id)} nil)))))
+     (rfe/push-state :page {:name (make-hls name)} (if id {:anchor (str "block-content-" + id)} nil)))))
 
 
 (rum/defc area-display
 (rum/defc area-display
   [block stamp]
   [block stamp]
   (let [id (:block/uuid block)
   (let [id (:block/uuid block)
         props (:block/properties block)]
         props (:block/properties block)]
     (when-let [page (db-utils/pull (:db/id (:block/page block)))]
     (when-let [page (db-utils/pull (:db/id (:block/page block)))]
-      (when-let [group-key (string/replace-first (:block/original-name page) #"^hls__" "")]
+      (when-let [group-key (string/replace-first (:block/original-name page) HLS-PREFIX-PATTERN "")]
         (when-let [hl-page (:hl-page props)]
         (when-let [hl-page (:hl-page props)]
-          (let [encoded-chars? (boolean (re-find #"(?i)%[0-9a-f]{2}" group-key))
+          (let [encoded-chars? (boolean (re-find gp-util/url-encoded-pattern group-key))
                 group-key (if encoded-chars? (js/encodeURI group-key) group-key)
                 group-key (if encoded-chars? (js/encodeURI group-key) group-key)
                 asset-path (editor-handler/make-asset-url
                 asset-path (editor-handler/make-asset-url
                              (str "/" gp-config/local-assets-dir "/" group-key "/" (str hl-page "_" id "_" stamp ".png")))]
                              (str "/" gp-config/local-assets-dir "/" group-key "/" (str hl-page "_" id "_" stamp ".png")))]
             [:span.hl-area
             [:span.hl-area
              [:img {:src asset-path}]]))))))
              [:img {:src asset-path}]]))))))
 
 
-(defn fix-local-asset-filename
-  [filename]
-  (when-not (string/blank? filename)
-    (let [local-asset? (re-find #"[0-9]{13}_\d$" filename)
-          hls? (and local-asset? (re-find #"^hls__" filename))]
-      (if (or local-asset? hls?)
-        (-> filename
-            (subs 0 (- (count filename) 15))
-            (string/replace #"^hls__" "")
+(defn fix-local-asset-pagename
+  [title]
+  (when-not (string/blank? title)
+    (let [local-asset? (re-find #"[0-9]{13}_\d$" title)]
+      (if local-asset?
+        (-> title
+            (subs 0 (- (count title) 15))
+            (string/replace HLS-PREFIX-PATTERN HLS-PREFIX-DISPLAY)
             (string/replace "_" " ")
             (string/replace "_" " ")
             (string/trimr))
             (string/trimr))
-        filename))))
+        (-> title
+            (string/replace HLS-PREFIX-PATTERN HLS-PREFIX-DISPLAY)
+            (gp-util/safe-url-decode)) ;; In case user import URI pdf resource like #6167
+        ))))
 
 
-(rum/defc human-hls-filename-display
+(rum/defc human-hls-pagename-display
+  "Ensure it's a hls page by `hls-page?` before hand"
   [title]
   [title]
-  (when (string/starts-with? title "hls__")
-    [:a.asset-ref
-     (fix-local-asset-filename title)]))
+  [:a.asset-ref
+   (fix-local-asset-pagename title)])

+ 0 - 1
src/main/frontend/extensions/pdf/highlights.cljs

@@ -940,7 +940,6 @@
   [url initial-hls ^js pdf-document ops]
   [url initial-hls ^js pdf-document ops]
 
 
   ;;(dd "==== render pdf-viewer ====")
   ;;(dd "==== render pdf-viewer ====")
-
   (let [*el-ref (rum/create-ref)
   (let [*el-ref (rum/create-ref)
         [state, set-state!] (rum/use-state {:viewer nil :bus nil :link nil :el nil})
         [state, set-state!] (rum/use-state {:viewer nil :bus nil :link nil :el nil})
         [ano-state, set-ano-state!] (rum/use-state {:loaded-pages []})
         [ano-state, set-ano-state!] (rum/use-state {:loaded-pages []})

+ 19 - 15
src/main/frontend/handler/common/file.cljs

@@ -57,18 +57,22 @@
                 (and (mobile-util/native-ios?) (not= "/" (first file)))
                 (and (mobile-util/native-ios?) (not= "/" (first file)))
                 file
                 file
 
 
-                :else
-                file)
-         file (gp-util/path-normalize file)
-         new? (nil? (db/entity [:file/path file]))
-         options (merge (dissoc options :verbose)
-                        {:new? new?
-                         :delete-blocks-fn (partial get-delete-blocks repo-url)
-                         :extract-options (merge
-                                           {:user-config (state/get-config)
-                                            :date-formatter (state/get-date-formatter)
-                                            :page-name-order (state/page-name-order)
-                                            :block-pattern (config/get-block-pattern (gp-util/get-format file))
-                                            :supported-formats (gp-config/supported-formats)}
-                                           (when (some? verbose) {:verbose verbose}))})]
-     (:tx (graph-parser/parse-file (db/get-db repo-url false) file content options)))))
+                  :else
+                  file)
+           file (gp-util/path-normalize file)
+           new? (nil? (db/entity [:file/path file]))
+           options (merge (dissoc options :verbose)
+                          {:new? new?
+                           :delete-blocks-fn (partial get-delete-blocks repo-url)
+                           :extract-options (merge
+                                             {:user-config (state/get-config)
+                                              :date-formatter (state/get-date-formatter)
+                                              :block-pattern (config/get-block-pattern (gp-util/get-format file))
+                                              :supported-formats (gp-config/supported-formats)
+                                              :uri-encoded? (boolean (util/mobile?))
+                                              :filename-format (state/get-filename-format repo-url)}
+                                             (when (some? verbose) {:verbose verbose}))})]
+       (:tx (graph-parser/parse-file (db/get-db repo-url false) file content options)))
+     (catch :default e
+       (prn "Reset file failed " {:file file})
+       (log/error :exception e)))))

+ 5 - 4
src/main/frontend/handler/config.cljs

@@ -30,10 +30,11 @@
         (file-handler/set-file-content! repo path new-content)))))
         (file-handler/set-file-content! repo path new-content)))))
 
 
 (defn set-config!
 (defn set-config!
-  "Sets config state for repo-specific config"
-  [k v]
-  (let [path (config/get-repo-config-path)]
-    (repo-config-set-key-value path k v)))
+  ([k v]
+   (set-config! (state/get-current-repo) k v))
+  ([repo k v]
+   (let [path (config/get-repo-config-path repo)]
+     (repo-config-set-key-value path k v))))
 
 
 (defn toggle-ui-show-brackets! []
 (defn toggle-ui-show-brackets! []
   (let [show-brackets? (state/show-brackets?)]
   (let [show-brackets? (state/show-brackets?)]

+ 103 - 0
src/main/frontend/handler/conversion.cljs

@@ -0,0 +1,103 @@
+;; Convert data on updating from earlier version of Logseq on demand
+
+(ns frontend.handler.conversion
+  "For conversion logic between old version and new version"
+  (:require [logseq.graph-parser.util :as gp-util]
+            [frontend.util.fs :as fs-util]
+            [frontend.handler.config :refer [set-config!]]))
+
+(defn write-filename-format!
+  "Return:
+     Promise <void>"
+  [repo format]
+  (js/console.log (str "Writing character escaping format " format " of repo " repo))
+  (set-config! repo :file/name-format format))
+
+(defn- calc-current-name
+  "If the file body is parsed as the same page name, but the page name has a 
+   different file sanitization result under the current sanitization form, return 
+   the new file name.
+   Return: 
+     the file name for the page name under the current file naming rules, or `nil`
+     if no change of path happens"
+  [format file-body prop-title]
+  (let [page-title    (or prop-title
+                          (gp-util/title-parsing file-body format))
+        cur-file-body (fs-util/file-name-sanity page-title format)]
+    (when-not (= file-body cur-file-body)
+      {:status        :informal
+       :target        cur-file-body
+       :old-title     page-title
+       :changed-title page-title})))
+
+(defn- calc-previous-name
+  "We want to recover user's title back under new file name sanity rules.
+   Return: 
+     the file name for that page name under the current file naming rules,
+     and the new title if no action applied, or `nil` if no break change happens"
+  [old-format new-format file-body]
+  (let [new-title (gp-util/title-parsing file-body new-format) ;; Rename even the prop-title is provided.
+        old-title (gp-util/title-parsing file-body old-format)
+        target    (fs-util/file-name-sanity old-title new-format)]
+    (when (not= new-title old-title)
+      (if (not= target file-body)
+        {:status        :breaking
+         :target        target
+         :old-title     old-title
+         :changed-title new-title}
+        ;; Even the same file body are producing mis-matched titles - it's unreachable!
+        {:status        :unreachable
+         :target        target
+         :old-title     old-title
+         :changed-title new-title}))))
+
+;; Register sanitization / parsing fns in:
+;; logseq.graph-parser.util (parsing only)
+;; frontend.util.fs         (sanitization only)
+;; frontend.handler.conversion (both)
+;;   - the special rule in `is-manual-title-prop?`
+(defonce supported-filename-formats [:triple-lowbar :legacy])
+
+(defn- is-manual-title-prop?
+  "If it's an user defined title property instead of the generated one"
+  [format file-body prop-title]
+  (if prop-title
+    (not (or (= file-body (fs-util/file-name-sanity prop-title format))
+             (when (= format :legacy)
+               (= file-body (fs-util/file-name-sanity prop-title :legacy-dot)))))
+    false))
+
+(defn- calc-rename-target-impl
+  [old-format new-format file-body prop-title]
+  ;; dont rename journal page. officially it's stored as `yyyy_mm_dd`
+  ;; If it's a journal file imported with custom :journal/page-title-format,
+  ;;   and it includes reserved characters, format config change / file renaming is required. 
+  ;;   It's about user's own data management decision and should be handled
+  ;;   by user manually.
+  ;; Don't rename page that with a custom setup `title` property
+  (when (not (is-manual-title-prop? old-format file-body prop-title))
+      ;; the 'expected' title of the user when updating from the previous format, or title will be broken in new format
+    (or (when (and (nil? prop-title)
+                   (not= old-format new-format))
+          (calc-previous-name old-format new-format file-body))
+      ;; if no break-change conversion triggered, check if file name is in an informal / outdated style.
+        (calc-current-name new-format file-body prop-title))))
+
+(defn calc-rename-target
+  "Return the renaming status and new file body to recover the original title of the file in previous version. 
+   The return title should be the same as the title in the index file in the previous version.
+   return nil if no rename is needed.
+   page: the page entity
+   path: the path of the file of the page
+   old-format, new-format: the filename formats
+   Return:
+     {:status        :informal | :breaking | :unreachable
+      :target        the new file name
+      :old-title     the old title
+      :chagned-title the new title} | nil"
+  [page path old-format new-format]
+  (let [prop-title (get-in page [:block/properties :title])
+        file-body  (gp-util/path->file-body path)
+        journal?   (:block/journal? page)]
+    (when (not journal?)
+      (calc-rename-target-impl old-format new-format file-body prop-title))))

+ 26 - 10
src/main/frontend/handler/events.cljs

@@ -122,6 +122,8 @@
 (defmethod handle :graph/refresh [_]
 (defmethod handle :graph/refresh [_]
   (repo-handler/refresh-repos!))
   (repo-handler/refresh-repos!))
 
 
+;; FIXME: awful multi-arty function.
+;; Should use a `-impl` function instead of the awful `skip-ios-check?` param with nested callback.
 (defn- graph-switch
 (defn- graph-switch
   ([graph]
   ([graph]
    (graph-switch graph false))
    (graph-switch graph false))
@@ -142,6 +144,7 @@
        (repo-handler/refresh-repos!)
        (repo-handler/refresh-repos!)
        (file-sync-restart!)))))
        (file-sync-restart!)))))
 
 
+;; Parameters for the `persist-db` function, to show the notification messages
 (def persist-db-noti-m
 (def persist-db-noti-m
   {:before     #(notification/show!
   {:before     #(notification/show!
                  (ui/loading (t :graph/persist))
                  (ui/loading (t :graph/persist))
@@ -152,7 +155,8 @@
 
 
 (defn- graph-switch-on-persisted
 (defn- graph-switch-on-persisted
   "Logic for keeping db sync when switching graphs
   "Logic for keeping db sync when switching graphs
-   Only works for electron"
+   Only works for electron
+   graph: the target graph to switch to"
   [graph {:keys [persist?]}]
   [graph {:keys [persist?]}]
   (let [current-repo (state/get-current-repo)]
   (let [current-repo (state/get-current-repo)]
     (p/do!
     (p/do!
@@ -321,13 +325,16 @@
   (p/let [content (when content (encrypt/decrypt content))]
   (p/let [content (when content (encrypt/decrypt content))]
     (state/set-modal! #(git-component/file-specific-version path hash content))))
     (state/set-modal! #(git-component/file-specific-version path hash content))))
 
 
-(defmethod handle :graph/ready [[_ repo]]
+;; Hook on a graph is ready to be shown to the user.
+;; It's different from :graph/resotred, as :graph/restored is for window reloaded
+(defmethod handle :graph/ready
+  [[_ repo]]
   (when (config/local-db? repo)
   (when (config/local-db? repo)
-    (p/let [dir (config/get-repo-dir repo)
-            dir-exists? (fs/dir-exists? dir)]
+    (p/let [dir               (config/get-repo-dir repo)
+            dir-exists?       (fs/dir-exists? dir)]
       (when-not dir-exists?
       (when-not dir-exists?
         (state/pub-event! [:graph/dir-gone dir]))))
         (state/pub-event! [:graph/dir-gone dir]))))
-  (search-handler/rebuild-indices-when-stale! repo)
+  ;; FIXME: an ugly implementation for redirecting to page on new window is restored
   (repo-handler/graph-ready! repo))
   (repo-handler/graph-ready! repo))
 
 
 (defmethod handle :notification/show [[_ {:keys [content status clear?]}]]
 (defmethod handle :notification/show [[_ {:keys [content status clear?]}]]
@@ -417,7 +424,7 @@
         (set! (.. right-sidebar-node -style -paddingBottom) "150px")))))
         (set! (.. right-sidebar-node -style -paddingBottom) "150px")))))
 
 
 (defn update-file-path [deprecated-repo current-repo deprecated-app-id current-app-id]
 (defn update-file-path [deprecated-repo current-repo deprecated-app-id current-app-id]
-  (let [files (db-model/get-files-v2 deprecated-repo)
+  (let [files (db-model/get-files-entity deprecated-repo)
         conn (conn/get-db deprecated-repo false)
         conn (conn/get-db deprecated-repo false)
         tx (mapv (fn [[id path]]
         tx (mapv (fn [[id path]]
                    (let [new-path (string/replace path deprecated-app-id current-app-id)]
                    (let [new-path (string/replace path deprecated-app-id current-app-id)]
@@ -563,15 +570,26 @@
                   (state/close-modal!)
                   (state/close-modal!)
                   (nfs-handler/refresh! (state/get-current-repo) refresh-cb)))]]))
                   (nfs-handler/refresh! (state/get-current-repo) refresh-cb)))]]))
 
 
-(defmethod handle :graph/ask-for-re-index [[_ *multiple-windows?]]
+(defmethod handle :graph/re-index [[_]]
+  ;; Ensure the graph only has ONE window instance
+  (repo-handler/re-index!
+   nfs-handler/rebuild-index!
+   #(do (page-handler/create-today-journal!)
+        (file-sync-restart!))))
+
+(defmethod handle :graph/ask-for-re-index [[_ *multiple-windows? ui]]
+  ;; *multiple-windows? - if the graph is opened in multiple windows, boolean atom
+  ;; ui - custom message to show on asking for re-index
   (if (and (util/atom? *multiple-windows?) @*multiple-windows?)
   (if (and (util/atom? *multiple-windows?) @*multiple-windows?)
     (handle
     (handle
      [:modal/show
      [:modal/show
       [:div
       [:div
+       (when (not (nil? ui)) ui)
        [:p (t :re-index-multiple-windows-warning)]]])
        [:p (t :re-index-multiple-windows-warning)]]])
     (handle
     (handle
      [:modal/show
      [:modal/show
       [:div {:style {:max-width 700}}
       [:div {:style {:max-width 700}}
+       (when (not (nil? ui)) ui)
        [:p (t :re-index-discard-unsaved-changes-warning)]
        [:p (t :re-index-discard-unsaved-changes-warning)]
        (ui/button
        (ui/button
          (t :yes)
          (t :yes)
@@ -580,9 +598,7 @@
          :large? true
          :large? true
          :on-click (fn []
          :on-click (fn []
                      (state/close-modal!)
                      (state/close-modal!)
-                     (repo-handler/re-index!
-                      nfs-handler/rebuild-index!
-                      page-handler/create-today-journal!)))]])))
+                     (state/pub-event! [:graph/re-index])))]])))
 
 
 ;; encryption
 ;; encryption
 (defmethod handle :modal/encryption-setup-dialog [[_ repo-url close-fn]]
 (defmethod handle :modal/encryption-setup-dialog [[_ repo-url close-fn]]

+ 42 - 34
src/main/frontend/handler/page.cljs

@@ -29,6 +29,7 @@
             [frontend.util :as util]
             [frontend.util :as util]
             [frontend.util.cursor :as cursor]
             [frontend.util.cursor :as cursor]
             [frontend.util.property :as property]
             [frontend.util.property :as property]
+            [frontend.util.fs :as fs-util]
             [frontend.util.page-property :as page-property]
             [frontend.util.page-property :as page-property]
             [goog.object :as gobj]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
             [lambdaisland.glogi :as log]
@@ -54,6 +55,7 @@
   [journal? title]
   [journal? title]
   (when-let [s (if journal?
   (when-let [s (if journal?
                  (date/journal-title->default title)
                  (date/journal-title->default title)
+                 ;; legacy in org-mode format, don't escape slashes except bug reported
                  (gp-util/page-name-sanity (string/lower-case title)))]
                  (gp-util/page-name-sanity (string/lower-case title)))]
     ;; Win10 file path has a length limit of 260 chars
     ;; Win10 file path has a length limit of 260 chars
     (gp-util/safe-subs s 0 200)))
     (gp-util/safe-subs s 0 200)))
@@ -97,19 +99,17 @@
 (defn- create-title-property?
 (defn- create-title-property?
   [journal? page-name]
   [journal? page-name]
   (and (not journal?)
   (and (not journal?)
-       (util/create-title-property? page-name)))
+       (fs-util/create-title-property? page-name)))
 
 
 (defn- build-page-tx [format properties page journal? whiteboard?]
 (defn- build-page-tx [format properties page journal? whiteboard?]
   (when (:block/uuid page)
   (when (:block/uuid page)
-    (let [page-entity [:block/uuid (:block/uuid page)]
-          create-title? (create-title-property? journal?
-                                                (or
-                                                 (:block/original-name page)
-                                                 (:block/name page)))
-          page (merge page
-                      (when (seq properties) {:block/properties properties})
-                      (when whiteboard? {:block/type "whiteboard"}))
-          page-empty? (db/page-empty? (state/get-current-repo) (:block/name page))]
+    (let [page-entity   [:block/uuid (:block/uuid page)]
+          title         (util/get-page-original-name page)
+          create-title? (create-title-property? journal? title)
+          page          (merge page
+                               (when (seq properties) {:block/properties properties})
+                               (when whiteboard? {:block/type "whiteboard"}))
+          page-empty?   (db/page-empty? (state/get-current-repo) (:block/name page))]
       (cond
       (cond
         (not page-empty?)
         (not page-empty?)
         [page]
         [page]
@@ -192,32 +192,42 @@
             (p/catch (fn [error] (js/console.error error))))))))
             (p/catch (fn [error] (js/console.error error))))))))
 
 
 (defn- compute-new-file-path
 (defn- compute-new-file-path
-  [old-path new-name]
+  "Construct the full path given old full path and the file sanitized body.
+   Ext. included in the `old-path`."
+  [old-path new-file-name-body]
   (let [result (string/split old-path "/")
   (let [result (string/split old-path "/")
-        file-name (gp-util/page-name-sanity new-name true)
         ext (last (string/split (last result) "."))
         ext (last (string/split (last result) "."))
-        new-file (str file-name "." ext)
+        new-file (str new-file-name-body "." ext)
         parts (concat (butlast result) [new-file])]
         parts (concat (butlast result) [new-file])]
     (string/join "/" parts)))
     (string/join "/" parts)))
 
 
 (defn rename-file!
 (defn rename-file!
-  "emit file-rename events to :file/rename-event-chan"
-  [file new-name ok-handler]
-  (let [repo (state/get-current-repo)
-        file (db/pull (:db/id file))
-        old-path (:file/path file)
-        new-path (compute-new-file-path old-path new-name)]
+  "emit file-rename events to :file/rename-event-chan
+   force-fs? - when true, rename file event the db transact is failed."
+  ([file new-file-name-body ok-handler]
+   (rename-file! file new-file-name-body ok-handler false))
+  ([file new-file-name-body ok-handler force-fs?]
+   (let [repo (state/get-current-repo)
+         file (db/pull (:db/id file))
+         old-path (:file/path file)
+         new-path (compute-new-file-path old-path new-file-name-body)
+         transact #(db/transact! repo [{:db/id (:db/id file)
+                                        :file/path new-path}])]
     ;; update db
     ;; update db
-    (db/transact! repo [{:db/id (:db/id file)
-                         :file/path new-path}])
-    (->
-     (p/let [_ (state/offer-file-rename-event-chan! {:repo repo
-                                                     :old-path old-path
-                                                     :new-path new-path})
-             _ (fs/rename! repo old-path new-path)]
-       (ok-handler))
-     (p/catch (fn [error]
-                (println "file rename failed: " error))))))
+     (if force-fs?
+       (try (transact) ;; capture error and continue FS rename if failed
+            (catch :default e
+              (log/error :rename-file e)))
+       (transact)) ;; interrupted if failed
+
+     (->
+      (p/let [_ (state/offer-file-rename-event-chan! {:repo repo
+                                                      :old-path old-path
+                                                      :new-path new-path})
+              _ (fs/rename! repo old-path new-path)]
+        (ok-handler))
+      (p/catch (fn [error]
+                 (println "file rename failed: " error)))))))
 
 
 (defn- replace-page-ref!
 (defn- replace-page-ref!
   "Unsanitized names"
   "Unsanitized names"
@@ -418,7 +428,7 @@
   "Only accepts unsanitized page names"
   "Only accepts unsanitized page names"
   [old-name new-name redirect?]
   [old-name new-name redirect?]
   (let [old-page-name       (util/page-name-sanity-lc old-name)
   (let [old-page-name       (util/page-name-sanity-lc old-name)
-        new-file-name       (util/file-name-sanity new-name)
+        new-file-name-body  (fs-util/file-name-sanity new-name) ;; w/o file extension
         new-page-name       (util/page-name-sanity-lc new-name)
         new-page-name       (util/page-name-sanity-lc new-name)
         repo                (state/get-current-repo)
         repo                (state/get-current-repo)
         page                (db/pull [:block/name old-page-name])]
         page                (db/pull [:block/name old-page-name])]
@@ -448,13 +458,11 @@
 
 
         (d/transact! (db/get-db repo false) page-txs)
         (d/transact! (db/get-db repo false) page-txs)
 
 
-        ;; If page name changed after sanitization
-        (when (or (util/create-title-property? new-page-name)
-                  (not= (gp-util/page-name-sanity new-name false) new-name))
+        (when (fs-util/create-title-property? new-page-name)
           (page-property/add-property! new-page-name :title new-name))
           (page-property/add-property! new-page-name :title new-name))
 
 
         (when (and file (not journal?))
         (when (and file (not journal?))
-          (rename-file! file new-file-name (fn [] nil)))
+          (rename-file! file new-file-name-body (fn [] nil)))
 
 
         (rename-update-refs! page old-original-name new-name)
         (rename-update-refs! page old-original-name new-name)
 
 

+ 5 - 2
src/main/frontend/handler/repo.cljs

@@ -477,6 +477,9 @@
        (on-success)))
        (on-success)))
     (p/catch (fn [error]
     (p/catch (fn [error]
                (js/console.error error)
                (js/console.error error)
+               (state/pub-event! [:instrument {:type :db/persist-failed
+                                               :payload {:error-str (str error)
+                                                         :error error}}])
                (when on-error
                (when on-error
                  (on-error)))))))
                  (on-error)))))))
 
 
@@ -487,7 +490,7 @@
      step 1. [In HERE]  a window         ---broadcastPersistGraph---->   electron
      step 1. [In HERE]  a window         ---broadcastPersistGraph---->   electron
      step 2.            electron         ---------persistGraph------->   window holds the graph
      step 2.            electron         ---------persistGraph------->   window holds the graph
      step 3.            window w/ graph  --broadcastPersistGraphDone->   electron
      step 3.            window w/ graph  --broadcastPersistGraphDone->   electron
-     step 4. [In HERE]  a window         <---broadcastPersistGraph----   electron"
+     step 4. [In HERE]  a window         <--broadcastPersistGraph-----   electron"
   [graph]
   [graph]
   (p/let [_ (ipc/ipc "broadcastPersistGraph" graph)] ;; invoke for chaining promise
   (p/let [_ (ipc/ipc "broadcastPersistGraph" graph)] ;; invoke for chaining promise
     nil))
     nil))
@@ -552,6 +555,6 @@
     repos'))
     repos'))
 
 
 (defn graph-ready!
 (defn graph-ready!
-  "Call electron that the graph is loaded."
+  ;; FIXME: Call electron that the graph is loaded, an ugly implementation for redirect to page when graph is restored
   [graph]
   [graph]
   (ipc/ipc "graphReady" graph))
   (ipc/ipc "graphReady" graph))

+ 0 - 12
src/main/frontend/handler/search.cljs

@@ -125,15 +125,3 @@
        (notification/show!
        (notification/show!
         "Search indices rebuilt successfully!"
         "Search indices rebuilt successfully!"
         :success)))))
         :success)))))
-
-(defn rebuild-indices-when-stale!
-  ([]
-   (rebuild-indices-when-stale! (state/get-current-repo)))
-  ([repo]
-   (p/let [cache-stale? (search/cache-stale? repo)]
-     (when cache-stale?
-       (js/console.log "cache stale: " repo)
-       (p/let [_ (search/rebuild-indices! repo)]
-         (notification/show!
-          "Stale search cache detected. Search indices rebuilt successfully!"
-          :success))))))

+ 1 - 0
src/main/frontend/handler/web/nfs.cljs

@@ -120,6 +120,7 @@
 
 
 ;; TODO: extract code for `ls-dir-files` and `reload-dir!`
 ;; TODO: extract code for `ls-dir-files` and `reload-dir!`
 (defn ^:large-vars/cleanup-todo ls-dir-files-with-handler!
 (defn ^:large-vars/cleanup-todo ls-dir-files-with-handler!
+  "Read files from directory and setup repo (for the first time setup a repo)"
   ([ok-handler] (ls-dir-files-with-handler! ok-handler nil))
   ([ok-handler] (ls-dir-files-with-handler! ok-handler nil))
   ([ok-handler {:keys [empty-dir?-or-pred dir-result-fn]}]
   ([ok-handler {:keys [empty-dir?-or-pred dir-result-fn]}]
    (let [path-handles (atom {})
    (let [path-handles (atom {})

+ 8 - 5
src/main/frontend/mobile/intent.cljs

@@ -15,6 +15,8 @@
             [frontend.util :as util]
             [frontend.util :as util]
             [frontend.util.text :as text-util]
             [frontend.util.text :as text-util]
             [lambdaisland.glogi :as log]
             [lambdaisland.glogi :as log]
+            [logseq.graph-parser.util :as gp-util]
+            [frontend.util.fs :as fs-util]
             [logseq.graph-parser.config :as gp-config]
             [logseq.graph-parser.config :as gp-config]
             [logseq.graph-parser.mldoc :as gp-mldoc]
             [logseq.graph-parser.mldoc :as gp-mldoc]
             [logseq.graph-parser.util.page-ref :as page-ref]
             [logseq.graph-parser.util.page-ref :as page-ref]
@@ -75,17 +77,18 @@
     (-> (string/replace template "{time}" time)
     (-> (string/replace template "{time}" time)
         (string/replace "{url}" (or url "")))))
         (string/replace "{url}" (or url "")))))
 
 
-(defn- embed-text-file [url title]
+(defn- embed-text-file 
+  "Store external content with url into Logseq repo" 
+  [url title]
   (p/let [time (date/get-current-time)
   (p/let [time (date/get-current-time)
           title (some-> (or title (path/basename url))
           title (some-> (or title (path/basename url))
                         js/decodeURIComponent
                         js/decodeURIComponent
                         util/node-path.name
                         util/node-path.name
-                        util/file-name-sanity
-                        js/decodeURIComponent
-                        (string/replace "." ""))
+                        ;; make the title more user friendly
+                        gp-util/page-name-sanity)
           path (path/join (config/get-repo-dir (state/get-current-repo))
           path (path/join (config/get-repo-dir (state/get-current-repo))
                           (config/get-pages-directory)
                           (config/get-pages-directory)
-                          (str (js/encodeURI title) (path/extname url)))
+                          (str (js/encodeURI (fs-util/file-name-sanity title)) (path/extname url)))
           _ (p/catch
           _ (p/catch
                 (.copy Filesystem (clj->js {:from url :to path}))
                 (.copy Filesystem (clj->js {:from url :to path}))
                 (fn [error]
                 (fn [error]

+ 3 - 3
src/main/frontend/modules/file/core.cljs

@@ -4,9 +4,9 @@
             [frontend.date :as date]
             [frontend.date :as date]
             [frontend.db :as db]
             [frontend.db :as db]
             [frontend.db.utils :as db-utils]
             [frontend.db.utils :as db-utils]
-            [frontend.util :as util]
-            [frontend.util.property :as property]
             [frontend.state :as state]
             [frontend.state :as state]
+            [frontend.util.property :as property]
+            [frontend.util.fs :as fs-util]
             [frontend.handler.file :as file-handler]))
             [frontend.handler.file :as file-handler]))
 
 
 (defn- indented-block-content
 (defn- indented-block-content
@@ -116,7 +116,7 @@
             filename (if journal-page?
             filename (if journal-page?
                        (date/date->file-name journal-page?)
                        (date/date->file-name journal-page?)
                        (-> (or (:block/original-name page) (:block/name page))
                        (-> (or (:block/original-name page) (:block/name page))
-                           (util/file-name-sanity)))
+                           (fs-util/file-name-sanity)))
             sub-dir (cond
             sub-dir (cond
                       journal-page?    (config/get-journals-directory)
                       journal-page?    (config/get-journals-directory)
                       whiteboard-page? (config/get-whiteboards-directory)
                       whiteboard-page? (config/get-whiteboards-directory)

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

@@ -312,7 +312,7 @@
 
 
    :graph/re-index                 {:fn (fn []
    :graph/re-index                 {:fn (fn []
                                           (p/let [multiple-windows? (ipc/ipc "graphHasMultipleWindows" (state/get-current-repo))]
                                           (p/let [multiple-windows? (ipc/ipc "graphHasMultipleWindows" (state/get-current-repo))]
-                                            (state/pub-event! [:graph/ask-for-re-index multiple-windows?])))
+                                                 (state/pub-event! [:graph/ask-for-re-index (atom multiple-windows?) nil])))
                                     :binding false}
                                     :binding false}
 
 
    :command/run                    {:binding "mod+shift+1"
    :command/run                    {:binding "mod+shift+1"

+ 0 - 5
src/main/frontend/search.cljs

@@ -271,8 +271,3 @@
   [repo]
   [repo]
   (when-let [engine (get-engine repo)]
   (when-let [engine (get-engine repo)]
     (protocol/remove-db! engine)))
     (protocol/remove-db! engine)))
-
-(defn cache-stale?
-  [repo]
-  (when-let [engine (get-engine repo)]
-    (protocol/cache-stale? engine repo)))

+ 0 - 1
src/main/frontend/search/browser.cljs

@@ -38,7 +38,6 @@
   (rebuild-blocks-indice! [_this]
   (rebuild-blocks-indice! [_this]
     (let [indice (search-db/make-blocks-indice! repo)]
     (let [indice (search-db/make-blocks-indice! repo)]
       (p/promise indice)))
       (p/promise indice)))
-  (cache-stale? [_this _repo] (p/promise false)) ;; fuse.js doesn't have cache
   (transact-blocks! [_this {:keys [blocks-to-remove-set
   (transact-blocks! [_this {:keys [blocks-to-remove-set
                                   blocks-to-add]}]
                                   blocks-to-add]}]
     (swap! search-db/indices update-in [repo :blocks]
     (swap! search-db/indices update-in [repo :blocks]

+ 0 - 3
src/main/frontend/search/node.cljs

@@ -17,9 +17,6 @@
                 {:block/uuid uuid
                 {:block/uuid uuid
                  :block/content content
                  :block/content content
                  :block/page page})) result)))
                  :block/page page})) result)))
-  (cache-stale? [_this repo]
-    ;; only FTS require cache validating
-    (ipc/ipc "searchVersionChanged?" repo))
   (rebuild-blocks-indice! [_this]
   (rebuild-blocks-indice! [_this]
     (let [indice (search-db/build-blocks-indice repo)]
     (let [indice (search-db/build-blocks-indice repo)]
       (ipc/ipc "rebuild-blocks-indice" repo indice)))
       (ipc/ipc "rebuild-blocks-indice" repo indice)))

+ 0 - 1
src/main/frontend/search/protocol.cljs

@@ -2,7 +2,6 @@
 
 
 (defprotocol Engine
 (defprotocol Engine
   (query [this q option])
   (query [this q option])
-  (cache-stale? [this repo])
   (rebuild-blocks-indice! [this])
   (rebuild-blocks-indice! [this])
   (transact-blocks! [this data])
   (transact-blocks! [this data])
   (truncate-blocks! [this])
   (truncate-blocks! [this])

+ 8 - 6
src/main/frontend/state.cljs

@@ -290,7 +290,10 @@
 (def default-config
 (def default-config
   "Default config for a repo-specific, user config"
   "Default config for a repo-specific, user config"
   {:feature/enable-search-remove-accents? true
   {:feature/enable-search-remove-accents? true
-   :default-arweave-gateway "https://arweave.net"})
+   :default-arweave-gateway "https://arweave.net"
+
+   ;; For flushing the settings of old versions. Don't bump this value.
+   :file/name-format :legacy})
 
 
 ;; State that most user config is dependent on
 ;; State that most user config is dependent on
 (declare get-current-repo)
 (declare get-current-repo)
@@ -440,11 +443,10 @@ should be done through this fn in order to get global config and config defaults
     "LATER"
     "LATER"
     "TODO"))
     "TODO"))
 
 
-(defn page-name-order
-  "Decide whether to use file name or :title as page name. If it returns \"file\", use the file
-  name unless it is missing."
-  []
-  (:page-name-order (get-config)))
+(defn get-filename-format
+  ([] (get-filename-format (get-current-repo)))
+  ([repo]
+   (:file/name-format (get-config repo))))
 
 
 (defn get-date-formatter
 (defn get-date-formatter
   []
   []

+ 7 - 37
src/main/frontend/util.cljc

@@ -910,24 +910,6 @@
      [string]
      [string]
      (some-> string str (js/encodeURIComponent) (.replace "+" "%20"))))
      (some-> string str (js/encodeURIComponent) (.replace "+" "%20"))))
 
 
-(def windows-reserved-chars #"[:\\*\\?\"<>|]+")
-
-#?(:cljs
-   (do
-     (defn include-windows-reserved-chars?
-      [s]
-       (safe-re-find windows-reserved-chars s))
-
-     (defn create-title-property?
-       [s]
-       (and (string? s)
-            (or (include-windows-reserved-chars? s)
-                (string/includes? s "_")
-                (string/includes? s "/")
-                (string/includes? s ".")
-                (string/includes? s "%")
-                (string/includes? s "#"))))))
-
 #?(:cljs
 #?(:cljs
    (defn search-normalize
    (defn search-normalize
      "Normalize string for searching (loose)"
      "Normalize string for searching (loose)"
@@ -937,19 +919,6 @@
         (removeAccents  normalize-str)
         (removeAccents  normalize-str)
         normalize-str))))
         normalize-str))))
 
 
-#?(:cljs
-   (defn file-name-sanity
-     "Sanitize page-name for file name (strict), for file writing."
-     [page-name]
-     (some-> page-name
-             gp-util/page-name-sanity
-             ;; for android filesystem compatiblity
-             (string/replace #"[\\#|%]+" url-encode)
-             ;; Windows reserved path characters
-             (string/replace windows-reserved-chars url-encode)
-             (string/replace #"/" url-encode)
-             (string/replace "*" "%2A"))))
-
 #?(:cljs
 #?(:cljs
    (def page-name-sanity-lc
    (def page-name-sanity-lc
      "Delegate to gp-util to loosely couple app usages to graph-parser"
      "Delegate to gp-util to loosely couple app usages to graph-parser"
@@ -1006,12 +975,13 @@
     [col1 col2]))
     [col1 col2]))
 
 
 ;; fs
 ;; fs
-(defn get-file-ext
-  [file]
-  (and
-   (string? file)
-   (string/includes? file ".")
-   (some-> (last (string/split file #"\.")) string/lower-case)))
+#?(:cljs
+   (defn get-file-ext
+     [file]
+     (and
+      (string? file)
+      (string/includes? file ".")
+      (some-> (gp-util/path->file-ext file) string/lower-case))))
 
 
 (defn get-dir-and-basename
 (defn get-dir-and-basename
   [path]
   [path]

+ 127 - 2
src/main/frontend/util/fs.cljs

@@ -1,14 +1,17 @@
+;; TODO: move all file path related util functions to here (excepts those fit graph-parser)
+
 (ns frontend.util.fs
 (ns frontend.util.fs
   "Misc util fns built on top of frontend.fs"
   "Misc util fns built on top of frontend.fs"
   (:require ["path" :as path]
   (:require ["path" :as path]
+            [frontend.util :as util]
+            [logseq.graph-parser.util :as gp-util]
             [clojure.string :as string]
             [clojure.string :as string]
+            [frontend.state :as state]
             [frontend.fs :as fs]
             [frontend.fs :as fs]
             [frontend.config :as config]
             [frontend.config :as config]
             [promesa.core :as p]
             [promesa.core :as p]
             [cljs.reader :as reader]))
             [cljs.reader :as reader]))
 
 
-;; TODO: move all file path related util functions to here
-
 ;; NOTE: This is not the same ignored-path? as src/electron/electron/utils.cljs.
 ;; NOTE: This is not the same ignored-path? as src/electron/electron/utils.cljs.
 ;;       The assets directory is ignored.
 ;;       The assets directory is ignored.
 ;;
 ;;
@@ -64,3 +67,125 @@
   [repo-url file]
   [repo-url file]
   (when-let [repo-dir (config/get-repo-dir repo-url)]
   (when-let [repo-dir (config/get-repo-dir repo-url)]
     (fs/read-file repo-dir file)))
     (fs/read-file repo-dir file)))
+
+(def multiplatform-reserved-chars ":\\*\\?\"<>|\\#\\\\")
+
+(def reserved-chars-pattern
+  (re-pattern (str "[" multiplatform-reserved-chars "]+")))
+
+(defn include-reserved-chars?
+  "Includes reserved charcters that would broken FS"
+  [s]
+  (util/safe-re-find reserved-chars-pattern s))
+
+(defn- encode-url-lowbar
+  [input]
+  (string/replace input "_" "%5F"))
+
+(defn- encode-url-percent
+  [input]
+  (string/replace input "%" "%25"))
+
+(defn- escape-namespace-slashes-and-multilowbars
+  "Encode slashes / as triple lowbars ___
+   Don't encode _ in most cases, except causing ambiguation"
+  [string]
+  (-> string
+      ;; The ambiguation is caused by the unbounded _ (possible continuation of `_`s)
+      (string/replace "___" encode-url-lowbar)
+      (string/replace "_/" encode-url-lowbar)
+      (string/replace "/_" encode-url-lowbar)
+      ;; After ambiguaous _ encoded, encode the slash
+      (string/replace "/" "___")))
+
+(def windows-reserved-filebodies
+  (set '("CON" "PRN" "AUX" "NUL" "COM1" "COM2" "COM3" "COM4" "COM5" "COM6"
+               "COM7" "COM8" "COM9" "LPT1" "LPT2" "LPT3" "LPT4" "LPT5" "LPT6" "LPT7"
+               "LPT8" "LPT9")))
+
+(defn- escape-windows-reserved-filebodies
+  "Encode reserved file names in Windows"
+  [file-body]
+  (str file-body (when (or (contains? windows-reserved-filebodies file-body)
+                           (string/ends-with? file-body "."))
+                   "/"))) ;; "___" would not break the title, but follow the Windows ruling
+
+(defn- url-encode-file-name
+  [file-name]
+  (-> file-name
+      js/encodeURIComponent
+      (string/replace "*" "%2A") ;; extra token that not involved in URI encoding
+      ))
+
+(defn- tri-lb-file-name-sanity
+  "Sanitize page-name for file name (strict), for file name in file writing.
+   Use triple lowbar as namespace separator"
+  [title]
+  (some-> title
+          gp-util/page-name-sanity ;; we want to preserve the case sensitive nature of most file systems, don't lowercase
+          (string/replace gp-util/url-encoded-pattern encode-url-percent) ;; pre-encode % in title on demand
+          (string/replace reserved-chars-pattern url-encode-file-name)
+          (escape-windows-reserved-filebodies) ;; do this before the lowbar encoding to avoid ambiguity
+          (escape-namespace-slashes-and-multilowbars)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;     Keep for backward compatibility     ;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; Rule of dir-ver 0 (before 2022 May)
+;; Source: https://github.com/logseq/logseq/blob/1519e35e0c8308d8db90b2525bfe7a716c4cdf04/src/main/frontend/util.cljc#L930
+(defn legacy-dot-file-name-sanity
+  [page-name]
+  (let [normalize (fn [s] (.normalize s "NFC"))
+        remove-boundary-slashes (fn [s] (when (string? s)
+                                          (let [s (if (= \/ (first s))
+                                                    (subs s 1)
+                                                    s)]
+                                            (if (= \/ (last s))
+                                              (subs s 0 (dec (count s)))
+                                              s))))
+        page (some-> page-name
+                     (remove-boundary-slashes)
+                      ;; Windows reserved path characters
+                     (string/replace #"[:\\*\\?\"<>|]+" "_")
+                      ;; for android filesystem compatiblity
+                     (string/replace #"[\\#|%]+" "_")
+                     (normalize))]
+    (string/replace page #"/" ".")))
+
+;; Rule of dir-ver 0 (after 2022 May)
+;; Source: https://github.com/logseq/logseq/blob/e7110eea6790eda5861fdedb6b02c2a78b504cd9/src/main/frontend/util.cljc#L927
+(defn legacy-url-file-name-sanity
+  [page-name]
+  (let [url-encode #(some-> % str (js/encodeURIComponent) (.replace "+" "%20"))]
+    (some-> page-name
+            gp-util/page-name-sanity
+             ;; for android filesystem compatiblity
+            (string/replace #"[\\#|%]+" url-encode)
+             ;; Windows reserved path characters
+            (string/replace #"[:\\*\\?\"<>|]+" url-encode)
+            (string/replace #"/" url-encode)
+            (string/replace "*" "%2A"))))
+
+;; Register sanitization / parsing fns in:
+;; logseq.graph-parser.util (parsing only)
+;; frontend.util.fs         (sanitization only)
+;; frontend.handler.conversion (both)
+(defn file-name-sanity
+  ([title]
+   (file-name-sanity title (state/get-filename-format)))
+  ([title file-name-format]
+   (case file-name-format
+     :triple-lowbar (tri-lb-file-name-sanity title)
+     :legacy-dot    (legacy-dot-file-name-sanity title) ;; The earliest file name rule (before May 2022). For file name check in the conversion logic only. Don't allow users to use this.
+     (legacy-url-file-name-sanity title))))
+
+(defn create-title-property?
+  [page-name]
+  (and (string? page-name)
+       (let [filename-format (state/get-filename-format)
+             file-name  (file-name-sanity page-name filename-format)
+             page-name' (gp-util/title-parsing file-name filename-format)
+             result     (or (not= page-name page-name')
+                            (include-reserved-chars? file-name))]
+         result)))

+ 9 - 0
src/main/frontend/utils.js

@@ -291,6 +291,15 @@ export const toPosixPath = (input) => {
   return input && input.replace(/\\+/g, '/')
   return input && input.replace(/\\+/g, '/')
 }
 }
 
 
+// Delegation of Path.js but unified into POXIS style
+// https://nodejs.org/api/path.html#pathparsepath
+// path.parse('/home/user/dir/file.txt');
+// Returns:
+// { root: '/',
+//   dir: '/home/user/dir',
+//   base: 'file.txt',
+//   ext: '.txt',
+//   name: 'file' }
 export const nodePath = Object.assign({}, path, {
 export const nodePath = Object.assign({}, path, {
   basename (input) {
   basename (input) {
     input = toPosixPath(input)
     input = toPosixPath(input)

+ 139 - 0
src/test/frontend/db/name_sanity_test.cljs

@@ -0,0 +1,139 @@
+(ns frontend.db.name-sanity-test
+  (:require [cljs.test :refer [deftest testing is are]]
+            [clojure.string :as string]
+            [logseq.graph-parser.util :as gp-util]
+            [frontend.handler.page :as page-handler]
+            [frontend.handler.conversion :as conversion-handler]
+            [frontend.util.fs :as fs-util]))
+
+(defn- test-page-name
+  "Check if page name can be preserved after escaping"
+  [page-name]
+  (testing (str "Test sanitization page-name: " page-name)
+    (let [file-name   (#'fs-util/tri-lb-file-name-sanity page-name)
+          page-name'  (#'gp-util/tri-lb-title-parsing file-name)
+          url-single  (js/encodeURIComponent file-name)
+          url-double  (js/encodeURIComponent url-single)
+          file-name'  (js/decodeURIComponent url-single)
+          file-name'' ( js/decodeURIComponent (js/decodeURIComponent url-double))]
+      (is (= page-name page-name'))
+      (is (not (fs-util/include-reserved-chars? file-name)))
+      (is (not (contains? fs-util/windows-reserved-filebodies file-name)))
+      (is (not (string/ends-with? file-name ".")))
+      (is (= file-name' file-name))
+      (is (= file-name'' file-name)))))
+
+(deftest ^:focus page-name-sanitization-tests
+  (test-page-name "Some.Content!")
+  (test-page-name "More _/_ Con tents")
+  (test-page-name "More _________/________ Con tents")
+  (test-page-name "More _________/___-_-_-_---___----__/_ Con tents")
+  (test-page-name "Cont./__cont_ cont/ lsdksdf")
+  (test-page-name "Cont.?/#__cont_ cont%/_ lsdksdf")
+  (test-page-name "Cont.?__byte/#__cont_ cont%/_ lsdksdf")
+  (test-page-name "__ont.?__byte/#__cont_ cont%/_ lsdksdf")
+  (test-page-name "______ont.?__byte/#__cont_ cont%/_ lsdksdf")
+  (test-page-name "__ont.?__byte/#__cont_ cont%/_ lsdksdf__")
+  (test-page-name "+*++***+++__byte/#__cont_ cont%/_ lsdksdf__")
+  (test-page-name "+*++_.x2A_.x2A***+++__byte/#__cont_ cont%/_ lsdksdf__")
+  (test-page-name "__ont.?__byte/#__0xbbcont_ cont%/_ lsdksdf__")
+  (test-page-name "__ont.?__byte/#_&amp;ont_ cont%/_ lsdksdf__")
+  (test-page-name "__ont.?__byte&lowbar;/#_&amp;ont_ cont%/_ lsdksdf__")
+  (test-page-name "dsa&amp&semi;l dsalfjk jkl")
+  (test-page-name "dsa&amp&semi;l dsalfjk jkl.")
+  (test-page-name "hls__&amp&semi;l dsalfjk jkl.")
+  (test-page-name "CON.")
+  (mapv test-page-name fs-util/windows-reserved-filebodies))
+
+(deftest new-path-computation-tests
+  (is (= (#'page-handler/compute-new-file-path "/data/app/dsal dsalfjk aldsaf.jkl" "ddd") "/data/app/ddd.jkl"))
+  (is (= (#'page-handler/compute-new-file-path "c://data/a sdfpp/dsal dsalf% * _ dsaf.mnk" "c d / f") "c://data/a sdfpp/c d / f.mnk")))
+
+(deftest break-change-conversion-tests
+  (let [conv-legacy #(:target (#'conversion-handler/calc-previous-name :legacy :triple-lowbar %))]
+    (is (= "dsal dsalfjk aldsaf___jkl" (conv-legacy "dsal dsalfjk aldsaf.jkl")))
+    (is (= nil (conv-legacy "dsal dsalfjk jkl")))
+    (is (= nil (conv-legacy "dsa&amp;l dsalfjk jkl")))
+    (is (= nil (conv-legacy "dsa&lt;l dsalfjk jkl")))
+    (is (= nil (conv-legacy "dsal dsal%2Ffjk jkl")))
+    (is (= nil (conv-legacy "dsal dsal%2Cfjk jkl")))) ;; %2C already parsed as `,` in the previous ver.
+  )
+
+(deftest formalize-conversion-tests
+  (let [conv-informal #(:target (#'conversion-handler/calc-current-name :triple-lowbar % nil))]
+    (is (= "sdaf ___dsakl" (conv-informal "sdaf %2Fdsakl")))
+    (is (= "sdaf ___dsakl" (conv-informal "sdaf /dsakl")))
+    (is (= nil (conv-informal "sdaf .dsakl")))))
+
+(deftest manual-title-prop-test
+  (are [x y z] (= z (#'conversion-handler/is-manual-title-prop? :legacy x y))
+    "aaa.bbb.ccc" "aaa/bbb/ccc"   false
+    "aa__.bbb.ccc" "aa?#/bbb/ccc" false
+    "aa?#.bbb.ccc" "aa__/bbb/ccc" true
+    "aaa__bbb__ccc" "aaa/bbb/ccc" true
+    "aaa__bbb__cccon" "aaa/bbb/cccon"  true
+    "aaa.bbb.ccc"     "adbcde/aks/sdf" true
+    "a__.bbb.ccc"     "adbcde/aks/sdf" true
+    ))
+
+(deftest rename-previous-tests
+  (are [x y] (= y (#'conversion-handler/calc-previous-name :legacy :triple-lowbar x))
+    "aa?#.bbb.ccc"   {:status :breaking,
+                      :target "aa%3F%23___bbb___ccc",
+                      :old-title "aa?#/bbb/ccc",
+                      :changed-title "aa?#.bbb.ccc"}
+    "aaa__bbb__ccc"  nil
+    "aaa__bbb__cccon" nil
+    "aaa.bbb.ccc"    {:status :breaking,
+                      :target "aaa___bbb___ccc",
+                      :old-title "aaa/bbb/ccc",
+                      :changed-title "aaa.bbb.ccc"}
+    "a__.bbb.ccc"    {:status :breaking,
+                      :target "a_%5F___bbb___ccc",
+                      :old-title "a__/bbb/ccc",
+                      :changed-title "a__.bbb.ccc"})
+  ;; is not a common used case
+  (are [x y] (= y (#'conversion-handler/calc-previous-name :triple-lowbar :legacy x))
+    "aa%3F%23.bbb.ccc" {:status :unreachable,
+                        :target "aa%3F%23.bbb.ccc",
+                        :old-title "aa?#.bbb.ccc",
+                        :changed-title "aa?#/bbb/ccc"}))
+
+(deftest rename-tests
+  ;; z: new title structure; x: old ver title; y: title property (if available)
+  (are [x y z] (= z (#'conversion-handler/calc-rename-target-impl :legacy :triple-lowbar x y))
+    "aaa.bbb.ccc" "aaa/bbb/ccc"   {:status :informal,
+                                   :target "aaa___bbb___ccc",
+                                   :old-title "aaa/bbb/ccc",
+                                   :changed-title "aaa/bbb/ccc"}
+    "aaa.bbb.ccc" nil             {:status :breaking,
+                                   :target "aaa___bbb___ccc",
+                                   :old-title "aaa/bbb/ccc",
+                                   :changed-title "aaa.bbb.ccc"}
+    "aa__.bbb.ccc" "aa?#/bbb/ccc" {:status :informal,
+                                   :target "aa%3F%23___bbb___ccc",
+                                   :old-title "aa?#/bbb/ccc",
+                                   :changed-title "aa?#/bbb/ccc"}
+    "aa?#.bbb.ccc" "aa__/bbb/ccc" nil
+    "aaa__bbb__ccc" "aaa/bbb/ccc" nil
+    "aaa__bbb__cccon" "aaa/bbb/cccon" nil
+    "aaa__bbb__ccc" nil               nil
+    "aaa_bbb_ccc"   nil               nil
+    "aaa.bbb.ccc"   "adbcde/aks/sdf"  nil
+    "a__.bbb.ccc"   "adbcde/aks/sdf"  nil
+    "CON" "CON" {:status :informal,
+                 :target "CON___",
+                 :old-title "CON",
+                 :changed-title "CON"}
+    "CON" nil   {:status :informal,
+                 :target "CON___",
+                 :old-title "CON",
+                 :changed-title "CON"}
+    "abc." "abc." {:status :informal,
+                   :target "abc.___",
+                   :old-title "abc.",
+                   :changed-title "abc."}
+    "abc." nil    {:status :breaking,
+                   :target "abc", ;; abc/ is an invalid file name
+                   :old-title "abc/",
+                   :changed-title "abc."}))

+ 5 - 4
src/test/frontend/extensions/pdf/assets_test.cljs

@@ -2,13 +2,14 @@
   (:require [clojure.test :as test :refer [are deftest testing]]
   (:require [clojure.test :as test :refer [are deftest testing]]
             [frontend.extensions.pdf.assets :as assets]))
             [frontend.extensions.pdf.assets :as assets]))
 
 
-(deftest fix-local-asset-filename
+(deftest fix-local-asset-pagename
   (testing "matched filenames"
   (testing "matched filenames"
-    (are [x y] (= y (assets/fix-local-asset-filename x))
+    (are [x y] (= y (assets/fix-local-asset-pagename x))
       "2015_Book_Intertwingled_1659920114630_0" "2015 Book Intertwingled"
       "2015_Book_Intertwingled_1659920114630_0" "2015 Book Intertwingled"
-      "hls__2015_Book_Intertwingled_1659920114630_0" "2015 Book Intertwingled"))
+      "hls__2015_Book_Intertwingled_1659920114630_0" "📒2015 Book Intertwingled"
+      "hls/2015_Book_Intertwingled_1659920114630_0" "hls/2015 Book Intertwingled"))
   (testing "non matched filenames"
   (testing "non matched filenames"
-    (are [x y] (= y (assets/fix-local-asset-filename x))
+    (are [x y] (= y (assets/fix-local-asset-pagename x))
       "foo" "foo"
       "foo" "foo"
       "foo_bar" "foo_bar"
       "foo_bar" "foo_bar"
       "foo__bar" "foo__bar"
       "foo__bar" "foo__bar"

+ 146 - 0
src/test/frontend/handler/repo_conversion_test.cljs

@@ -0,0 +1,146 @@
+(ns frontend.handler.repo-conversion-test
+  "Repo tests of directory conversion"
+  (:require [cljs.test :refer [deftest use-fixtures is testing]]
+            [clojure.string :as string]
+            [logseq.graph-parser.cli :as gp-cli]
+            [logseq.graph-parser.util :as gp-util]
+            [logseq.graph-parser.test.docs-graph-helper :as docs-graph-helper]
+            [logseq.graph-parser.config :as gp-config]
+            [frontend.test.helper :as test-helper]
+            [frontend.handler.page :as page-handler]
+            [frontend.handler.conversion :as conversion-handler]
+            [frontend.handler.repo :as repo-handler]
+            [frontend.db.conn :as conn]
+            [datascript.core :as d]))
+
+(use-fixtures :each {:before test-helper/start-test-db!
+                     :after test-helper/destroy-test-db!})
+
+(defn- query-assertions-v067
+  [db files]
+  (testing "Query based stats"
+    (is (= (->> files
+                ;; logseq files aren't saved under :block/file
+                (remove #(string/includes? % (str "/" gp-config/app-name "/")))
+                set)
+           (->> (d/q '[:find (pull ?b [* {:block/file [:file/path]}])
+                       :where [?b :block/name] [?b :block/file]]
+                     db)
+                (map (comp #(get-in % [:block/file :file/path]) first))
+                set))
+        "Files on disk should equal ones in db")
+
+    (is (= (count (filter #(re-find #"journals/" %) files))
+           (->> (d/q '[:find (count ?b)
+                       :where
+                       [?b :block/journal? true]
+                       [?b :block/name]
+                       [?b :block/file]]
+                     db)
+                ffirst))
+        "Journal page count on disk equals count in db")
+
+    (is (= {"CANCELED" 2 "DONE" 6 "LATER" 4 "NOW" 5}
+           (->> (d/q '[:find (pull ?b [*]) :where [?b :block/marker]]
+                     db)
+                (map first)
+                (group-by :block/marker)
+                (map (fn [[k v]] [k (count v)]))
+                (into {})))
+        "Task marker counts")
+
+    (is (= {:markdown 3141 :org 460}
+           (docs-graph-helper/get-block-format-counts db))
+        "Block format counts")
+
+    (is (= {:title 98 :id 98
+            :updated-at 47 :created-at 47
+            :card-last-score 6 :card-repeats 6 :card-next-schedule 6
+            :card-last-interval 6 :card-ease-factor 6 :card-last-reviewed 6
+            :alias 6 :logseq.macro-arguments 94 :logseq.macro-name 94 :heading 64}
+           (docs-graph-helper/get-top-block-properties db))
+        "Counts for top block properties")
+
+    (is (= {:title 98
+            :alias 6
+            :tags 2 :permalink 2
+            :name 1 :type 1 :related 1 :sample 1 :click 1 :id 1 :example 1}
+           (docs-graph-helper/get-all-page-properties db))
+        "Counts for all page properties")
+
+    (is (= {:block/scheduled 2
+            :block/priority 4
+            :block/deadline 1
+            :block/collapsed? 22
+            :block/repeated? 1}
+           (->> [:block/scheduled :block/priority :block/deadline :block/collapsed?
+                 :block/repeated?]
+                (map (fn [attr]
+                       [attr
+                        (ffirst (d/q [:find (list 'count '?b) :where ['?b attr]]
+                                     db))]))
+                (into {})))
+        "Counts for blocks with common block attributes")
+
+    (is (= #{"term" "setting" "book" "templates" "Query" "Query/table" "page"}
+           (->> (d/q '[:find (pull ?n [*]) :where [?b :block/namespace ?n]] db)
+                (map (comp :block/original-name first))
+                set))
+        "Has correct namespaces")))
+
+(defn docs-graph-assertions-v067
+  "These are common assertions that should pass in both graph-parser and main
+  logseq app. It is important to run these in both contexts to ensure that the
+  functionality in frontend.handler.repo and logseq.graph-parser remain the
+  same"
+  [db files]
+  ;; Counts assertions help check for no major regressions. These counts should
+  ;; only increase over time as the docs graph rarely has deletions
+  (testing "Counts"
+    (is (= 211 (count files)) "Correct file count")
+    (is (= 42208 (count (d/datoms db :eavt))) "Correct datoms count")
+
+    (is (= 3600
+           (ffirst
+            (d/q '[:find (count ?b)
+                   :where [?b :block/path-refs ?bp] [?bp :block/name]] db)))
+        "Correct referenced blocks count")
+    (is (= 21
+           (ffirst
+            (d/q '[:find (count ?b)
+                   :where [?b :block/content ?content]
+                   [(clojure.string/includes? ?content "+BEGIN_QUERY")]]
+                 db)))
+        "Advanced query count"))
+
+  (query-assertions-v067 db files))
+
+(defn- converst-to-triple-lowbar
+  [path]
+  (let [original-body (gp-util/path->file-body path)
+        ;; only test file name parsing, don't consider title prop overriding
+        rename-target (:target (#'conversion-handler/calc-rename-target-impl :legacy :triple-lowbar original-body nil))]
+    (if rename-target
+      (do (prn "conversion triple-lowbar: " original-body " -> " rename-target)
+          (#'page-handler/compute-new-file-path path rename-target))
+      path)))
+
+(defn- convert-graph-files-path
+  "Given a list of files, converts them according to the given conversion function"
+  [files conversion-fn]
+  (map (fn [file]
+         (assoc file :file/path (conversion-fn (:file/path file)))) files))
+
+;; Integration test that test parsing a large graph like docs
+;; Check if file name conversion from old version of docs is working
+(deftest ^:integration convert-v067-filesnames-parse-and-load-files-to-db
+  (let [graph-dir "src/test/docs"
+        _ (docs-graph-helper/clone-docs-repo-if-not-exists graph-dir "v0.6.7")
+        files (gp-cli/build-graph-files graph-dir)
+        ;; Converting the v0.6.7 ver docs graph under the old namespace naming rule to the new one (:repo/dir-version 0->3)
+        files (convert-graph-files-path files converst-to-triple-lowbar)
+        _ (repo-handler/parse-files-and-load-to-db! test-helper/test-db files {:re-render? false :verbose false})
+        db (conn/get-db test-helper/test-db)]
+
+    ;; Result under new naming rule after conversion should be the same as the old one
+    (docs-graph-assertions-v067 db (map :file/path files))))

+ 3 - 2
src/test/frontend/handler/repo_test.cljs

@@ -9,10 +9,11 @@
 (use-fixtures :each {:before test-helper/start-test-db!
 (use-fixtures :each {:before test-helper/start-test-db!
                      :after test-helper/destroy-test-db!})
                      :after test-helper/destroy-test-db!})
 
 
-;; Integration test that test parsing a large graph like docs
+;; TODO update docs filename rules to the latest version when the namespace PR is released
 (deftest ^:integration parse-and-load-files-to-db
 (deftest ^:integration parse-and-load-files-to-db
   (let [graph-dir "src/test/docs"
   (let [graph-dir "src/test/docs"
-        _ (docs-graph-helper/clone-docs-repo-if-not-exists graph-dir)
+        ;; TODO update me to the latest version of doc when namespace is updated
+        _ (docs-graph-helper/clone-docs-repo-if-not-exists graph-dir "v0.6.7")
         files (gp-cli/build-graph-files graph-dir)
         files (gp-cli/build-graph-files graph-dir)
         _ (repo-handler/parse-files-and-load-to-db! test-helper/test-db files {:re-render? false :verbose false})
         _ (repo-handler/parse-files-and-load-to-db! test-helper/test-db files {:re-render? false :verbose false})
         db (conn/get-db test-helper/test-db)]
         db (conn/get-db test-helper/test-db)]

+ 15 - 5
templates/config.edn

@@ -88,11 +88,6 @@
  ;; For more, see https://github.com/logseq/logseq/issues/672
  ;; For more, see https://github.com/logseq/logseq/issues/672
  ;; :org-mode/insert-file-link? true
  ;; :org-mode/insert-file-link? true
 
 
- ;; If you prefer to use the file name as the page title
- ;; instead of the first heading's title
- ;; the only option for now is `file`
- ;; :page-name-order "file"
-
  ;; Setup custom shortcuts under `:shortcuts` key
  ;; Setup custom shortcuts under `:shortcuts` key
  ;; Syntax:
  ;; Syntax:
  ;; 1. `+` means keys pressing simultaneously. eg: `ctrl+shift+a`
  ;; 1. `+` means keys pressing simultaneously. eg: `ctrl+shift+a`
@@ -286,4 +281,19 @@
  ;   :list?            true
  ;   :list?            true
  ; }
  ; }
 
 
+ ;; Decide the way to escape the special characters in the page title.
+ ;; Warning:
+ ;;   This is a dangerous operation. If you want to change the setting,
+ ;;   should access the setting `Filename format` and follow the instructions. 
+ ;;   Or you have to rename all the affected files manually then re-index on all
+ ;;   clients after the files are synced. Wrong handling may cause page titles
+ ;;   containing special characters to be messy.
+ ;; Available values:
+ ;;   :file/name-format :triple-lowbar
+ ;;     ;use triple underscore `___` for slash `/` in page title
+ ;;     ;use Percent-encoding for other invalid characters
+ ;;   :file/name-format :legacy
+ ;;     ;use Percent-encoding for slash and other invalid characters
+ ;;     ;parse `.` in file name as slash `/` in page title
+ :file/name-format :triple-lowbar
  }
  }