text.cljs 21 KB

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