export.cljs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. (ns frontend.handler.export
  2. (:require [frontend.state :as state]
  3. [frontend.db :as db]
  4. [frontend.format.protocol :as fp]
  5. [frontend.format :as f]
  6. [frontend.config :as config]
  7. [datascript.core :as d]
  8. [frontend.util :as util]
  9. [cljs-bean.core :as bean]
  10. [clojure.string :as string]
  11. [goog.dom :as gdom]
  12. [frontend.publishing.html :as html]
  13. [frontend.text :as text]
  14. [frontend.handler.common :as common-handler]
  15. [frontend.extensions.zip :as zip]
  16. [frontend.modules.file.core :as outliner-file]
  17. [frontend.modules.outliner.tree :as outliner-tree]
  18. [promesa.core :as p]))
  19. (defn- get-page-content
  20. [page]
  21. (outliner-file/tree->file-content
  22. (outliner-tree/blocks->vec-tree
  23. (db/get-page-blocks-no-cache page) page) {:init-level 1}))
  24. (defn- get-page-content-debug
  25. [page]
  26. (outliner-file/tree->file-content
  27. (outliner-tree/blocks->vec-tree
  28. (db/get-page-blocks-no-cache page) page) {:init-level 1
  29. :heading-to-list? true}))
  30. (defn- get-file-content
  31. [file-path]
  32. (let [page-name
  33. (ffirst (d/q '[:find ?pn
  34. :where
  35. [?e :file/path file-path]
  36. [?p :block/file ?e]
  37. [?p :block/name ?pn]] (db/get-conn)))]
  38. (get-page-content page-name)))
  39. (defn- get-blocks-contents
  40. [repo root-block-uuid]
  41. (->
  42. (db/get-block-and-children repo root-block-uuid)
  43. (outliner-tree/blocks->vec-tree (str root-block-uuid))
  44. (outliner-file/tree->file-content {:init-level 1})))
  45. (defn copy-block!
  46. [block-id]
  47. (when-let [block (db/pull [:block/uuid block-id])]
  48. (let [content (:block/content block)]
  49. (common-handler/copy-to-clipboard-without-id-property! (:block/format block) content))))
  50. (defn copy-block-as-json!
  51. [block-id]
  52. (when-let [repo (state/get-current-repo)]
  53. (let [block-children (db/get-block-and-children repo block-id)]
  54. (util/copy-to-clipboard! (js/JSON.stringify (bean/->js block-children))))))
  55. (defn copy-page-as-json!
  56. [page-name]
  57. (when-let [repo (state/get-current-repo)]
  58. (let [properties (db/get-page-properties page-name)
  59. blocks (db/get-page-blocks repo page-name)]
  60. (util/copy-to-clipboard!
  61. (js/JSON.stringify
  62. (bean/->js
  63. {:properties properties
  64. :blocks blocks}))))))
  65. (defn export-repo-as-json!
  66. [repo]
  67. (when-let [db (db/get-conn repo)]
  68. (let [db-json (db/db->json db)
  69. data-str (str "data:text/json;charset=utf-8," (js/encodeURIComponent db-json))]
  70. (when-let [anchor (gdom/getElement "download-as-json")]
  71. (.setAttribute anchor "href" data-str)
  72. (.setAttribute anchor "download" (str (last (string/split repo #"/")) ".json"))
  73. (.click anchor)))))
  74. (defn export-repo-as-edn!
  75. [repo]
  76. (when-let [db (db/get-conn repo)]
  77. (let [db-edn (db/db->edn-str db)
  78. data-str (str "data:text/edn;charset=utf-8," (js/encodeURIComponent db-edn))]
  79. (when-let [anchor (gdom/getElement "download-as-edn")]
  80. (.setAttribute anchor "href" data-str)
  81. (.setAttribute anchor "download" (str (last (string/split repo #"/")) ".edn"))
  82. (.click anchor)))))
  83. (defn download-file!
  84. [file-path]
  85. (when-let [content (get-file-content file-path)]
  86. (let [data (js/Blob. ["\ufeff" (array content)] ; prepend BOM
  87. (clj->js {:type "text/plain;charset=utf-8,"}))]
  88. (let [anchor (gdom/getElement "download")
  89. url (js/window.URL.createObjectURL data)]
  90. (.setAttribute anchor "href" url)
  91. (.setAttribute anchor "download" file-path)
  92. (.click anchor)))))
  93. (defn export-repo-as-html!
  94. [repo]
  95. (when-let [db (db/get-conn repo)]
  96. (let [[db asset-filenames] (if (state/all-pages-public?)
  97. (db/clean-export! db)
  98. (db/filter-only-public-pages-and-blocks db))
  99. db-str (db/db->string db)
  100. state (select-keys @state/state
  101. [:ui/theme :ui/cycle-collapse
  102. :ui/sidebar-collapsed-blocks
  103. :ui/show-recent?
  104. :config])
  105. state (update state :config (fn [config]
  106. {"local" (get config repo)}))
  107. raw-html-str (html/publishing-html db-str (pr-str state))
  108. html-str (str "data:text/html;charset=UTF-8,"
  109. (js/encodeURIComponent raw-html-str))]
  110. (if (util/electron?)
  111. (js/window.apis.exportPublishAssets raw-html-str (config/get-custom-css-path) (config/get-repo-dir repo) (clj->js asset-filenames))
  112. (when-let [anchor (gdom/getElement "download-as-html")]
  113. (.setAttribute anchor "href" html-str)
  114. (.setAttribute anchor "download" "index.html")
  115. (.click anchor))))))
  116. (defn- get-file-contents
  117. ([repo]
  118. (get-file-contents repo {:init-level 1}))
  119. ([repo file-opts]
  120. (let [conn (db/get-conn repo)]
  121. (->> (d/q '[:find ?n ?fp
  122. :where
  123. [?e :block/file ?f]
  124. [?f :file/path ?fp]
  125. [?e :block/name ?n]] conn)
  126. (mapv (fn [[page-name file-path]]
  127. [file-path
  128. (outliner-file/tree->file-content
  129. (outliner-tree/blocks->vec-tree
  130. (db/get-page-blocks-no-cache page-name) page-name)
  131. file-opts)]))))))
  132. (defn export-repo-as-zip!
  133. [repo]
  134. (let [files (get-file-contents repo)
  135. [owner repo-name] (util/get-git-owner-and-repo repo)
  136. repo-name (str owner "-" repo-name)]
  137. (when (seq files)
  138. (p/let [zipfile (zip/make-zip repo-name files repo)]
  139. (when-let [anchor (gdom/getElement "download-as-zip")]
  140. (.setAttribute anchor "href" (js/window.URL.createObjectURL zipfile))
  141. (.setAttribute anchor "download" (.-name zipfile))
  142. (.click anchor))))))
  143. (defn- get-md-file-contents
  144. [repo]
  145. (let [conn (db/get-conn repo)]
  146. (filter (fn [[path _]]
  147. (let [path (string/lower-case path)]
  148. (or (string/ends-with? path ".md")
  149. (string/ends-with? path ".markdown"))))
  150. (get-file-contents repo {:init-level 1
  151. :heading-to-list? true}))))
  152. (defn- get-embed-and-refs-blocks-pages-aux []
  153. (let [mem (atom {})]
  154. (letfn [(f [repo page-or-block is-block? exclude-blocks exclude-pages]
  155. (let [v (get @mem [repo page-or-block])]
  156. (if v v
  157. (let [[ref-blocks ref-pages]
  158. (->> (if is-block?
  159. [page-or-block]
  160. (db/get-page-blocks-no-cache
  161. repo page-or-block {:pull-keys '[:block/refs]}))
  162. (filterv :block/refs)
  163. (mapcat :block/refs)
  164. (group-by #(boolean (:block/page (db/entity (:db/id %)))))
  165. ((fn [g] [(get g true []) (get g false [])]))
  166. (mapv #(vec (distinct (flatten (remove nil? %))))))
  167. ref-block-ids
  168. (->> ref-blocks
  169. (#(remove (fn [b] (contains? exclude-blocks (:db/id b))) %))
  170. (mapv #(:db/id %)))
  171. ref-page-ids
  172. (->> ref-pages
  173. (#(remove (fn [b] (contains? exclude-pages (:db/id b))) %))
  174. (mapv #(:db/id %)))
  175. ref-blocks
  176. (->> ref-block-ids
  177. (db/pull-many repo '[*])
  178. (flatten))
  179. ref-pages
  180. (->> ref-page-ids
  181. (db/pull-many repo '[*])
  182. (filterv :block/name)
  183. (flatten))
  184. [next-ref-blocks1 next-ref-pages1]
  185. (->> ref-blocks
  186. (mapv #(f repo % true (set (concat ref-block-ids exclude-blocks)) exclude-pages))
  187. (apply mapv vector))
  188. [next-ref-blocks2 next-ref-pages2]
  189. (->> ref-pages
  190. (mapv #(f repo (:block/name %) false exclude-blocks (set (concat ref-page-ids exclude-pages))))
  191. (apply mapv vector))
  192. result
  193. [(->> (concat ref-block-ids next-ref-blocks1 next-ref-blocks2)
  194. (flatten)
  195. (distinct))
  196. (->> (concat ref-page-ids next-ref-pages1 next-ref-pages2)
  197. (flatten)
  198. (distinct))]]
  199. (when (and (empty? exclude-blocks) (empty? exclude-pages))
  200. (swap! mem assoc [repo page-or-block] result))
  201. result))))]
  202. f)))
  203. (defn- get-page&block-refs-by-query
  204. [repo page get-page&block-refs-by-query-aux]
  205. (let [[block-ids page-ids]
  206. (get-page&block-refs-by-query-aux repo page false #{} #{})
  207. blocks
  208. (db/pull-many repo '[*] block-ids)
  209. pages-name-and-content
  210. (->> page-ids
  211. (d/q '[:find ?n ?n2 (pull ?p [:file/path])
  212. :in $ [?e ...]
  213. :where
  214. [?e :block/file ?p]
  215. [?e :block/name ?n]
  216. [?e :block/original-name ?n2]] (db/get-conn repo))
  217. (mapv (fn [[name origin-name file-path]]
  218. (if (= name origin-name)
  219. [[name file-path]]
  220. [[name file-path] [origin-name file-path]])))
  221. (apply concat)
  222. (mapv (fn [[page-name file-path]]
  223. [page-name (get-page-content page-name)])))
  224. embed-blocks
  225. (mapv (fn [b] [(str (:block/uuid b))
  226. [(get-blocks-contents repo (:block/uuid b))
  227. (:block/content b)]])
  228. blocks)]
  229. {:embed_blocks embed-blocks
  230. :embed_pages pages-name-and-content}))
  231. (defn- page&block-refs
  232. [repo]
  233. (let [block-refs
  234. (->>
  235. (d/q '[:find ?pn ?pon ?bt ?bc ?bid ?e ?rb
  236. :where
  237. [?e :block/refs ?rb]
  238. [?e :block/page ?p]
  239. [?p :block/name ?pn]
  240. [?p :block/original-name ?pon]
  241. [?rb :block/title ?bt]
  242. [?rb :block/content ?bc]
  243. [?rb :block/uuid ?bid]] (db/get-conn repo)))
  244. page-block-refs
  245. (->> block-refs
  246. (mapv (fn [[pn pon bt bc bid _ rb]]
  247. (if (= pn pon)
  248. [[pn bt bc bid rb]]
  249. [[pn bt bc bid rb] [pon bt bc bid rb]])))
  250. (apply concat)
  251. (reduce (fn [r [k & v]] (assoc r k (cons v (get r k)))) {})
  252. (mapv (fn [[k v]] [k (distinct v)]))
  253. (into {}))
  254. block-block-refs
  255. (->> block-refs
  256. (mapv (fn [[_ _ bt bc bid e rb]] [e bt bc bid rb]))
  257. (reduce (fn [r [k & v]] (assoc r k (cons v (get r k)))) {})
  258. (mapv (fn [[k v]] [k (distinct v)]))
  259. (into {}))
  260. page-refs
  261. (->> (d/q '[:find ?pn ?pon ?rpn ?rpon ?fp ?e
  262. :where
  263. [?e :block/refs ?rp]
  264. [?e :block/page ?p]
  265. [?p :block/name ?pn]
  266. [?p :block/original-name ?pon]
  267. [?rp :block/name ?rpn]
  268. [?rp :block/original-name ?rpon]
  269. [?rp :block/file ?pf]
  270. [?pf :file/path ?fp]] (db/get-conn repo))
  271. (d/q '[:find ?pn ?pon ?rpn ?rpon ?fc ?be
  272. :in $ [[?pn ?pon ?rpn ?rpon ?fp ?be] ...]
  273. :where
  274. [?e :file/path ?fp]
  275. [?e :file/content ?fc]] (db/get-conn repo)))
  276. page-page-refs
  277. (->>
  278. page-refs
  279. (mapv (fn [[pn pon rpn rpon fc _]]
  280. (case [(= pn pon) (= rpn rpon)]
  281. [true true] [[pn rpn fc]]
  282. [true false] [[pn rpn fc] [pn rpon fc]]
  283. [false true] [[pn rpn fc] [pon rpn fc]]
  284. [false false] [[pn rpn fc] [pn rpon fc] [pon rpn fc] [pon rpon fc]])))
  285. (apply concat)
  286. (reduce (fn [r [k & v]] (assoc r k (cons v (get r k)))) {})
  287. (mapv (fn [[k v]] [k (distinct v)]))
  288. (into {}))
  289. block-page-refs
  290. (->>
  291. page-refs
  292. (mapv (fn [[pn pon rpn rpon fc e]]
  293. (if (= rpn rpon) [[e rpn fc]] [[e rpn fc] [e rpon fc]])))
  294. (apply concat)
  295. (reduce (fn [r [k & v]] (assoc r k (cons v (get r k)))) {})
  296. (mapv (fn [[k v]] [k (distinct v)]))
  297. (into {}))]
  298. [page-block-refs page-page-refs block-block-refs block-page-refs]))
  299. (defn- get-page&block-refs-aux
  300. [repo page-or-block-id is-block-id? page&block-refs exclude-blocks exclude-pages]
  301. (let [[page-block-refs page-page-refs block-block-refs block-page-refs] page&block-refs]
  302. (if is-block-id?
  303. (when (not (contains? exclude-blocks page-or-block-id))
  304. (let [block-refs (get block-block-refs page-or-block-id)
  305. block-ref-ids (->>
  306. (mapv (fn [[_ _ _ rb]] rb) block-refs)
  307. (remove #(contains? exclude-blocks %)))
  308. page-refs (get block-page-refs page-or-block-id)
  309. page-ref-names (->>
  310. (mapv (fn [[rpn _]] rpn) page-refs)
  311. (remove #(contains? exclude-pages %)))
  312. [other-block-refs1 other-page-refs1]
  313. (->>
  314. (mapv
  315. #(get-page&block-refs-aux repo % true
  316. page&block-refs
  317. (conj exclude-blocks %)
  318. exclude-pages)
  319. block-ref-ids)
  320. (apply mapv vector))
  321. [other-block-refs2 other-page-refs2]
  322. (->>
  323. (mapv
  324. #(get-page&block-refs-aux repo % false
  325. page&block-refs
  326. exclude-blocks
  327. (conj exclude-pages %))
  328. page-ref-names)
  329. (apply mapv vector))
  330. block-refs* (apply concat (concat other-block-refs1 other-block-refs2 [block-refs]))
  331. page-refs* (apply concat (concat other-page-refs1 other-page-refs2 [page-refs]))]
  332. [block-refs* page-refs*]))
  333. (when (not (contains? exclude-pages page-or-block-id))
  334. (let [block-refs (get page-block-refs page-or-block-id)
  335. block-ref-ids (->>
  336. (mapv (fn [[_ _ _ rb]] rb) block-refs)
  337. (remove #(contains? exclude-blocks %)))
  338. page-refs (get page-page-refs page-or-block-id)
  339. page-ref-names (->>
  340. (mapv (fn [[rpn _]] rpn) page-refs)
  341. (remove #(contains? exclude-pages %)))
  342. [other-block-refs1 other-page-refs1]
  343. (->>
  344. (mapv
  345. #(get-page&block-refs-aux repo % true
  346. page&block-refs
  347. (conj exclude-blocks %)
  348. exclude-pages)
  349. block-ref-ids)
  350. (apply mapv vector))
  351. [other-block-refs2 other-page-refs2]
  352. (->>
  353. (mapv
  354. #(get-page&block-refs-aux repo % false
  355. page&block-refs
  356. exclude-blocks
  357. (conj exclude-pages %))
  358. page-ref-names)
  359. (apply mapv vector))
  360. block-refs* (apply concat (concat other-block-refs1 other-block-refs2 [block-refs]))
  361. page-refs* (apply concat (concat other-page-refs1 other-page-refs2 [page-refs]))]
  362. [block-refs* page-refs*])))))
  363. (defn- get-page&block-refs
  364. [repo page page&block-refs]
  365. (let [[block-refs page-refs]
  366. (get-page&block-refs-aux repo page false page&block-refs #{} #{})]
  367. {:embed_blocks
  368. (mapv (fn [[_title content uuid id]]
  369. [(str uuid)
  370. [(get-blocks-contents repo uuid)
  371. content]])
  372. block-refs)
  373. :embed_pages (vec page-refs)}))
  374. (defn- export-files-as-markdown
  375. [repo files heading-to-list?]
  376. (let [get-page&block-refs-by-query-aux (get-embed-and-refs-blocks-pages-aux)
  377. f (if (< (count files) 30) ;query db for per page if (< (count files) 30), or pre-compute whole graph's page&block-refs
  378. #(get-page&block-refs-by-query repo % get-page&block-refs-by-query-aux)
  379. (let [page&block-refs (page&block-refs repo)]
  380. #(get-page&block-refs repo % page&block-refs)))]
  381. (->> files
  382. (mapv (fn [{:keys [path content names format]}]
  383. (when (first names)
  384. [path (fp/exportMarkdown f/mldoc-record content
  385. (f/get-default-config format heading-to-list?)
  386. (js/JSON.stringify
  387. (clj->js (f (first names)))))])))
  388. (remove nil?))))
  389. (defn- convert-md-files-unordered-list-or-heading
  390. [repo files heading-to-list?]
  391. (->> files
  392. (mapv (fn [{:keys [path content names format]}]
  393. (when (first names)
  394. [path (fp/exportMarkdown f/mldoc-record content
  395. (f/get-default-config format heading-to-list? true)
  396. nil)])))
  397. (remove nil?)))
  398. (defn- get-file-contents-with-suffix
  399. [repo]
  400. (let [conn (db/get-conn repo)
  401. md-files (get-md-file-contents repo)]
  402. (->>
  403. md-files
  404. (map (fn [[path content]] {:path path :content content
  405. :names (d/q '[:find [?n ?n2]
  406. :in $ ?p
  407. :where [?e :file/path ?p]
  408. [?e2 :block/file ?e]
  409. [?e2 :block/name ?n]
  410. [?e2 :block/original-name ?n2]] conn path)
  411. :format (f/get-format path)})))))
  412. (defn export-repo-as-markdown!
  413. [repo]
  414. (when-let [repo (state/get-current-repo)]
  415. (when-let [files (get-file-contents-with-suffix repo)]
  416. (let [heading-to-list? (state/export-heading-to-list?)
  417. files
  418. (export-files-as-markdown repo files heading-to-list?)
  419. zip-file-name (str repo "_markdown_" (quot (util/time-ms) 1000))]
  420. (p/let [zipfile (zip/make-zip zip-file-name files repo)]
  421. (when-let [anchor (gdom/getElement "export-as-markdown")]
  422. (.setAttribute anchor "href" (js/window.URL.createObjectURL zipfile))
  423. (.setAttribute anchor "download" (.-name zipfile))
  424. (.click anchor)))))))
  425. (defn export-page-as-markdown!
  426. [page-name]
  427. (when-let [repo (state/get-current-repo)]
  428. (when-let [file (db/get-page-file page-name)]
  429. (when-let [path (:file/path file)]
  430. (when-let [content (get-page-content page-name)]
  431. (let [names [page-name]
  432. format (f/get-format path)
  433. files [{:path path :content content :names names :format format}]]
  434. (let [files
  435. (export-files-as-markdown repo files (state/export-heading-to-list?))]
  436. (let [data (js/Blob. [(second (first files))]
  437. (clj->js {:type "text/plain;charset=utf-8,"}))]
  438. (let [anchor (gdom/getElement "export-page-as-markdown")
  439. url (js/window.URL.createObjectURL data)]
  440. (.setAttribute anchor "href" url)
  441. (.setAttribute anchor "download" path)
  442. (.click anchor))))))))))
  443. (defn convert-page-markdown-unordered-list-or-heading!
  444. [page-name]
  445. (when-let [repo (state/get-current-repo)]
  446. (when-let [file (db/get-page-file page-name)]
  447. (when-let [path (:file/path file)]
  448. (when-let [content (get-page-content page-name)]
  449. (let [names [page-name]
  450. format (f/get-format path)
  451. files [{:path path :content content :names names :format format}]]
  452. (let [files (convert-md-files-unordered-list-or-heading repo files (state/export-heading-to-list?))]
  453. (let [data (js/Blob. [(second (first files))]
  454. (clj->js {:type "text/plain;charset=utf-8,"}))]
  455. (let [anchor (gdom/getElement "convert-markdown-to-unordered-list-or-heading")
  456. url (js/window.URL.createObjectURL data)]
  457. (.setAttribute anchor "href" url)
  458. (.setAttribute anchor "download" path)
  459. (.click anchor))))))))))
  460. (defn convert-repo-markdown-v2!
  461. [repo]
  462. (when repo
  463. (when-let [files (get-md-file-contents repo)]
  464. (let [zip-file-name (-> (string/replace repo config/local-db-prefix "")
  465. (string/replace #"^/+" ""))
  466. zip-file-name (str zip-file-name
  467. "_markdown_"
  468. (quot (util/time-ms) 1000))]
  469. (p/let [zipfile (zip/make-zip zip-file-name files repo)]
  470. (when-let [anchor (gdom/getElement "convert-markdown-to-unordered-list-or-heading")]
  471. (.setAttribute anchor "href" (js/window.URL.createObjectURL zipfile))
  472. (.setAttribute anchor "download" (.-name zipfile))
  473. (.click anchor)))))))