text.cljs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. (ns frontend.handler.export.text
  2. "export blocks/pages as text"
  3. (:refer-clojure :exclude [map filter mapcat concat remove newline])
  4. (:require [clojure.string :as string]
  5. [frontend.db :as db]
  6. [frontend.extensions.zip :as zip]
  7. [frontend.handler.export.common :as common :refer
  8. [*state* indent newline* raw-text simple-ast-malli-schema
  9. simple-asts->string space]]
  10. [logseq.graph-parser.schema.mldoc :as mldoc-schema]
  11. [frontend.state :as state]
  12. [frontend.util :as util :refer [concatv mapcatv removev]]
  13. [goog.dom :as gdom]
  14. [logseq.graph-parser.mldoc :as gp-mldoc]
  15. [malli.core :as m]
  16. [promesa.core :as p]))
  17. ;;; block-ast, inline-ast -> simple-ast
  18. (defn indent-with-2-spaces
  19. "also consider (get-in *state* [:export-options :indent-style])"
  20. [level]
  21. (let [indent-style (get-in *state* [:export-options :indent-style])]
  22. (case indent-style
  23. "dashes" (indent level 2)
  24. ("spaces" "no-indent") (indent level 0)
  25. (assert false (print-str "unknown indent-style:" indent-style)))))
  26. (declare inline-ast->simple-ast
  27. block-ast->simple-ast)
  28. (defn- block-heading
  29. [{:keys [title _tags marker level _numbering priority _anchor _meta _unordered size]}]
  30. (let [indent-style (get-in *state* [:export-options :indent-style])
  31. priority* (and priority (raw-text (common/priority->string priority)))
  32. heading* (if (= indent-style "dashes")
  33. [(indent (dec level) 0) (raw-text "-")]
  34. [(indent (dec level) 0)])
  35. size* (and size [space (raw-text (reduce str (repeat size "#")))])
  36. marker* (and marker (raw-text marker))]
  37. (set! *state* (assoc *state* :current-level level))
  38. (let [simple-asts
  39. (removev nil? (concatv
  40. (when (and (get-in *state* [:export-options :newline-after-block])
  41. (not (get-in *state* [:newline-after-block :current-block-is-first-heading-block?])))
  42. [(newline* 2)])
  43. heading* size*
  44. [space marker* space priority* space]
  45. (mapcatv inline-ast->simple-ast title)
  46. [(newline* 1)]))]
  47. (set! *state* (assoc-in *state* [:newline-after-block :current-block-is-first-heading-block?] false))
  48. simple-asts)))
  49. (declare block-list)
  50. (defn- block-list-item
  51. [{:keys [content items number _name checkbox]}]
  52. (let [content* (mapcatv block-ast->simple-ast content)
  53. number* (raw-text
  54. (if number
  55. (str number ". ")
  56. "* "))
  57. checkbox* (raw-text
  58. (if (some? checkbox)
  59. (if (boolean checkbox)
  60. "[X]" "[ ]")
  61. ""))
  62. current-level (get *state* :current-level 1)
  63. indent (when (> current-level 1)
  64. (indent (dec current-level) 0))
  65. items* (block-list items :in-list? true)]
  66. (concatv [indent number* checkbox* space]
  67. content*
  68. [(newline* 1)]
  69. items*
  70. [(newline* 1)])))
  71. (defn- block-list
  72. [l & {:keys [in-list?]}]
  73. (binding [*state* (update *state* :current-level inc)]
  74. (concatv (mapcatv block-list-item l)
  75. (when (and (pos? (count l))
  76. (not in-list?))
  77. [(newline* 2)]))))
  78. (defn- block-property-drawer
  79. [properties]
  80. (when-not (get-in *state* [:export-options :remove-properties?])
  81. (let [level (dec (get *state* :current-level 1))
  82. indent (indent-with-2-spaces level)]
  83. (reduce
  84. (fn [r [k v]]
  85. (conj r indent (raw-text k "::") space (raw-text v) (newline* 1)))
  86. [] properties))))
  87. (defn- block-example
  88. [l]
  89. (let [level (dec (get *state* :current-level 1))]
  90. (mapcatv
  91. (fn [line]
  92. [(indent-with-2-spaces level)
  93. (raw-text " ")
  94. (raw-text line)
  95. (newline* 1)])
  96. l)))
  97. (defn- remove-max-prefix-spaces
  98. [lines]
  99. (let [common-prefix-spaces
  100. (reduce
  101. (fn [r line]
  102. (if (string/blank? line)
  103. r
  104. (let [leading-spaces (re-find #"^\s+" line)]
  105. (if (nil? r)
  106. leading-spaces
  107. (if (string/starts-with? r leading-spaces)
  108. leading-spaces
  109. r)))))
  110. nil
  111. lines)
  112. pattern (re-pattern (str "^" common-prefix-spaces))]
  113. (mapv (fn [line] (string/replace-first line pattern "")) lines)))
  114. (defn- block-src
  115. [{:keys [lines language]}]
  116. (let [level (dec (get *state* :current-level 1))
  117. lines* (if (= "no-indent" (get-in *state* [:export-options :indent-style]))
  118. (remove-max-prefix-spaces lines)
  119. lines)]
  120. (concatv
  121. [(indent-with-2-spaces level) (raw-text "```")]
  122. (when language [(raw-text language)])
  123. [(newline* 1)]
  124. (mapv raw-text lines*)
  125. [(indent-with-2-spaces level) (raw-text "```") (newline* 1)])))
  126. (defn- block-quote
  127. [block-coll]
  128. (let [level (dec (get *state* :current-level 1))]
  129. (binding [*state* (assoc *state* :indent-after-break-line? true)]
  130. (concatv (mapcatv (fn [block]
  131. (let [block-simple-ast (block-ast->simple-ast block)]
  132. (when (seq block-simple-ast)
  133. (concatv [(indent-with-2-spaces level) (raw-text ">") space]
  134. block-simple-ast))))
  135. block-coll)
  136. [(newline* 2)]))))
  137. (declare inline-latex-fragment)
  138. (defn- block-latex-fragment
  139. [ast-content]
  140. (inline-latex-fragment ast-content))
  141. (defn- block-latex-env
  142. [[name options content]]
  143. (let [level (dec (get *state* :current-level 1))]
  144. [(indent-with-2-spaces level) (raw-text "\\begin{" name "}" options)
  145. (newline* 1)
  146. (indent-with-2-spaces level) (raw-text content)
  147. (newline* 1)
  148. (indent-with-2-spaces level) (raw-text "\\end{" name "}")
  149. (newline* 1)]))
  150. (defn- block-displayed-math
  151. [ast-content]
  152. [space (raw-text "$$" ast-content "$$") space])
  153. (defn- block-drawer
  154. [[name lines]]
  155. (let [level (dec (get *state* :current-level))]
  156. (concatv
  157. [(raw-text ":" name ":")
  158. (newline* 1)]
  159. (mapcatv (fn [line] [(indent-with-2-spaces level) (raw-text line)]) lines)
  160. [(newline* 1) (raw-text ":END:") (newline* 1)])))
  161. (defn- block-footnote-definition
  162. [[name content]]
  163. (concatv
  164. [(raw-text "[^" name "]:") space]
  165. (mapcatv inline-ast->simple-ast content)
  166. [(newline* 1)]))
  167. (def ^:private block-horizontal-rule [(newline* 1) (raw-text "---") (newline* 1)])
  168. (defn- block-table
  169. [{:keys [header groups]}]
  170. (let [level (dec (get *state* :current-level 1))
  171. sep-line (raw-text "|" (string/join "|" (repeat (count header) "---")) "|")
  172. header-line
  173. (concatv (mapcatv
  174. (fn [h] (concatv [space (raw-text "|") space] (mapcatv inline-ast->simple-ast h)))
  175. header)
  176. [space (raw-text "|")])
  177. group-lines
  178. (mapcatv
  179. (fn [group]
  180. (mapcatv
  181. (fn [row]
  182. (concatv [(indent-with-2-spaces level)]
  183. (mapcatv
  184. (fn [col]
  185. (concatv [(raw-text "|") space]
  186. (mapcatv inline-ast->simple-ast col)
  187. [space]))
  188. row)
  189. [(raw-text "|") (newline* 1)]))
  190. group))
  191. groups)]
  192. (concatv [(newline* 1) (indent-with-2-spaces level)]
  193. (when (seq header) header-line)
  194. (when (seq header) [(newline* 1) (indent-with-2-spaces level) sep-line (newline* 1)])
  195. group-lines)))
  196. (defn- block-comment
  197. [s]
  198. (let [level (dec (get *state* :current-level 1))]
  199. [(indent-with-2-spaces level) (raw-text "<!---") (newline* 1)
  200. (indent-with-2-spaces level) (raw-text s) (newline* 1)
  201. (indent-with-2-spaces level) (raw-text "-->") (newline* 1)]))
  202. (defn- block-raw-html
  203. [s]
  204. (let [level (dec (get *state* :current-level 1))]
  205. [(indent-with-2-spaces level) (raw-text s) (newline* 1)]))
  206. (defn- block-hiccup
  207. [s]
  208. (let [level (dec (get *state* :current-level 1))]
  209. [(indent-with-2-spaces level) (raw-text s) space]))
  210. (defn- inline-link
  211. [{full-text :full_text}]
  212. [(raw-text full-text)])
  213. (defn- inline-nested-link
  214. [{content :content}]
  215. [(raw-text content)])
  216. (defn- inline-subscript
  217. [inline-coll]
  218. (concatv [(raw-text "_{")]
  219. (mapcatv (fn [inline] (cons space (inline-ast->simple-ast inline))) inline-coll)
  220. [(raw-text "}")]))
  221. (defn- inline-superscript
  222. [inline-coll]
  223. (concatv [(raw-text "^{")]
  224. (mapcatv (fn [inline] (cons space (inline-ast->simple-ast inline))) inline-coll)
  225. [(raw-text "}")]))
  226. (defn- inline-footnote-reference
  227. [{name :name}]
  228. [(raw-text "[" name "]")])
  229. (defn- inline-cookie
  230. [ast-content]
  231. [(raw-text
  232. (case (first ast-content)
  233. "Absolute"
  234. (let [[_ current total] ast-content]
  235. (str "[" current "/" total "]"))
  236. "Percent"
  237. (str "[" (second ast-content) "%]")))])
  238. (defn- inline-latex-fragment
  239. [ast-content]
  240. (let [[type content] ast-content
  241. wrapper (case type
  242. "Inline" "$"
  243. "Displayed" "$$")]
  244. [space (raw-text (str wrapper content wrapper)) space]))
  245. (defn- inline-macro
  246. [{:keys [name arguments]}]
  247. (->
  248. (if (= name "cloze")
  249. (string/join "," arguments)
  250. (let [l (cond-> ["{{" name]
  251. (pos? (count arguments)) (conj "(" (string/join "," arguments) ")")
  252. true (conj "}}"))]
  253. (string/join l)))
  254. raw-text
  255. vector))
  256. (defn- inline-entity
  257. [{unicode :unicode}]
  258. [(raw-text unicode)])
  259. (defn- inline-timestamp
  260. [ast-content]
  261. (let [[type timestamp-content] ast-content]
  262. (-> (case type
  263. "Scheduled" ["SCHEDULED: " (common/timestamp-to-string timestamp-content)]
  264. "Deadline" ["DEADLINE: " (common/timestamp-to-string timestamp-content)]
  265. "Date" [(common/timestamp-to-string timestamp-content)]
  266. "Closed" ["CLOSED: " (common/timestamp-to-string timestamp-content)]
  267. "Clock" ["CLOCK: " (common/timestamp-to-string (second timestamp-content))]
  268. "Range" (let [{:keys [start stop]} timestamp-content]
  269. [(str (common/timestamp-to-string start) "--" (common/timestamp-to-string stop))]))
  270. string/join
  271. raw-text
  272. vector)))
  273. (defn- inline-email
  274. [{:keys [local_part domain]}]
  275. [(raw-text (str "<" local_part "@" domain ">"))])
  276. (defn- emphasis-wrap-with
  277. [inline-coll em-symbol]
  278. (binding [*state* (assoc *state* :outside-em-symbol (first em-symbol))]
  279. (concatv [(raw-text em-symbol)]
  280. (mapcatv inline-ast->simple-ast inline-coll)
  281. [(raw-text em-symbol)])))
  282. (defn- inline-emphasis
  283. [emphasis]
  284. (let [[[type] inline-coll] emphasis
  285. outside-em-symbol (:outside-em-symbol *state*)]
  286. (case type
  287. "Bold"
  288. (emphasis-wrap-with inline-coll (if (= outside-em-symbol "*") "__" "**"))
  289. "Italic"
  290. (emphasis-wrap-with inline-coll (if (= outside-em-symbol "*") "_" "*"))
  291. "Underline"
  292. (binding [*state* (assoc *state* :outside-em-symbol outside-em-symbol)]
  293. (mapcatv (fn [inline] (cons space (inline-ast->simple-ast inline))) inline-coll))
  294. "Strike_through"
  295. (emphasis-wrap-with inline-coll "~~")
  296. "Highlight"
  297. (emphasis-wrap-with inline-coll "^^")
  298. ;; else
  299. (assert false (print-str :inline-emphasis emphasis "is invalid")))))
  300. (defn- inline-break-line
  301. []
  302. [(if (= "no-indent" (get-in *state* [:export-options :indent-style]))
  303. (raw-text "\n")
  304. (raw-text " \n"))
  305. (when (:indent-after-break-line? *state*)
  306. (let [current-level (get *state* :current-level 1)]
  307. (when (> current-level 1)
  308. (indent-with-2-spaces (dec current-level)))))])
  309. ;; {:malli/schema ...} only works on public vars, so use m/=> here
  310. (m/=> block-ast->simple-ast [:=> [:cat mldoc-schema/block-ast-schema] [:sequential simple-ast-malli-schema]])
  311. (defn- block-ast->simple-ast
  312. [block]
  313. (let [newline-after-block? (get-in *state* [:export-options :newline-after-block])]
  314. (removev
  315. nil?
  316. (let [[ast-type ast-content] block]
  317. (case ast-type
  318. "Paragraph"
  319. (let [{:keys [origin-ast]} (meta block)
  320. current-block-is-first-heading-block? (get-in *state* [:newline-after-block :current-block-is-first-heading-block?])]
  321. (set! *state* (assoc-in *state* [:newline-after-block :current-block-is-first-heading-block?] false))
  322. (concatv
  323. (when (and origin-ast newline-after-block? (not current-block-is-first-heading-block?))
  324. [(newline* 2)])
  325. (mapcatv inline-ast->simple-ast ast-content)
  326. (let [last-element (last ast-content)
  327. [last-element-type] last-element]
  328. (when (and newline-after-block? (= "Break_Line" last-element-type))
  329. (inline-break-line)))
  330. [(newline* 1)]))
  331. "Paragraph_line"
  332. (assert false "Paragraph_line is mldoc internal ast")
  333. "Paragraph_Sep"
  334. [(newline* ast-content)]
  335. "Heading"
  336. (block-heading ast-content)
  337. "List"
  338. (block-list ast-content)
  339. ("Directive" "Results" "Export" "CommentBlock" "Custom")
  340. nil
  341. "Example"
  342. (block-example ast-content)
  343. "Src"
  344. (block-src ast-content)
  345. "Quote"
  346. (block-quote ast-content)
  347. "Latex_Fragment"
  348. (block-latex-fragment ast-content)
  349. "Latex_Environment"
  350. (block-latex-env (rest block))
  351. "Displayed_Math"
  352. (block-displayed-math ast-content)
  353. "Drawer"
  354. (block-drawer (rest block))
  355. "Property_Drawer"
  356. (block-property-drawer ast-content)
  357. "Footnote_Definition"
  358. (block-footnote-definition (rest block))
  359. "Horizontal_Rule"
  360. block-horizontal-rule
  361. "Table"
  362. (block-table ast-content)
  363. "Comment"
  364. (block-comment ast-content)
  365. "Raw_Html"
  366. (block-raw-html ast-content)
  367. "Hiccup"
  368. (block-hiccup ast-content)
  369. (assert false (print-str :block-ast->simple-ast ast-type "not implemented yet")))))))
  370. (defn- inline-ast->simple-ast
  371. [inline]
  372. (let [[ast-type ast-content] inline]
  373. (case ast-type
  374. "Emphasis"
  375. (inline-emphasis ast-content)
  376. ("Break_Line" "Hard_Break_Line")
  377. (inline-break-line)
  378. "Verbatim"
  379. [(raw-text ast-content)]
  380. "Code"
  381. [(raw-text "`" ast-content "`")]
  382. "Tag"
  383. [(raw-text (str "#" (common/hashtag-value->string ast-content)))]
  384. "Spaces" ; what's this ast-type for ?
  385. nil
  386. "Plain"
  387. [(raw-text ast-content)]
  388. "Link"
  389. (inline-link ast-content)
  390. "Nested_link"
  391. (inline-nested-link ast-content)
  392. "Target"
  393. [(raw-text (str "<<" ast-content ">>"))]
  394. "Subscript"
  395. (inline-subscript ast-content)
  396. "Superscript"
  397. (inline-superscript ast-content)
  398. "Footnote_Reference"
  399. (inline-footnote-reference ast-content)
  400. "Cookie"
  401. (inline-cookie ast-content)
  402. "Latex_Fragment"
  403. (inline-latex-fragment ast-content)
  404. "Macro"
  405. (inline-macro ast-content)
  406. "Entity"
  407. (inline-entity ast-content)
  408. "Timestamp"
  409. (inline-timestamp ast-content)
  410. "Radio_Target"
  411. [(raw-text (str "<<<" ast-content ">>>"))]
  412. "Email"
  413. (inline-email ast-content)
  414. "Inline_Hiccup"
  415. [(raw-text ast-content)]
  416. "Inline_Html"
  417. [(raw-text ast-content)]
  418. ("Export_Snippet" "Inline_Source_Block")
  419. nil
  420. (assert false (print-str :inline-ast->simple-ast ast-type "not implemented yet")))))
  421. ;;; block-ast, inline-ast -> simple-ast (ends)
  422. ;;; export fns
  423. (defn- export-helper
  424. [content format options]
  425. (let [remove-options (set (:remove-options options))
  426. other-options (:other-options options)]
  427. (binding [*state* (merge *state*
  428. {:export-options
  429. {:indent-style (or (:indent-style options) "dashes")
  430. :remove-emphasis? (contains? remove-options :emphasis)
  431. :remove-page-ref-brackets? (contains? remove-options :page-ref)
  432. :remove-tags? (contains? remove-options :tag)
  433. :remove-properties? (contains? remove-options :property)
  434. :keep-only-level<=N (:keep-only-level<=N other-options)
  435. :newline-after-block (:newline-after-block other-options)}})]
  436. (let [ast (gp-mldoc/->edn content (gp-mldoc/default-config format))
  437. ast (mapv common/remove-block-ast-pos ast)
  438. ast (removev common/Properties-block-ast? ast)
  439. ast* (common/replace-block&page-reference&embed ast)
  440. keep-level<=n (get-in *state* [:export-options :keep-only-level<=N])
  441. ast* (if (pos? keep-level<=n)
  442. (common/keep-only-level<=n ast* keep-level<=n)
  443. ast*)
  444. ast** (if (= "no-indent" (get-in *state* [:export-options :indent-style]))
  445. (mapv common/replace-Heading-with-Paragraph ast*)
  446. ast*)
  447. config-for-walk-block-ast (cond-> {}
  448. (get-in *state* [:export-options :remove-emphasis?])
  449. (update :mapcat-fns-on-inline-ast conj common/remove-emphasis)
  450. (get-in *state* [:export-options :remove-page-ref-brackets?])
  451. (update :map-fns-on-inline-ast conj common/remove-page-ref-brackets)
  452. (get-in *state* [:export-options :remove-tags?])
  453. (update :mapcat-fns-on-inline-ast conj common/remove-tags)
  454. (= "no-indent" (get-in *state* [:export-options :indent-style]))
  455. (update :fns-on-inline-coll conj common/remove-prefix-spaces-in-Plain))
  456. ast*** (if-not (empty? config-for-walk-block-ast)
  457. (mapv (partial common/walk-block-ast config-for-walk-block-ast) ast**)
  458. ast**)
  459. simple-asts (mapcatv block-ast->simple-ast ast***)]
  460. (simple-asts->string simple-asts)))))
  461. (defn export-blocks-as-markdown
  462. "options:
  463. :indent-style \"dashes\" | \"spaces\" | \"no-indent\"
  464. :remove-options [:emphasis :page-ref :tag :property]
  465. :other-options {:keep-only-level<=N int :newline-after-block bool}"
  466. [repo root-block-uuids-or-page-name options]
  467. {:pre [(or (coll? root-block-uuids-or-page-name)
  468. (string? root-block-uuids-or-page-name))]}
  469. (util/profile
  470. :export-blocks-as-markdown
  471. (let [content
  472. (if (string? root-block-uuids-or-page-name)
  473. ;; page
  474. (common/get-page-content root-block-uuids-or-page-name)
  475. (common/root-block-uuids->content repo root-block-uuids-or-page-name))
  476. first-block (db/entity [:block/uuid (first root-block-uuids-or-page-name)])
  477. format (or (:block/format first-block) (state/get-preferred-format))]
  478. (export-helper content format options))))
  479. (defn export-files-as-markdown
  480. "options see also `export-blocks-as-markdown`"
  481. [files options]
  482. (mapv
  483. (fn [{:keys [path content names format]}]
  484. (when (first names)
  485. (util/profile (print-str :export-files-as-markdown path)
  486. [path (export-helper content format options)])))
  487. files))
  488. (defn export-repo-as-markdown!
  489. "TODO: indent-style and remove-options"
  490. [repo]
  491. (when-let [files (util/profile :get-file-content (common/get-file-contents-with-suffix repo))]
  492. (let [files (export-files-as-markdown files nil)
  493. zip-file-name (str repo "_markdown_" (quot (util/time-ms) 1000))]
  494. (p/let [zipfile (zip/make-zip zip-file-name files repo)]
  495. (when-let [anchor (gdom/getElement "export-as-markdown")]
  496. (.setAttribute anchor "href" (js/window.URL.createObjectURL zipfile))
  497. (.setAttribute anchor "download" (.-name zipfile))
  498. (.click anchor))))))
  499. ;;; export fns (ends)