paste.cljs 11 KB

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