editor.cljs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. (ns frontend.handler.file-based.editor
  2. "File-based graph implementation"
  3. (:require [clojure.string :as string]
  4. [electron.ipc :as ipc]
  5. [frontend.commands :as commands]
  6. [frontend.config :as config]
  7. [frontend.date :as date]
  8. [frontend.db :as db]
  9. [frontend.db.file-based.model :as file-model]
  10. [frontend.db.query-dsl :as query-dsl]
  11. [frontend.format.block :as block]
  12. [frontend.format.mldoc :as mldoc]
  13. [frontend.handler.assets :as assets-handler]
  14. [frontend.handler.block :as block-handler]
  15. [frontend.handler.common.editor :as editor-common-handler]
  16. [frontend.handler.file-based.property :as file-property-handler]
  17. [frontend.handler.file-based.property.util :as property-util]
  18. [frontend.handler.file-based.repeated :as repeated]
  19. [frontend.handler.file-based.status :as status]
  20. [frontend.handler.property.file :as property-file]
  21. [frontend.modules.outliner.op :as outliner-op]
  22. [frontend.modules.outliner.ui :as ui-outliner-tx]
  23. [frontend.state :as state]
  24. [frontend.util :as util]
  25. [frontend.util.file-based.clock :as clock]
  26. [frontend.util.file-based.drawer :as drawer]
  27. [logseq.common.path :as path]
  28. [logseq.common.util :as common-util]
  29. [logseq.common.util.block-ref :as block-ref]
  30. [logseq.db :as ldb]
  31. [logseq.db.file-based.schema :as file-schema]
  32. [promesa.core :as p]))
  33. (defn- remove-non-existed-refs!
  34. [refs]
  35. (remove (fn [x] (or
  36. (and (vector? x)
  37. (= :block/uuid (first x))
  38. (nil? (db/entity x)))
  39. (nil? x))) refs))
  40. (defn- with-marker-time
  41. [content block format new-marker old-marker]
  42. (if (and (state/enable-timetracking?) new-marker)
  43. (try
  44. (let [logbook-exists? (and (:block.temp/ast-body block) (drawer/get-logbook (:block.temp/ast-body block)))
  45. new-marker (string/trim (string/lower-case (name new-marker)))
  46. old-marker (when old-marker (string/trim (string/lower-case (name old-marker))))
  47. new-content (cond
  48. (or (and (nil? old-marker) (or (= new-marker "doing")
  49. (= new-marker "now")))
  50. (and (= old-marker "todo") (= new-marker "doing"))
  51. (and (= old-marker "later") (= new-marker "now"))
  52. (and (= old-marker new-marker "now") (not logbook-exists?))
  53. (and (= old-marker new-marker "doing") (not logbook-exists?)))
  54. (clock/clock-in format content)
  55. (or
  56. (and (= old-marker "doing") (= new-marker "todo"))
  57. (and (= old-marker "now") (= new-marker "later"))
  58. (and (contains? #{"now" "doing"} old-marker)
  59. (= new-marker "done")))
  60. (clock/clock-out format content)
  61. :else
  62. content)]
  63. new-content)
  64. (catch :default _e
  65. content))
  66. content))
  67. (defn- with-timetracking
  68. [block value]
  69. (if (and (state/enable-timetracking?)
  70. (not= (:block/title block) value))
  71. (let [format (get block :block/format :markdown)
  72. new-marker (last (util/safe-re-find (status/marker-pattern format) (or value "")))
  73. new-value (with-marker-time value block format
  74. new-marker
  75. (:block/marker block))]
  76. new-value)
  77. value))
  78. (defn wrap-parse-block
  79. [{:block/keys [title format uuid level pre-block?] :as block
  80. :or {format :markdown}}]
  81. (let [repo (state/get-current-repo)
  82. block (or (and (:db/id block) (db/pull (:db/id block))) block)
  83. page (:block/page block)
  84. block (merge block
  85. (block/parse-title-and-body uuid format pre-block? (:block/title block)))
  86. properties (:block/properties block)
  87. properties (if (and (= format :markdown)
  88. (number? (:heading properties)))
  89. (dissoc properties :heading)
  90. properties)
  91. real-content (:block/title block)
  92. content (if (and (seq properties) real-content (not= real-content title))
  93. (property-file/with-built-in-properties-when-file-based repo properties title format)
  94. title)
  95. content (drawer/with-logbook block content)
  96. content (with-timetracking block content)
  97. first-block? (= (:block/uuid (ldb/get-first-child (db/get-db) (:db/id page)))
  98. (:block/uuid block))
  99. ast (mldoc/->edn (string/trim content) format)
  100. first-elem-type (first (ffirst ast))
  101. first-elem-meta (second (ffirst ast))
  102. properties? (contains? #{"Property_Drawer" "Properties"} first-elem-type)
  103. markdown-heading? (and (= format :markdown)
  104. (= "Heading" first-elem-type)
  105. (nil? (:size first-elem-meta)))
  106. block-with-title? (mldoc/block-with-title? first-elem-type)
  107. content (string/triml content)
  108. content (string/replace content (block-ref/->block-ref uuid) "")
  109. [content content'] (cond
  110. (and first-block? properties?)
  111. [content content]
  112. markdown-heading?
  113. [content content]
  114. :else
  115. (let [content' (str (config/get-block-pattern format) (if block-with-title? " " "\n") content)]
  116. [content content']))
  117. block (assoc block
  118. :block/title content'
  119. :block/format format)
  120. block (apply dissoc block (remove #{:block/pre-block?} file-schema/retract-attributes))
  121. block (block/parse-block block)
  122. block (if (and first-block? (:block/pre-block? block))
  123. block
  124. (dissoc block :block/pre-block?))
  125. block (update block :block/refs remove-non-existed-refs!)
  126. new-properties (merge
  127. (select-keys properties (file-property-handler/hidden-properties))
  128. (:block/properties block))]
  129. (-> block
  130. (assoc :block/title content
  131. :block/properties new-properties)
  132. (merge (if level {:block/level level} {})))))
  133. (defn- set-block-property-aux!
  134. [block-or-id key value]
  135. (when-let [block (cond (string? block-or-id) (db/entity [:block/uuid (uuid block-or-id)])
  136. (uuid? block-or-id) (db/entity [:block/uuid block-or-id])
  137. :else block-or-id)]
  138. (let [format (get block :block/format :markdown)
  139. content (:block/title block)
  140. properties (:block/properties block)
  141. properties (if (nil? value)
  142. (dissoc properties key)
  143. (assoc properties key value))
  144. content (if (nil? value)
  145. (property-util/remove-property format key content)
  146. (property-util/insert-property format content key value))
  147. content (property-util/remove-empty-properties content)]
  148. {:block/uuid (:block/uuid block)
  149. :block/properties properties
  150. :block/properties-order (or (keys properties) [])
  151. :block/title content})))
  152. (defn- set-heading-aux!
  153. [block-id heading]
  154. (let [block (db/pull [:block/uuid block-id])
  155. format (get block :block/format :markdown)
  156. old-heading (get-in block [:block/properties :heading])]
  157. (if (= format :markdown)
  158. (cond
  159. ;; nothing changed
  160. (or (and (nil? old-heading) (nil? heading))
  161. (and (true? old-heading) (true? heading))
  162. (= old-heading heading))
  163. nil
  164. (or (and (nil? old-heading) (true? heading))
  165. (and (true? old-heading) (nil? heading)))
  166. (set-block-property-aux! block :heading heading)
  167. (and (or (nil? heading) (true? heading))
  168. (number? old-heading))
  169. (let [block' (set-block-property-aux! block :heading heading)
  170. content (commands/clear-markdown-heading (:block/title block'))]
  171. (merge block' {:block/title content}))
  172. (and (or (nil? old-heading) (true? old-heading))
  173. (number? heading))
  174. (let [block' (set-block-property-aux! block :heading nil)
  175. properties (assoc (:block/properties block) :heading heading)
  176. content (commands/file-based-set-markdown-heading (:block/title block') heading)]
  177. (merge block' {:block/title content :block/properties properties}))
  178. ;; heading-num1 -> heading-num2
  179. :else
  180. (let [properties (assoc (:block/properties block) :heading heading)
  181. content (-> block
  182. :block/title
  183. commands/clear-markdown-heading
  184. (commands/file-based-set-markdown-heading heading))]
  185. {:block/uuid (:block/uuid block)
  186. :block/properties properties
  187. :block/title content}))
  188. (set-block-property-aux! block :heading heading))))
  189. (defn batch-set-heading! [block-ids heading]
  190. (ui-outliner-tx/transact!
  191. {:outliner-op :save-block}
  192. (doseq [block-id block-ids]
  193. (when-let [block (set-heading-aux! block-id heading)]
  194. (outliner-op/save-block! block {:retract-attributes? false})))))
  195. (defn set-blocks-id!
  196. "Persist block uuid to file if the uuid is valid, and it's not persisted in file.
  197. Accepts a list of uuids."
  198. [block-ids]
  199. (let [block-ids (remove nil? block-ids)
  200. col (map (fn [block-id]
  201. (when-let [block (db/entity [:block/uuid block-id])]
  202. (when-not (:block/pre-block? block)
  203. [block-id :id (str block-id)])))
  204. block-ids)
  205. col (remove nil? col)]
  206. (file-property-handler/batch-set-block-property-aux! col)))
  207. (defn valid-dsl-query-block?
  208. "Whether block has a valid dsl query."
  209. [block]
  210. (->> (:block/macros (db/entity (:db/id block)))
  211. (some (fn [macro]
  212. (let [properties (:block/properties macro)
  213. macro-name (:logseq.macro-name properties)
  214. macro-arguments (:logseq.macro-arguments properties)]
  215. (when-let [query-body (and (= "query" macro-name) (not-empty (string/join " " macro-arguments)))]
  216. (seq (:query
  217. (try
  218. (query-dsl/parse-query query-body)
  219. (catch :default _e
  220. nil))))))))))
  221. (defn valid-custom-query-block?
  222. "Whether block has a valid custom query."
  223. [block]
  224. (let [entity (db/entity (:db/id block))
  225. content (:block/title entity)]
  226. (when content
  227. (when (and (string/includes? content "#+BEGIN_QUERY")
  228. (string/includes? content "#+END_QUERY"))
  229. (let [ast (mldoc/->edn (string/trim content) (get entity :block/format :markdown))
  230. q (mldoc/extract-first-query-from-ast ast)]
  231. (some? (:query (common-util/safe-read-map-string q))))))))
  232. (defn update-timestamps-content!
  233. [{:block/keys [repeated? marker format] :as block} content]
  234. (if repeated?
  235. (let [scheduled-ast (block-handler/get-scheduled-ast block)
  236. deadline-ast (block-handler/get-deadline-ast block)
  237. content (some->> (filter repeated/repeated? [scheduled-ast deadline-ast])
  238. (map (fn [ts]
  239. [(repeated/timestamp->text ts)
  240. (repeated/next-timestamp-text ts)]))
  241. (reduce (fn [content [old new]]
  242. (string/replace content old new))
  243. content))
  244. content (string/replace-first
  245. content marker
  246. (case marker
  247. "DOING"
  248. "TODO"
  249. "NOW"
  250. "LATER"
  251. marker))
  252. content (clock/clock-out format content)
  253. content (drawer/insert-drawer
  254. format content "logbook"
  255. (util/format (str (if (= :org format) "-" "*")
  256. " State \"DONE\" from \"%s\" [%s]")
  257. marker
  258. (date/get-date-time-string-3)))]
  259. content)
  260. content))
  261. (defn file-based-save-assets!
  262. "Save incoming(pasted) assets to assets directory.
  263. Returns: [file-rpath file-obj file-fpath matched-alias]"
  264. ([repo files]
  265. (p/let [[repo-dir assets-dir] (assets-handler/ensure-assets-dir! repo)]
  266. (file-based-save-assets! repo repo-dir assets-dir files
  267. (fn [index file-stem]
  268. ;; TODO: maybe there're other chars we need to handle?
  269. (let [file-base (-> file-stem
  270. (string/replace " " "_")
  271. (string/replace "%" "_")
  272. (string/replace "/" "_"))
  273. file-name (str file-base "_" (.now js/Date) "_" index)]
  274. (string/replace file-name #"_+" "_"))))))
  275. ([repo repo-dir asset-dir-rpath files gen-filename]
  276. (p/all
  277. (for [[index ^js file] (map-indexed vector files)]
  278. ;; WARN file name maybe fully qualified path when paste file
  279. (let [file-name (util/node-path.basename (.-name file))
  280. [file-stem ext-full ext-base] (if file-name
  281. (let [ext-base (util/node-path.extname file-name)
  282. ext-full (if-not (config/extname-of-supported? ext-base)
  283. (util/full-path-extname file-name) ext-base)]
  284. [(subs file-name 0 (- (count file-name)
  285. (count ext-full))) ext-full ext-base])
  286. ["" "" ""])
  287. filename (str (gen-filename index file-stem) ext-full)
  288. file-rpath (str asset-dir-rpath "/" filename)
  289. matched-alias (assets-handler/get-matched-alias-by-ext ext-base)
  290. file-rpath (cond-> file-rpath
  291. (not (nil? matched-alias))
  292. (string/replace #"^[.\/\\]*assets[\/\\]+" ""))
  293. dir (or (:dir matched-alias) repo-dir)]
  294. (p/do! (js/console.debug "Debug: Writing Asset #" dir file-rpath)
  295. (p/let [content (.arrayBuffer file)
  296. file-fpath (path/path-join dir file-rpath)]
  297. ;; file based version support electron only
  298. (ipc/ipc "writeFile" repo file-fpath content))
  299. [file-rpath file (path/path-join dir file-rpath) matched-alias]))))))
  300. ;; assets/journals_2021_02_03_1612350230540_0.png
  301. (defn resolve-relative-path
  302. "Relative path to current file path.
  303. Requires editing state"
  304. [file-path]
  305. (if-let [current-file-rpath (or (file-model/get-block-file-path (state/get-edit-block))
  306. ;; fix dummy file path of page
  307. (when (config/get-pages-directory)
  308. (path/path-join (config/get-pages-directory) "_.md"))
  309. "pages/contents.md")]
  310. (let [repo-dir (config/get-repo-dir (state/get-current-repo))
  311. current-file-fpath (path/path-join repo-dir current-file-rpath)]
  312. (path/get-relative-path current-file-fpath file-path))
  313. file-path))
  314. (defn file-upload-assets!
  315. "Paste asset for file graph and insert link to current editing block"
  316. [repo id ^js files format uploading? *asset-uploading? *asset-uploading-process drop-or-paste?]
  317. (-> (file-based-save-assets! repo (js->clj files))
  318. ;; FIXME: only the first asset is handled
  319. (p/then
  320. (fn [res]
  321. (when-let [[asset-file-name file-obj asset-file-fpath matched-alias] (first res)]
  322. (let [image? (config/ext-of-image? asset-file-name)]
  323. (editor-common-handler/insert-command!
  324. id
  325. (assets-handler/get-asset-file-link format
  326. (if matched-alias
  327. (str
  328. (if image? "../assets/" "")
  329. "@" (:name matched-alias) "/" asset-file-name)
  330. (resolve-relative-path (or asset-file-fpath asset-file-name)))
  331. (if file-obj (.-name file-obj) (if image? "image" "asset"))
  332. image?)
  333. format
  334. {:last-pattern (if drop-or-paste? "" commands/command-trigger)
  335. :restore? true
  336. :command :insert-asset})
  337. (recur (rest res))))))
  338. (p/catch (fn [e]
  339. (js/console.error e)))
  340. (p/finally
  341. (fn []
  342. (reset! uploading? false)
  343. (reset! *asset-uploading? false)
  344. (reset! *asset-uploading-process 0)))))