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. [frontend.format.mldoc :as mldoc]
  15. [malli.core :as m]
  16. [promesa.core :as p]
  17. [frontend.config :as config]))
  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. [(newline* 1)]))
  328. "Paragraph_line"
  329. (assert false "Paragraph_line is mldoc internal ast")
  330. "Paragraph_Sep"
  331. [(newline* ast-content)]
  332. "Heading"
  333. (block-heading ast-content)
  334. "List"
  335. (block-list ast-content)
  336. ("Directive" "Results" "Export" "CommentBlock" "Custom")
  337. nil
  338. "Example"
  339. (block-example ast-content)
  340. "Src"
  341. (block-src ast-content)
  342. "Quote"
  343. (block-quote ast-content)
  344. "Latex_Fragment"
  345. (block-latex-fragment ast-content)
  346. "Latex_Environment"
  347. (block-latex-env (rest block))
  348. "Displayed_Math"
  349. (block-displayed-math ast-content)
  350. "Drawer"
  351. (block-drawer (rest block))
  352. "Property_Drawer"
  353. (block-property-drawer ast-content)
  354. "Footnote_Definition"
  355. (block-footnote-definition (rest block))
  356. "Horizontal_Rule"
  357. block-horizontal-rule
  358. "Table"
  359. (block-table ast-content)
  360. "Comment"
  361. (block-comment ast-content)
  362. "Raw_Html"
  363. (block-raw-html ast-content)
  364. "Hiccup"
  365. (block-hiccup ast-content)
  366. (assert false (print-str :block-ast->simple-ast ast-type "not implemented yet")))))))
  367. (defn- inline-ast->simple-ast
  368. [inline]
  369. (let [[ast-type ast-content] inline]
  370. (case ast-type
  371. "Emphasis"
  372. (inline-emphasis ast-content)
  373. ("Break_Line" "Hard_Break_Line")
  374. (inline-break-line)
  375. "Verbatim"
  376. [(raw-text ast-content)]
  377. "Code"
  378. [(raw-text "`" ast-content "`")]
  379. "Tag"
  380. [(raw-text (str "#" (common/hashtag-value->string ast-content)))]
  381. "Spaces" ; what's this ast-type for ?
  382. nil
  383. "Plain"
  384. [(raw-text ast-content)]
  385. "Link"
  386. (inline-link ast-content)
  387. "Nested_link"
  388. (inline-nested-link ast-content)
  389. "Target"
  390. [(raw-text (str "<<" ast-content ">>"))]
  391. "Subscript"
  392. (inline-subscript ast-content)
  393. "Superscript"
  394. (inline-superscript ast-content)
  395. "Footnote_Reference"
  396. (inline-footnote-reference ast-content)
  397. "Cookie"
  398. (inline-cookie ast-content)
  399. "Latex_Fragment"
  400. (inline-latex-fragment ast-content)
  401. "Macro"
  402. (inline-macro ast-content)
  403. "Entity"
  404. (inline-entity ast-content)
  405. "Timestamp"
  406. (inline-timestamp ast-content)
  407. "Radio_Target"
  408. [(raw-text (str "<<<" ast-content ">>>"))]
  409. "Email"
  410. (inline-email ast-content)
  411. "Inline_Hiccup"
  412. [(raw-text ast-content)]
  413. "Inline_Html"
  414. [(raw-text ast-content)]
  415. ("Export_Snippet" "Inline_Source_Block")
  416. nil
  417. (assert false (print-str :inline-ast->simple-ast ast-type "not implemented yet")))))
  418. ;;; block-ast, inline-ast -> simple-ast (ends)
  419. ;;; export fns
  420. (defn- export-helper
  421. [content format options]
  422. (let [remove-options (set (:remove-options options))
  423. other-options (:other-options options)]
  424. (binding [*state* (merge *state*
  425. {:export-options
  426. {:indent-style (or (:indent-style options) "dashes")
  427. :remove-emphasis? (contains? remove-options :emphasis)
  428. :remove-page-ref-brackets? (contains? remove-options :page-ref)
  429. :remove-tags? (contains? remove-options :tag)
  430. :remove-properties? (contains? remove-options :property)
  431. :keep-only-level<=N (:keep-only-level<=N other-options)
  432. :newline-after-block (:newline-after-block other-options)}})]
  433. (let [ast (mldoc/->edn content format)
  434. ast (mapv common/remove-block-ast-pos ast)
  435. ast (removev common/Properties-block-ast? ast)
  436. ast* (common/replace-block&page-reference&embed ast)
  437. keep-level<=n (get-in *state* [:export-options :keep-only-level<=N])
  438. ast* (if (pos? keep-level<=n)
  439. (common/keep-only-level<=n ast* keep-level<=n)
  440. ast*)
  441. ast** (if (= "no-indent" (get-in *state* [:export-options :indent-style]))
  442. (mapv common/replace-Heading-with-Paragraph ast*)
  443. ast*)
  444. config-for-walk-block-ast (cond-> {}
  445. (get-in *state* [:export-options :remove-emphasis?])
  446. (update :mapcat-fns-on-inline-ast conj common/remove-emphasis)
  447. (get-in *state* [:export-options :remove-page-ref-brackets?])
  448. (update :map-fns-on-inline-ast conj common/remove-page-ref-brackets)
  449. (get-in *state* [:export-options :remove-tags?])
  450. (update :mapcat-fns-on-inline-ast conj common/remove-tags)
  451. (= "no-indent" (get-in *state* [:export-options :indent-style]))
  452. (update :fns-on-inline-coll conj common/remove-prefix-spaces-in-Plain))
  453. ast*** (if-not (empty? config-for-walk-block-ast)
  454. (mapv (partial common/walk-block-ast config-for-walk-block-ast) ast**)
  455. ast**)
  456. simple-asts (mapcatv block-ast->simple-ast ast***)]
  457. (simple-asts->string simple-asts)))))
  458. (defn export-blocks-as-markdown
  459. "options:
  460. :indent-style \"dashes\" | \"spaces\" | \"no-indent\"
  461. :remove-options [:emphasis :page-ref :tag :property]
  462. :other-options {:keep-only-level<=N int :newline-after-block bool}"
  463. [repo root-block-uuids-or-page-name options]
  464. {:pre [(or (coll? root-block-uuids-or-page-name)
  465. (string? root-block-uuids-or-page-name))]}
  466. (util/profile
  467. :export-blocks-as-markdown
  468. (let [content
  469. (if (string? root-block-uuids-or-page-name)
  470. ;; page
  471. (common/get-page-content root-block-uuids-or-page-name)
  472. (common/root-block-uuids->content repo root-block-uuids-or-page-name))
  473. first-block (db/entity [:block/uuid (first root-block-uuids-or-page-name)])
  474. format (or (:block/format first-block) (state/get-preferred-format))]
  475. (export-helper content format options))))
  476. (defn export-files-as-markdown
  477. "options see also `export-blocks-as-markdown`"
  478. [files options]
  479. (mapv
  480. (fn [{:keys [path title content]}]
  481. (util/profile (print-str :export-files-as-markdown title)
  482. [(or path title) (export-helper content :markdown options)]))
  483. files))
  484. (defn export-repo-as-markdown!
  485. "TODO: indent-style and remove-options"
  486. [repo]
  487. (p/let [files (util/profile :get-file-content (common/<get-file-contents repo "md"))]
  488. (when (seq files)
  489. (let [files (export-files-as-markdown files nil)
  490. repo (-> repo
  491. (string/replace config/db-version-prefix "")
  492. (string/replace config/local-db-prefix ""))
  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)