소스 검색

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")
 
 (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)
      [?c :block/parent ?p]]
     [(parent ?p ?c)

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

@@ -21,7 +21,9 @@
         {:keys [tx ast]}
         (let [extract-options' (merge {:block-pattern (gp-config/get-block-pattern format)
                                        :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
                                       {:db @conn})
               {:keys [pages blocks ast]

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

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

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

@@ -28,9 +28,19 @@
         result))))
 
 (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
-  (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.")
       "Contents"
       (let [first-block (last (first (filter gp-block/heading-block? ast)))
@@ -42,11 +52,13 @@
                                (and first-block
                                     (string? 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
-            (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
   [page-m page page-name properties]
@@ -114,14 +126,14 @@
 
 ;; TODO: performance improvement
 (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
-    (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)
           options' (-> options
                        (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')
                       (gp-block/with-parent-and-left {:block/name page-name})
                       (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))))
 
 (defn clone-docs-repo-if-not-exists
-  [dir]
+  [dir branch]
   (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] {})))
 
 
 ;; Fns for common test assertions
 ;; ==============================
-(defn- get-top-block-properties
+(defn get-top-block-properties
   [db]
   (->> (d/q '[:find (pull ?b [*])
               :where
@@ -39,7 +39,7 @@
        (filter #(>= (val %) 5))
        (into {})))
 
-(defn- get-all-page-properties
+(defn get-all-page-properties
   [db]
   (->> (d/q '[:find (pull ?b [*])
               :where
@@ -51,7 +51,7 @@
        (apply merge-with +)
        (into {})))
 
-(defn- get-block-format-counts
+(defn get-block-format-counts
   [db]
   (->> (d/q '[:find (pull ?b [*]) :where [?b :block/format]] db)
        (map first)
@@ -84,7 +84,7 @@
         "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] ]
+           (->> (d/q '[:find (pull ?b [*]) :where [?b :block/marker]]
                      db)
                 (map first)
                 (group-by :block/marker)
@@ -92,7 +92,7 @@
                 (into {})))
         "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))
         "Block format counts")
 
@@ -100,8 +100,7 @@
             :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}
+            :alias 6 :logseq.macro-arguments 94 :logseq.macro-name 94 :heading 64}
            (get-top-block-properties db))
         "Counts for top block properties")
 
@@ -132,6 +131,7 @@
                 set))
         "Has correct namespaces")))
 
+;; TODO update me to the number of the latest version of doc when namespace is updated
 (defn docs-graph-assertions
   "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

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

@@ -7,8 +7,17 @@
             [clojure.walk :as walk]
             [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
-  "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]
   (.normalize s "NFC"))
 
@@ -75,14 +84,6 @@
     (str "0" 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
   [s]
   (when (string? s)
@@ -104,17 +105,40 @@
                  (conj result (str prev "/" (first others)))))
         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
-  "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
   "Sanitize the query string for a page name (mandate for :block/name)"
@@ -146,10 +170,38 @@
     ;; default
     (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
   [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?
   "Determine if string is a valid edn keyword"
@@ -160,6 +212,27 @@
     (catch :default _
       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
   [content]
   (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
 (deftest ^:integration parse-graph
   (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)
 

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

@@ -122,7 +122,7 @@ body"
 
 (deftest ^:integration test->edn
   (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)
         asts-by-file (->> files
                           (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.util.page-ref-test]
             [logseq.graph-parser.util-test]
+            [logseq.graph-parser.util.file-name-test]
             [logseq.graph-parser-test]))
 
 (defmethod cljs.test/report [:cljs.test/default :end-run-tests] [m]
@@ -26,4 +27,5 @@
    'logseq.graph-parser.cli-test
    'logseq.graph-parser.util.page-ref-test
    'logseq.graph-parser-test
+   'logseq.graph-parser.util.file-name-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)
   ;; unneeded serialization
   (search/upsert-blocks! repo (bean/->js data))
-  (search/write-search-version! repo)
   [])
 
 (defmethod handle :transact-blocks [_window [_ repo data]]
@@ -526,10 +525,6 @@
     (f window graph-name)
     (state/set-state! :window/once-graph-ready nil)))
 
-(defmethod handle :searchVersionChanged?
-  [^js _win [_ graph]]
-  (search/version-changed? graph))
-
 (defmethod handle :reloadWindowPage [^js win]
   (logger/warn ::reload-window-page)
   (when-let [web-content (.-webContents win)]

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

@@ -6,12 +6,6 @@
             ["electron" :refer [app]]
             [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))
 
 (defn close!
@@ -79,15 +73,9 @@
   (let [path (.getPath ^object app "userData")]
     (path/join path "search")))
 
-(defn get-search-ver-dir
-  []
-  (let [path (.getPath ^object app "userData")]
-    (path/join path "search.versions")))
-
 (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
   [db-name]
@@ -95,29 +83,12 @@
         search-dir (get-search-dir)]
     [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]
   (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!
   [db-name]
@@ -224,10 +195,9 @@
   [repo]
   (when-let [database (get-db repo)]
     (.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-ver-path)
       (swap! databases dissoc db-name))))
 
 (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))]
     (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
    [:h1.title
     (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
   {:did-mount (fn [state]

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

@@ -186,7 +186,7 @@
                       :options (cond->
                                 {:on-click
                                  (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 button in menu bar of macOS is available.
                                    (not util/mac?))

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

@@ -203,7 +203,7 @@
 (defn- search-item-render
   [search-q {:keys [type data alias]}]
   (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"}
      (case type
        :graph-add-filter

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

@@ -28,7 +28,8 @@
             [reitit.frontend.easy :as rfe]
             [rum.core :as rum]
             [frontend.mobile.util :as mobile-util]
-            [frontend.db :as db]))
+            [frontend.db :as db]
+            [frontend.components.conversion :as conversion-component]))
 
 (defn toggle
   [label-for name state on-toggle & [detail-text]]
@@ -416,7 +417,8 @@
              (config-handler/set-config! :feature/enable-encryption? value)
              (when value
                (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)))
           [:p.text-sm.opacity-50 "⚠️ This feature is experimental! "
            [:span "You can use "]
@@ -537,6 +539,14 @@
    {:left-label (t :settings-page/network-proxy)
     :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
   [_state current-repo]
   (let [preferred-language (state/sub [:preferred-language])
@@ -604,7 +614,7 @@
      [:p (t :settings-page/git-confirm)])])
 
 (rum/defc settings-advanced < rum/reactive
-  []
+  [current-repo]
   (let [instrument-disabled? (state/sub :instrument/disabled?)
         developer-mode? (state/sub [:ui/developer-mode?])
         https-agent-opts (state/sub [:electron/user-cfgs :settings/agent])]
@@ -613,6 +623,7 @@
      (usage-diagnostics-row t instrument-disabled?)
      (when-not (mobile-util/native-platform?) (developer-mode-row t developer-mode?))
      (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)
 
      (ui/admonition
@@ -769,7 +780,7 @@
          (settings-git)
 
          :advanced
-         (settings-advanced)
+         (settings-advanced current-repo)
 
          :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,

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

@@ -88,7 +88,7 @@
               (route-handler/redirect-to-whiteboard! name)
               (route-handler/redirect-to-page! name {:click-from-recent? recent?})))))}
      [: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]
   (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-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-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-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

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

@@ -70,10 +70,7 @@
               nil)
      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
   [repo tag-name]
@@ -124,6 +121,16 @@
      [?page :block/name]]
    (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
   [repo page-name]
   (when-let [db (and repo (conn/get-db repo))]
@@ -138,6 +145,7 @@
              distinct)))
 
 (defn get-alias-source-page
+  "return the source page (page-name) of an alias"
   [repo alias]
   (when-let [db (and repo (conn/get-db repo))]
     (let [alias (util/page-name-sanity-lc alias)
@@ -150,6 +158,8 @@
                       db
                       alias)
                  (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)
         (some (fn [page]
                 (let [aliases (->> (get-in page [:block/properties :alias])
@@ -174,16 +184,13 @@
          ;; (sort-by last)
          (reverse))))
 
-(defn get-files-v2
+(defn get-files-entity
   [repo]
   (when-let [db (conn/get-db repo)]
     (->> (d/q
           '[:find ?file ?path
-            ;; ?modified-at
             :where
-            [?file :file/path ?path]
-            ;; [?file :file/last-modified-at ?modified-at]
-            ]
+            [?file :file/path ?path]]
           db)
          (seq)
          ;; (sort-by last)

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

@@ -122,6 +122,33 @@
         :file/last-modified-at "Last modified at"
         :file/no-data "No data"
         :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/updated-at "Updated At"
         :page/backlinks "Back Links"
@@ -172,6 +199,7 @@
         :settings-page/edit-global-config-edn "Edit global config.edn"
         :settings-page/edit-custom-css "Edit custom.css"
         :settings-page/edit-export-css "Edit export.css"
+        :settings-page/edit-setting "Edit"
         :settings-page/custom-configuration "Custom configuration"
         :settings-page/custom-global-configuration "Custom global configuration"
         :settings-page/custom-theme "Custom theme"
@@ -211,6 +239,7 @@
         :settings-page/plugin-system "Plugins"
         :settings-page/enable-flashcards "Flashcards"
         :settings-page/network-proxy "Network proxy"
+        :settings-page/filename-format "Filename format"
         :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/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/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"
         :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/plugin-system "插件系统"
            :settings-page/network-proxy "网络代理"
+           :settings-page/filename-format "文件名格式"
            :logseq "Logseq"
            :on "已打开"
            :more-options "更多选项"
@@ -2749,7 +2782,7 @@
            :settings-page/edit-export-css "Editar export.css"
            :settings-page/enable-flashcards "Flashcards"
            :settings-page/export-theme "Exportar Tema"
-           
+
            :discourse-title "Nosso fórum!"
            :importing "Importando"
            :asset/copy "Copiar imagem"
@@ -3092,7 +3125,7 @@
         :settings-page/export-theme "Exportar tema"
         :settings-page/network-proxy "Proxy de rede"
         :settings-page/plugin-system "Sistema de plugins"
-        
+
         :discourse-title "Nosso fórum!"
         :importing "Importando"
         :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.state :as state]
             [frontend.util :as util]
+            [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.config :as gp-config]
             [logseq.graph-parser.util.block-ref :as block-ref]
             [medley.core :as medley]
@@ -17,9 +18,21 @@
             [reitit.frontend.easy :as rfe]
             [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
   [full-path]
@@ -154,7 +167,7 @@
   [pdf-current]
   (let [page-name (:key pdf-current)
         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)
         url (:url pdf-current)
         format (state/get-preferred-format)
@@ -222,7 +235,7 @@
         page (db-utils/pull (:db/id (:block/page block)))
         page-name (:block/original-name 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)
               hls (and hls (:highlights hls))]
         (let [file-path (if file-path file-path (str target-key ".pdf"))]
@@ -242,37 +255,39 @@
   ([current] (goto-annotations-page! current nil))
   ([current id]
    (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
   [block stamp]
   (let [id (:block/uuid block)
         props (:block/properties 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)]
-          (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)
                 asset-path (editor-handler/make-asset-url
                              (str "/" gp-config/local-assets-dir "/" group-key "/" (str hl-page "_" id "_" stamp ".png")))]
             [:span.hl-area
              [: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/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]
-  (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]
 
   ;;(dd "==== render pdf-viewer ====")
-
   (let [*el-ref (rum/create-ref)
         [state, set-state!] (rum/use-state {:viewer nil :bus nil :link nil :el nil})
         [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)))
                 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)))))
 
 (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! []
   (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 [_]
   (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
   ([graph]
    (graph-switch graph false))
@@ -142,6 +144,7 @@
        (repo-handler/refresh-repos!)
        (file-sync-restart!)))))
 
+;; Parameters for the `persist-db` function, to show the notification messages
 (def persist-db-noti-m
   {:before     #(notification/show!
                  (ui/loading (t :graph/persist))
@@ -152,7 +155,8 @@
 
 (defn- graph-switch-on-persisted
   "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?]}]
   (let [current-repo (state/get-current-repo)]
     (p/do!
@@ -321,13 +325,16 @@
   (p/let [content (when content (encrypt/decrypt 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)
-    (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?
         (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))
 
 (defmethod handle :notification/show [[_ {:keys [content status clear?]}]]
@@ -417,7 +424,7 @@
         (set! (.. right-sidebar-node -style -paddingBottom) "150px")))))
 
 (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)
         tx (mapv (fn [[id path]]
                    (let [new-path (string/replace path deprecated-app-id current-app-id)]
@@ -563,15 +570,26 @@
                   (state/close-modal!)
                   (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?)
     (handle
      [:modal/show
       [:div
+       (when (not (nil? ui)) ui)
        [:p (t :re-index-multiple-windows-warning)]]])
     (handle
      [:modal/show
       [:div {:style {:max-width 700}}
+       (when (not (nil? ui)) ui)
        [:p (t :re-index-discard-unsaved-changes-warning)]
        (ui/button
          (t :yes)
@@ -580,9 +598,7 @@
          :large? true
          :on-click (fn []
                      (state/close-modal!)
-                     (repo-handler/re-index!
-                      nfs-handler/rebuild-index!
-                      page-handler/create-today-journal!)))]])))
+                     (state/pub-event! [:graph/re-index])))]])))
 
 ;; encryption
 (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.cursor :as cursor]
             [frontend.util.property :as property]
+            [frontend.util.fs :as fs-util]
             [frontend.util.page-property :as page-property]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
@@ -54,6 +55,7 @@
   [journal? title]
   (when-let [s (if journal?
                  (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)))]
     ;; Win10 file path has a length limit of 260 chars
     (gp-util/safe-subs s 0 200)))
@@ -97,19 +99,17 @@
 (defn- create-title-property?
   [journal? page-name]
   (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?]
   (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
         (not page-empty?)
         [page]
@@ -192,32 +192,42 @@
             (p/catch (fn [error] (js/console.error error))))))))
 
 (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 "/")
-        file-name (gp-util/page-name-sanity new-name true)
         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])]
     (string/join "/" parts)))
 
 (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
-    (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!
   "Unsanitized names"
@@ -418,7 +428,7 @@
   "Only accepts unsanitized page names"
   [old-name new-name redirect?]
   (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)
         repo                (state/get-current-repo)
         page                (db/pull [:block/name old-page-name])]
@@ -448,13 +458,11 @@
 
         (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))
 
         (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)
 

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

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

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

@@ -125,15 +125,3 @@
        (notification/show!
         "Search indices rebuilt successfully!"
         :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!`
 (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 {:keys [empty-dir?-or-pred dir-result-fn]}]
    (let [path-handles (atom {})

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

@@ -15,6 +15,8 @@
             [frontend.util :as util]
             [frontend.util.text :as text-util]
             [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.mldoc :as gp-mldoc]
             [logseq.graph-parser.util.page-ref :as page-ref]
@@ -75,17 +77,18 @@
     (-> (string/replace template "{time}" time)
         (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)
           title (some-> (or title (path/basename url))
                         js/decodeURIComponent
                         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))
                           (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
                 (.copy Filesystem (clj->js {:from url :to path}))
                 (fn [error]

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

@@ -4,9 +4,9 @@
             [frontend.date :as date]
             [frontend.db :as db]
             [frontend.db.utils :as db-utils]
-            [frontend.util :as util]
-            [frontend.util.property :as property]
             [frontend.state :as state]
+            [frontend.util.property :as property]
+            [frontend.util.fs :as fs-util]
             [frontend.handler.file :as file-handler]))
 
 (defn- indented-block-content
@@ -116,7 +116,7 @@
             filename (if journal-page?
                        (date/date->file-name journal-page?)
                        (-> (or (:block/original-name page) (:block/name page))
-                           (util/file-name-sanity)))
+                           (fs-util/file-name-sanity)))
             sub-dir (cond
                       journal-page?    (config/get-journals-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 []
                                           (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}
 
    :command/run                    {:binding "mod+shift+1"

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

@@ -271,8 +271,3 @@
   [repo]
   (when-let [engine (get-engine repo)]
     (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]
     (let [indice (search-db/make-blocks-indice! repo)]
       (p/promise indice)))
-  (cache-stale? [_this _repo] (p/promise false)) ;; fuse.js doesn't have cache
   (transact-blocks! [_this {:keys [blocks-to-remove-set
                                   blocks-to-add]}]
     (swap! search-db/indices update-in [repo :blocks]

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

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

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

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

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

@@ -290,7 +290,10 @@
 (def default-config
   "Default config for a repo-specific, user config"
   {: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
 (declare get-current-repo)
@@ -440,11 +443,10 @@ should be done through this fn in order to get global config and config defaults
     "LATER"
     "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
   []

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

@@ -910,24 +910,6 @@
      [string]
      (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
    (defn search-normalize
      "Normalize string for searching (loose)"
@@ -937,19 +919,6 @@
         (removeAccents  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
    (def page-name-sanity-lc
      "Delegate to gp-util to loosely couple app usages to graph-parser"
@@ -1006,12 +975,13 @@
     [col1 col2]))
 
 ;; 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
   [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
   "Misc util fns built on top of frontend.fs"
   (:require ["path" :as path]
+            [frontend.util :as util]
+            [logseq.graph-parser.util :as gp-util]
             [clojure.string :as string]
+            [frontend.state :as state]
             [frontend.fs :as fs]
             [frontend.config :as config]
             [promesa.core :as p]
             [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.
 ;;       The assets directory is ignored.
 ;;
@@ -64,3 +67,125 @@
   [repo-url file]
   (when-let [repo-dir (config/get-repo-dir repo-url)]
     (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, '/')
 }
 
+// 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, {
   basename (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]]
             [frontend.extensions.pdf.assets :as assets]))
 
-(deftest fix-local-asset-filename
+(deftest fix-local-asset-pagename
   (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"
-      "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"
-    (are [x y] (= y (assets/fix-local-asset-filename x))
+    (are [x y] (= y (assets/fix-local-asset-pagename x))
       "foo" "foo"
       "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!
                      :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
   (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)
         _ (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)]

+ 15 - 5
templates/config.edn

@@ -88,11 +88,6 @@
  ;; For more, see https://github.com/logseq/logseq/issues/672
  ;; :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
  ;; Syntax:
  ;; 1. `+` means keys pressing simultaneously. eg: `ctrl+shift+a`
@@ -286,4 +281,19 @@
  ;   :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
  }