export.cljs 22 KB

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