page.cljs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. (ns frontend.handler.page
  2. (:require [clojure.string :as string]
  3. [frontend.db :as db]
  4. [datascript.core :as d]
  5. [frontend.state :as state]
  6. [frontend.util :as util :refer-macros [profile]]
  7. [frontend.config :as config]
  8. [frontend.handler.common :as common-handler]
  9. [frontend.handler.route :as route-handler]
  10. [frontend.handler.repo :as repo-handler]
  11. [frontend.handler.editor :as editor-handler]
  12. [frontend.handler.web.nfs :as web-nfs]
  13. [frontend.handler.notification :as notification]
  14. [frontend.handler.config :as config-handler]
  15. [frontend.handler.ui :as ui-handler]
  16. [frontend.modules.outliner.file :as outliner-file]
  17. [frontend.modules.outliner.core :as outliner-core]
  18. [frontend.modules.outliner.tree :as outliner-tree]
  19. [frontend.commands :as commands]
  20. [frontend.date :as date]
  21. [clojure.walk :as walk]
  22. [frontend.git :as git]
  23. [frontend.fs :as fs]
  24. [frontend.util.property :as property]
  25. [promesa.core :as p]
  26. [lambdaisland.glogi :as log]
  27. [frontend.format.block :as block]
  28. [cljs.reader :as reader]
  29. [goog.object :as gobj]
  30. [clojure.data :as data]))
  31. (defn- get-directory
  32. [journal?]
  33. (if journal?
  34. (config/get-journals-directory)
  35. (config/get-pages-directory)))
  36. (defn- get-file-name
  37. [journal? title]
  38. (when-let [s (if journal?
  39. (date/journal-title->default title)
  40. (util/page-name-sanity (string/lower-case title)))]
  41. ;; Win10 file path has a length limit of 260 chars
  42. (util/safe-subs s 0 200)))
  43. (defn get-page-file-path
  44. ([] (get-page-file-path (state/get-current-page)))
  45. ([page-name] (when-let [page (db/entity [:block/name page-name])]
  46. (:file/path (:block/file page)))))
  47. (defn default-properties-block
  48. [title format page]
  49. (let [properties (common-handler/get-page-default-properties title)
  50. content (property/build-properties-str format properties)]
  51. {:block/pre-block? true
  52. :block/uuid (db/new-block-id)
  53. :block/properties properties
  54. :block/left page
  55. :block/format format
  56. :block/content content
  57. :block/parent page
  58. :block/unordered true
  59. :block/page page}))
  60. (defn create!
  61. ([title]
  62. (create! title {}))
  63. ([title {:keys [redirect?]
  64. :or {redirect? true}}]
  65. (let [title (string/trim title)
  66. page (string/lower-case title)
  67. tx (block/page-name->map title true)
  68. format (state/get-preferred-format)
  69. page-entity [:block/uuid (:block/uuid tx)]
  70. create-title-property? (util/include-windows-reserved-chars? title)
  71. default-properties (default-properties-block title format page-entity)
  72. empty-block {:block/uuid (db/new-block-id)
  73. :block/left [:block/uuid (:block/uuid default-properties)]
  74. :block/format format
  75. :block/content ""
  76. :block/parent page-entity
  77. :block/unordered true
  78. :block/page page-entity}
  79. txs (if create-title-property?
  80. [tx default-properties empty-block]
  81. [tx])]
  82. (db/transact! txs)
  83. (when redirect?
  84. (route-handler/redirect! {:to :page
  85. :path-params {:name page}})
  86. (when create-title-property?
  87. (js/setTimeout (fn []
  88. (editor-handler/edit-block! empty-block 0 format (:block/uuid empty-block))) 50))))))
  89. (defn page-add-property!
  90. [page-name key value]
  91. (when-let [page (db/pull [:block/name (string/lower-case page-name)])]
  92. (let [repo (state/get-current-repo)
  93. key (keyword key)
  94. pre-block (db/get-pre-block repo (:db/id page))
  95. format (state/get-preferred-format)
  96. page-id {:db/id (:db/id page)}
  97. org? (= format :org)
  98. value (if (contains? #{:filters} key) (pr-str value) value)]
  99. (if pre-block
  100. (let [properties (:block/properties pre-block)
  101. new-properties (assoc properties key value)
  102. content (:block/content pre-block)
  103. front-matter? (property/front-matter? content)
  104. new-content (property/insert-property format content key value front-matter?)
  105. block {:db/id (:db/id pre-block)
  106. :block/properties new-properties
  107. :block/content new-content
  108. :block/page page-id}
  109. tx [(assoc page-id :block/properties new-properties)
  110. block]]
  111. ;; (util/pprint tx)
  112. (db/transact! tx)
  113. (db/refresh! repo {:key :block/change
  114. :data [block]}))
  115. (let [block {:block/uuid (db/new-block-id)
  116. :block/left page-id
  117. :block/parent page-id
  118. :block/page page-id
  119. :block/title []
  120. :block/content (if org?
  121. (str "#+" (string/upper-case (name key)) ": " value)
  122. (str (name key) ":: " value))
  123. :block/format format
  124. :block/properties {key value}
  125. :block/file (:block/file page)
  126. :block/pre-block? true}]
  127. (outliner-core/insert-node (outliner-core/block block)
  128. (outliner-core/block page)
  129. false)
  130. (db/transact! [(assoc page-id :block/properties {key value})])
  131. (db/refresh! repo {:key :block/change
  132. :data [block]})
  133. ;; (ui-handler/re-render-root!)
  134. ))
  135. (outliner-file/sync-to-file page-id))))
  136. (defn get-plugins
  137. [blocks]
  138. (let [plugins (atom {})
  139. add-plugin #(swap! plugins assoc % true)]
  140. (walk/postwalk
  141. (fn [x]
  142. (if (and (vector? x)
  143. (>= (count x) 2))
  144. (let [[type option] x]
  145. (case type
  146. "Src" (when (:language option)
  147. (add-plugin "highlight"))
  148. "Export" (when (= option "latex")
  149. (add-plugin "latex"))
  150. "Latex_Fragment" (add-plugin "latex")
  151. "Math" (add-plugin "latex")
  152. "Latex_Environment" (add-plugin "latex")
  153. nil)
  154. x)
  155. x))
  156. (map :block/body blocks))
  157. @plugins))
  158. (defn delete!
  159. [page-name ok-handler]
  160. (when page-name
  161. (when-let [repo (state/get-current-repo)]
  162. (let [page-name (string/lower-case page-name)]
  163. (let [file (db/get-page-file page-name)
  164. file-path (:file/path file)]
  165. ;; delete file
  166. (when-not (string/blank? file-path)
  167. (db/transact! [[:db.fn/retractEntity [:file/path file-path]]])
  168. (let [blocks (db/get-page-blocks page-name)
  169. tx-data (mapv
  170. (fn [block]
  171. [:db.fn/retractEntity [:block/uuid (:block/uuid block)]])
  172. blocks)]
  173. (db/transact! tx-data)
  174. ;; remove file
  175. (->
  176. (p/let [_ (or (config/local-db? repo) (git/remove-file repo file-path))
  177. _ (fs/unlink! (config/get-repo-path repo file-path) nil)]
  178. (common-handler/check-changed-files-status)
  179. (repo-handler/push-if-auto-enabled! repo))
  180. (p/catch (fn [err]
  181. (js/console.error "error: " err))))))
  182. (db/transact! [[:db.fn/retractEntity [:block/name page-name]]])
  183. (ok-handler))))))
  184. (defn- compute-new-file-path
  185. [old-path new-page-name]
  186. (let [result (string/split old-path "/")
  187. file-name (util/page-name-sanity new-page-name)
  188. ext (last (string/split (last result) "."))
  189. new-file (str file-name "." ext)
  190. parts (concat (butlast result) [new-file])]
  191. (string/join "/" parts)))
  192. (defn rename-file!
  193. [file new-name ok-handler]
  194. (let [repo (state/get-current-repo)
  195. file (db/pull (:db/id file))
  196. old-path (:file/path file)
  197. new-path (compute-new-file-path old-path new-name)]
  198. ;; update db
  199. (db/transact! repo [{:db/id (:db/id file)
  200. :file/path new-path}])
  201. (->
  202. (p/let [_ (fs/rename! repo
  203. (if (util/electron?)
  204. old-path
  205. (str (config/get-repo-dir repo) "/" old-path))
  206. (if (util/electron?)
  207. new-path
  208. (str (config/get-repo-dir repo) "/" new-path)))
  209. _ (when-not (config/local-db? repo)
  210. (git/rename repo old-path new-path))]
  211. (common-handler/check-changed-files-status)
  212. (ok-handler))
  213. (p/catch (fn [error]
  214. (println "file rename failed: " error))))))
  215. ;; FIXME: not safe
  216. (defn- replace-old-page!
  217. [s old-name new-name]
  218. (-> s
  219. (string/replace (util/format "[[%s]]" old-name) (util/format "[[%s]]" new-name))
  220. (string/replace (str "#" old-name) (str "#" new-name))))
  221. (defn- walk-replace-old-page!
  222. [form old-name new-name]
  223. (walk/postwalk (fn [f] (if (string? f)
  224. (if (= f old-name)
  225. new-name
  226. (replace-old-page! f old-name new-name))
  227. f)) form))
  228. (defn rename!
  229. [old-name new-name]
  230. (let [new-name (string/trim new-name)]
  231. (when-not (string/blank? new-name)
  232. (when (and old-name new-name)
  233. (let [name-changed? (not= (string/lower-case (string/trim old-name))
  234. (string/lower-case (string/trim new-name)))]
  235. (when-let [repo (state/get-current-repo)]
  236. (when-let [page (db/pull [:block/name (string/lower-case old-name)])]
  237. (let [old-original-name (:block/original-name page)
  238. file (:block/file page)
  239. journal? (:block/journal? page)
  240. properties-block (:data (outliner-tree/-get-down (outliner-core/block page)))
  241. properties-block-tx (when (and properties-block
  242. (string/includes? (string/lower-case (:block/content properties-block))
  243. (string/lower-case old-name)))
  244. {:db/id (:db/id properties-block)
  245. :block/content (property/insert-property (:block/format properties-block)
  246. (:block/content properties-block)
  247. :title
  248. new-name)})
  249. page-txs [{:db/id (:db/id page)
  250. :block/uuid (:block/uuid page)
  251. :block/name (string/lower-case new-name)
  252. :block/original-name new-name}]
  253. page-txs (if properties-block-tx (conj page-txs properties-block-tx) page-txs)]
  254. (d/transact! (db/get-conn repo false) page-txs)
  255. (when (and file (not journal?) name-changed?)
  256. (rename-file! file new-name (fn [] nil)))
  257. ;; update all files which have references to this page
  258. (let [blocks (db/get-page-referenced-blocks-no-cache (:db/id page))
  259. page-ids (->> (map :block/page blocks)
  260. (remove nil?)
  261. (set))
  262. tx (->> (map (fn [{:block/keys [uuid title content properties] :as block}]
  263. (let [title (let [title' (walk-replace-old-page! title old-original-name new-name)]
  264. (when-not (= title' title)
  265. title'))
  266. content (let [content' (replace-old-page! content old-original-name new-name)]
  267. (when-not (= content' content)
  268. content'))
  269. properties (let [properties' (walk-replace-old-page! properties old-original-name new-name)]
  270. (when-not (= properties' properties)
  271. properties'))]
  272. (when (or title content properties)
  273. (util/remove-nils-non-nested
  274. {:block/uuid uuid
  275. :block/title title
  276. :block/content content
  277. :block/properties properties})))) blocks)
  278. (remove nil?))]
  279. (db/transact! repo tx)
  280. (doseq [page-id page-ids]
  281. (outliner-file/sync-to-file page-id)))
  282. (outliner-file/sync-to-file page))
  283. ;; TODO: update browser history, remove the current one
  284. ;; Redirect to the new page
  285. (route-handler/redirect! {:to :page
  286. :path-params {:name (string/lower-case new-name)}})
  287. (notification/show! "Page renamed successfully!" :success)
  288. (repo-handler/push-if-auto-enabled! repo)
  289. (ui-handler/re-render-root!))))))))
  290. (defn handle-add-page-to-contents!
  291. [page-name]
  292. (let [content (str "[[" page-name "]]")]
  293. (editor-handler/api-insert-new-block!
  294. content
  295. {:page "Contents"
  296. :sibling? true})
  297. (notification/show! "Added to contents!" :success)
  298. (editor-handler/clear-when-saved!)))
  299. (defn has-more-journals?
  300. []
  301. (let [current-length (:journals-length @state/state)]
  302. (< current-length (db/get-journals-length))))
  303. (defn load-more-journals!
  304. []
  305. (when (has-more-journals?)
  306. (state/update-state! :journals-length inc)))
  307. (defn update-public-attribute!
  308. [page-name value]
  309. (page-add-property! page-name :public value))
  310. (defn get-page-ref-text
  311. [page]
  312. (let [edit-block-file-path (some-> (state/get-edit-block)
  313. (get-in [:block/file :db/id])
  314. db/entity
  315. :file/path)
  316. page-name (string/lower-case page)]
  317. (if (and edit-block-file-path
  318. (state/org-mode-file-link? (state/get-current-repo)))
  319. (if-let [ref-file-path (:file/path (:file/file (db/entity [:file/name page-name])))]
  320. (util/format "[[file:%s][%s]]"
  321. (util/get-relative-path edit-block-file-path ref-file-path)
  322. page)
  323. (let [journal? (date/valid-journal-title? page)
  324. ref-file-path (str (get-directory journal?)
  325. "/"
  326. (get-file-name journal? page)
  327. ".org")]
  328. (create! page {:redirect? false})
  329. (util/format "[[file:%s][%s]]"
  330. (util/get-relative-path edit-block-file-path ref-file-path)
  331. page)))
  332. (util/format "[[%s]]" page))))
  333. (defn init-commands!
  334. []
  335. (commands/init-commands! get-page-ref-text))
  336. (defn add-page-to-recent!
  337. [repo page]
  338. (let [pages (or (db/get-key-value repo :recent/pages)
  339. '())
  340. new-pages (take 12 (distinct (cons page pages)))]
  341. (db/set-key-value repo :recent/pages new-pages)))
  342. (defn template-exists?
  343. [title]
  344. (when title
  345. (let [templates (keys (db/get-all-templates))]
  346. (when (seq templates)
  347. (let [templates (map string/lower-case templates)]
  348. (contains? (set templates) (string/lower-case title)))))))
  349. (defn ls-dir-files!
  350. []
  351. (web-nfs/ls-dir-files-with-handler!
  352. (fn []
  353. (init-commands!))))
  354. ;; TODO: add use :file/last-modified-at
  355. (defn get-pages-with-modified-at
  356. [repo]
  357. (->> (db/get-modified-pages repo)
  358. (remove util/file-page?)))
  359. (defn get-filters
  360. [page-name]
  361. (let [properties (db/get-page-properties page-name)]
  362. (reader/read-string (get properties :filters "{}"))))
  363. (defn save-filter!
  364. [page-name filter-state]
  365. (page-add-property! page-name :filters filter-state))
  366. (defn page-exists?
  367. [page-name]
  368. (when page-name
  369. (db/entity [:block/name page-name])))
  370. ;; Editor
  371. (defn page-not-exists-handler
  372. [input id q current-pos]
  373. (state/set-editor-show-page-search! false)
  374. (if (state/org-mode-file-link? (state/get-current-repo))
  375. (let [page-ref-text (get-page-ref-text q)
  376. value (gobj/get input "value")
  377. old-page-ref (util/format "[[%s]]" q)
  378. new-value (string/replace value
  379. old-page-ref
  380. page-ref-text)]
  381. (state/set-edit-content! id new-value)
  382. (let [new-pos (+ current-pos
  383. (- (count page-ref-text)
  384. (count old-page-ref))
  385. 2)]
  386. (util/move-cursor-to input new-pos)))
  387. (util/cursor-move-forward input 2)))
  388. (defn on-chosen-handler
  389. [input id q pos format]
  390. (let [current-pos (:pos (util/get-caret-pos input))
  391. edit-content (state/sub [:editor/content id])
  392. edit-block (state/sub :editor/block)
  393. q (or
  394. @editor-handler/*selected-text
  395. (when (state/sub :editor/show-page-search-hashtag?)
  396. (util/safe-subs edit-content pos current-pos))
  397. (when (> (count edit-content) current-pos)
  398. (util/safe-subs edit-content pos current-pos)))]
  399. (if (state/sub :editor/show-page-search-hashtag?)
  400. (fn [chosen _click?]
  401. (state/set-editor-show-page-search! false)
  402. (let [wrapped? (= "[[" (util/safe-subs edit-content (- pos 2) pos))
  403. chosen (if (and (re-find #"\s+" chosen) (not wrapped?))
  404. (util/format "[[%s]]" chosen)
  405. chosen)]
  406. (editor-handler/insert-command! id
  407. (str "#" (when wrapped? "[[") chosen)
  408. format
  409. {:last-pattern (let [q (if @editor-handler/*selected-text "" q)]
  410. (str "#" (when wrapped? "[[") q))
  411. :forward-pos (if wrapped? 3 2)})))
  412. (fn [chosen _click?]
  413. (state/set-editor-show-page-search! false)
  414. (let [page-ref-text (get-page-ref-text chosen)]
  415. (editor-handler/insert-command! id
  416. page-ref-text
  417. format
  418. {:last-pattern (str "[[" (if @editor-handler/*selected-text "" q))
  419. :postfix-fn (fn [s] (util/replace-first "]]" s ""))}))))))