rename.cljs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. (ns frontend.worker.handler.page.file-based.rename
  2. "File based page rename"
  3. (:require [frontend.worker.handler.page :as worker-page]
  4. [datascript.core :as d]
  5. [clojure.string :as string]
  6. [clojure.walk :as walk]
  7. [logseq.common.util.page-ref :as page-ref]
  8. [frontend.worker.file.util :as wfu]
  9. [logseq.db :as ldb]
  10. [logseq.common.util :as common-util]
  11. [logseq.common.config :as common-config]
  12. [logseq.graph-parser.text :as text]
  13. [logseq.graph-parser.property :as gp-property]
  14. [logseq.db.frontend.order :as db-order]))
  15. (defn- replace-page-ref-aux
  16. "Unsanitized names"
  17. [config content old-name new-name]
  18. (let [preferred-format (common-config/get-preferred-format config)
  19. [original-old-name original-new-name] (map string/trim [old-name new-name])
  20. [old-ref new-ref] (map page-ref/->page-ref [old-name new-name])
  21. [old-name new-name] (map #(if (string/includes? % "/")
  22. (string/replace % "/" ".")
  23. %)
  24. [original-old-name original-new-name])
  25. old-org-ref (and (= :org preferred-format)
  26. (:org-mode/insert-file-link? config)
  27. (re-find
  28. (re-pattern
  29. (common-util/format
  30. "\\[\\[file:\\.*/.*%s\\.org\\]\\[(.*?)\\]\\]" old-name))
  31. content))]
  32. (-> (if old-org-ref
  33. (let [[old-full-ref old-label] old-org-ref
  34. new-label (if (= old-label original-old-name)
  35. original-new-name
  36. old-label)
  37. new-full-ref (-> (string/replace old-full-ref old-name new-name)
  38. (string/replace (str "[" old-label "]")
  39. (str "[" new-label "]")))]
  40. (string/replace content old-full-ref new-full-ref))
  41. content)
  42. (string/replace old-ref new-ref))))
  43. (defn replace-tag-ref!
  44. [content old-name new-name]
  45. (let [old-tag (common-util/format "#%s" old-name)
  46. new-tag (if (re-find #"[\s\t]+" new-name)
  47. (common-util/format "#[[%s]]" new-name)
  48. (str "#" new-name))]
  49. ;; hash tag parsing rules https://github.com/logseq/mldoc/blob/701243eaf9b4157348f235670718f6ad19ebe7f8/test/test_markdown.ml#L631
  50. ;; Safari doesn't support look behind, don't use
  51. ;; TODO: parse via mldoc
  52. (string/replace content
  53. (re-pattern (str "(?i)(^|\\s)(" (common-util/escape-regex-chars old-tag) ")(?=[,\\.]*($|\\s))"))
  54. ;; case_insense^ ^lhs ^_grp2 look_ahead^ ^_grp3
  55. (fn [[_match lhs _grp2 _grp3]]
  56. (str lhs new-tag)))))
  57. (defn- replace-property-ref!
  58. [content old-name new-name format]
  59. (let [new-name (keyword (string/replace (string/lower-case new-name) #"\s+" "-"))
  60. org-format? (= :org format)
  61. old-property (if org-format? (gp-property/colons-org old-name) (str old-name gp-property/colons))
  62. new-property (if org-format? (gp-property/colons-org (name new-name)) (str (name new-name) gp-property/colons))]
  63. (common-util/replace-ignore-case content old-property new-property)))
  64. (defn- replace-old-page!
  65. "Unsanitized names"
  66. [config content old-name new-name format]
  67. (when (and (string? content) (string? old-name) (string? new-name))
  68. (-> (replace-page-ref-aux config content old-name new-name)
  69. (replace-tag-ref! old-name new-name)
  70. (replace-property-ref! old-name new-name format))))
  71. (defn- walk-replace-old-page!
  72. "Unsanitized names"
  73. [config form old-name new-name format]
  74. (walk/postwalk (fn [f]
  75. (cond
  76. (and (vector? f)
  77. (contains? #{"Search" "Label"} (first f))
  78. (string/starts-with? (second f) (str old-name "/")))
  79. [(first f) (string/replace-first (second f)
  80. (str old-name "/")
  81. (str new-name "/"))]
  82. (string? f)
  83. (if (= f old-name)
  84. new-name
  85. (replace-old-page! config f old-name new-name format))
  86. (and (keyword f) (= (name f) old-name))
  87. (keyword (string/replace (string/lower-case new-name) #"\s+" "-"))
  88. :else
  89. f))
  90. form))
  91. (defn- rename-update-block-refs!
  92. [refs from-id to-id]
  93. (if to-id
  94. (->> refs
  95. (remove #{{:db/id from-id}})
  96. (cons {:db/id to-id})
  97. (distinct)
  98. (vec))
  99. ;; New page not exists so that we keep using the old page's block as a ref
  100. refs))
  101. (defn replace-page-ref
  102. "Unsanitized only"
  103. [db config page new-name]
  104. ;; update all pages which have references to this page
  105. (let [to-page (ldb/get-page db new-name)
  106. old-title (:block/title page)
  107. blocks (:block/_refs (d/entity db (:db/id page)))
  108. tx (->> (map (fn [{:block/keys [uuid title properties format] :as block}]
  109. (let [content (let [content' (replace-old-page! config title old-title new-name format)]
  110. (when-not (= content' title)
  111. content'))
  112. properties (let [properties' (walk-replace-old-page! config properties old-title new-name format)]
  113. (when-not (= properties' properties)
  114. properties'))]
  115. (when (or content properties)
  116. (common-util/remove-nils-non-nested
  117. {:block/uuid uuid
  118. :block/title content
  119. :block/properties properties
  120. :block/properties-order (when (seq properties)
  121. (map first properties))
  122. :block/refs (->> (rename-update-block-refs! (:block/refs block) (:db/id page) (:db/id to-page))
  123. (map :db/id)
  124. (set))})))) blocks)
  125. (remove nil?))]
  126. tx))
  127. (defn rename-update-namespace!
  128. "update :block/namespace of the renamed block"
  129. [repo conn config page old-title new-name]
  130. (let [old-namespace? (text/namespace-page? old-title)
  131. new-namespace? (text/namespace-page? new-name)]
  132. (cond
  133. new-namespace?
  134. ;; update namespace
  135. (let [namespace (first (common-util/split-last "/" new-name))]
  136. (when namespace
  137. (worker-page/create! repo conn config namespace) ;; create parent page if not exist, creation of namespace ref is handled in `create!`
  138. (let [namespace-block (d/entity @conn [:block/name (common-util/page-name-sanity-lc namespace)])
  139. page-txs [{:db/id (:db/id page)
  140. :block/namespace (:db/id namespace-block)}]]
  141. (ldb/transact! conn page-txs {:persist-op? true}))))
  142. old-namespace?
  143. ;; retract namespace
  144. (ldb/transact! conn [[:db/retract (:db/id page) :block/namespace]] {:persist-op? true})
  145. :else
  146. nil)))
  147. (declare rename-page-aux)
  148. (defn- based-merge-pages!
  149. [repo conn config from-page-name to-page-name {:keys [old-name new-name]}]
  150. (when (and (ldb/page-exists? @conn from-page-name)
  151. (ldb/page-exists? @conn to-page-name)
  152. (not= from-page-name to-page-name))
  153. (let [db @conn
  154. to-page (d/entity db [:block/name to-page-name])
  155. to-id (:db/id to-page)
  156. from-page (d/entity db [:block/name from-page-name])
  157. from-id (:db/id from-page)
  158. datoms (d/datoms @conn :avet :block/page from-id)
  159. block-eids (mapv :e datoms)
  160. blocks (d/pull-many db '[:db/id :block/page :block/refs :block/path-refs :block/order :block/parent] block-eids)
  161. blocks-tx-data (map (fn [block]
  162. (let [id (:db/id block)]
  163. (cond->
  164. {:db/id id
  165. :block/page {:db/id to-id}
  166. :block/refs (rename-update-block-refs! (:block/refs block) from-id to-id)
  167. :block/order (db-order/gen-key nil)}
  168. (= (:block/parent block) {:db/id from-id})
  169. (assoc :block/parent {:db/id to-id})))) blocks)
  170. replace-ref-tx-data (replace-page-ref db config from-page to-page-name)
  171. tx-data (concat blocks-tx-data replace-ref-tx-data)]
  172. (rename-page-aux repo conn config old-name new-name
  173. :merge? true
  174. :other-tx tx-data)
  175. (worker-page/delete! repo conn (:block/uuid from-page) {:rename? true}))))
  176. (defn- compute-new-file-path
  177. "Construct the full path given old full path and the file sanitized body.
  178. Ext. included in the `old-path`."
  179. [old-path new-file-name-body]
  180. (let [result (string/split old-path "/")
  181. ext (last (string/split (last result) "."))
  182. new-file (str new-file-name-body "." ext)
  183. parts (concat (butlast result) [new-file])]
  184. (common-util/string-join-path parts)))
  185. (defn- update-file-tx
  186. [db old-page-name new-page-name]
  187. (let [page (d/entity db [:block/name old-page-name])
  188. file (:block/file page)]
  189. (when (and file (not (ldb/journal-page? page)))
  190. (let [old-path (:file/path file)
  191. new-file-name (wfu/file-name-sanity new-page-name) ;; w/o file extension
  192. new-path (compute-new-file-path old-path new-file-name)]
  193. {:old-path old-path
  194. :new-path new-path
  195. :tx-data [{:db/id (:db/id file)
  196. :file/path new-path}]}))))
  197. (defn- rename-page-aux
  198. "Only accepts unsanitized page names"
  199. [repo conn config old-name new-name & {:keys [merge? other-tx]}]
  200. (let [db @conn
  201. old-page-name (common-util/page-name-sanity-lc old-name)
  202. new-page-name (common-util/page-name-sanity-lc new-name)
  203. page (d/pull @conn '[*] [:block/name old-page-name])]
  204. (when (and repo page)
  205. (let [old-title (:block/title page)
  206. page-txs (when-not merge?
  207. [{:db/id (:db/id page)
  208. :block/uuid (:block/uuid page)
  209. :block/name new-page-name
  210. :block/title new-name}])
  211. {:keys [old-path new-path tx-data]} (update-file-tx db old-page-name new-name)
  212. txs (concat page-txs
  213. other-tx
  214. (->>
  215. (concat
  216. ;; update page refes in block content when ref name changes
  217. (replace-page-ref db config page new-name)
  218. ;; update file path
  219. tx-data)
  220. (remove nil?)))]
  221. (ldb/transact! conn txs {:outliner-op :rename-page
  222. :data (cond->
  223. {:old-name old-name
  224. :new-name new-name}
  225. (and old-path new-path)
  226. (merge {:old-path old-path
  227. :new-path new-path}))})
  228. (rename-update-namespace! repo conn config page old-title new-name)))))
  229. (defn- rename-namespace-pages!
  230. "Original names (unsanitized only)"
  231. [repo conn config old-name new-name]
  232. (let [pages (ldb/get-namespace-pages @conn old-name {})
  233. page (d/pull @conn '[*] [:block/name (common-util/page-name-sanity-lc old-name)])
  234. pages (cons page pages)]
  235. (doseq [{:block/keys [name title]} pages]
  236. (let [old-page-title (or title name)
  237. ;; only replace one time, for the case that the namespace is a sub-string of the sub-namespace page name
  238. ;; Example: has pages [[work]] [[work/worklog]],
  239. ;; we want to rename [[work/worklog]] to [[work1/worklog]] when rename [[work]] to [[work1]],
  240. ;; but don't rename [[work/worklog]] to [[work1/work1log]]
  241. new-page-title (common-util/replace-first-ignore-case old-page-title old-name new-name)]
  242. (when (and old-page-title new-page-title)
  243. (rename-page-aux repo conn config old-page-title new-page-title)
  244. (println "Renamed " old-page-title " to " new-page-title))))))
  245. (defn- rename-nested-pages
  246. "Unsanitized names only"
  247. [repo conn config old-ns-name new-ns-name]
  248. (let [nested-page-str (page-ref/->page-ref (common-util/page-name-sanity-lc old-ns-name))
  249. ns-prefix-format-str (str page-ref/left-brackets "%s/")
  250. ns-prefix (common-util/format ns-prefix-format-str (common-util/page-name-sanity-lc old-ns-name))
  251. nested-pages (ldb/get-pages-by-name-partition @conn nested-page-str)
  252. nested-pages-ns (ldb/get-pages-by-name-partition @conn ns-prefix)]
  253. (when nested-pages
  254. ;; rename page "[[obsidian]] is a tool" to "[[logseq]] is a tool"
  255. (doseq [{:block/keys [name title]} nested-pages]
  256. (let [old-page-title (or title name)
  257. new-page-title (string/replace
  258. old-page-title
  259. (page-ref/->page-ref old-ns-name)
  260. (page-ref/->page-ref new-ns-name))]
  261. (when (and old-page-title new-page-title)
  262. (rename-page-aux repo conn config old-page-title new-page-title)
  263. (println "Renamed " old-page-title " to " new-page-title)))))
  264. (when nested-pages-ns
  265. ;; rename page "[[obsidian/page1]] is a tool" to "[[logseq/page1]] is a tool"
  266. (doseq [{:block/keys [name title]} nested-pages-ns]
  267. (let [old-page-title (or title name)
  268. new-page-title (string/replace
  269. old-page-title
  270. (common-util/format ns-prefix-format-str old-ns-name)
  271. (common-util/format ns-prefix-format-str new-ns-name))]
  272. (when (and old-page-title new-page-title)
  273. (rename-page-aux repo conn config old-page-title new-page-title)
  274. (println "Renamed " old-page-title " to " new-page-title)))))))
  275. (defn rename!
  276. [repo conn config page-uuid new-name & {:keys [persist-op?]
  277. :or {persist-op? true}}]
  278. (let [db @conn
  279. page-e (d/entity db [:block/uuid page-uuid])
  280. old-name (:block/title page-e)
  281. new-name (string/trim new-name)
  282. old-page-name (common-util/page-name-sanity-lc old-name)
  283. new-page-name (common-util/page-name-sanity-lc new-name)
  284. new-page-e (d/entity db [:block/name new-page-name])
  285. name-changed? (not= old-name new-name)]
  286. (cond
  287. (ldb/built-in? page-e)
  288. :built-in-page
  289. (string/blank? new-name)
  290. :invalid-empty-name
  291. (and page-e new-page-e
  292. (or (contains? (:block/type page-e) "whiteboard")
  293. (contains? (:block/type new-page-e) "whiteboard")))
  294. :merge-whiteboard-pages
  295. (and old-name new-name name-changed?)
  296. (do
  297. (cond
  298. (= old-page-name new-page-name) ; case changed
  299. (ldb/transact! conn
  300. [{:db/id (:db/id page-e)
  301. :block/title new-name}]
  302. {:persist-op? persist-op?
  303. :outliner-op :rename-page})
  304. (and (not= old-page-name new-page-name)
  305. (d/entity @conn [:block/name new-page-name])) ; merge page
  306. (based-merge-pages! repo conn config old-page-name new-page-name {:old-name old-name
  307. :new-name new-name
  308. :persist-op? persist-op?})
  309. :else ; rename
  310. (rename-namespace-pages! repo conn config old-name new-name))
  311. (rename-nested-pages repo conn config old-name new-name)))))