paste.cljs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. (ns ^:no-doc frontend.handler.paste
  2. (:require [frontend.state :as state]
  3. [frontend.db :as db]
  4. [frontend.format.block :as block]
  5. [logseq.common.util :as common-util]
  6. [logseq.graph-parser.block :as gp-block]
  7. [logseq.common.util.block-ref :as block-ref]
  8. [clojure.string :as string]
  9. [frontend.util :as util]
  10. [frontend.handler.editor :as editor-handler]
  11. [frontend.extensions.html-parser :as html-parser]
  12. [goog.object :as gobj]
  13. [frontend.mobile.util :as mobile-util]
  14. [frontend.util.thingatpt :as thingatpt]
  15. ["/frontend/utils" :as utils]
  16. [frontend.commands :as commands]
  17. [frontend.util.text :as text-util]
  18. [frontend.format.mldoc :as mldoc]
  19. [lambdaisland.glogi :as log]
  20. [promesa.core :as p]))
  21. (defn- paste-text-parseable
  22. [format text]
  23. (when-let [editing-block (state/get-edit-block)]
  24. (let [page-id (:db/id (:block/page editing-block))
  25. blocks (block/extract-blocks
  26. (mldoc/->edn text format)
  27. text format
  28. {:page-name (:block/name (db/entity page-id))})
  29. blocks' (gp-block/with-parent-and-order page-id blocks)]
  30. (editor-handler/paste-blocks blocks' {:keep-uuid? true}))))
  31. (defn- paste-segmented-text
  32. [format text]
  33. (let [paragraphs (string/split text #"(?:\r?\n){2,}")
  34. updated-paragraphs
  35. (string/join "\n"
  36. (mapv (fn [p] (->> (string/trim p)
  37. ((fn [p]
  38. (if (util/safe-re-find (if (= format :org)
  39. #"\s*\*+\s+"
  40. #"\s*-\s+") p)
  41. p
  42. (str (if (= format :org) "* " "- ") p))))))
  43. paragraphs))]
  44. (paste-text-parseable format updated-paragraphs)))
  45. (defn- wrap-macro-url
  46. [url]
  47. (cond
  48. (boolean (text-util/get-matched-video url))
  49. (util/format "{{video %s}}" url)
  50. (or (string/includes? url "twitter.com") (string/includes? url "x.com"))
  51. (util/format "{{twitter %s}}" url)))
  52. (defn- try-parse-as-json
  53. "Result is not only to be an Object.
  54. Maybe JSON types like string, number, boolean, null, array"
  55. [text]
  56. (try (js/JSON.parse text)
  57. (catch :default _ #js{})))
  58. (defn- get-whiteboard-tldr-from-text
  59. [text]
  60. (when-let [matched-text (util/safe-re-find #"<whiteboard-tldr>(.*)</whiteboard-tldr>"
  61. (common-util/safe-decode-uri-component text))]
  62. (try-parse-as-json (second matched-text))))
  63. (defn- selection-within-link?
  64. [selection-and-format]
  65. (let [{:keys [format selection-start selection-end selection value]} selection-and-format]
  66. (and (not= selection-start selection-end)
  67. (->> (case format
  68. :markdown (util/re-pos #"\[.*?\]\(.*?\)" value)
  69. :org (util/re-pos #"\[\[.*?\]\[.*?\]\]" value))
  70. (some (fn [[start-index matched-text]]
  71. (and (<= start-index selection-start)
  72. (>= (+ start-index (count matched-text)) selection-end)
  73. (clojure.string/includes? matched-text selection))))
  74. some?))))
  75. ;; See https://developer.chrome.com/blog/web-custom-formats-for-the-async-clipboard-api/
  76. ;; for a similar example
  77. (defn get-copied-blocks []
  78. ;; NOTE: Avoid using navigator clipboard API on Android, it will report a permission error
  79. (p/let [clipboard-items (when (and (not (mobile-util/native-android?))
  80. js/window (gobj/get js/window "navigator") js/navigator.clipboard)
  81. (js/navigator.clipboard.read))
  82. blocks-blob ^js (when clipboard-items
  83. (let [types (.-types ^js (first clipboard-items))]
  84. (when (contains? (set types) "web application/logseq")
  85. (.getType ^js (first clipboard-items)
  86. "web application/logseq"))))
  87. blocks-str (when blocks-blob (.text blocks-blob))]
  88. (when blocks-str
  89. (common-util/safe-read-string blocks-str))))
  90. (defn- markdown-blocks?
  91. [text]
  92. (boolean (util/safe-re-find #"(?m)^\s*(?:[-+*]|#+)\s+" text)))
  93. (defn- org-blocks?
  94. [text]
  95. (boolean (util/safe-re-find #"(?m)^\s*\*+\s+" text)))
  96. (defn- get-revert-cut-txs
  97. "Get reverted previous cut tx when paste"
  98. [blocks]
  99. (let [{:keys [retracted-block-ids revert-tx]} (get-in @state/state [:editor/last-replace-ref-content-tx (state/get-current-repo)])
  100. recent-cut-block-ids (->> retracted-block-ids (map second) (set))]
  101. (state/set-state! [:editor/last-replace-ref-content-tx (state/get-current-repo)] nil)
  102. (when (= (set (map :block/uuid blocks)) recent-cut-block-ids)
  103. (seq revert-tx))))
  104. (defn- paste-copied-text
  105. [input *text html]
  106. (let [replace-text-f (fn [text]
  107. (let [input-id (state/get-edit-input-id)]
  108. (commands/delete-selection! input-id)
  109. (commands/simple-insert! input-id text nil)))
  110. text (string/replace *text "\r\n" "\n") ;; Fix for Windows platform
  111. input-id (state/get-edit-input-id)
  112. shape-refs-text (when (and (not (string/blank? html))
  113. (get-whiteboard-tldr-from-text html))
  114. ;; text should always be prepared block-ref generated in tldr
  115. text)
  116. {:keys [selection] :as selection-and-format} (editor-handler/get-selection-and-format)
  117. text-url? (common-util/url? text)
  118. selection-url? (common-util/url? selection)]
  119. (cond
  120. (not (string/blank? shape-refs-text))
  121. (commands/simple-insert! input-id shape-refs-text nil)
  122. ;; When a url is selected in a formatted link, replaces it with pasted text
  123. (or (and (or text-url? selection-url?)
  124. (selection-within-link? selection-and-format))
  125. (and text-url? selection-url?))
  126. (replace-text-f text)
  127. ;; Paste a formatted link over selected text or paste text over a selected formatted link
  128. (and (or text-url? selection-url?)
  129. (not (string/blank? (util/get-selected-text))))
  130. (editor-handler/html-link-format! text)
  131. ;; Pastes only block id when inside of '(())'
  132. (and (block-ref/block-ref? text)
  133. (editor-handler/wrapped-by? input block-ref/left-parens block-ref/right-parens))
  134. (commands/simple-insert! input-id (block-ref/get-block-ref-id text) nil)
  135. :else
  136. ;; from external
  137. (let [format (or (db/get-page-format (state/get-current-page)) :markdown)
  138. html-text (let [result (when-not (string/blank? html)
  139. (try
  140. (html-parser/convert format html)
  141. (catch :default e
  142. (log/error :exception e)
  143. nil)))]
  144. (if (string/blank? result) nil result))
  145. text-blocks? (if (= format :markdown) markdown-blocks? org-blocks?)
  146. text' (or html-text
  147. (when (common-util/url? text)
  148. (wrap-macro-url text))
  149. text)
  150. blocks? (text-blocks? text')]
  151. (cond
  152. blocks?
  153. (paste-text-parseable format text')
  154. (util/safe-re-find #"(?:\r?\n){2,}" text')
  155. (paste-segmented-text format text')
  156. :else
  157. (replace-text-f text'))))))
  158. (defn- paste-copied-blocks-or-text
  159. ;; todo: logseq/whiteboard-shapes is now text/html
  160. [input text e html]
  161. (util/stop e)
  162. (->
  163. (p/let [{:keys [graph blocks]} (get-copied-blocks)]
  164. (if (and (seq blocks) (= graph (state/get-current-repo)))
  165. ;; Handle internal paste
  166. (let [revert-cut-txs (get-revert-cut-txs blocks)
  167. keep-uuid? (= (state/get-block-op-type) :cut)]
  168. (editor-handler/paste-blocks blocks {:revert-cut-txs revert-cut-txs
  169. :keep-uuid? keep-uuid?}))
  170. (paste-copied-text input text html)))
  171. (p/catch (fn [error]
  172. (log/error :msg "Paste failed" :exception error)
  173. (state/pub-event! [:capture-error {:error error
  174. :payload {:type ::paste-copied-blocks-or-text}}])))))
  175. (defn paste-text-in-one-block-at-point
  176. []
  177. (utils/getClipText
  178. (fn [clipboard-data]
  179. (when-let [_ (state/get-input)]
  180. (if (common-util/url? clipboard-data)
  181. (if (string/blank? (util/get-selected-text))
  182. (editor-handler/insert (or (wrap-macro-url clipboard-data) clipboard-data) true)
  183. (editor-handler/html-link-format! clipboard-data))
  184. (editor-handler/insert clipboard-data true))))
  185. (fn [error]
  186. (js/console.error error))))
  187. (defn- paste-text-or-blocks-aux
  188. [input e text html]
  189. (if (or (thingatpt/markdown-src-at-point input)
  190. (thingatpt/org-admonition&src-at-point input))
  191. (when-not (mobile-util/native-ios?)
  192. (util/stop e)
  193. (paste-text-in-one-block-at-point))
  194. (paste-copied-blocks-or-text input text e html)))
  195. (defn- paste-file-if-exists [id e]
  196. (when id
  197. (let [clipboard-data (gobj/get e "clipboardData")
  198. files (.-files clipboard-data)]
  199. (when-let [file (first files)]
  200. (when-let [block (state/get-edit-block)]
  201. (editor-handler/upload-asset! id #js[file] (:block/format block)
  202. editor-handler/*asset-uploading? true)))
  203. (util/stop e))))
  204. (defn editor-on-paste!
  205. "Pastes with formatting and includes the following features:
  206. - handles internal pastes to correctly paste at the block level
  207. - formatted paste includes HTML if detected
  208. - special handling for newline and new blocks
  209. - pastes file if it exists
  210. - wraps certain urls with macros
  211. - wraps selected urls with link formatting
  212. - whiteboard friendly pasting
  213. - paste replaces selected text"
  214. [id]
  215. (fn [e]
  216. (state/set-state! :editor/on-paste? true)
  217. (let [clipboard-data (gobj/get e "clipboardData")
  218. html (.getData clipboard-data "text/html")
  219. text (.getData clipboard-data "text")
  220. has-files? (seq (.-files clipboard-data))]
  221. (cond
  222. (and (string/blank? text) (string/blank? html))
  223. ;; When both text and html are blank, paste file if exists.
  224. ;; NOTE: util/stop is not called here if no file is provided,
  225. ;; so the default paste behavior of the native platform will be used.
  226. (when has-files?
  227. (paste-file-if-exists id e))
  228. ;; both file attachment and text/html exist
  229. (and has-files? (state/preferred-pasting-file?))
  230. (paste-file-if-exists id e)
  231. :else
  232. (paste-text-or-blocks-aux (state/get-input) e text html)))))
  233. (defn editor-on-paste-raw!
  234. "Raw pastes without _any_ formatting. Can also replace selected text with a paste"
  235. []
  236. (state/set-state! :editor/on-paste? true)
  237. (utils/getClipText
  238. (fn [clipboard-data]
  239. (when (state/get-input)
  240. (commands/delete-selection! (state/get-edit-input-id))
  241. (editor-handler/insert clipboard-data true)))
  242. (fn [error]
  243. (js/console.error error))))