page.cljs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  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.tools.html-export :as html-export]
  8. [frontend.config :as config]
  9. [frontend.handler.common :as common-handler]
  10. [frontend.handler.route :as route-handler]
  11. [frontend.handler.file :as file-handler]
  12. [frontend.handler.repo :as repo-handler]
  13. [frontend.handler.git :as git-handler]
  14. [frontend.handler.editor :as editor-handler]
  15. [frontend.handler.project :as project-handler]
  16. [frontend.handler.notification :as notification]
  17. [frontend.handler.ui :as ui-handler]
  18. [frontend.commands :as commands]
  19. [frontend.date :as date]
  20. [clojure.walk :as walk]
  21. [frontend.git :as git]
  22. [frontend.fs :as fs]
  23. [promesa.core :as p]
  24. [lambdaisland.glogi :as log]
  25. [frontend.format.mldoc :as mldoc]))
  26. (defn- get-directory
  27. [journal?]
  28. (if journal?
  29. config/default-journals-directory
  30. (config/get-pages-directory)))
  31. (defn- get-file-name
  32. [journal? title]
  33. (if journal?
  34. (date/journal-title->default title)
  35. (util/page-name-sanity (string/lower-case title))))
  36. (defn create!
  37. ([title]
  38. (create! title {}))
  39. ([title {:keys [redirect?]
  40. :or {redirect? true}}]
  41. (let [repo (state/get-current-repo)
  42. dir (util/get-repo-dir repo)
  43. journal-page? (date/valid-journal-title? title)
  44. directory (get-directory journal-page?)]
  45. (when dir
  46. (p/let [_ (-> (fs/mkdir (str dir "/" directory))
  47. (p/catch (fn [_e])))]
  48. (let [format (name (state/get-preferred-format))
  49. page (string/lower-case title)
  50. path (str (get-file-name journal-page? title)
  51. "."
  52. (if (= format "markdown") "md" format))
  53. path (str directory "/" path)
  54. file-path (str "/" path)]
  55. (p/let [exists? (fs/file-exists? dir file-path)]
  56. (if exists?
  57. (notification/show!
  58. [:p.content
  59. (util/format "File %s already exists!" file-path)]
  60. :error)
  61. ;; create the file
  62. (let [content (util/default-content-with-title format title)]
  63. (p/let [_ (fs/create-if-not-exists dir file-path content)]
  64. (db/reset-file! repo path content)
  65. (git-handler/git-add repo path)
  66. (when redirect?
  67. (route-handler/redirect! {:to :page
  68. :path-params {:name page}})
  69. (let [blocks (db/get-page-blocks page)
  70. last-block (last blocks)]
  71. (when last-block
  72. (js/setTimeout
  73. #(editor-handler/edit-last-block-for-new-page! last-block 0)
  74. 100))))))))))))))
  75. (defn page-add-properties!
  76. [page-name properties]
  77. (let [page (db/entity [:page/name page-name])
  78. page-format (db/get-page-format page-name)
  79. properties-content (db/get-page-properties-content page-name)
  80. properties-content (if properties-content
  81. (string/trim properties-content)
  82. (config/properties-wrapper page-format))]
  83. (let [file (db/entity (:db/id (:page/file page)))
  84. file-path (:file/path file)
  85. file-content (db/get-file file-path)
  86. after-content (subs file-content (inc (count properties-content)))
  87. new-properties-content (db/add-properties! page-format properties-content properties)
  88. full-content (str new-properties-content "\n\n" (string/trim after-content))]
  89. (file-handler/alter-file (state/get-current-repo)
  90. file-path
  91. full-content
  92. {:reset? true
  93. :re-render-root? true}))))
  94. (defn page-remove-property!
  95. [page-name k]
  96. (when-let [properties-content (string/trim (db/get-page-properties-content page-name))]
  97. (let [page (db/entity [:page/name page-name])
  98. file (db/entity (:db/id (:page/file page)))
  99. file-path (:file/path file)
  100. file-content (db/get-file file-path)
  101. after-content (subs file-content (count properties-content))
  102. page-format (db/get-page-format page-name)
  103. new-properties-content (let [lines (string/split-lines properties-content)
  104. prefix (case page-format
  105. :org (str "#+" (string/upper-case k) ": ")
  106. :markdown (str (string/lower-case k) ": ")
  107. "")
  108. exists? (atom false)
  109. lines (remove #(util/starts-with? % prefix) lines)]
  110. (string/join "\n" lines))
  111. full-content (str new-properties-content "\n\n" (string/trim after-content))]
  112. (file-handler/alter-file (state/get-current-repo)
  113. file-path
  114. full-content
  115. {:reset? true
  116. :re-render-root? true}))))
  117. (defn published-success-handler
  118. [page-name]
  119. (fn [result]
  120. (let [permalink (:permalink result)]
  121. (page-add-properties! page-name {"permalink" permalink})
  122. (let [win (js/window.open (str
  123. config/website
  124. "/"
  125. (state/get-current-project)
  126. "/"
  127. permalink))]
  128. (.focus win)))))
  129. (defn published-failed-handler
  130. [error]
  131. (notification/show!
  132. "Publish failed, please give it another try."
  133. :error))
  134. (defn get-plugins
  135. [blocks]
  136. (let [plugins (atom {})
  137. add-plugin #(swap! plugins assoc % true)]
  138. (walk/postwalk
  139. (fn [x]
  140. (if (and (vector? x)
  141. (>= (count x) 2))
  142. (let [[type option] x]
  143. (case type
  144. "Src" (when (:language option)
  145. (add-plugin "highlight"))
  146. "Export" (when (= option "latex")
  147. (add-plugin "latex"))
  148. "Latex_Fragment" (add-plugin "latex")
  149. "Math" (add-plugin "latex")
  150. "Latex_Environment" (add-plugin "latex")
  151. nil)
  152. x)
  153. x))
  154. (map :block/body blocks))
  155. @plugins))
  156. (defn publish-page-as-slide!
  157. ([page-name project-add-modal]
  158. (publish-page-as-slide! page-name (db/get-page-blocks page-name) project-add-modal))
  159. ([page-name blocks project-add-modal]
  160. (project-handler/exists-or-create!
  161. (fn [project]
  162. (page-add-properties! page-name {"published" true
  163. "slide" true})
  164. (let [properties (db/get-page-properties page-name)
  165. plugins (get-plugins blocks)
  166. data {:project project
  167. :title page-name
  168. :permalink (:permalink properties)
  169. :html (html-export/export-page page-name blocks notification/show!)
  170. :tags (:tags properties)
  171. :settings (merge
  172. (assoc properties
  173. :slide true
  174. :published true)
  175. plugins)
  176. :repo (state/get-current-repo)}]
  177. (util/post (str config/api "pages")
  178. data
  179. (published-success-handler page-name)
  180. published-failed-handler)))
  181. project-add-modal)))
  182. (defn publish-page!
  183. [page-name project-add-modal]
  184. (project-handler/exists-or-create!
  185. (fn [project]
  186. (let [properties (db/get-page-properties page-name)
  187. slide? (let [slide (:slide properties)]
  188. (or (true? slide)
  189. (= "true" slide)))
  190. blocks (db/get-page-blocks page-name)
  191. plugins (get-plugins blocks)]
  192. (if slide?
  193. (publish-page-as-slide! page-name blocks project-add-modal)
  194. (do
  195. (page-add-properties! page-name {"published" true})
  196. (let [data {:project project
  197. :title page-name
  198. :permalink (:permalink properties)
  199. :html (html-export/export-page page-name blocks notification/show!)
  200. :tags (:tags properties)
  201. :settings (merge properties plugins)
  202. :repo (state/get-current-repo)}]
  203. (util/post (str config/api "pages")
  204. data
  205. (published-success-handler page-name)
  206. published-failed-handler))))))
  207. project-add-modal))
  208. (defn unpublished-success-handler
  209. [page-name]
  210. (fn [result]
  211. (notification/show!
  212. "Un-publish successfully!"
  213. :success)))
  214. (defn unpublished-failed-handler
  215. [error]
  216. (notification/show!
  217. "Un-publish failed, please give it another try."
  218. :error))
  219. (defn unpublish-page!
  220. [page-name]
  221. (page-add-properties! page-name {"published" false})
  222. (let [properties (db/get-page-properties page-name)
  223. permalink (:permalink properties)
  224. project (state/get-current-project)]
  225. (if (and project permalink)
  226. (util/delete (str config/api project "/" permalink)
  227. (unpublished-success-handler page-name)
  228. unpublished-failed-handler)
  229. (notification/show!
  230. "Can't find the permalink of this page!"
  231. :error))))
  232. (defn delete!
  233. [page-name ok-handler]
  234. (when page-name
  235. (when-let [repo (state/get-current-repo)]
  236. (let [page-name (string/lower-case page-name)]
  237. (let [file (db/get-page-file page-name)
  238. file-path (:file/path file)]
  239. ;; delete file
  240. (when file-path
  241. (db/transact! [[:db.fn/retractEntity [:file/path file-path]]])
  242. (when-let [files-conn (db/get-files-conn repo)]
  243. (d/transact! files-conn [[:db.fn/retractEntity [:file/path file-path]]]))
  244. (let [blocks (db/get-page-blocks page-name)
  245. tx-data (mapv
  246. (fn [block]
  247. [:db.fn/retractEntity [:block/uuid (:block/uuid block)]])
  248. blocks)]
  249. (db/transact! tx-data)
  250. ;; remove file
  251. (->
  252. (p/let [_ (git/remove-file repo file-path)
  253. _ (fs/unlink (str (util/get-repo-dir repo)
  254. "/"
  255. file-path)
  256. nil)]
  257. (common-handler/check-changed-files-status)
  258. (repo-handler/push-if-auto-enabled! repo))
  259. (p/catch (fn [err]
  260. (prn "error: " err))))))
  261. (db/transact! [[:db.fn/retractEntity [:page/name page-name]]])
  262. (ok-handler))))))
  263. (defn- compute-new-file-path
  264. [old-path new-page-name]
  265. (let [result (string/split old-path "/")
  266. file-name (util/page-name-sanity new-page-name)
  267. ext (last (string/split (last result) "."))
  268. new-file (str file-name "." ext)
  269. parts (concat (butlast result) [new-file])]
  270. (string/join "/" parts)))
  271. (defn rename-file!
  272. [file new-name ok-handler]
  273. (let [repo (state/get-current-repo)
  274. old-path (:file/path file)
  275. new-path (compute-new-file-path old-path new-name)]
  276. (->
  277. (p/let [_ (fs/rename (str (util/get-repo-dir repo) "/" old-path)
  278. (str (util/get-repo-dir repo) "/" new-path))]
  279. ;; update db
  280. (db/transact! repo [{:db/id (:db/id file)
  281. :file/path new-path}])
  282. ;; update files db
  283. (let [conn (db/get-files-conn repo)]
  284. (when-let [file (d/entity (d/db conn) [:file/path old-path])]
  285. (d/transact! conn [{:db/id (:db/id file)
  286. :file/path new-path}])))
  287. (p/let [_ (git/rename repo old-path new-path)]
  288. (common-handler/check-changed-files-status)
  289. (ok-handler)))
  290. (p/catch (fn [error]
  291. (println "file rename failed: " error))))))
  292. (defn rename!
  293. [old-name new-name]
  294. (when (and old-name new-name
  295. (not= (string/lower-case old-name) (string/lower-case new-name)))
  296. (when-let [repo (state/get-current-repo)]
  297. (if (db/entity [:page/name (string/lower-case new-name)])
  298. (notification/show! "Page already exists!" :error)
  299. (when-let [page (db/entity [:page/name (string/lower-case old-name)])]
  300. (let [old-original-name (:page/original-name page)
  301. file (:page/file page)
  302. journal? (:page/journal? page)]
  303. (d/transact! (db/get-conn repo false)
  304. [{:db/id (:db/id page)
  305. :page/name (string/lower-case new-name)
  306. :page/original-name new-name}])
  307. (when (and file (not journal?))
  308. (rename-file! file new-name
  309. (fn []
  310. (page-add-properties! (string/lower-case new-name) {:title new-name}))))
  311. ;; update all files which have references to this page
  312. (let [files (db/get-files-that-referenced-page (:db/id page))]
  313. (doseq [file-path files]
  314. (let [file-content (db/get-file file-path)
  315. ;; FIXME: not safe
  316. new-content (string/replace file-content
  317. (util/format "[[%s]]" old-original-name)
  318. (util/format "[[%s]]" new-name))]
  319. (file-handler/alter-file repo
  320. file-path
  321. new-content
  322. {:reset? true
  323. :re-render-root? false})))))
  324. ;; TODO: update browser history, remove the current one
  325. ;; Redirect to the new page
  326. (route-handler/redirect! {:to :page
  327. :path-params {:name (string/lower-case new-name)}})
  328. (notification/show! "Page renamed successfully!" :success)
  329. (repo-handler/push-if-auto-enabled! repo)
  330. (ui-handler/re-render-root!))))))
  331. (defn rename-when-alter-title-property!
  332. [page path format original-content content]
  333. (when (and page (contains? config/mldoc-support-formats format))
  334. (let [old-name page
  335. new-name (let [ast (mldoc/->edn content (mldoc/default-config format))]
  336. (db/get-page-name path ast))]
  337. (when (not= old-name new-name)
  338. (rename! old-name new-name)))))
  339. (defn handle-add-page-to-contents!
  340. [page-name]
  341. (let [last-block (last (db/get-page-blocks (state/get-current-repo) "contents"))
  342. last-empty? (>= 3 (count (:block/content last-block)))
  343. heading-pattern (config/get-block-pattern (state/get-preferred-format))
  344. pre-str (str heading-pattern heading-pattern)
  345. new-content (if last-empty? (str pre-str " [[" page-name "]]") (str (:block/content last-block) pre-str " [[" page-name "]]"))]
  346. (editor-handler/insert-new-block-aux!
  347. last-block
  348. new-content
  349. {:create-new-block? false
  350. :ok-handler
  351. (fn [[_first-block last-block _new-block-content]]
  352. (notification/show! "Added to contents!" :success)
  353. (editor-handler/clear-when-saved!))
  354. :with-level? true
  355. :new-level 2
  356. :current-page "Contents"})))
  357. (defn load-more-journals!
  358. []
  359. (let [current-length (:journals-length @state/state)]
  360. (when (< current-length (db/get-journals-length))
  361. (state/update-state! :journals-length inc))))
  362. (defn update-public-attribute!
  363. [page-name value]
  364. (page-add-properties! page-name {:public value}))
  365. (defn get-page-ref-text
  366. [page]
  367. (when-let [edit-block (state/get-edit-block)]
  368. (let [page-name (string/lower-case page)
  369. edit-block-file-path (-> (:db/id (:block/file edit-block))
  370. (db/entity)
  371. :file/path)]
  372. (if (and edit-block-file-path
  373. (state/org-mode-file-link? (state/get-current-repo)))
  374. (if-let [ref-file-path (:file/path (:page/file (db/entity [:page/name page-name])))]
  375. (util/format "[[file:%s][%s]]"
  376. (util/get-relative-path edit-block-file-path ref-file-path)
  377. page)
  378. (let [journal? (date/valid-journal-title? page)
  379. ref-file-path (str (get-directory journal?)
  380. "/"
  381. (get-file-name journal? page)
  382. ".org")]
  383. (create! page {:redirect? false})
  384. (util/format "[[file:%s][%s]]"
  385. (util/get-relative-path edit-block-file-path ref-file-path)
  386. page)))
  387. (util/format "[[%s]]" page)))))
  388. (defn init-commands!
  389. []
  390. (commands/init-commands! get-page-ref-text))
  391. (defn delete-page-from-logseq
  392. [project permalink]
  393. (let [url (util/format "%s%s/%s" config/api project permalink)]
  394. (js/Promise.
  395. (fn [resolve reject]
  396. (util/delete url
  397. (fn [result]
  398. (resolve result))
  399. (fn [error]
  400. (log/error :page/http-delete-failed error)
  401. (reject error)))))))