opml.cljs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. (ns frontend.handler.export.opml
  2. "export blocks/pages as opml"
  3. (:refer-clojure :exclude [map filter mapcat concat remove newline])
  4. (:require ["/frontend/utils" :as utils]
  5. [clojure.string :as string]
  6. [clojure.zip :as z]
  7. [frontend.db :as db]
  8. [frontend.extensions.zip :as zip]
  9. [frontend.handler.export.common :as common :refer
  10. [*state* raw-text simple-asts->string space]]
  11. [frontend.handler.export.zip-helper :refer [get-level goto-last
  12. goto-level]]
  13. [frontend.state :as state]
  14. [frontend.util :as util :refer [concatv mapcatv removev]]
  15. [goog.dom :as gdom]
  16. [hiccups.runtime :as h]
  17. [logseq.graph-parser.mldoc :as gp-mldoc]
  18. [promesa.core :as p]))
  19. ;;; *opml-state*
  20. (def ^:private ^:dynamic
  21. *opml-state*
  22. {:outside-em-symbol nil})
  23. ;;; utils for construct opml hiccup
  24. ;; - a
  25. ;; - b
  26. ;; - c
  27. ;; - d
  28. ;; [:outline
  29. ;; {:text "a"}
  30. ;; [:outline {:text "b"} [:outline {:text "c"}]]
  31. ;; [:outline {:text "d"}]]
  32. (defn- branch? [node] (= :outline (first node)))
  33. (defn- outline-hiccup-zip
  34. [root]
  35. (z/zipper branch?
  36. rest
  37. (fn [node children] (with-meta (apply vector :outline children) (meta node)))
  38. root))
  39. (def ^:private init-opml-body-hiccup
  40. (z/down (outline-hiccup-zip [:outline [:placeholder]])))
  41. (defn- goto-last-outline
  42. "[:outline [:outline [:outline]]]
  43. ^
  44. goto here"
  45. [loc]
  46. (-> loc
  47. goto-last
  48. z/up))
  49. (defn- add-same-level-outline-at-right
  50. [loc attr-map]
  51. {:pre [(map? attr-map)]}
  52. (-> loc
  53. (z/insert-right [:outline attr-map])
  54. z/right))
  55. (defn- add-next-level-outline
  56. [loc attr-map]
  57. {:pre [(map? attr-map)]}
  58. (-> loc
  59. (z/append-child [:outline attr-map])
  60. goto-last-outline))
  61. (defn- append-text-to-current-outline
  62. [loc text]
  63. (-> loc
  64. z/down
  65. (z/edit #(update % :text str text))
  66. z/up))
  67. (defn- append-text-to-current-outline*
  68. "if current-level = 0(it's just `init-opml-body-hiccup`), need to add a new outline item."
  69. [loc text]
  70. (if (pos? (get-level loc))
  71. (append-text-to-current-outline loc text)
  72. ;; at root
  73. (-> loc
  74. z/down
  75. (add-same-level-outline-at-right {:text nil})
  76. (append-text-to-current-outline text))))
  77. (defn- zip-loc->opml
  78. [hiccup title]
  79. (let [[_ _ & body] hiccup]
  80. (str
  81. "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
  82. (utils/prettifyXml
  83. (h/render-html
  84. [:opml {:version "2.0"}
  85. [:head [:title title]]
  86. (concatv [:body] body)])))))
  87. ;;; utils for construct opml hiccup (ends)
  88. ;;; block/inline-ast -> hiccup & simple-ast
  89. (declare inline-ast->simple-ast
  90. block-ast->hiccup)
  91. (defn- emphasis-wrap-with
  92. [inline-coll em-symbol]
  93. (binding [*opml-state* (assoc *opml-state* :outside-em-symbol (first em-symbol))]
  94. (concatv [(raw-text em-symbol)]
  95. (mapcatv inline-ast->simple-ast inline-coll)
  96. [(raw-text em-symbol)])))
  97. (defn- inline-emphasis
  98. [[[type] inline-coll]]
  99. (let [outside-em-symbol (:outside-em-symbol *opml-state*)]
  100. (case type
  101. "Bold"
  102. (emphasis-wrap-with
  103. inline-coll (if (= outside-em-symbol "*") "__" "**"))
  104. "Italic"
  105. (emphasis-wrap-with
  106. inline-coll (if (= outside-em-symbol "*") "_" "*"))
  107. "Underline"
  108. (binding [*opml-state* (assoc *opml-state* :outside-em-symbol outside-em-symbol)]
  109. (mapcatv (fn [inline] (cons space (inline-ast->simple-ast inline))) inline-coll))
  110. "Strike_through"
  111. (emphasis-wrap-with inline-coll "~~")
  112. "Highlight"
  113. (emphasis-wrap-with inline-coll "^^")
  114. ;; else
  115. (assert false (print-str :inline-emphasis type "is invalid")))))
  116. ;; FIXME: how to add newlines to opml text attr?
  117. (defn- inline-break-line
  118. []
  119. [space])
  120. (defn- inline-link
  121. [{full-text :full_text}]
  122. [(raw-text full-text)])
  123. (defn- inline-nested-link
  124. [{content :content}]
  125. [(raw-text content)])
  126. (defn- inline-subscript
  127. [inline-coll]
  128. (concatv [(raw-text "_{")]
  129. (mapcatv (fn [inline] (cons space (inline-ast->simple-ast inline))) inline-coll)
  130. [(raw-text "}")]))
  131. (defn- inline-superscript
  132. [inline-coll]
  133. (concatv [(raw-text "^{")]
  134. (mapcatv (fn [inline] (cons space (inline-ast->simple-ast inline))) inline-coll)
  135. [(raw-text "}")]))
  136. (defn- inline-footnote-reference
  137. [{name :name}]
  138. [(raw-text "[" name "]")])
  139. (defn- inline-cookie
  140. [ast-content]
  141. [(raw-text
  142. (case (first ast-content)
  143. "Absolute"
  144. (let [[_ current total] ast-content]
  145. (str "[" current "/" total "]"))
  146. "Percent"
  147. (str "[" (second ast-content) "%]")))])
  148. (defn- inline-latex-fragment
  149. [ast-content]
  150. (let [[type content] ast-content
  151. wrapper (case type
  152. "Inline" "$"
  153. "Displayed" "$$")]
  154. [space (raw-text (str wrapper content wrapper)) space]))
  155. (defn- inline-macro
  156. [{:keys [name arguments]}]
  157. (->
  158. (if (= name "cloze")
  159. (string/join "," arguments)
  160. (let [l (cond-> ["{{" name]
  161. (pos? (count arguments)) (conj "(" (string/join "," arguments) ")")
  162. true (conj "}}"))]
  163. (string/join l)))
  164. raw-text
  165. vector))
  166. (defn- inline-entity
  167. [{unicode :unicode}]
  168. [(raw-text unicode)])
  169. (defn- inline-timestamp
  170. [ast-content]
  171. (let [[type timestamp-content] ast-content]
  172. (-> (case type
  173. "Scheduled" ["SCHEDULED: " (common/timestamp-to-string timestamp-content)]
  174. "Deadline" ["DEADLINE: " (common/timestamp-to-string timestamp-content)]
  175. "Date" [(common/timestamp-to-string timestamp-content)]
  176. "Closed" ["CLOSED: " (common/timestamp-to-string timestamp-content)]
  177. "Clock" ["CLOCK: " (common/timestamp-to-string (second timestamp-content))]
  178. "Range" (let [{:keys [start stop]} timestamp-content]
  179. [(str (common/timestamp-to-string start) "--" (common/timestamp-to-string stop))]))
  180. string/join
  181. raw-text
  182. vector)))
  183. (defn- inline-email
  184. [{:keys [local_part domain]}]
  185. [(raw-text (str "<" local_part "@" domain ">"))])
  186. (defn- inline-ast->simple-ast
  187. [inline]
  188. (let [[ast-type ast-content] inline]
  189. (case ast-type
  190. "Emphasis"
  191. (inline-emphasis ast-content)
  192. ("Break_Line" "Hard_Break_Line")
  193. (inline-break-line)
  194. "Verbatim"
  195. [(raw-text ast-content)]
  196. "Code"
  197. [(raw-text "`" ast-content "`")]
  198. "Tag"
  199. [(raw-text "#" (common/hashtag-value->string ast-content))]
  200. "Spaces" ; what's this ast-type for ?
  201. nil
  202. "Plain"
  203. [(raw-text ast-content)]
  204. "Link"
  205. (inline-link ast-content)
  206. "Nested_link"
  207. (inline-nested-link ast-content)
  208. "Target"
  209. [(raw-text (str "<<" ast-content ">>"))]
  210. "Subscript"
  211. (inline-subscript ast-content)
  212. "Superscript"
  213. (inline-superscript ast-content)
  214. "Footnote_Reference"
  215. (inline-footnote-reference ast-content)
  216. "Cookie"
  217. (inline-cookie ast-content)
  218. "Latex_Fragment"
  219. (inline-latex-fragment ast-content)
  220. "Macro"
  221. (inline-macro ast-content)
  222. "Entity"
  223. (inline-entity ast-content)
  224. "Timestamp"
  225. (inline-timestamp ast-content)
  226. "Radio_Target"
  227. [(raw-text (str "<<<" ast-content ">>>"))]
  228. "Email"
  229. (inline-email ast-content)
  230. "Inline_Hiccup"
  231. [(raw-text ast-content)]
  232. "Inline_Html"
  233. [(raw-text ast-content)]
  234. ("Export_Snippet" "Inline_Source_Block")
  235. nil
  236. (assert false (print-str :inline-ast->simple-ast ast-type "not implemented yet")))))
  237. (defn- block-paragraph
  238. [loc inline-coll]
  239. (-> loc
  240. goto-last-outline
  241. (append-text-to-current-outline*
  242. (simple-asts->string
  243. (cons space (mapcatv inline-ast->simple-ast inline-coll))))))
  244. (defn- block-heading
  245. [loc {:keys [title _tags marker level _numbering priority _anchor _meta _unordered _size]}]
  246. (let [loc (goto-last-outline loc)
  247. current-level (get-level loc)
  248. title* (mapcatv inline-ast->simple-ast title)
  249. marker* (and marker (raw-text marker))
  250. priority* (and priority (raw-text (common/priority->string priority)))
  251. simple-asts (removev nil? (concatv [marker* space priority* space] title*))
  252. ;; remove leading spaces
  253. simple-asts (drop-while #(= % space) simple-asts)
  254. s (simple-asts->string simple-asts)]
  255. (if (> level current-level)
  256. (add-next-level-outline loc {:text s})
  257. (-> loc
  258. (goto-level level)
  259. z/rightmost
  260. (add-same-level-outline-at-right {:text s})))))
  261. (declare block-list)
  262. (defn- block-list-item
  263. [loc {:keys [content items]}]
  264. (let [current-level (get-level loc)
  265. ;; if current loc node is empty(= {}),
  266. ;; the outline node is already created.
  267. loc (if (empty? (second (z/node loc)))
  268. loc
  269. (add-same-level-outline-at-right loc {:text nil}))
  270. loc* (reduce block-ast->hiccup loc content)
  271. loc** (if (seq items) (block-list loc* items) loc*)]
  272. (-> loc**
  273. (goto-level current-level)
  274. z/rightmost)))
  275. (defn- block-list
  276. [loc list-items]
  277. (reduce block-list-item (add-next-level-outline loc {}) list-items))
  278. (defn- block-example
  279. [loc str-coll]
  280. (append-text-to-current-outline* loc (string/join " " str-coll)))
  281. (defn- block-src
  282. [loc {:keys [_language lines]}]
  283. (append-text-to-current-outline* loc (string/join " " lines)))
  284. (defn- block-quote
  285. [loc block-ast-coll]
  286. (reduce block-ast->hiccup loc block-ast-coll))
  287. (defn- block-latex-env
  288. [loc [name options content]]
  289. (append-text-to-current-outline*
  290. loc
  291. (str "\\begin{" name "}" options "\n"
  292. content "\n"
  293. "\\end{" name "}")))
  294. (defn- block-displayed-math
  295. [loc s]
  296. (append-text-to-current-outline* loc s))
  297. (defn- block-footnote-definition
  298. [loc [name inline-coll]]
  299. (let [inline-simple-asts (mapcatv inline-ast->simple-ast inline-coll)]
  300. (append-text-to-current-outline*
  301. loc
  302. (str "[^" name "]: " (simple-asts->string inline-simple-asts)))))
  303. (defn- block-ast->hiccup
  304. [loc block-ast]
  305. (let [[ast-type ast-content] block-ast]
  306. (case ast-type
  307. "Paragraph"
  308. (block-paragraph loc ast-content)
  309. "Paragraph_line"
  310. (assert false "Paragraph_line is mldoc internal ast")
  311. "Paragraph_Sep"
  312. loc
  313. "Heading"
  314. (block-heading loc ast-content)
  315. "List"
  316. (block-list loc ast-content)
  317. ("Directive" "Results" "Property_Drawer" "Export" "CommentBlock" "Custom")
  318. loc
  319. "Example"
  320. (block-example loc ast-content)
  321. "Src"
  322. (block-src loc ast-content)
  323. "Quote"
  324. (block-quote loc ast-content)
  325. "Latex_Fragment"
  326. (append-text-to-current-outline* loc (simple-asts->string (inline-latex-fragment ast-content)))
  327. "Latex_Environment"
  328. (block-latex-env loc (rest block-ast))
  329. "Displayed_Math"
  330. (block-displayed-math loc ast-content)
  331. "Drawer"
  332. loc
  333. "Footnote_Definition"
  334. (block-footnote-definition loc (rest block-ast))
  335. "Horizontal_Rule"
  336. loc
  337. "Table"
  338. loc
  339. "Comment"
  340. loc
  341. "Raw_Html"
  342. loc
  343. "Hiccup"
  344. loc
  345. (assert false (print-str :block-ast->simple-ast ast-type "not implemented yet")))))
  346. ;;; block/inline-ast -> hiccup (ends)
  347. ;;; export fns
  348. (defn- export-helper
  349. [content format options & {:keys [title] :or {title "untitled"}}]
  350. (let [remove-options (set (:remove-options options))
  351. other-options (:other-options options)]
  352. (binding [*state* (merge *state*
  353. {:export-options
  354. {:remove-emphasis? (contains? remove-options :emphasis)
  355. :remove-page-ref-brackets? (contains? remove-options :page-ref)
  356. :remove-tags? (contains? remove-options :tag)
  357. :keep-only-level<=N (:keep-only-level<=N other-options)}})
  358. *opml-state* *opml-state*]
  359. (let [ast (gp-mldoc/->edn content (gp-mldoc/default-config format))
  360. ast (mapv common/remove-block-ast-pos ast)
  361. ast (removev common/Properties-block-ast? ast)
  362. keep-level<=n (get-in *state* [:export-options :keep-only-level<=N])
  363. ast (if (pos? keep-level<=n)
  364. (common/keep-only-level<=n ast keep-level<=n)
  365. ast)
  366. ast* (common/replace-block&page-reference&embed ast)
  367. ast** (if (= "no-indent" (get-in *state* [:export-options :indent-style]))
  368. (mapv common/replace-Heading-with-Paragraph ast*)
  369. ast*)
  370. config-for-walk-block-ast (cond-> {}
  371. (get-in *state* [:export-options :remove-emphasis?])
  372. (update :mapcat-fns-on-inline-ast conj common/remove-emphasis)
  373. (get-in *state* [:export-options :remove-page-ref-brackets?])
  374. (update :map-fns-on-inline-ast conj common/remove-page-ref-brackets)
  375. (get-in *state* [:export-options :remove-tags?])
  376. (update :mapcat-fns-on-inline-ast conj common/remove-tags)
  377. (= "no-indent" (get-in *state* [:export-options :indent-style]))
  378. (update :mapcat-fns-on-inline-ast conj common/remove-prefix-spaces-in-Plain))
  379. ast*** (if-not (empty? config-for-walk-block-ast)
  380. (mapv (partial common/walk-block-ast config-for-walk-block-ast) ast**)
  381. ast**)
  382. hiccup (z/root (reduce block-ast->hiccup init-opml-body-hiccup ast***))]
  383. (zip-loc->opml hiccup title)))))
  384. (defn export-blocks-as-opml
  385. "options: see also `export-blocks-as-markdown`"
  386. [repo root-block-uuids-or-page-name options]
  387. {:pre [(or (coll? root-block-uuids-or-page-name)
  388. (string? root-block-uuids-or-page-name))]}
  389. (util/profile
  390. :export-blocks-as-opml
  391. (let [content
  392. (if (string? root-block-uuids-or-page-name)
  393. ;; page
  394. (common/get-page-content root-block-uuids-or-page-name)
  395. (common/root-block-uuids->content repo root-block-uuids-or-page-name))
  396. title (if (string? root-block-uuids-or-page-name)
  397. root-block-uuids-or-page-name
  398. "untitled")
  399. first-block (db/entity [:block/uuid (first root-block-uuids-or-page-name)])
  400. format (or (:block/format first-block) (state/get-preferred-format))]
  401. (export-helper content format options :title title))))
  402. (defn export-files-as-opml
  403. "options see also `export-blocks-as-opml`"
  404. [files options]
  405. (mapv
  406. (fn [{:keys [path content names format]}]
  407. (when (first names)
  408. (util/profile (print-str :export-files-as-opml path)
  409. [path (export-helper content format options :title (first names))])))
  410. files))
  411. (defn export-repo-as-opml!
  412. [repo]
  413. (when-let [files (common/get-file-contents-with-suffix repo)]
  414. (let [files (export-files-as-opml files nil)
  415. zip-file-name (str repo "_opml_" (quot (util/time-ms) 1000))]
  416. (p/let [zipfile (zip/make-zip zip-file-name files repo)]
  417. (when-let [anchor (gdom/getElement "export-as-opml")]
  418. (.setAttribute anchor "href" (js/window.URL.createObjectURL zipfile))
  419. (.setAttribute anchor "download" (.-name zipfile))
  420. (.click anchor))))))
  421. ;;; export fns (ends)