export.cljs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. (ns frontend.handler.export
  2. (:require [cljs.pprint :as pprint]
  3. [clojure.set :as s]
  4. [clojure.string :as string]
  5. [clojure.walk :as walk]
  6. [datascript.core :as d]
  7. [frontend.config :as config]
  8. [frontend.db :as db]
  9. [frontend.extensions.zip :as zip]
  10. [frontend.external.roam-export :as roam-export]
  11. [frontend.format :as f]
  12. [frontend.format.protocol :as fp]
  13. [frontend.modules.file.core :as outliner-file]
  14. [frontend.modules.outliner.tree :as outliner-tree]
  15. [frontend.publishing.html :as html]
  16. [frontend.state :as state]
  17. [frontend.util :as util]
  18. [frontend.format.mldoc :as mldoc]
  19. [logseq.graph-parser.mldoc :as gp-mldoc]
  20. [logseq.graph-parser.util :as gp-util]
  21. [goog.dom :as gdom]
  22. [promesa.core :as p])
  23. (:import [goog.string StringBuffer]))
  24. (defn- get-page-content
  25. [repo page]
  26. (outliner-file/tree->file-content
  27. (outliner-tree/blocks->vec-tree
  28. (db/get-page-blocks-no-cache repo page) page) {:init-level 1}))
  29. (defn- get-file-content
  30. [repo file-path]
  31. (if-let [page-name
  32. (ffirst (d/q '[:find ?pn
  33. :in $ ?path
  34. :where
  35. [?p :block/file ?f]
  36. [?p :block/name ?pn]
  37. [?f :file/path ?path]]
  38. (db/get-db repo) file-path))]
  39. (get-page-content repo page-name)
  40. (ffirst
  41. (d/q '[:find ?content
  42. :in $ ?path
  43. :where
  44. [?f :file/path ?path]
  45. [?f :file/content ?content]]
  46. (db/get-db repo) file-path))))
  47. (defn- get-blocks-contents
  48. [repo root-block-uuid]
  49. (->
  50. (db/get-block-and-children repo root-block-uuid)
  51. (outliner-tree/blocks->vec-tree (str root-block-uuid))
  52. (outliner-file/tree->file-content {:init-level 1})))
  53. (defn- get-block-content
  54. [block]
  55. (->
  56. [block]
  57. (outliner-tree/blocks->vec-tree (str (:block/uuid block)))
  58. (outliner-file/tree->file-content {:init-level 1})))
  59. (defn download-file!
  60. [file-path]
  61. (when-let [repo (state/get-current-repo)]
  62. (when-let [content (get-file-content repo file-path)]
  63. (let [data (js/Blob. ["\ufeff" (array content)] ; prepend BOM
  64. (clj->js {:type "text/plain;charset=utf-8,"}))
  65. anchor (gdom/getElement "download")
  66. url (js/window.URL.createObjectURL data)]
  67. (.setAttribute anchor "href" url)
  68. (.setAttribute anchor "download" file-path)
  69. (.click anchor)))))
  70. (defn export-repo-as-html!
  71. [repo]
  72. (when-let [db (db/get-db repo)]
  73. (let [[db asset-filenames] (if (state/all-pages-public?)
  74. (db/clean-export! db)
  75. (db/filter-only-public-pages-and-blocks db))
  76. db-str (db/db->string db)
  77. state (select-keys @state/state
  78. [:ui/theme
  79. :ui/sidebar-collapsed-blocks
  80. :ui/show-recent?
  81. :config])
  82. state (update state :config (fn [config]
  83. {"local" (get config repo)}))
  84. raw-html-str (html/publishing-html db-str (pr-str state))
  85. html-str (str "data:text/html;charset=UTF-8,"
  86. (js/encodeURIComponent raw-html-str))]
  87. (if (util/electron?)
  88. (js/window.apis.exportPublishAssets
  89. raw-html-str
  90. (config/get-custom-css-path)
  91. (config/get-repo-dir repo)
  92. (clj->js asset-filenames)
  93. (util/mocked-open-dir-path))
  94. (when-let [anchor (gdom/getElement "download-as-html")]
  95. (.setAttribute anchor "href" html-str)
  96. (.setAttribute anchor "download" "index.html")
  97. (.click anchor))))))
  98. (defn- get-file-contents
  99. ([repo]
  100. (get-file-contents repo {:init-level 1}))
  101. ([repo file-opts]
  102. (let [db (db/get-db repo)]
  103. (->> (d/q '[:find ?n ?fp
  104. :where
  105. [?e :block/file ?f]
  106. [?f :file/path ?fp]
  107. [?e :block/name ?n]] db)
  108. (mapv (fn [[page-name file-path]]
  109. [file-path
  110. (outliner-file/tree->file-content
  111. (outliner-tree/blocks->vec-tree
  112. (db/get-page-blocks-no-cache page-name) page-name)
  113. file-opts)]))))))
  114. (defn export-repo-as-zip!
  115. [repo]
  116. (let [files (get-file-contents repo)
  117. [owner repo-name] (util/get-git-owner-and-repo repo)
  118. repo-name (str owner "-" repo-name)]
  119. (when (seq files)
  120. (p/let [zipfile (zip/make-zip repo-name files repo)]
  121. (when-let [anchor (gdom/getElement "download")]
  122. (.setAttribute anchor "href" (js/window.URL.createObjectURL zipfile))
  123. (.setAttribute anchor "download" (.-name zipfile))
  124. (.click anchor))))))
  125. (defn get-md-file-contents
  126. [repo]
  127. #_:clj-kondo/ignore
  128. (filter (fn [[path _]]
  129. (let [path (string/lower-case path)]
  130. (re-find #"\.(?:md|markdown)$" path)))
  131. (get-file-contents repo {:init-level 1
  132. :heading-to-list? true})))
  133. (defn- get-embed-pages-from-ast [ast]
  134. (let [result (transient #{})]
  135. (doseq [item ast]
  136. (walk/prewalk (fn [i]
  137. (cond
  138. (and (vector? i)
  139. (= "Macro" (first i))
  140. (= "embed" (some-> (:name (second i))
  141. (string/lower-case)))
  142. (some-> (:arguments (second i))
  143. (first)
  144. (string/starts-with? "[["))
  145. (some-> (:arguments (second i))
  146. (first)
  147. (string/ends-with? "]]")))
  148. (let [arguments (:arguments (second i))
  149. page-ref (first arguments)
  150. page-name (-> page-ref
  151. (subs 2)
  152. (#(subs % 0 (- (count %) 2)))
  153. (string/lower-case))]
  154. (conj! result page-name)
  155. i)
  156. :else
  157. i))
  158. item))
  159. (persistent! result)))
  160. (defn- get-embed-blocks-from-ast [ast]
  161. (let [result (transient #{})]
  162. (doseq [item ast]
  163. (walk/prewalk (fn [i]
  164. (cond
  165. (and (vector? i)
  166. (= "Macro" (first i))
  167. (= "embed" (some-> (:name (second i))
  168. (string/lower-case)))
  169. (some-> (:arguments (second i))
  170. (first)
  171. (string/starts-with? "(("))
  172. (some-> (:arguments (second i))
  173. (first)
  174. (string/ends-with? "))")))
  175. (let [arguments (:arguments (second i))
  176. block-ref (first arguments)
  177. block-uuid (-> block-ref
  178. (subs 2)
  179. (#(subs % 0 (- (count %) 2))))]
  180. (conj! result block-uuid)
  181. i)
  182. :else
  183. i)) item))
  184. (persistent! result)))
  185. (defn- get-block-refs-from-ast [ast]
  186. (let [result (transient #{})]
  187. (doseq [item ast]
  188. (walk/prewalk (fn [i]
  189. (cond
  190. (and (vector? i)
  191. (= "Block_ref" (first i))
  192. (some? (second i)))
  193. (let [block-uuid (second i)]
  194. (conj! result block-uuid)
  195. i)
  196. :else
  197. i)) item))
  198. (persistent! result)))
  199. (declare get-page-page&block-refs)
  200. (defn get-block-page&block-refs [repo block-uuid embed-pages embed-blocks block-refs]
  201. (let [block (db/entity [:block/uuid (uuid block-uuid)])
  202. block-content (get-blocks-contents repo (:block/uuid block))
  203. format (:block/format block)
  204. ast (mldoc/->edn block-content (gp-mldoc/default-config format))
  205. embed-pages-new (get-embed-pages-from-ast ast)
  206. embed-blocks-new (get-embed-blocks-from-ast ast)
  207. block-refs-new (get-block-refs-from-ast ast)
  208. embed-pages-diff (s/difference embed-pages-new embed-pages)
  209. embed-blocks-diff (s/difference embed-blocks-new embed-blocks)
  210. block-refs-diff (s/difference block-refs-new block-refs)
  211. embed-pages* (s/union embed-pages-new embed-pages)
  212. embed-blocks* (s/union embed-blocks-new embed-blocks)
  213. block-refs* (s/union block-refs-new block-refs)
  214. [embed-pages-1 embed-blocks-1 block-refs-1]
  215. (->>
  216. (mapv (fn [page-name]
  217. (let [{:keys [embed-pages embed-blocks block-refs]}
  218. (get-page-page&block-refs repo page-name embed-pages* embed-blocks* block-refs*)]
  219. [embed-pages embed-blocks block-refs])) embed-pages-diff)
  220. (apply mapv vector) ; [[1 2 3] [4 5 6] [7 8 9]] -> [[1 4 7] [2 5 8] [3 6 9]]
  221. (mapv #(apply s/union %)))
  222. [embed-pages-2 embed-blocks-2 block-refs-2]
  223. (->>
  224. (mapv (fn [block-uuid]
  225. (let [{:keys [embed-pages embed-blocks block-refs]}
  226. (get-block-page&block-refs repo block-uuid embed-pages* embed-blocks* block-refs*)]
  227. [embed-pages embed-blocks block-refs])) (s/union embed-blocks-diff block-refs-diff))
  228. (apply mapv vector)
  229. (mapv #(apply s/union %)))]
  230. {:embed-pages (s/union embed-pages-1 embed-pages-2 embed-pages*)
  231. :embed-blocks (s/union embed-blocks-1 embed-blocks-2 embed-blocks*)
  232. :block-refs (s/union block-refs-1 block-refs-2 block-refs*)}))
  233. (defn get-blocks-page&block-refs [repo block-uuids embed-pages embed-blocks block-refs]
  234. (let [[embed-pages embed-blocks block-refs]
  235. (reduce (fn [[embed-pages embed-blocks block-refs] block-uuid]
  236. (let [result (get-block-page&block-refs repo block-uuid embed-pages embed-blocks block-refs)]
  237. [(:embed-pages result) (:embed-blocks result) (:block-refs result)]))
  238. [embed-pages embed-blocks block-refs] block-uuids)]
  239. {:embed-pages embed-pages
  240. :embed-blocks embed-blocks
  241. :block-refs block-refs}))
  242. (defn get-page-page&block-refs [repo page-name embed-pages embed-blocks block-refs]
  243. (let [page-name* (util/page-name-sanity-lc page-name)
  244. page-content (get-page-content repo page-name*)
  245. format (:block/format (db/entity [:block/name page-name*]))
  246. ast (mldoc/->edn page-content (gp-mldoc/default-config format))
  247. embed-pages-new (get-embed-pages-from-ast ast)
  248. embed-blocks-new (get-embed-blocks-from-ast ast)
  249. block-refs-new (get-block-refs-from-ast ast)
  250. embed-pages-diff (s/difference embed-pages-new embed-pages)
  251. embed-blocks-diff (s/difference embed-blocks-new embed-blocks)
  252. block-refs-diff (s/difference block-refs-new block-refs)
  253. embed-pages* (s/union embed-pages-new embed-pages)
  254. embed-blocks* (s/union embed-blocks-new embed-blocks)
  255. block-refs* (s/union block-refs-new block-refs)
  256. [embed-pages-1 embed-blocks-1 block-refs-1]
  257. (->>
  258. (mapv (fn [page-name]
  259. (let [{:keys [embed-pages embed-blocks block-refs]}
  260. (get-page-page&block-refs repo page-name embed-pages* embed-blocks* block-refs*)]
  261. [embed-pages embed-blocks block-refs])) embed-pages-diff)
  262. (apply mapv vector)
  263. (mapv #(apply s/union %)))
  264. [embed-pages-2 embed-blocks-2 block-refs-2]
  265. (->>
  266. (mapv (fn [block-uuid]
  267. (let [{:keys [embed-pages embed-blocks block-refs]}
  268. (get-block-page&block-refs repo block-uuid embed-pages* embed-blocks* block-refs*)]
  269. [embed-pages embed-blocks block-refs])) (s/union embed-blocks-diff block-refs-diff))
  270. (apply mapv vector)
  271. (mapv #(apply s/union %)))]
  272. {:embed-pages (s/union embed-pages-1 embed-pages-2 embed-pages*)
  273. :embed-blocks (s/union embed-blocks-1 embed-blocks-2 embed-blocks*)
  274. :block-refs (s/union block-refs-1 block-refs-2 block-refs*)}))
  275. (defn- get-export-references [repo {:keys [embed-pages embed-blocks block-refs]}]
  276. (let [embed-blocks-and-contents
  277. (mapv (fn [id]
  278. (let [id-s (str id)
  279. id (uuid id-s)]
  280. [id-s
  281. [(get-blocks-contents repo id)
  282. (get-block-content (db/pull [:block/uuid id]))]]))
  283. (s/union embed-blocks block-refs))
  284. embed-pages-and-contents
  285. (mapv (fn [page-name] [page-name (get-page-content repo page-name)]) embed-pages)]
  286. {:embed_blocks embed-blocks-and-contents
  287. :embed_pages embed-pages-and-contents}))
  288. (defn- export-files-as-markdown [repo files heading-to-list?]
  289. (->> files
  290. (mapv (fn [{:keys [path content names format]}]
  291. (when (first names)
  292. [path (fp/exportMarkdown f/mldoc-record content
  293. (f/get-default-config format {:export-heading-to-list? heading-to-list?})
  294. (js/JSON.stringify
  295. (clj->js (get-export-references
  296. repo
  297. (get-page-page&block-refs repo (first names) #{} #{} #{})))))])))))
  298. (defn- export-files-as-opml [repo files]
  299. (->> files
  300. (mapv (fn [{:keys [path content names format]}]
  301. (when (first names)
  302. (let [path
  303. (string/replace
  304. (string/lower-case path) #"(.+)\.(md|markdown|org)" "$1.opml")]
  305. [path (fp/exportOPML f/mldoc-record content
  306. (f/get-default-config format)
  307. (first names)
  308. (js/JSON.stringify
  309. (clj->js (get-export-references
  310. repo
  311. (get-page-page&block-refs repo (first names) #{} #{} #{})))))]))))))
  312. (defn export-blocks-as-aux
  313. [repo root-block-uuids auxf]
  314. {:pre [(> (count root-block-uuids) 0)]}
  315. (let [f #(get-export-references repo (get-blocks-page&block-refs repo % #{} #{} #{}))
  316. root-blocks (mapv #(db/entity [:block/uuid %]) root-block-uuids)
  317. blocks (mapcat #(db/get-block-and-children repo %) root-block-uuids)
  318. refs (f (mapv #(str (:block/uuid %)) blocks))
  319. contents (mapv #(get-blocks-contents repo %) root-block-uuids)
  320. content (string/join "\n" (mapv string/trim-newline contents))
  321. format (or (:block/format (first root-blocks)) (state/get-preferred-format))]
  322. (auxf content format refs)))
  323. (defn export-blocks-as-opml
  324. [repo root-block-uuids]
  325. (export-blocks-as-aux repo root-block-uuids
  326. #(fp/exportOPML f/mldoc-record %1
  327. (f/get-default-config %2)
  328. "untitled"
  329. (js/JSON.stringify (clj->js %3)))))
  330. (defn export-blocks-as-markdown
  331. [repo root-block-uuids indent-style remove-options]
  332. (export-blocks-as-aux repo root-block-uuids
  333. #(fp/exportMarkdown f/mldoc-record %1
  334. (f/get-default-config
  335. %2
  336. {:export-md-indent-style indent-style
  337. :export-md-remove-options remove-options})
  338. (js/JSON.stringify (clj->js %3)))))
  339. (defn export-blocks-as-html
  340. [repo root-block-uuids]
  341. (export-blocks-as-aux repo root-block-uuids
  342. #(fp/toHtml f/mldoc-record %1
  343. (f/get-default-config %2)
  344. (js/JSON.stringify (clj->js %3)))))
  345. (defn- get-file-contents-with-suffix
  346. [repo]
  347. (let [db (db/get-db repo)
  348. md-files (get-md-file-contents repo)]
  349. (->>
  350. md-files
  351. (map (fn [[path content]] {:path path :content content
  352. :names (d/q '[:find [?n ?n2]
  353. :in $ ?p
  354. :where [?e :file/path ?p]
  355. [?e2 :block/file ?e]
  356. [?e2 :block/name ?n]
  357. [?e2 :block/original-name ?n2]] db path)
  358. :format (gp-util/get-format path)})))))
  359. (defn export-repo-as-markdown!
  360. [repo]
  361. (when-let [files (get-file-contents-with-suffix repo)]
  362. (let [heading-to-list? (state/export-heading-to-list?)
  363. files
  364. (export-files-as-markdown repo files heading-to-list?)
  365. zip-file-name (str repo "_markdown_" (quot (util/time-ms) 1000))]
  366. (p/let [zipfile (zip/make-zip zip-file-name files repo)]
  367. (when-let [anchor (gdom/getElement "export-as-markdown")]
  368. (.setAttribute anchor "href" (js/window.URL.createObjectURL zipfile))
  369. (.setAttribute anchor "download" (.-name zipfile))
  370. (.click anchor))))))
  371. (defn export-repo-as-opml!
  372. #_:clj-kondo/ignore
  373. [repo]
  374. (when-let [repo (state/get-current-repo)]
  375. (when-let [files (get-file-contents-with-suffix repo)]
  376. (let [files (export-files-as-opml repo files)
  377. zip-file-name (str repo "_opml_" (quot (util/time-ms) 1000))]
  378. (p/let [zipfile (zip/make-zip zip-file-name files repo)]
  379. (when-let [anchor (gdom/getElement "export-as-opml")]
  380. (.setAttribute anchor "href" (js/window.URL.createObjectURL zipfile))
  381. (.setAttribute anchor "download" (.-name zipfile))
  382. (.click anchor)))))))
  383. (defn- dissoc-properties [m ks]
  384. (if (:block/properties m)
  385. (update m :block/properties
  386. (fn [v]
  387. (apply dissoc v ks)))
  388. m))
  389. (defn- nested-select-keys
  390. [keyseq vec-tree]
  391. (walk/postwalk
  392. (fn [x]
  393. (cond
  394. (and (map? x) (contains? x :block/uuid))
  395. (-> x
  396. (s/rename-keys {:block/uuid :block/id
  397. :block/original-name :block/page-name})
  398. (dissoc-properties [:id])
  399. (select-keys keyseq))
  400. :else
  401. x))
  402. vec-tree))
  403. (defn- blocks [db]
  404. {:version 1
  405. :blocks
  406. (->> (d/q '[:find (pull ?b [*])
  407. :in $
  408. :where
  409. [?b :block/file]
  410. [?b :block/original-name]
  411. [?b :block/name]] db)
  412. (map (fn [[{:block/keys [name] :as page}]]
  413. (assoc page
  414. :block/children
  415. (outliner-tree/blocks->vec-tree
  416. (db/get-page-blocks-no-cache
  417. (state/get-current-repo)
  418. name
  419. {:transform? false}) name))))
  420. (nested-select-keys
  421. [:block/id
  422. :block/page-name
  423. :block/properties
  424. :block/heading-level
  425. :block/format
  426. :block/children
  427. :block/content]))})
  428. (defn- file-name [repo extension]
  429. (-> (string/replace repo config/local-db-prefix "")
  430. (string/replace #"^/+" "")
  431. (str "_" (quot (util/time-ms) 1000))
  432. (str "." (string/lower-case (name extension)))))
  433. (defn- export-repo-as-edn-str [repo]
  434. (when-let [db (db/get-db repo)]
  435. (let [sb (StringBuffer.)]
  436. (pprint/pprint (blocks db) (StringBufferWriter. sb))
  437. (str sb))))
  438. (defn export-repo-as-edn-v2!
  439. [repo]
  440. (when-let [data-str (some->> (export-repo-as-edn-str repo)
  441. js/encodeURIComponent
  442. (str "data:text/edn;charset=utf-8,"))]
  443. (when-let [anchor (gdom/getElement "download-as-edn-v2")]
  444. (.setAttribute anchor "href" data-str)
  445. (.setAttribute anchor "download" (file-name repo :edn))
  446. (.click anchor))))
  447. (defn- nested-update-id
  448. [vec-tree]
  449. (walk/postwalk
  450. (fn [x]
  451. (if (and (map? x) (contains? x :block/id))
  452. (update x :block/id str)
  453. x))
  454. vec-tree))
  455. (defn export-repo-as-json-v2!
  456. [repo]
  457. (when-let [db (db/get-db repo)]
  458. (let [json-str
  459. (-> (blocks db)
  460. nested-update-id
  461. clj->js
  462. js/JSON.stringify)
  463. data-str (str "data:text/json;charset=utf-8,"
  464. (js/encodeURIComponent json-str))]
  465. (when-let [anchor (gdom/getElement "download-as-json-v2")]
  466. (.setAttribute anchor "href" data-str)
  467. (.setAttribute anchor "download" (file-name repo :json))
  468. (.click anchor)))))
  469. ;;;;;;;;;;;;;;;;;;;;;;;;;
  470. ;; Export to roam json ;;
  471. ;;;;;;;;;;;;;;;;;;;;;;;;;
  472. ;; https://roamresearch.com/#/app/help/page/Nxz8u0vXU
  473. ;; export to roam json according to above spec
  474. (defn- roam-json [db]
  475. (->> (d/q '[:find (pull ?b [*])
  476. :in $
  477. :where
  478. [?b :block/file]
  479. [?b :block/original-name]
  480. [?b :block/name]] db)
  481. (map (fn [[{:block/keys [name] :as page}]]
  482. (assoc page
  483. :block/children
  484. (outliner-tree/blocks->vec-tree
  485. (db/get-page-blocks-no-cache
  486. (state/get-current-repo)
  487. name
  488. {:transform? false}) name))))
  489. (roam-export/traverse
  490. [:page/title
  491. :block/string
  492. :block/uid
  493. :block/children])))
  494. (defn export-repo-as-roam-json!
  495. [repo]
  496. (when-let [db (db/get-db repo)]
  497. (let [json-str
  498. (-> (roam-json db)
  499. clj->js
  500. js/JSON.stringify)
  501. data-str (str "data:text/json;charset=utf-8,"
  502. (js/encodeURIComponent json-str))]
  503. (when-let [anchor (gdom/getElement "download-as-roam-json")]
  504. (.setAttribute anchor "href" data-str)
  505. (.setAttribute anchor "download" (file-name (str repo "_roam") :json))
  506. (.click anchor)))))