core.cljs 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. (ns frontend.common.file.core
  2. "Convert blocks to file content. Used for exports and saving file to disk. Shared
  3. by worker and frontend namespaces"
  4. (:require [clojure.string :as string]
  5. [datascript.core :as d]
  6. [logseq.db :as ldb]
  7. [logseq.db.frontend.content :as db-content]
  8. [logseq.db.frontend.entity-plus :as entity-plus]
  9. [logseq.db.sqlite.util :as sqlite-util]
  10. [logseq.graph-parser.property :as gp-property]
  11. [logseq.outliner.tree :as otree]))
  12. (defn- indented-block-content
  13. [content spaces-tabs]
  14. (let [lines (string/split-lines content)]
  15. (string/join (str "\n" spaces-tabs) lines)))
  16. (defn- content-with-collapsed-state
  17. "Only accept nake content (without any indentation)"
  18. [repo format content collapsed?]
  19. (cond
  20. collapsed?
  21. (gp-property/insert-property repo format content :collapsed true)
  22. ;; Don't check properties. Collapsed is an internal state log as property in file, but not counted into properties
  23. (false? collapsed?)
  24. (gp-property/remove-property format :collapsed content)
  25. :else
  26. content))
  27. (defn- recur-replace-uuid-in-block-title
  28. "Return block-title"
  29. [ent max-depth]
  30. (let [ref-set (loop [result-refs (:block/refs ent)
  31. current-refs (:block/refs ent)
  32. depth 0]
  33. (if (or (>= depth max-depth) (empty? current-refs))
  34. result-refs
  35. (let [next-refs (mapcat :block/refs current-refs)]
  36. (recur (apply conj result-refs next-refs) next-refs (inc depth)))))]
  37. (loop [result (db-content/id-ref->title-ref (:block/title ent) ref-set true)
  38. last-result nil
  39. depth 0]
  40. (if (or (>= depth max-depth)
  41. (= last-result result))
  42. result
  43. (recur (db-content/id-ref->title-ref result ref-set true) result (inc depth))))))
  44. (defn- transform-content
  45. [repo db {:block/keys [collapsed? format pre-block? title page properties] :as b} level {:keys [heading-to-list?]} context]
  46. (let [db-based? (sqlite-util/db-based-graph? repo)
  47. block-ref-not-saved? (and (seq (:block/_refs (d/entity db (:db/id b))))
  48. (not (string/includes? title (str (:block/uuid b))))
  49. (not db-based?))
  50. heading (:heading properties)
  51. markdown? (= :markdown format)
  52. title (if db-based?
  53. ;; replace [[uuid]] with block's content
  54. (recur-replace-uuid-in-block-title (d/entity db (:db/id b)) 10)
  55. title)
  56. content (or title "")
  57. page-first-child? (= (:db/id b) (ldb/get-first-child db (:db/id page)))
  58. pre-block? (or pre-block?
  59. (and page-first-child?
  60. markdown?
  61. (string/includes? (first (string/split-lines content)) ":: ")))
  62. content (cond
  63. pre-block?
  64. (let [content (string/trim content)]
  65. (str content "\n"))
  66. :else
  67. (let [;; first block is a heading, Markdown users prefer to remove the `-` before the content
  68. markdown-top-heading? (and markdown?
  69. page-first-child?
  70. heading)
  71. [prefix spaces-tabs]
  72. (cond
  73. (= format :org)
  74. [(->>
  75. (repeat level "*")
  76. (apply str)) ""]
  77. markdown-top-heading?
  78. ["" ""]
  79. :else
  80. (let [level (if (and heading-to-list? heading)
  81. (if (> heading 1)
  82. (dec heading)
  83. heading)
  84. level)
  85. spaces-tabs (->>
  86. (repeat (dec level) (:export-bullet-indentation context))
  87. (apply str))]
  88. [(str spaces-tabs "-") (str spaces-tabs " ")]))
  89. content (if heading-to-list?
  90. (-> (string/replace content #"^\s?#+\s+" "")
  91. (string/replace #"^\s?#+\s?$" ""))
  92. content)
  93. content (content-with-collapsed-state repo format content collapsed?)
  94. new-content (indented-block-content (string/trim content) spaces-tabs)
  95. sep (if (or markdown-top-heading?
  96. (string/blank? new-content))
  97. ""
  98. " ")]
  99. (str prefix sep new-content)))]
  100. (if block-ref-not-saved?
  101. (gp-property/insert-property repo format content :id (str (:block/uuid b)))
  102. content)))
  103. (defn- tree->file-content-aux
  104. [repo db tree {:keys [init-level] :as opts} context]
  105. (let [block-contents (transient [])]
  106. (loop [[f & r] tree level init-level]
  107. (if (nil? f)
  108. (->> block-contents persistent! flatten (remove nil?))
  109. (let [page? (nil? (:block/page f))
  110. content (if page? nil (transform-content repo db f level opts context))
  111. new-content
  112. (if-let [children (seq (:block/children f))]
  113. (cons content (tree->file-content-aux repo db children {:init-level (inc level)} context))
  114. [content])]
  115. (conj! block-contents new-content)
  116. (recur r level))))))
  117. (defn tree->file-content
  118. "Used by both file and DB graphs for export and for file-graph specific features"
  119. [repo db tree opts context]
  120. (->> (tree->file-content-aux repo db tree opts context) (string/join "\n")))
  121. (defn- update-block-content
  122. [db item eid]
  123. ;; This may not be needed if this becomes a file-graph only context
  124. (if (entity-plus/db-based-graph? db)
  125. (db-content/update-block-content db item eid)
  126. item))
  127. (defn block->content
  128. "Converts a block including its children (recursively) to plain-text."
  129. [repo db root-block-uuid tree->file-opts context]
  130. (assert (uuid? root-block-uuid))
  131. (let [init-level (or (:init-level tree->file-opts)
  132. (if (ldb/page? (d/entity db [:block/uuid root-block-uuid]))
  133. 0
  134. 1))
  135. blocks (->> (d/pull-many db '[*] (keep :db/id (ldb/get-block-and-children db root-block-uuid)))
  136. (map #(update-block-content db % (:db/id %))))
  137. tree (otree/blocks->vec-tree repo db blocks (str root-block-uuid))]
  138. (tree->file-content repo db tree
  139. (assoc tree->file-opts :init-level init-level)
  140. context)))