opml.cljs 16 KB

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